[PATCH] Upstream patch - 16102021

This commit is contained in:
Parthiv Patel committed 2021-10-16 09:36:11 +00:00
1 parent f045c58122
commit f0f3085cb9
33 files changed
+2600 -641

No files matched your search

@@ -277,7 +277,7 @@ class AccountReconcileModel(models.Model):
self.ensure_one()
balance = base_line_dict['balance']
tax_type = tax.type_tax_use
is_refund = (tax_type == 'sale' and balance < 0) or (tax_type == 'purchase' and balance > 0)
is_refund = (tax_type == 'sale' and balance > 0) or (tax_type == 'purchase' and balance < 0)
res = tax.compute_all(balance, is_refund=is_refund)
+1 -1
View File
@@ -199,7 +199,7 @@
<p t-if="o.invoice_incoterm_id" name="incoterm">
<strong>Incoterm: </strong><span t-field="o.invoice_incoterm_id.code"/> - <span t-field="o.invoice_incoterm_id.name"/>
</p>
<div id="qrcode" t-if="o.display_qr_code">
<div id="qrcode" t-if="o.display_qr_code and o.amount_residual > 0">
<p t-if="qr_code_urls.get(o.id)">
<strong class="text-center">Scan me with your banking app.</strong><br/><br/>
<img class="border border-dark rounded" t-att-src="qr_code_urls[o.id]"/>
+2
View File
@@ -82,6 +82,8 @@ class ProviderGrid(models.Model):
total_delivery += line.price_total
if not line.product_id or line.is_delivery:
continue
if line.product_id.type == "service":
continue
qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id)
weight += (line.product_id.weight or 0.0) * qty
volume += (line.product_id.volume or 0.0) * qty
+3
View File
@@ -0,0 +1,3 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_manual
+694
View File
@@ -0,0 +1,694 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import fields
from flectra.tests.common import Form
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
import random
import logging
import time
_logger = logging.getLogger(__name__)
class TestAr(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref='l10n_ar.l10nar_ri_chart_template'):
super(TestAr, cls).setUpClass(chart_template_ref=chart_template_ref)
# ==== Company ====
cls.company_data['company'].write({
'parent_id': cls.env.ref('base.main_company').id,
'currency_id': cls.env.ref('base.ARS').id,
'name': '(AR) Responsable Inscripto (Unit Tests)',
"l10n_ar_afip_start_date": time.strftime('%Y-01-01'),
'l10n_ar_gross_income_type': 'local',
'l10n_ar_gross_income_number': '901-21885123',
})
cls.company_ri = cls.company_data['company']
cls.company_ri.partner_id.write({
'name': '(AR) Responsable Inscripto (Unit Tests)',
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVARI").id,
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
'vat': '30111111118',
"street": 'Calle Falsa 123',
"city": 'Rosario',
"country_id": cls.env.ref("base.ar").id,
"state_id": cls.env.ref("base.state_ar_s").id,
"zip": '2000',
"phone": '+1 555 123 7073',
"email": 'info@example.com',
"website": 'www.example.com',
})
cls.partner_ri = cls.company_ri.partner_id
# ==== Company MONO ====
cls.company_mono = cls.setup_company_data('(AR) Monotributista (Unit Tests)', chart_template=cls.env.ref('l10n_ar.l10nar_base_chart_template'))['company']
cls.company_mono.write({
'parent_id': cls.env.ref('base.main_company').id,
'currency_id': cls.env.ref('base.ARS').id,
'name': '(AR) Monotributista (Unit Tests)',
"l10n_ar_afip_start_date": time.strftime('%Y-01-01'),
'l10n_ar_gross_income_type': 'exempt',
})
cls.company_mono.partner_id.write({
'name': '(AR) Monotributista (Unit Tests)',
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_RM").id,
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
'vat': '20222222223',
"street": 'Calle Falsa 123',
"city": 'Rosario',
"country_id": cls.env.ref("base.ar").id,
"state_id": cls.env.ref("base.state_ar_s").id,
"zip": '2000',
"phone": '+1 555 123 7073',
"email": 'info@example.com',
"website": 'www.example.com',
})
cls.partner_mono = cls.company_mono.partner_id
# ==== Bank Account ====
cls.bank_account_ri = cls.env['res.partner.bank'].create({
'acc_number': '7982898111100056688080',
'partner_id': cls.company_ri.partner_id.id,
'company_id': cls.company_ri.id,
})
# ==== Partners / Customers ====
cls.res_partner_adhoc = cls.env['res.partner'].create({
"name": "ADHOC SA",
"is_company": 1,
"city": "Rosario",
"zip": "2000",
"state_id": cls.env.ref("base.state_ar_s").id,
"country_id": cls.env.ref("base.ar").id,
"street": "Ovidio Lagos 41 bis",
"email": "info@adhoc.com.ar",
"phone": "(+54) (341) 208 0203",
"website": "http://www.adhoc.com.ar",
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
'vat': "30714295698",
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVARI").id,
})
cls.partner_cf = cls.env['res.partner'].create({
"name": "Consumidor Final Anónimo",
"l10n_latam_identification_type_id": cls.env.ref('l10n_ar.it_Sigd').id,
"l10n_ar_afip_responsibility_type_id": cls.env.ref("l10n_ar.res_CF").id,
})
cls.res_partner_gritti_mono = cls.env['res.partner'].create({
"name": "Gritti Agrimensura (Monotributo)",
"is_company": 1,
"city": "Rosario",
"zip": "2000",
"state_id": cls.env.ref("base.state_ar_s").id,
"country_id": cls.env.ref("base.ar").id,
"street": "Calle Falsa 123",
"email": "info@example.com.ar",
"phone": "(+54) (341) 111 2222",
"website": "http://www.grittiagrimensura.com",
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
'vat': "27320732811",
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_RM").id,
})
cls.res_partner_cerrocastor = cls.env['res.partner'].create({
"name": "Cerro Castor (Tierra del Fuego)",
"is_company": 1,
"city": "Ushuaia",
"state_id": cls.env.ref("base.state_ar_v").id,
"country_id": cls.env.ref("base.ar").id,
"street": "Ruta 3 km 26",
"email": "info@cerrocastor.com",
"phone": "(+00) (11) 4444 5556",
"website": "http://www.cerrocastor.com",
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
'vat': "27333333339",
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVA_LIB").id,
})
cls.res_partner_cmr = cls.env['res.partner'].create({
"name": "Concejo Municipal de Rosario (IVA Sujeto Exento)",
"is_company": 1,
"city": "Rosario",
"zip": "2000",
"state_id": cls.env.ref("base.state_ar_s").id,
"country_id": cls.env.ref("base.ar").id,
"street": "Cordoba 501",
"email": "info@example.com.ar",
"phone": "(+54) (341) 222 3333",
"website": "http://www.concejorosario.gov.ar/",
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
'vat': "30684679372",
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVAE").id,
})
cls.res_partner_expresso = cls.env['res.partner'].create({
"name": "Expresso",
"is_company": 1,
"city": "Barcelona",
"zip": "11002",
"country_id": cls.env.ref("base.es").id,
"street": "La gran avenida 123",
"email": "info@expresso.com",
"phone": "(+00) (11) 222 3333",
"website": "http://www.expresso.com/",
'l10n_latam_identification_type_id': cls.env.ref("l10n_latam_base.it_fid").id,
'vat': "2222333344445555",
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_EXT").id,
})
cls.partner_mipyme = cls.env['res.partner'].create({
"name": "Belgrano Cargas Y Logistica S (Mipyme)",
"is_company": 1,
"city": "Buenos Aires",
"zip": "1425",
"state_id": cls.env.ref("base.state_ar_c").id,
"country_id": cls.env.ref("base.ar").id,
"street": "Av. Santa Fe 4636",
"email": "mipyme@example.com",
"phone": "(123)-456-7890",
"website": "http://www.mypime-inc.com",
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
'vat': "30714101443",
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVARI").id,
})
cls.partner_mipyme_ex = cls.partner_mipyme.copy({'name': 'MiPyme Exento', 'l10n_ar_afip_responsibility_type_id': cls.env.ref('l10n_ar.res_IVAE').id})
# ==== Taxes ====
cls.tax_21 = cls._search_tax(cls, 'iva_21')
cls.tax_27 = cls._search_tax(cls, 'iva_27')
cls.tax_0 = cls._search_tax(cls, 'iva_0')
cls.tax_10_5 = cls._search_tax(cls, 'iva_105')
cls.tax_no_gravado = cls._search_tax(cls, 'iva_no_gravado')
cls.tax_perc_iibb = cls._search_tax(cls, 'percepcion_iibb_ba')
cls.tax_iva_exento = cls._search_tax(cls, 'iva_exento')
# ==== Products ====
uom_unit = cls.env.ref('uom.product_uom_unit')
uom_hour = cls.env.ref('uom.product_uom_hour')
cls.product_iva_21 = cls.env['product.product'].create({
'name': 'Large Cabinet (VAT 21)',
'uom_id': uom_unit.id,
'uom_po_id': uom_unit.id,
'lst_price': 320.0,
'type': "consu",
'default_code': 'E-COM07',
})
cls.service_iva_27 = cls.env['product.product'].create({
# demo 'product_product_telefonia'
'name': 'Telephone service (VAT 27)',
'uom_id': uom_unit.id,
'uom_po_id': uom_unit.id,
'lst_price': 130.0,
'type': 'service',
'default_code': 'TELEFONIA',
'taxes_id': [(6, 0, cls.tax_27.ids)],
})
cls.product_iva_cero = cls.env['product.product'].create({
# demo 'product_product_cero'
'name': 'Non-industrialized animals and vegetables (VAT Zero)',
'uom_id': uom_unit.id,
'uom_po_id': uom_unit.id,
'list_price': 160.0,
'type': 'consu',
'default_code': 'CERO',
'taxes_id': [(6, 0, cls.tax_0.ids)],
})
cls.product_iva_105 = cls.env['product.product'].create({
# demo 'product.product_product_27'
'name': 'Laptop Customized (VAT 10,5)',
'uom_id': uom_unit.id,
'uom_po_id': uom_unit.id,
'type': 'consu',
'default_code': '10,5',
'taxes_id': [(6, 0, cls.tax_10_5.ids)],
})
cls.service_iva_21 = cls.env['product.product'].create({
# demo data product.product_product_2
'name': 'Virtual Home Staging (VAT 21)',
'uom_id': uom_hour.id,
'uom_po_id': uom_hour.id,
'list_price': 38.25,
'type': 'service',
'default_code': 'VAT 21',
'taxes_id': [(6, 0, cls.tax_21.ids)],
})
cls.product_no_gravado = cls.env['product.product'].create({
# demo data product_product_no_gravado
'name': 'Untaxed concepts (VAT NT)',
'uom_id': uom_unit.id,
'uom_po_id': uom_unit.id,
'list_price': 40.00,
'type': 'consu',
'default_code': 'NOGRAVADO',
'taxes_id': [(6, 0, cls.tax_no_gravado.ids)],
})
cls.product_iva_105_perc = cls.product_iva_105.copy({
# product.product_product_25
"name": "Laptop E5023 (VAT 10,5)",
# agregamos percecipn aplicada y sufrida tambien
'taxes_id': [(6, 0, [cls.tax_10_5.id, cls.tax_perc_iibb.id])],
})
cls.product_iva_exento = cls.env['product.product'].create({
# demo product_product_exento
'name': 'Book: Development in Flectra (VAT Exempt)',
'uom_id': uom_unit.id,
'uom_po_id': uom_unit.id,
"list_price": 80.0,
'type': 'consu',
'default_code': 'EXENTO',
'taxes_id': [(6, 0, cls.tax_iva_exento.ids)],
})
# ==== Document Types ====
cls.document_type = {
'invoice_a': cls.env.ref('l10n_ar.dc_a_f'),
'credit_note_a': cls.env.ref('l10n_ar.dc_a_nc'),
'invoice_b': cls.env.ref('l10n_ar.dc_b_f'),
'credit_note_b': cls.env.ref('l10n_ar.dc_b_nc'),
'invoice_e': cls.env.ref('l10n_ar.dc_e_f'),
'invoice_mipyme_a': cls.env.ref('l10n_ar.dc_fce_a_f'),
'invoice_mipyme_b': cls.env.ref('l10n_ar.dc_fce_b_f'),
}
# ==== Journals ====
cls.sale_expo_journal_ri = cls.env["account.journal"].create({
'name': "Expo Sales Journal",
'company_id': cls.company_ri.id,
'type': "sale",
'code': "S0002",
'l10n_latam_use_documents': "True",
'l10n_ar_afip_pos_number': 2,
'l10n_ar_afip_pos_partner_id': cls.partner_ri.id,
'l10n_ar_afip_pos_system': "FEERCEL",
'refund_sequence': False,
})
# ==== Invoices ====
cls.demo_invoices = {}
def _create_test_invoices_like_demo(self, use_current_date=True):
""" Create in the unit tests the same invoices created in demo data """
payment_term_id = self.env.ref("account.account_payment_term_end_following_month")
invoice_user_id = self.env.user
incoterm = self.env.ref("account.incoterm_EXW")
invoices_to_create = {
'test_invoice_1': {
"ref": "test_invoice_1: Invoice to gritti support service, vat 21",
"partner_id": self.res_partner_gritti_mono,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": "out_invoice",
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.service_iva_21}
],
},
'test_invoice_2': {
"ref": "test_invoice_2: Invoice to CMR with vat 21, 27 and 10,5",
"partner_id": self.res_partner_cmr,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": "out_invoice",
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 2}
],
},
'test_invoice_3': {
"ref": "test_invoice_3: Invoice to ADHOC with vat cero and 21",
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
{'product_id': self.product_iva_cero, 'price_unit': 200.0, 'quantity': 1}
],
},
'test_invoice_4': {
'ref': 'test_invoice_4: Invoice to ADHOC with vat exempt and 21',
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
{'product_id': self.product_iva_exento, 'price_unit': 100.0, 'quantity': 1},
],
},
'test_invoice_5': {
'ref': 'test_invoice_5: Invoice to ADHOC with all type of taxes',
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 2},
{'product_id': self.product_no_gravado, 'price_unit': 50.0, 'quantity': 10},
{'product_id': self.product_iva_cero, 'price_unit': 200.0, 'quantity': 1},
{'product_id': self.product_iva_exento, 'price_unit': 100.0, 'quantity': 1}
],
},
'test_invoice_6': {
'ref': 'test_invoice_6: Invoice to cerro castor, fiscal position changes taxes to exempt',
"partner_id": self.res_partner_cerrocastor,
"journal_id": self.sale_expo_journal_ri,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_incoterm_id": incoterm,
"invoice_line_ids": [
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 2},
{'product_id': self.product_no_gravado, 'price_unit': 50.0, 'quantity': 10},
{'product_id': self.product_iva_cero, 'price_unit': 200.0, 'quantity': 1},
{'product_id': self.product_iva_exento, 'price_unit': 100.0, 'quantity': 1},
],
},
'test_invoice_7': {
'ref': 'test_invoice_7: Export invoice to expresso, fiscal position changes tax to exempt (type 4 because it have services)',
"partner_id": self.res_partner_expresso,
"journal_id": self.sale_expo_journal_ri,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_incoterm_id": incoterm,
"invoice_line_ids": [
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 2},
{'product_id': self.product_no_gravado, 'price_unit': 50.0, 'quantity': 10},
{'product_id': self.product_iva_cero, 'price_unit': 200.0, 'quantity': 1},
{'product_id': self.product_iva_exento, 'price_unit': 100.0, 'quantity': 1},
],
},
'test_invoice_8': {
'ref': 'test_invoice_8: Invoice to consumidor final',
"partner_id": self.partner_cf,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.service_iva_21, 'price_unit': 642.0, 'quantity': 1},
],
},
'test_invoice_10': {
'ref': 'test_invoice_10; Invoice to ADHOC in USD and vat 21',
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.product_iva_105, 'price_unit': 1000.0, 'quantity': 5},
],
"currency_id": self.env.ref("base.USD"),
},
'test_invoice_11': {
'ref': 'test_invoice_11: Invoice to ADHOC with many lines in order to prove rounding error, with 4 decimals of precision for the currency and 2 decimals for the product the error apperar',
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.service_iva_21, 'price_unit': 1.12, 'quantity': 1, 'name': 'Support Services 1'},
{'product_id': self.service_iva_21, 'price_unit': 1.12, 'quantity': 1, 'name': 'Support Services 2'},
{'product_id': self.service_iva_21, 'price_unit': 1.12, 'quantity': 1, 'name': 'Support Services 3'},
{'product_id': self.service_iva_21, 'price_unit': 1.12, 'quantity': 1, 'name': 'Support Services 4'},
],
},
'test_invoice_12': {
'ref': 'test_invoice_12: Invoice to ADHOC with many lines in order to test rounding error, it is required to use a 4 decimal precision in prodct in order to the error occur',
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.service_iva_21, 'price_unit': 15.7076, 'quantity': 1, 'name': 'Support Services 1'},
{'product_id': self.service_iva_21, 'price_unit': 5.3076, 'quantity': 2, 'name': 'Support Services 2'},
{'product_id': self.service_iva_21, 'price_unit': 3.5384, 'quantity': 2, 'name': 'Support Services 3'},
{'product_id': self.service_iva_21, 'price_unit': 1.6376, 'quantity': 2, 'name': 'Support Services 4'},
],
},
'test_invoice_13': {
'ref': 'test_invoice_13: Invoice to ADHOC with many lines in order to test zero amount invoices y rounding error. it is required to set the product decimal precision to 4 and change 260.59 for 260.60 in order to reproduce the error',
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.service_iva_21, 'price_unit': 24.3, 'quantity': 3, 'name': 'Support Services 1'},
{'product_id': self.service_iva_21, 'price_unit': 260.59, 'quantity': 1, 'name': 'Support Services 2'},
{'product_id': self.service_iva_21, 'price_unit': 48.72, 'quantity': 1, 'name': 'Support Services 3'},
{'product_id': self.service_iva_21, 'price_unit': 13.666, 'quantity': 1, 'name': 'Support Services 4'},
{'product_id': self.service_iva_21, 'price_unit': 11.329, 'quantity': 2, 'name': 'Support Services 5'},
{'product_id': self.service_iva_21, 'price_unit': 68.9408, 'quantity': 1, 'name': 'Support Services 6'},
{'product_id': self.service_iva_21, 'price_unit': 4.7881, 'quantity': 2, 'name': 'Support Services 7'},
{'product_id': self.service_iva_21, 'price_unit': 12.0625, 'quantity': 2, 'name': 'Support Services 8'},
],
},
'test_invoice_14': {
'ref': 'test_invoice_14: Export invoice to expresso, fiscal position changes tax to exempt (type 1 because only products)',
"partner_id": self.res_partner_expresso,
"journal_id": self.sale_expo_journal_ri,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_incoterm_id": incoterm,
"invoice_line_ids": [
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
],
},
'test_invoice_15': {
'ref': 'test_invoice_15: Export invoice to expresso, fiscal position changes tax to exempt (type 2 because only service)',
"partner_id": self.res_partner_expresso,
"journal_id": self.sale_expo_journal_ri,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_incoterm_id": incoterm,
"invoice_line_ids": [
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
],
},
'test_invoice_16': {
'ref': 'test_invoice_16: Export invoice to expresso, fiscal position changes tax to exempt (type 1 because it have products only, used to test refund of expo)',
"partner_id": self.res_partner_expresso,
"journal_id": self.sale_expo_journal_ri,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_incoterm_id": incoterm,
"invoice_line_ids": [
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
],
},
'test_invoice_17': {
'ref': 'test_invoice_17: Invoice to ADHOC with 100%% of discount',
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.service_iva_21, 'price_unit': 24.3, 'quantity': 3, 'name': 'Support Services 8', 'discount': 100},
],
},
'test_invoice_18': {
'ref': 'test_invoice_18: Invoice to ADHOC with 100%% of discount and with different VAT aliquots',
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.service_iva_21, 'price_unit': 24.3, 'quantity': 3, 'name': 'Support Services 8', 'discount': 100},
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1, 'discount': 100},
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 1},
],
},
'test_invoice_19': {
'ref': 'test_invoice_19: Invoice to ADHOC with multiple taxes and perceptions',
"partner_id": self.res_partner_adhoc,
"invoice_user_id": invoice_user_id,
"invoice_payment_term_id": payment_term_id,
"move_type": 'out_invoice',
"company_id": self.company_ri,
"invoice_line_ids": [
{'product_id': self.service_iva_21, 'price_unit': 24.3, 'quantity': 3, 'name': 'Support Services 8'},
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 1},
],
}
}
for key, values in invoices_to_create.items():
with Form(self.env['account.move'].with_context(default_move_type=values['move_type'])) as invoice_form:
invoice_form.ref = values['ref']
invoice_form.partner_id = values['partner_id']
invoice_form.invoice_user_id = values['invoice_user_id']
invoice_form.invoice_payment_term_id = values['invoice_payment_term_id']
if not use_current_date:
invoice_form.invoice_date = values['invoice_date']
if values.get('invoice_incoterm_id'):
invoice_form.invoice_incoterm_id = values['invoice_incoterm_id']
for line in values['invoice_line_ids']:
with invoice_form.invoice_line_ids.new() as line_form:
line_form.product_id = line.get('product_id')
line_form.price_unit = line.get('price_unit')
line_form.quantity = line.get('quantity')
line_form.name = 'xxxx'
line_form.account_id = self.company_data['default_account_revenue']
invoice = invoice_form.save()
self.demo_invoices[key] = invoice
# Helpers
@classmethod
def _get_afip_pos_system_real_name(cls):
return {'PREPRINTED': 'II_IM'}
def _create_journal(self, afip_ws, data=None):
""" Create a journal of a given AFIP ws type.
If there is a problem because we are using a AFIP certificate that is already been in use then change the certificate and try again """
data = data or {}
afip_ws = afip_ws.upper()
pos_number = str(random.randint(0, 99999))
if 'l10n_ar_afip_pos_number' in data:
pos_number = data.get('l10n_ar_afip_pos_number')
values = {'name': '%s %s' % (afip_ws.replace('WS', ''), pos_number),
'type': 'sale',
'code': afip_ws,
'l10n_ar_afip_pos_system': self._get_afip_pos_system_real_name().get(afip_ws),
'l10n_ar_afip_pos_number': pos_number,
'l10n_latam_use_documents': True,
'company_id': self.env.company.id,
'l10n_ar_afip_pos_partner_id': self.partner_ri.id,
'sequence': 1}
values.update(data)
journal = self.env['account.journal'].create(values)
_logger.info('Created journal %s for company %s' % (journal.name, self.env.company.name))
return journal
def _create_invoice(self, data=None, invoice_type='out_invoice'):
data = data or {}
with Form(self.env['account.move'].with_context(default_move_type=invoice_type)) as invoice_form:
invoice_form.partner_id = data.get('partner', self.partner)
if 'in_' not in invoice_type:
invoice_form.journal_id = data.get('journal', self.journal)
if data.get('document_type'):
invoice_form.l10n_latam_document_type_id = data.get('document_type')
if data.get('document_number'):
invoice_form.l10n_latam_document_number = data.get('document_number')
if data.get('incoterm'):
invoice_form.invoice_incoterm_id = data.get('incoterm')
if data.get('currency'):
invoice_form.currency_id = data.get('currency')
for line in data.get('lines', [{}]):
with invoice_form.invoice_line_ids.new() as invoice_line_form:
if line.get('display_type'):
invoice_line_form.display_type = line.get('display_type')
invoice_line_form.name = line.get('name', 'not invoice line')
else:
invoice_line_form.product_id = line.get('product', self.product_iva_21)
invoice_line_form.quantity = line.get('quantity', 1)
invoice_line_form.price_unit = line.get('price_unit', 100)
invoice_form.invoice_date = invoice_form.date
invoice = invoice_form.save()
return invoice
def _create_invoice_product(self, data=None):
data = data or {}
return self._create_invoice(data)
def _create_invoice_service(self, data=None):
data = data or {}
newlines = []
for line in data.get('lines', [{}]):
line.update({'product': self.service_iva_27})
newlines.append(line)
data.update({'lines': newlines})
return self._create_invoice(data)
def _create_invoice_product_service(self, data=None):
data = data or {}
newlines = []
for line in data.get('lines', [{}]):
line.update({'product': self.product_iva_21})
newlines.append(line)
data.update({'lines': newlines + [{'product': self.service_iva_27}]})
return self._create_invoice(data)
def _create_credit_note(self, invoice, data=None):
data = data or {}
refund_wizard = self.env['account.move.reversal'].with_context({'active_ids': [invoice.id], 'active_model': 'account.move'}).create({
'reason': data.get('reason', 'Mercadería defectuosa'),
'refund_method': data.get('refund_method', 'refund')})
forced_document_type = data.get('document_type')
if forced_document_type:
refund_wizard.l10n_latam_document_type_id = forced_document_type.id
res = refund_wizard.reverse_moves()
refund = self.env['account.move'].browse(res['res_id'])
return refund
def _create_debit_note(self, invoice, data=None):
data = data or {}
debit_note_wizard = self.env['account.debit.note'].with_context(
{'active_ids': [invoice.id], 'active_model': 'account.move', 'default_copy_lines': True}).create({
'reason': data.get('reason', 'Mercadería defectuosa')})
res = debit_note_wizard.create_debit()
debit_note = self.env['account.move'].browse(res['res_id'])
return debit_note
def _search_tax(self, tax_type):
res = self.env['account.tax'].with_context(active_test=False).search([
('type_tax_use', '=', 'sale'),
('company_id', '=', self.env.company.id),
('tax_group_id', '=', self.env.ref('l10n_ar.tax_group_' + tax_type).id)], limit=1)
self.assertTrue(res, '%s Tax was not found' % (tax_type))
return res
def _search_fp(self, name):
return self.env['account.fiscal.position'].search([('company_id', '=', self.env.company.id), ('name', '=', name)])
def _post(self, invoice):
invoice.action_post()
self.assertEqual(invoice.state, 'posted')
def _prepare_multicurrency_values(self):
# Enable multi currency
self.env.user.write({'groups_id': [(4, self.env.ref('base.group_multi_currency').id)]})
# Set ARS as main currency
self._set_today_rate(self.env.ref('base.ARS'), 1.0)
# Set Rates for USD currency
self._set_today_rate(self.env.ref('base.USD'), 1.0 / 162.013)
def _set_today_rate(self, currency, value):
rate_obj = self.env['res.currency.rate']
rate = rate_obj.search([('currency_id', '=', currency.id), ('name', '=', fields.Date.to_string(fields.Date.today())),
('company_id', '=', self.env.company.id)])
if rate:
rate.rate = value
else:
rate_obj.create({'company_id': self.env.company.id, 'currency_id': currency.id, 'rate': value})
_logger.log(25, 'Using %s rate %s' % (currency.name, currency.rate))
+99
View File
@@ -0,0 +1,99 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import common
from flectra.tests import tagged
@tagged('-at_install', 'post_install')
class TestManual(common.TestAr):
@classmethod
def setUpClass(cls):
super(TestManual, cls).setUpClass()
cls.journal = cls._create_journal(cls, 'preprinted')
cls.partner = cls.res_partner_adhoc
cls._create_test_invoices_like_demo(cls)
def test_01_create_invoice(self):
""" Create and validate an invoice for a Responsable Inscripto
* Proper set the current user company
* Properly set the tax amount of the product / partner
* Proper fiscal position (this case not fiscal position is selected)
"""
invoice = self._create_invoice()
self.assertEqual(invoice.company_id, self.company_ri, 'created with wrong company')
self.assertEqual(invoice.amount_tax, 21, 'invoice taxes are not properly set')
self.assertEqual(invoice.amount_total, 121.0, 'invoice taxes has not been applied to the total')
self.assertEqual(invoice.l10n_latam_document_type_id, self.document_type['invoice_a'], 'selected document type should be Factura A')
self._post(invoice)
self.assertEqual(invoice.state, 'posted', 'invoice has not been validate in Flectra')
self.assertEqual(invoice.name, 'FA-A %05d-00000002' % self.journal.l10n_ar_afip_pos_number, 'Invoice number is wrong')
def test_02_fiscal_position(self):
# ADHOC SA > IVA Responsable Inscripto > Without Fiscal Positon
invoice = self._create_invoice({'partner': self.partner})
self.assertFalse(invoice.fiscal_position_id, 'Fiscal position should be set to empty')
# Consumidor Final > IVA Responsable Inscripto > Without Fiscal Positon
invoice = self._create_invoice({'partner': self.partner_cf})
self.assertFalse(invoice.fiscal_position_id, 'Fiscal position should be set to empty')
# Cerro Castor > IVA Liberado – Ley Nº 19.640 > Compras / Ventas Zona Franca > IVA Exento
invoice = self._create_invoice({'partner': self.res_partner_cerrocastor})
self.assertEqual(invoice.fiscal_position_id, self._search_fp('Compras / Ventas Zona Franca'))
# Expresso > Cliente / Proveedor del Exterior > > IVA Exento
invoice = self._create_invoice({'partner': self.res_partner_expresso})
self.assertEqual(invoice.fiscal_position_id, self._search_fp('Compras / Ventas al exterior'))
def test_03_corner_cases(self):
""" Mono partner of type Service and VAT 21 """
self._post(self.demo_invoices['test_invoice_1'])
def test_04_corner_cases(self):
""" Exento partner with multiple VAT types 21, 27 and 10,5' """
self._post(self.demo_invoices['test_invoice_2'])
def test_05_corner_cases(self):
""" RI partner with VAT 0 and 21 """
self._post(self.demo_invoices['test_invoice_3'])
def test_06_corner_cases(self):
""" RI partner with VAT exempt and 21 """
self._post(self.demo_invoices['test_invoice_4'])
def test_07_corner_cases(self):
""" RI partner with all type of taxes """
self._post(self.demo_invoices['test_invoice_5'])
def test_08_corner_cases(self):
""" Consumidor Final """
self._post(self.demo_invoices['test_invoice_8'])
def test_09_corner_cases(self):
""" RI partner with many lines in order to prove rounding error, with 4 decimals of precision for the
currency and 2 decimals for the product the error appear """
self._post(self.demo_invoices['test_invoice_11'])
def test_10_corner_cases(self):
""" RI partner with many lines in order to test rounding error, it is required to use a 4 decimal precision
in product in order to the error occur """
self._post(self.demo_invoices['test_invoice_12'])
def test_11_corner_cases(self):
""" RI partner with many lines in order to test zero amount invoices y rounding error. it is required to
set the product decimal precision to 4 and change 260,59 for 260.60 in order to reproduce the error """
self._post(self.demo_invoices['test_invoice_13'])
def test_12_corner_cases(self):
""" RI partner with 100%% of discount """
self._post(self.demo_invoices['test_invoice_17'])
def test_13_corner_cases(self):
""" RI partner with 100%% of discount and with different VAT aliquots """
self._post(self.demo_invoices['test_invoice_18'])
def test_14_corner_cases(self):
""" Responsable Inscripto" in USD and VAT 21 """
self._prepare_multicurrency_values()
self._post(self.demo_invoices['test_invoice_10'])
+3 -3
View File
@@ -4,13 +4,13 @@
<!-- Account Tax Group -->
<record id="tax_group_0" model="account.tax.group">
<field name="name">Thuế GTGT 0%</field>
<field name="name">VAT 0%</field>
</record>
<record id="tax_group_5" model="account.tax.group">
<field name="name">Thuế GTGT 5%</field>
<field name="name">VAT 5%</field>
</record>
<record id="tax_group_10" model="account.tax.group">
<field name="name">Thuế GTGT 10%</field>
<field name="name">VAT 10%</field>
</record>
</data>
+12 -12
View File
@@ -4,8 +4,8 @@
<!-- for purchase -->
<record id="tax_purchase_vat10" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT được khấu trừ 10%</field>
<field name="description">Thuế GTGT được khấu trừ 10%</field>
<field name="name">Deductible VAT 10%</field>
<field name="description">Deductible VAT 10%</field>
<field name="amount" eval="10"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">purchase</field>
@@ -39,8 +39,8 @@
</record>
<record id="tax_purchase_vat5" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT được khấu trừ 5%</field>
<field name="description">Thuế GTGT được khấu trừ 5%</field>
<field name="name">Deductible VAT 5%</field>
<field name="description">Deductible VAT 5%</field>
<field name="amount" eval="5"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">purchase</field>
@@ -74,8 +74,8 @@
</record>
<record id="tax_purchase_vat0" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT được khấu trừ 0%</field>
<field name="description">Thuế GTGT được khấu trừ 0%</field>
<field name="name">Deductible VAT 0%</field>
<field name="description">Deductible VAT 0%</field>
<field name="amount" eval="0"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">purchase</field>
@@ -107,8 +107,8 @@
<!-- for sale -->
<record id="tax_sale_vat10" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT phải nộp 10%</field>
<field name="description">Thuế GTGT phải nộp 10%</field>
<field name="name">Value Added Tax (VAT) 10%</field>
<field name="description">Value Added Tax (VAT) 10%</field>
<field name="amount" eval="10"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">sale</field>
@@ -142,8 +142,8 @@
</record>
<record id="tax_sale_vat5" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT phải nộp 5%</field>
<field name="description">Thuế GTGT phải nộp 5%</field>
<field name="name">Value Added Tax (VAT) 5%</field>
<field name="description">Value Added Tax (VAT) 5%</field>
<field name="amount" eval="5"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">sale</field>
@@ -177,8 +177,8 @@
</record>
<record id="tax_sale_vat0" model="account.tax.template">
<field name="chart_template_id" ref="vn_template"/>
<field name="name">Thuế GTGT phải nộp 0%</field>
<field name="description">Thuế GTGT phải nộp 0%</field>
<field name="name">Value Added Tax (VAT) 0%</field>
<field name="description">Value Added Tax (VAT) 0%</field>
<field name="amount" eval="0"/>
<field name="amount_type">percent</field>
<field name="type_tax_use">sale</field>
+30 -30
View File
@@ -7,136 +7,136 @@
</record>
<record id="account_tax_report_line_01_vn" model="account.tax.report.line">
<field name="name">HHDV mua vào</field>
<field name="name">Purchase of Goods and Services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
</record>
<record id="account_tax_report_line_01_01_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV mua vào</field>
<field name="name">VAT on purchase of goods and services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_01_vn"/>
</record>
<record id="account_tax_report_line_01_01_01_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV mua vào chịu thuế 0%</field>
<field name="tag_name">Thuế GTGT HHDV mua vào chịu thuế 0%</field>
<field name="name">VAT on purchase of goods and services 0%</field>
<field name="tag_name">VAT on purchase of goods and services 0%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_01_01_vn"/>
</record>
<record id="account_tax_report_line_02_01_01_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV mua vào chịu thuế 5%</field>
<field name="tag_name">Thuế GTGT HHDV mua vào chịu thuế 5%</field>
<field name="name">VAT on purchase of goods and services 5%</field>
<field name="tag_name">VAT on purchase of goods and services 5%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_01_01_vn"/>
</record>
<record id="account_tax_report_line_03_01_01_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV mua vào chịu thuế 10%</field>
<field name="tag_name">Thuế GTGT HHDV mua vào chịu thuế 10%</field>
<field name="name">VAT on purchase of goods and services 10%</field>
<field name="tag_name">VAT on purchase of goods and services 10%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="3"/>
<field name="parent_id" ref="account_tax_report_line_01_01_vn"/>
</record>
<record id="account_tax_report_line_02_01_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV mua vào</field>
<field name="name">Untaxed Purchase of Goods and Services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_01_vn"/>
</record>
<record id="account_tax_report_line_01_02_01_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV mua vào chịu thuế 0%</field>
<field name="tag_name">Giá trị HHDV mua vào chịu thuế 0%</field>
<field name="name">Untaxed Purchase of Goods and Services taxed 0%</field>
<field name="tag_name">Untaxed Purchase of Goods and Services taxed 0%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_02_01_vn"/>
</record>
<record id="account_tax_report_line_02_02_01_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV mua vào chịu thuế 5%</field>
<field name="tag_name">Giá trị HHDV mua vào chịu thuế 5%</field>
<field name="name">Untaxed Purchase of Goods and Services taxed 5%</field>
<field name="tag_name">Untaxed Purchase of Goods and Services taxed 5%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_02_01_vn"/>
</record>
<record id="account_tax_report_line_03_02_01_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV mua vào chịu thuế 10%</field>
<field name="tag_name">Giá trị HHDV mua vào chịu thuế 10%</field>
<field name="name">Untaxed Purchase of Goods and Services taxed 10%</field>
<field name="tag_name">Untaxed Purchase of Goods and Services taxed 10%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="3"/>
<field name="parent_id" ref="account_tax_report_line_02_01_vn"/>
</record>
<record id="account_tax_report_line_02_vn" model="account.tax.report.line">
<field name="name">HHDV bán ra</field>
<field name="name">Sales of Goods and Services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
</record>
<record id="account_tax_report_line_01_02_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV bán ra</field>
<field name="name">VAT on sales of goods and services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_02_vn"/>
</record>
<record id="account_tax_report_line_01_01_02_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV bán ra chịu thuế 0%</field>
<field name="tag_name">Thuế GTGT HHDV bán ra chịu thuế 0%</field>
<field name="name">VAT on sales of goods and services 0%</field>
<field name="tag_name">VAT on sales of goods and services 0%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_01_02_vn"/>
</record>
<record id="account_tax_report_line_02_01_02_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV bán ra chịu thuế 5%</field>
<field name="tag_name">Thuế GTGT HHDV bán ra chịu thuế 5%</field>
<field name="name">VAT on sales of goods and services 5%</field>
<field name="tag_name">VAT on sales of goods and services 5%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_01_02_vn"/>
</record>
<record id="account_tax_report_line_03_01_02_vn" model="account.tax.report.line">
<field name="name">Thuế GTGT HHDV bán ra chịu thuế 10%</field>
<field name="tag_name">Thuế GTGT HHDV bán ra chịu thuế 10%</field>
<field name="name">VAT on sales of goods and services 10%</field>
<field name="tag_name">VAT on sales of goods and services 10%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="3"/>
<field name="parent_id" ref="account_tax_report_line_01_02_vn"/>
</record>
<record id="account_tax_report_line_02_02_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV bán ra</field>
<field name="name">Untaxed Sales of Goods and Services</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_02_vn"/>
</record>
<record id="account_tax_report_line_01_02_02_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV bán ra chịu thuế 0%</field>
<field name="tag_name">Giá trị HHDV bán ra chịu thuế 0%</field>
<field name="name">Untaxed sales of goods and services taxed 0%</field>
<field name="tag_name">Untaxed sales of goods and services taxed 0%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="1"/>
<field name="parent_id" ref="account_tax_report_line_02_02_vn"/>
</record>
<record id="account_tax_report_line_02_02_02_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV bán ra chịu thuế 5%</field>
<field name="tag_name">Giá trị HHDV bán ra chịu thuế 5%</field>
<field name="name">Untaxed sales of goods and services taxed 5%</field>
<field name="tag_name">Untaxed sales of goods and services taxed 5%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="2"/>
<field name="parent_id" ref="account_tax_report_line_02_02_vn"/>
</record>
<record id="account_tax_report_line_03_02_02_vn" model="account.tax.report.line">
<field name="name">Giá trị HHDV bán ra chịu thuế 10%</field>
<field name="tag_name">Giá trị HHDV bán ra chịu thuế 10%</field>
<field name="name">Untaxed sales of goods and services taxed 10%</field>
<field name="tag_name">Untaxed sales of goods and services taxed 10%</field>
<field name="report_id" ref="tax_report"/>
<field name="sequence" eval="3"/>
<field name="parent_id" ref="account_tax_report_line_02_02_vn"/>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -395,7 +395,7 @@ class StockMove(models.Model):
# Then, we collect every relevant moves related to a specific component
# to know how many are considered delivered.
uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty']
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id)
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, round=False)
if not qty_per_kit:
continue
incoming_moves = bom_line_moves.filtered(filters['incoming_moves'])
@@ -135,6 +135,8 @@ class MrpProduction(models.Model):
return False
if not all(line.lot_id for line in mo.move_raw_ids.filtered(lambda sm: sm.has_tracking != 'none').move_line_ids):
return False
if mo.product_id.tracking != 'none' and not mo.lot_producing_id:
return False
return True
return self.filtered(filter_in)
@@ -735,3 +735,75 @@ class TestSubcontractingTracking(TransactionCase):
self.assertEqual(avail_qty_comp1, -todo_nb)
self.assertEqual(avail_qty_comp2, -todo_nb)
self.assertEqual(avail_qty_finished, todo_nb)
def test_flow_tracked_backorder02(self):
""" Both component and finished product are tracked by lot. """
todo_nb = 4
resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
finished_product, component = self.env['product.product'].create([{
'name': 'SuperProduct',
'type': 'product',
'tracking': 'lot',
}, {
'name': 'Component',
'type': 'product',
'tracking': 'lot',
'route_ids': [(4, resupply_sub_on_order_route.id)],
}])
bom_form = Form(self.env['mrp.bom'])
bom_form.type = 'subcontract'
bom_form.subcontractor_ids.add(self.subcontractor_partner1)
bom_form.product_tmpl_id = finished_product.product_tmpl_id
with bom_form.bom_line_ids.new() as bom_line:
bom_line.product_id = component
bom_line.product_qty = 1
bom = bom_form.save()
finished_lot, component_lot = self.env['stock.production.lot'].create([{
'name': 'lot_%s' % product.name,
'product_id': product.id,
'company_id': self.env.company.id,
} for product in [finished_product, component]])
self.env['stock.quant']._update_available_quantity(component, self.env.ref('stock.stock_location_stock'), todo_nb, lot_id=component_lot)
# Create a receipt picking from the subcontractor
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
picking_form.partner_id = self.subcontractor_partner1
with picking_form.move_ids_without_package.new() as move:
move.product_id = finished_product
move.product_uom_qty = todo_nb
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)])
# Process the delivery of the components
compo_picking = mo.picking_ids
compo_picking.action_assign()
wizard_data = compo_picking.button_validate()
wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
wizard.process()
for qty in [3, 1]:
# Record the receiption of <qty> finished products
picking_receipt = self.env['stock.picking'].search([('partner_id', '=', self.subcontractor_partner1.id), ('state', '!=', 'done')])
action = picking_receipt.action_record_components()
mo = self.env['mrp.production'].browse(action['res_id'])
mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
mo_form.qty_producing = qty
mo_form.lot_producing_id = finished_lot
with mo_form.move_line_raw_ids.edit(0) as ml:
ml.lot_id = component_lot
mo = mo_form.save()
mo.subcontracting_record_component()
# Validate the picking and create a backorder
wizard_data = picking_receipt.button_validate()
if qty == 3:
wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
wizard.process()
self.assertEqual(picking_receipt.state, 'done')
+2
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from random import randint
from datetime import datetime
from flectra import fields, tools
from flectra.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
@@ -272,6 +273,7 @@ class TestPoSCommon(ValuationReconciliationTestCommon):
cls.env['res.currency.rate'].create({
'rate': 0.5,
'currency_id': cls.other_currency.id,
'name': datetime.today().date(),
})
other_cash_journal = cls.env['account.journal'].create({
'name': 'Cash Other',
+2 -2
View File
@@ -40,8 +40,8 @@
<attribute name="invisible">0</attribute>
</xpath>
<group name="purchase" position="before">
<field name="seller_ids" context="{'default_product_tmpl_id':context.get('product_tmpl_id',active_id), 'product_template_invisible_variant': True, 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','&gt;',1)]}"/>
<field name="variant_seller_ids" context="{'default_product_tmpl_id': context.get('product_tmpl_id', active_id), 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','&lt;=',1)]}"/>
<field name="seller_ids" context="{'default_product_tmpl_id':context.get('product_tmpl_id',active_id), 'product_template_invisible_variant': True, 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','&gt;',1)], 'readonly': [('product_variant_count','&gt;',1)]}"/>
<field name="variant_seller_ids" context="{'default_product_tmpl_id': context.get('product_tmpl_id', active_id), 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','&lt;=',1)], 'readonly': [('product_variant_count','&lt;=',1)]}"/>
</group>
<group name="bill" position="attributes">
<attribute name="groups">purchase.group_purchase_manager</attribute>
@@ -296,3 +296,50 @@ class TestSaleMrpKitBom(TransactionCase):
purchase_price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
self.assertEqual(purchase_price, 92, "The purchase price must be the total cost of the components multiplied by their unit of measure")
def test_qty_delivered_with_bom(self):
"""Check the quantity delivered, when a bom line has a non integer quantity"""
self.env['ir.model.data'].xmlid_to_object('product.decimal_product_uom').digits = 5
self.kit = self._create_product('Kit', 'product', 0.00)
self.comp = self._create_product('Component', 'product', 0.00)
# Create BoM for Kit
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.kit
bom_product_form.product_tmpl_id = self.kit.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'phantom'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.comp
bom_line.product_qty = 0.08600
self.bom = bom_product_form.save()
self.customer = self.env['res.partner'].create({
'name': 'customer',
})
so = self.env['sale.order'].create({
'partner_id': self.customer.id,
'order_line': [
(0, 0, {
'name': self.kit.name,
'product_id': self.kit.id,
'product_uom_qty': 10.0,
'product_uom': self.kit.uom_id.id,
'price_unit': 1,
'tax_id': False,
})],
})
so.action_confirm()
self.assertTrue(so.picking_ids)
self.assertEqual(so.order_line.qty_delivered, 0)
picking = so.picking_ids
picking.move_lines.quantity_done = 0.86000
picking.button_validate()
# Checks the delivery amount (must be 10).
self.assertEqual(so.order_line.qty_delivered, 10)
+2 -1
View File
@@ -162,7 +162,8 @@ class SaleOrder(models.Model):
@api.onchange('user_id')
def onchange_user_id(self):
super().onchange_user_id()
self.warehouse_id = self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id
if self.state in ['draft','sent']:
self.warehouse_id = self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id
@api.onchange('partner_shipping_id')
def _onchange_partner_shipping_id(self):
+13 -1
View File
@@ -202,10 +202,11 @@ class ProjectTask(models.Model):
# override sale_order_id and make it computed stored field instead of regular field.
sale_order_id = fields.Many2one(compute='_compute_sale_order_id', store=True, readonly=False,
domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]")
# content in related parameter of the field definition is removed to manually define the compute and the search method.
project_sale_order_id = fields.Many2one(compute='_compute_project_sale_order_id', search='_search_project_sale_order_id', related=None)
analytic_account_id = fields.Many2one('account.analytic.account', related='sale_order_id.analytic_account_id')
bill_type = fields.Selection(related="project_id.bill_type")
pricing_type = fields.Selection(related="project_id.pricing_type")
sale_line_id = fields.Many2one(domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), '|', ('project_id.bill_type', '=', 'customer_task'), ('order_id', '=?', project_sale_order_id)]")
is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty')
has_multi_sol = fields.Boolean(compute='_compute_has_multi_sol', compute_sudo=True)
allow_billable = fields.Boolean(related="project_id.allow_billable")
@@ -271,6 +272,14 @@ class ProjectTask(models.Model):
for task in self:
task.analytic_account_active = task.analytic_account_active or task.analytic_account_id.active
@api.depends('project_id.bill_type', 'project_id.sale_order_id')
def _compute_project_sale_order_id(self):
for task in self:
if task.bill_type != 'customer_task':
task.project_sale_order_id = task.project_id.sale_order_id
else:
task.project_sale_order_id = False
@api.depends('sale_line_id', 'project_id', 'allow_billable', 'non_allow_billable')
def _compute_sale_order_id(self):
for task in self:
@@ -309,6 +318,9 @@ class ProjectTask(models.Model):
if not self.sale_line_id:
self.sale_line_id = self.project_id.sale_line_id
def _search_project_sale_order_id(self, operator, value):
return [('bill_type', '!=', 'customer_task'), ('project_id.sale_order_id', operator, value)]
def write(self, values):
res = super(ProjectTask, self).write(values)
# Done after super to avoid constraints on field recomputation
@@ -101,9 +101,10 @@ class Project(models.Model):
'amount_untaxed_to_invoice': 'to_invoice',
'timesheet_cost': 'cost',
'expense_cost': 'expense_cost',
'expense_amount_untaxed_invoiced': 'expense_amount_untaxed_invoiced',
'expense_amount_untaxed_invoiced': 'expense_amount_untaxed_invoiced',
'expense_amount_untaxed_to_invoice': 'expense_amount_untaxed_to_invoice',
'other_revenues': 'other_revenues'
}
}
profit = dict.fromkeys(list(field_map.values()) + ['other_revenues', 'total'], 0.0)
profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', 'in', self.ids)], ['project_id'] + list(field_map), ['project_id'])
for data in profitability_raw_data:
@@ -132,7 +132,6 @@ class ProfitabilityAnalysis(models.Model):
SUM(timesheet_cost) AS timesheet_cost,
SUM(expense_cost) AS expense_cost,
SUM(other_revenues) AS other_revenues,
SUM(downpayment_invoiced) AS downpayment_invoiced,
SUM(expense_amount_untaxed_to_invoice) AS expense_amount_untaxed_to_invoice,
SUM(expense_amount_untaxed_invoiced) AS expense_amount_untaxed_invoiced,
SUM(amount_untaxed_to_invoice) AS amount_untaxed_to_invoice,
@@ -148,7 +147,6 @@ class ProfitabilityAnalysis(models.Model):
TS.amount AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
@@ -159,7 +157,7 @@ class ProfitabilityAnalysis(models.Model):
UNION ALL
-- Get the other revenues
-- Get the other revenues (products that are not services)
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
@@ -168,7 +166,6 @@ class ProfitabilityAnalysis(models.Model):
0.0 AS timesheet_cost,
AAL.amount AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
@@ -177,16 +174,12 @@ class ProfitabilityAnalysis(models.Model):
FROM project_project P
JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
JOIN account_analytic_line AAL ON AAL.account_id = AA.id
JOIN product_product PP ON PP.id = AAL.product_id
JOIN product_template PT ON PT.id = PP.product_tmpl_id
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = AAL.move_id
LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id
WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't'
AND P.allow_timesheets = 't'
AND PT.service_type = 'manual' -- default value or Milestone service for services products
AND PT.service_tracking = 'no' -- default value or not a tracking service for services products
AND (SOL.id IS NULL
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE))
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE AND SOL.is_service IS NOT TRUE))
UNION ALL
@@ -199,64 +192,32 @@ class ProfitabilityAnalysis(models.Model):
0.0 AS timesheet_cost,
0.0 AS other_revenues,
AAL.amount AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
AAL.date AS line_date
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
UNION ALL
-- Get the invoiced downpayments
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
MY_SOLS.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
MY_S.date_order AS line_date
FROM project_project P
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
WHERE MY_SOLS.is_downpayment = 't'
UNION ALL
-- Get the expense costs from sale order line
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
OLIS.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS other_revenues,
OLIS.price_reduce AS expense_cost,
0.0 AS downpayment_invoiced,
0.0 AS expense_amount_untaxed_to_invoice,
0.0 AS expense_amount_untaxed_invoiced,
0.0 AS amount_untaxed_to_invoice,
0.0 AS amount_untaxed_invoiced,
ANLI.date AS line_date
FROM project_project P
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
JOIN account_analytic_line AAL ON AAL.account_id = AA.id
LEFT JOIN account_move_line RINVL ON AAL.move_id = RINVL.id
AND RINVL.parent_state = 'posted'
AND RINVL.exclude_from_invoice_tab = 'f'
-- Check if the AAL is not related to a reversed credit note
LEFT JOIN account_move RINV ON RINV.id = RINVL.move_id
LEFT JOIN account_move_line INVL ON INVL.move_id = RINV.reversed_entry_id
AND INVL.parent_state = 'posted'
AND INVL.exclude_from_invoice_tab = 'f'
AND INVL.product_id = RINVL.product_id
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = INVL.id
LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id
AND SOL.product_id = AAL.product_id
-- Check if the AAL is not related to a consumed downpayment (when the SOL is fully invoiced - with downpayment discounted.)
LEFT JOIN sale_order_line_invoice_rel SOINVDOWN ON SOINVDOWN.invoice_line_id = RINVL.id
LEFT JOIN sale_order_line SOLDOWN on SOINVDOWN.order_line_id = SOLDOWN.id AND SOLDOWN.is_downpayment = 't'
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL
AND SOL.id IS NULL AND SOLDOWN.id IS NULL -- Not linked to a credit note and not a downpayment
AND P.active = 't' AND P.allow_timesheets = 't'
UNION ALL
@@ -270,17 +231,16 @@ class ProfitabilityAnalysis(models.Model):
0.0 AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced,
CASE
WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE 0.0
END AS expense_amount_untaxed_to_invoice,
CASE
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status != 'no'
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status = 'invoiced'
THEN
CASE
WHEN T.expense_policy = 'sales_price'
THEN (SOL.price_reduce / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) * SOL.qty_invoiced
THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE -AMOUNT_UNTAXED.expense_cost
END
ELSE 0.0
@@ -290,31 +250,80 @@ class ProfitabilityAnalysis(models.Model):
ELSE 0.0
END AS amount_untaxed_to_invoice,
CASE
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (COALESCE(SOL.untaxed_amount_invoiced, AMOUNT_UNTAXED.downpayment_invoiced) / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE 0.0
END AS amount_untaxed_invoiced,
S.date_order AS line_date
FROM project_project P
JOIN res_company C ON C.id = P.company_id
LEFT JOIN (
-- Gets SOL linked to timesheets
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
0.0 AS expense_cost
FROM account_analytic_line AAL, project_project P
WHERE AAL.project_id IS NOT NULL AND P.id = AAL.project_id AND P.active = 't'
GROUP BY P.id, AAL.so_line
UNION
-- Service SOL linked to a project task AND not yet timesheeted
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost
FROM sale_order_line SOL
JOIN project_task T ON T.sale_line_id = SOL.id
JOIN project_project P ON T.project_id = P.id
LEFT JOIN account_analytic_line AAL ON AAL.task_id = T.id
WHERE SOL.is_service = 't'
AND AAL.id IS NULL -- not timesheeted
AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, SOL.id
UNION
-- Service SOL linked to project AND not yet timesheeted
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost
FROM sale_order_line SOL
JOIN project_project P ON P.sale_line_id = SOL.id
LEFT JOIN account_analytic_line AAL ON AAL.project_id = P.id
LEFT JOIN project_task T ON T.sale_line_id = SOL.id
WHERE SOL.is_service = 't'
AND AAL.id IS NULL -- not timesheeted
AND (T.id IS NULL OR T.project_id != P.id) -- not linked to a task in this project
AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, SOL.id
UNION
-- Service SOL linked to analytic account AND not yet timesheeted
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost
FROM sale_order_line SOL
JOIN sale_order SO ON SO.id = SOL.order_id
JOIN account_analytic_account AA ON AA.id = SO.analytic_account_id
JOIN project_project P ON P.analytic_account_id = AA.id
LEFT JOIN project_project PSOL ON PSOL.sale_line_id = SOL.id
LEFT JOIN project_task TSOL ON TSOL.sale_line_id = SOL.id
LEFT JOIN account_analytic_line AAL ON AAL.so_line = SOL.id
WHERE SOL.is_service = 't'
AND AAL.id IS NULL -- not timesheeted
AND TSOL.id IS NULL -- not linked to a task
AND PSOL.id IS NULL -- not linked to a project
AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, SOL.id
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
0.0 AS expense_cost
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
@@ -325,8 +334,7 @@ class ProfitabilityAnalysis(models.Model):
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
SUM(AAL.amount) AS expense_cost,
0.0 AS downpayment_invoiced
SUM(AAL.amount) AS expense_cost
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
@@ -336,37 +344,31 @@ class ProfitabilityAnalysis(models.Model):
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
MY_SOLS.id AS sale_line_id,
0.0 AS expense_cost,
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced
SOLDOWN.id AS sale_line_id,
0.0 AS expense_cost
FROM project_project P
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
WHERE MY_SOLS.is_downpayment = 't'
GROUP BY P.id, MY_SOLS.id
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
OLIS.id AS sale_line_id,
OLIS.price_reduce AS expense_cost,
0.0 AS downpayment_invoiced
FROM project_project P
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, OLIS.id
LEFT JOIN sale_order_line SOL ON P.sale_line_id = SOL.id
LEFT JOIN sale_order SO ON SO.id = SOL.order_id OR SO.analytic_account_id = P.analytic_account_id
LEFT JOIN sale_order_line SOLDOWN ON SOLDOWN.order_id = SO.id AND SOLDOWN.is_downpayment = 't'
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.order_line_id = SOLDOWN.id
LEFT JOIN account_move_line INVL ON SOINV.invoice_line_id = INVL.id
AND INVL.parent_state = 'posted'
AND INVL.exclude_from_invoice_tab = 'f'
LEFT JOIN account_move RINV ON INVL.move_id = RINV.reversed_entry_id
LEFT JOIN account_move_line RINVL ON RINV.id = RINVL.move_id
AND RINVL.parent_state = 'posted'
AND RINVL.exclude_from_invoice_tab = 'f'
AND RINVL.product_id = SOLDOWN.product_id
LEFT JOIN account_analytic_line ANLI ON ANLI.move_id = RINVL.id AND ANLI.amount < 0.0
WHERE ANLI.id IS NULL -- there are no credit note for this downpayment
AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, SOLDOWN.id
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
0.0 AS expense_cost
FROM sale_order_line SOL
INNER JOIN project_project P ON SOL.project_id = P.id
WHERE P.active = 't' AND P.allow_timesheets = 't'
@@ -375,8 +377,7 @@ class ProfitabilityAnalysis(models.Model):
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS expense_cost,
0.0 AS downpayment_invoiced
0.0 AS expense_cost
FROM sale_order_line SOL
INNER JOIN project_task T ON SOL.task_id = T.id
INNER JOIN project_project P ON P.id = T.project_id
+1
View File
@@ -2,6 +2,7 @@
# # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import common
from . import common_reporting
from . import test_sale_timesheet
from . import test_sale_service
from . import test_project_billing
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.tools import float_is_zero, float_compare
from flectra.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
class TestCommonReporting(TestCommonSaleTimesheet):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# expense product
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'name': "Expense service",
'standard_price': 10,
'list_price': 20,
'type': 'service',
'invoice_policy': 'delivery',
'expense_policy': 'sales_price',
'default_code': 'EXP',
'service_type': 'manual',
'taxes_id': False,
'property_account_income_id': cls.company_data['default_account_revenue'].id,
})
# create Analytic Accounts
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
'name': 'Test AA 1',
'code': 'AA1',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
'name': 'Test AA 2',
'code': 'AA2',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
cls.analytic_account_3 = cls.env['account.analytic.account'].create({
'name': 'Test AA 3',
'code': 'AA3',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
# Sale orders each will create project and a task in a global project (one SO is 'delivered', the other is 'ordered')
# and a third one using fixed_price (which is 'delivered')
cls.sale_order_1 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_1.id,
})
cls.so_line_deliver_project = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet3.name,
'product_id': cls.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_delivery_timesheet3.uom_id.id,
'price_unit': cls.product_delivery_timesheet3.list_price,
'order_id': cls.sale_order_1.id,
})
cls.so_line_deliver_task = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet2.name,
'product_id': cls.product_delivery_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_delivery_timesheet2.uom_id.id,
'price_unit': cls.product_delivery_timesheet2.list_price,
'order_id': cls.sale_order_1.id,
})
cls.sale_order_2 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_2.id,
})
cls.so_line_order_project = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet3.name,
'product_id': cls.product_order_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_order_timesheet3.uom_id.id,
'price_unit': cls.product_order_timesheet3.list_price,
'order_id': cls.sale_order_2.id,
})
cls.so_line_order_task = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet2.name,
'product_id': cls.product_order_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_order_timesheet2.uom_id.id,
'price_unit': cls.product_order_timesheet2.list_price,
'order_id': cls.sale_order_2.id,
})
cls.sale_order_3 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_3.id,
})
cls.so_line_deliver_manual_project = cls.env['sale.order.line'].create({
'name': cls.product_delivery_manual3.name,
'product_id': cls.product_delivery_manual3.id,
'product_uom_qty': 11,
'product_uom': cls.product_delivery_manual3.uom_id.id,
'price_unit': cls.product_delivery_manual3.list_price,
'order_id': cls.sale_order_3.id,
})
def _log_timesheet_user(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_user.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
def _log_timesheet_manager(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_manager.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.addons.sale_timesheet.tests.test_reporting import TestReporting
from flectra.tools import float_compare
from flectra.addons.sale_timesheet.tests.common_reporting import TestCommonReporting
from flectra.tools import float_compare, float_is_zero
from flectra.tests import tagged
@tagged('-at_install', 'post_install')
class TestSaleProject(TestReporting):
class TestSaleProject(TestCommonReporting):
def test_project_overview_by_project(self):
rounding = self.env.company.currency_id.rounding
@@ -34,7 +34,6 @@ class TestSaleProject(TestReporting):
project_so = self.so_line_order_project.project_id
# log timesheet for billable time
timesheet1 = self._log_timesheet_manager(project_so, 10, so_line_deliver_global_project.task_id)
task_so = self.so_line_order_project.task_id
# logged some timesheets: on project only, then on tasks with different employees
timesheet2 = self._log_timesheet_user(project_so, 2)
@@ -89,7 +88,7 @@ class TestSaleProject(TestReporting):
})
other_revenues = self.env['account.analytic.line'].create({
'name': 'pther revenues on project_so',
'name': 'other revenues on project_so',
'account_id': project_so.analytic_account_id.id,
'employee_id': self.employee_user.id,
'unit_amount': 1,
@@ -103,10 +102,10 @@ class TestSaleProject(TestReporting):
dashboard_value = timesheet2.unit_amount + timesheet3.unit_amount + timesheet4.unit_amount + timesheet5.unit_amount + timesheet1.unit_amount
project_so_timesheet_sold_unit = timesheet3.unit_amount + timesheet4.unit_amount
project_rate_non_billable = timesheet5.unit_amount / dashboard_value * 100
project_rate_non_billable_project = timesheet2.unit_amount / dashboard_value * 100
project_rate_billable_time = timesheet1.unit_amount / dashboard_value * 100
project_rate_billable_fixed = project_so_timesheet_sold_unit / dashboard_value * 100
project_rate_non_billable = round(timesheet5.unit_amount / dashboard_value * 100, 2)
project_rate_non_billable_project = round(timesheet2.unit_amount / dashboard_value * 100, 2)
project_rate_billable_time = round(timesheet1.unit_amount / dashboard_value * 100, 2)
project_rate_billable_fixed = round(project_so_timesheet_sold_unit / dashboard_value * 100, 2)
project_rate_total = project_rate_non_billable + project_rate_non_billable_project + project_rate_billable_time + project_rate_billable_fixed
project_invoiced = self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty * timesheet1.unit_amount
project_timesheet_cost = timesheet2.amount + timesheet3.amount + timesheet4.amount + timesheet5.amount + timesheet1.amount
@@ -122,9 +121,11 @@ class TestSaleProject(TestReporting):
self.assertEqual(float_compare(vals['dashboard']['rates']['billable_time'], project_rate_billable_time, precision_rounding=rounding), 0, "The rate billable-time should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['rates']['billable_fixed'], project_rate_billable_fixed, precision_rounding=rounding), 0, "The rate billable-fixed should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['rates']['total'], project_rate_total, precision_rounding=rounding), 0, "The total rates should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['invoiced'], project_invoiced, precision_rounding=rounding), 0, "The amount invoiced should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['invoiced'], project_invoiced + project_other_revenues.price_total, precision_rounding=rounding), 0, "The amount invoiced should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['cost'], project_timesheet_cost, precision_rounding=rounding), 0, "The amount cost should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['expense_cost'], expense.amount, precision_rounding=rounding), 0, "The amount expense-cost should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['profit']['other_revenues'], other_revenues.amount + project_other_revenues.price_total, precision_rounding=rounding), 0, "The amount of the other revenues should be equal to the corresponding account move line and the one from the SO line")
self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + expense.amount + other_revenues.amount + project_other_revenues.price_total, precision_rounding=rounding), 0, "The total amount should be the sum of the SO2 line and the created other_revenues account analytic line")
self.assertEqual(float_compare(vals['dashboard']['profit']['other_revenues'], other_revenues.amount, precision_rounding=rounding), 0, "The amount of the other revenues should be equal to the corresponding account move line and the one from the SO line")
self.assertEqual(float_compare(vals['dashboard']['profit']['expense_amount_untaxed_to_invoice'], -expense.amount, precision_rounding=rounding), 0, "The amount of expense to re-invoice shoul be equal to the opposite of expense amount")
self.assertTrue(float_is_zero(vals['dashboard']['profit']['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The amount of expense re-invoiced should be equal to 0")
self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + other_revenues.amount + project_other_revenues.price_total, precision_rounding=rounding), 0, "The total amount should be the sum of the SO2 line and the created other_revenues account analytic line")
self.assertEqual(float_compare(vals['repartition_employee_max'], 11.0, precision_rounding=rounding), 0, "The amount of repartition-employee-max should be the one from SO2 line")
+552 -124
View File
@@ -1,115 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.osv import expression
from flectra.tools import float_is_zero, float_compare
from flectra.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
from flectra.addons.sale_timesheet.tests.common_reporting import TestCommonReporting
from flectra.tests import tagged
@tagged('-at_install', 'post_install')
class TestReporting(TestCommonSaleTimesheet):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# expense product
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'name': "Expense service",
'standard_price': 10,
'list_price': 20,
'type': 'service',
'invoice_policy': 'delivery',
'expense_policy': 'sales_price',
'default_code': 'EXP',
'service_type': 'manual',
'taxes_id': False,
'property_account_income_id': cls.company_data['default_account_revenue'].id,
})
# create Analytic Accounts
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
'name': 'Test AA 1',
'code': 'AA1',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
'name': 'Test AA 2',
'code': 'AA2',
'company_id': cls.company_data['company'].id,
'partner_id': cls.partner_a.id
})
# Sale orders each will create project and a task in a global project (one SO is 'delivered', the other is 'ordered')
cls.sale_order_1 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_1.id,
})
cls.so_line_deliver_project = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet3.name,
'product_id': cls.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_delivery_timesheet3.uom_id.id,
'price_unit': cls.product_delivery_timesheet3.list_price,
'order_id': cls.sale_order_1.id,
})
cls.so_line_deliver_task = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet2.name,
'product_id': cls.product_delivery_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_delivery_timesheet2.uom_id.id,
'price_unit': cls.product_delivery_timesheet2.list_price,
'order_id': cls.sale_order_1.id,
})
cls.sale_order_2 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'analytic_account_id': cls.analytic_account_2.id,
})
cls.so_line_order_project = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet3.name,
'product_id': cls.product_order_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_order_timesheet3.uom_id.id,
'price_unit': cls.product_order_timesheet3.list_price,
'order_id': cls.sale_order_2.id,
})
cls.so_line_order_task = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet2.name,
'product_id': cls.product_order_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_order_timesheet2.uom_id.id,
'price_unit': cls.product_order_timesheet2.list_price,
'order_id': cls.sale_order_2.id,
})
def _log_timesheet_user(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_user.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
def _log_timesheet_manager(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_manager.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
class TestReporting(TestCommonReporting):
def test_profitability_report(self):
@@ -125,31 +23,38 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# confirm sales orders
self.sale_order_1.action_confirm()
self.sale_order_2.action_confirm()
self.sale_order_3.action_confirm()
self.env['project.profitability.report'].flush()
project_so_1 = self.so_line_deliver_project.project_id
project_so_2 = self.so_line_order_project.project_id
project_so_3 = self.so_line_deliver_manual_project.project_id
task_so_1 = self.so_line_deliver_project.task_id
task_so_2 = self.so_line_order_project.task_id
task_so_3 = self.so_line_deliver_manual_project.task_id
task_in_global_1 = self.so_line_deliver_task.task_id
task_in_global_2 = self.so_line_order_task.task_id
# deliver project should not be impacted, as no timesheet are logged yet
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the global project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO1 should be 0.0")
# order project should be to invoice, but nothing has been delivered yet
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice should be the one from the SO line, as we are in ordered quantity")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO2 should be 0.0")
@@ -157,9 +62,21 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# global project now contain 2 tasks: 'deliver' one which has no effect (no timesheet yet), and the 'order' one which should update the 'to invoice' amount
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
# fixed price project should not be impacted, as no delivered quantity are logged yet
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project now contain 3 tasks: 'deliver' ones which have no effect (no timesheet or delivered yet), and the 'order' one which should update the 'to invoice' amount
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the global project should be 0.0")
self.assertEqual(float_compare(project_global_stat['amount_untaxed_to_invoice'], self.so_line_order_task.price_unit * self.so_line_order_task.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of global project should take the task in 'oredered qty' into account")
self.assertTrue(float_is_zero(project_global_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the global project should be 0.0")
@@ -167,21 +84,22 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# logged some timesheets: on project only, then on tasks with different employees
timesheet1 = self._log_timesheet_user(project_so_1, 2)
timesheet2 = self._log_timesheet_user(project_so_2, 2)
timesheet3 = self._log_timesheet_user(project_so_1, 3, task_so_1)
timesheet4 = self._log_timesheet_user(project_so_2, 3, task_so_2)
timesheet5 = self._log_timesheet_manager(project_so_1, 1, task_so_1)
timesheet6 = self._log_timesheet_manager(project_so_2, 1, task_so_2)
timesheet7 = self._log_timesheet_manager(self.project_global, 3, task_in_global_1)
timesheet8 = self._log_timesheet_manager(self.project_global, 3, task_in_global_2)
timesheet9 = self._log_timesheet_user(project_so_3, 4, task_so_3)
self.env['project.profitability.report'].flush()
# deliver project should now have cost and something to invoice
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
@@ -191,9 +109,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project still have something to invoice but has costs now
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
@@ -203,9 +122,23 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project should ow have cost and nothing to invoice as no delivered quantity are logged yet
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should have more to invoice, and should now have costs
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_to_invoice = (self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty) + (self.so_line_deliver_task.price_unit * timesheet7.unit_amount)
@@ -216,6 +149,63 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
self.so_line_deliver_manual_project.write({'qty_delivered': 7.0})
self.env['project.profitability.report'].flush()
# deliver project should now have cost and something to invoice
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_to_invoice'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The amount to invoice of the project from SO1 should only include timesheet linked to task")
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project still have something to invoice but has costs now
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice should be the one from the SO line, as we are in ordered quantity")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project should ow have cost and something to invoice as some delivered quantity are logged in
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should have more to invoice, and should now have costs
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_to_invoice = (self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty) + (self.so_line_deliver_task.price_unit * timesheet7.unit_amount)
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the global project should be 0.0")
self.assertEqual(float_compare(project_global_stat['amount_untaxed_to_invoice'], project_global_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
InvoiceWizard = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True)
@@ -236,7 +226,7 @@ class TestReporting(TestCommonSaleTimesheet):
self.env['project.profitability.report'].flush()
# deliver project should now have cost and something invoiced
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
@@ -247,9 +237,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project has still nothing invoiced
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
@@ -259,9 +250,23 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project has still nothing invoice
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should have more to invoice, and should now have costs
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_to_invoice = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty
@@ -273,6 +278,7 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# invoice the Sales Order SO2
context = {
@@ -291,7 +297,7 @@ class TestReporting(TestCommonSaleTimesheet):
self.env['project.profitability.report'].flush()
# deliver project should not be impacted by the invoice of the other SO
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
@@ -301,9 +307,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project is now totally invoiced, as we are in ordered qty
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
@@ -313,9 +320,23 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project has still nothing invoice
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should be totally invoiced
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
@@ -326,6 +347,76 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# invoice the Sales Order SO3
context = {
"active_model": 'sale.order',
"active_ids": [self.sale_order_3.id],
"active_id": self.sale_order_3.id,
'open_invoices': True,
}
payment = InvoiceWizard.create({
'advance_payment_method': 'delivered',
})
action_invoice = payment.with_context(context).create_invoices()
invoice_id = action_invoice['res_id']
invoice_3 = self.env['account.move'].browse(invoice_id)
invoice_3.action_post()
self.env['project.profitability.report'].flush()
# deliver project should not be impacted by the invoice of the other SO
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project should not be impacted by the invoice of the other SO
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice should be the one 0.0, as all ordered quantity is invoiced")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# fixed price project is now partially invoiced, as we are in delivered qty
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_invoiced'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_delivered, precision_rounding=rounding), 0, "The invoiced amount of the project from SO3 should be the delivered quantity * the unit price")
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project should be totally invoiced
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
self.assertEqual(float_compare(project_global_stat['amount_untaxed_invoiced'], project_global_invoiced, precision_rounding=rounding), 0, "The invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# simulate the auto creation of the SO line for expense, like we confirm a vendor bill.
so_line_expense = self.env['sale.order.line'].create({
@@ -362,7 +453,7 @@ class TestReporting(TestCommonSaleTimesheet):
self.env['project.profitability.report'].flush()
# deliver project should now have expense cost, and expense to reinvoice as there is a still open sales order linked to the AA1
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
@@ -372,9 +463,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertEqual(float_compare(project_so_1_stat['expense_amount_untaxed_to_invoice'], -1 * expense1.amount, precision_rounding=rounding), 0, "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0, "The expense cost of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project is not impacted by the expenses
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
@@ -384,9 +476,10 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
# global project should have an expense, but not reinvoiceable
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
@@ -397,3 +490,338 @@ class TestReporting(TestCommonSaleTimesheet):
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_global_stat['expense_cost'], expense2.amount, precision_rounding=rounding), 0, "The expense cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
def test_reversed_downpayment(self):
# this test suppose everything is in the same currency as the current one
currency = self.env.company.currency_id
rounding = currency.rounding
self.sale_order_2.action_confirm()
context = {
'active_model': 'sale.order',
'active_ids': self.sale_order_2.ids,
'active_id': self.sale_order_2.id,
'default_journal_id': self.company_data['default_journal_sale'].id,
'open_invoices': True,
}
# Let's do an invoice for a deposit of 100
downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({
'advance_payment_method': 'percentage',
'amount': 10,
})
action_invoice = downpayment.create_invoices()
invoice_id = action_invoice['res_id']
invoice_downpayment = self.env['account.move'].browse(invoice_id)
invoice_downpayment.action_post()
posted_invoice_res_ids = [invoice_id]
downpayment2 = self.env['sale.advance.payment.inv'].with_context(context).create({
'advance_payment_method': 'percentage',
'amount': 25,
})
action_invoice = downpayment2.create_invoices()
invoice_downpayment2 = self.env['account.move'].search(expression.AND([action_invoice['domain'], [('id', 'not in', posted_invoice_res_ids)]]))
invoice_downpayment2.action_post()
posted_invoice_res_ids += invoice_downpayment2.ids
milestone_to_invoice = self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice
timesheets_to_invoice = self.so_line_order_task.price_unit * self.so_line_order_task.qty_to_invoice
total_product_price = milestone_to_invoice + timesheets_to_invoice
credit_note_wizard = self.env['account.move.reversal'].with_context({
'active_ids': invoice_downpayment2.ids,
'active_id': invoice_downpayment2.id,
'active_model': 'account.move'
}).create({
'refund_method': 'refund',
'reason': 'reason test create',
})
action_moves = credit_note_wizard.reverse_moves()
credit_id = action_moves['res_id']
invoice_credit = self.env['account.move'].browse(credit_id)
invoice_credit.action_post()
posted_invoice_res_ids += invoice_credit.ids
project_so_2 = self.so_line_order_project.project_id
task_so_2 = self.so_line_order_project.task_id
task_in_global_2 = self.so_line_order_task.task_id
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
"The invoiced amount is the amount of downpayments not reversed")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
"The amount to invoice is the milestone product minus the downpayment not reversed")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
"The timesheet unit amount of the project from SO2 is 0")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
"The timesheet cost of the project from SO2 is 0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
# logged some timesheets: on project only, then on tasks with different employees
timesheet2 = self._log_timesheet_user(project_so_2, 2)
timesheet4 = self._log_timesheet_user(project_so_2, 3, task_so_2)
timesheet6 = self._log_timesheet_manager(project_so_2, 2, task_so_2)
timesheet8 = self._log_timesheet_manager(self.project_global, 3, task_in_global_2)
# simulate the auto creation of the SO line for expense, like we confirm a vendor bill.
so_line_expense = self.env['sale.order.line'].create({
'name': self.product_expense.name,
'product_id': self.product_expense.id,
'product_uom_qty': 0.0,
'product_uom': self.product_expense.uom_id.id,
'price_unit': self.product_expense.list_price, # reinvoice at sales price
'order_id': self.sale_order_2.id,
'is_expense': True,
})
expense1 = self.env['account.analytic.line'].create({
'name': 'expense on project_so_2',
'account_id': project_so_2.analytic_account_id.id,
'so_line': so_line_expense.id,
'employee_id': self.employee_user.id,
'unit_amount': 4,
'amount': 4 * self.product_expense.list_price * -1,
'product_id': self.product_expense.id,
'product_uom_id': self.product_expense.uom_id.id,
})
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_to_invoice'], -expense1.amount, precision_rounding=rounding), 0,
"The expense cost to reinvoice of the project from SO2 should be the expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
"The expense cost of the project from SO1 should be expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
# invoice the Sales Order SO2
context = {
"active_model": 'sale.order',
"active_ids": self.sale_order_2.ids,
"active_id": self.sale_order_2.id,
'open_invoices': True,
'mail_notrack': True,
}
payment = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True).create({
'advance_payment_method': 'delivered',
})
action_invoice = payment.with_context(context).create_invoices()
invoice_payment = self.env['account.move'].search(expression.AND([action_invoice['domain'], [('id', 'not in', posted_invoice_res_ids)]]))
invoice_payment.action_post()
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], milestone_to_invoice, precision_rounding=rounding), 0,
"The invoiced amount of the project from SO2 should only include timesheet linked to task")
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding),
"The amount to invoice of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense to invoice amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_invoiced'], -1 * expense1.amount, precision_rounding=rounding), 0,
"The expense cost reinvoiced of the project from SO2 should be the expense amount")
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
"The expense cost of the project from SO2 should be the expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
credit_note_wizard = self.env['account.move.reversal'].with_context({
'active_ids': invoice_payment.ids,
'active_id': invoice_payment.id,
'active_model': 'account.move'
}).create({
'refund_method': 'refund',
'reason': 'reason test create',
})
action_moves = credit_note_wizard.reverse_moves()
credit_id = action_moves['res_id']
invoice_credit = self.env['account.move'].browse(credit_id)
invoice_credit.action_post()
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_to_invoice'], -expense1.amount, precision_rounding=rounding), 0,
"The expense cost to reinvoice of the project from SO2 should be the expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
"The expense cost of the project from SO1 should be expense amount")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
def test_milestone_no_tracking(self):
# this test suppose everything is in the same currency as the current one
currency = self.env.company.currency_id
rounding = currency.rounding
so_line_deliver_no_task = self.env['sale.order.line'].create({
'name': self.product_delivery_manual1.name,
'product_id': self.product_delivery_manual1.id,
'product_uom_qty': 50,
'product_uom': self.product_delivery_manual1.uom_id.id,
'price_unit': self.product_delivery_manual1.list_price,
'order_id': self.sale_order_2.id,
})
so_line_deliver_no_task.write({'qty_delivered': 1.0})
self.sale_order_2.action_confirm()
milestone_to_invoice = self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice
milestone_no_task = so_line_deliver_no_task.price_unit * so_line_deliver_no_task.qty_to_invoice
self.env['project.profitability.report'].flush()
project_so_2 = self.so_line_order_project.project_id
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding),
"The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
"The timesheet unit amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
"The timesheet cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
task_using_milestone_not_tracked = self.env['project.task'].create({
'name': 'Task with milestone not tracked',
'project_id': project_so_2.id,
'partner_id': project_so_2.partner_id.id,
'sale_line_id': so_line_deliver_no_task.id,
})
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding),
"The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice linked to the project or project's task")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
"The timesheet unit amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
"The timesheet cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
timesheet = self._log_timesheet_user(project_so_2, 3, task_using_milestone_not_tracked)
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
project_so_2_timesheet_cost = timesheet.amount
project_so_2_timesheet_sold_unit = timesheet.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding),
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
# invoice the Sales Order SO2
context = {
"active_model": 'sale.order',
"active_ids": self.sale_order_2.ids,
"active_id": self.sale_order_2.id,
'open_invoices': True,
'mail_notrack': True,
}
payment = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True).create({
'advance_payment_method': 'delivered',
})
action_invoice = payment.with_context(context).create_invoices()
invoice_payment = self.env['account.move'].browse(action_invoice['res_id'])
invoice_payment.action_post()
self.env['project.profitability.report'].flush()
project_so_2_stat = self.env['project.profitability.report'].read_group(
[('project_id', 'in', project_so_2.ids)],
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
['project_id']
)[0]
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The invoiced amount of the project from SO2 should include the milestone invoiced")
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding),
"The amount to invoice of the project from SO2 should include the milestone to invoice")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
"The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
"The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
"The expense invoiced amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
"The expense cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
"The other revenues of the project from SO2 should be 0.0")
@@ -241,6 +241,14 @@
Re-invoiced costs
</td>
</tr>
<tr t-if="display_cost &amp; (dashboard['profit']['expense_amount_untaxed_to_invoice'] != 0)">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['expense_amount_untaxed_to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td title="Costs from expenses that still need to be reinvoiced to your customer (provided that the Analytic Account of the Project was set on the Expense).">
To re-invoice costs
</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_total">
<b>
+8 -8
View File
@@ -1270,17 +1270,16 @@ class StockMove(models.Model):
# Find a candidate move line to update or create a new one.
for reserved_quant, quantity in quants:
to_update = self.move_line_ids.filtered(lambda ml: ml._reservation_is_updatable(quantity, reserved_quant))
to_update = next((line for line in self.move_line_ids if line._reservation_is_updatable(quantity, reserved_quant)), False)
if to_update:
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, to_update.product_uom_id, rounding_method='HALF-UP')
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
uom_quantity_back_to_product_uom = to_update[0].product_uom_id._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
uom_quantity_back_to_product_uom = to_update.product_uom_id._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
if to_update and float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += uom_quantity
to_update.with_context(bypass_reservation_update=True).product_uom_qty += uom_quantity
else:
if self.product_id.tracking == 'serial':
for i in range(0, int(quantity)):
self.env['stock.move.line'].create(self._prepare_move_line_vals(quantity=1, reserved_quant=reserved_quant))
self.env['stock.move.line'].create([self._prepare_move_line_vals(quantity=1, reserved_quant=reserved_quant) for i in range(int(quantity))])
else:
self.env['stock.move.line'].create(self._prepare_move_line_vals(quantity=quantity, reserved_quant=reserved_quant))
return taken_quantity
@@ -1652,7 +1651,7 @@ class StockMove(models.Model):
else:
moves_state_to_write['confirmed'].add(move.id)
for state, moves_ids in moves_state_to_write.items():
self.browse(moves_ids).write({'state': state})
self.browse(moves_ids).filtered(lambda m: m.state != state).state = state
@api.model
def _consuming_picking_types(self):
@@ -1818,5 +1817,6 @@ class StockMove(models.Model):
for move in self:
domains.append([('product_id', '=', move.product_id.id), ('location_id', '=', move.location_dest_id.id)])
static_domain = [('state', 'in', ['confirmed', 'partially_available']), ('procure_method', '=', 'make_to_stock')]
moves_to_reserve = self.env['stock.move'].search(expression.AND([static_domain, expression.OR(domains)]))
moves_to_reserve = self.env['stock.move'].search(expression.AND([static_domain, expression.OR(domains)]),
order='priority desc, date asc, id asc')
moves_to_reserve._action_assign()
+1 -1
View File
@@ -77,7 +77,7 @@ class ProductionLot(models.Model):
@api.model_create_multi
def create(self, vals_list):
self._check_create()
return super(ProductionLot, self).create(vals_list)
return super(ProductionLot, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
def write(self, vals):
if 'company_id' in vals:
+1 -1
View File
@@ -521,7 +521,7 @@ class ProcurementGroup(models.Model):
# Search all confirmed stock_moves and try to assign them
domain = self._get_moves_to_assign_domain(company_id)
moves_to_assign = self.env['stock.move'].search(domain, limit=None,
order='priority desc, date asc')
order='priority desc, date asc, id asc')
for moves_chunk in split_every(100, moves_to_assign.ids):
self.env['stock.move'].browse(moves_chunk).sudo()._action_assign()
if use_new_cursor:
+47
View File
@@ -2092,3 +2092,50 @@ class TestStockFlow(TestStockCommon):
# Validates the two receipts => It should raise an error as there is duplicate SN.
with self.assertRaises(ValidationError):
(receipt_1 | receipt_2).button_validate()
def test_assign_qty_to_first_move(self):
""" Suppose two out picking waiting for an available quantity. When receiving such
a quantity, the latter should be assign to the picking with the highest priority
and the earliest scheduled date. """
def create_picking(type, from_loc, to_loc, sequence=10, delay=0):
picking = self.PickingObj.create({
'picking_type_id': type,
'location_id': from_loc,
'location_dest_id': to_loc,
})
self.MoveObj.create({
'name': self.productA.name,
'sequence': sequence,
'date': fields.Datetime.add(fields.Datetime.now(), second=delay),
'product_id': self.productA.id,
'product_uom_qty': 1,
'product_uom': self.productA.uom_id.id,
'picking_id': picking.id,
'location_id': from_loc,
'location_dest_id': to_loc,
})
picking.action_confirm()
return picking
def validate_picking(picking):
res_dict = picking.button_validate()
wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
wizard.process()
out01 = create_picking(self.picking_type_out, self.stock_location, self.customer_location)
out02 = create_picking(self.picking_type_out, self.stock_location, self.customer_location, sequence=2, delay=1)
in01 = create_picking(self.picking_type_in, self.supplier_location, self.stock_location, delay=2)
validate_picking(in01)
self.assertEqual(out01.state, 'assigned')
self.assertEqual(out02.state, 'confirmed')
validate_picking(out01)
out03 = create_picking(self.picking_type_out, self.stock_location, self.customer_location, delay=3)
out03.priority = "1"
in02 = create_picking(self.picking_type_in, self.supplier_location, self.stock_location, delay=4)
validate_picking(in02)
self.assertEqual(out02.state, 'confirmed')
self.assertEqual(out03.state, 'assigned')
+1 -1
View File
@@ -460,7 +460,7 @@
<field name="name" string="Transfer" filter_domain="['|', ('name', 'ilike', self), ('origin', 'ilike', self)]"/>
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"/>
<field name="origin"/>
<field name="product_id"/>
<field name="product_id" filter_domain="['|', '|', ('product_id.default_code', 'ilike', self), ('product_id.name', 'ilike', self), ('product_id.barcode', 'ilike', self)]"/>
<field name="picking_type_id"/>
<filter name="my_transfers" string="My Transfers" domain="[('user_id', '=', uid)]"/>
<filter string="Starred" name="starred" domain="[('priority', '=', '1')]"/>
@@ -1660,8 +1660,11 @@ var SnippetsMenu = Widget.extend({
* when the DOM elements must be in an editable environment to be
* considered (@see noCheck), this is true if the DOM elements'
* parent must also be in an editable environment to be considered.
* @param {string} excludeParent
* jQuery selector that the parents of DOM elements must *not* match
* to be considered as potential snippet.
*/
_computeSelectorFunctions: function (selector, exclude, target, noCheck, isChildren) {
_computeSelectorFunctions: function (selector, exclude, target, noCheck, isChildren, excludeParent) {
var self = this;
exclude += `${exclude && ', '}.o_snippet_not_selectable`;
@@ -1675,6 +1678,12 @@ var SnippetsMenu = Widget.extend({
return oldFilter.apply(this) && $(this).find(target).length !== 0;
};
}
if (excludeParent) {
const oldFilter = filterFunc;
filterFunc = function () {
return oldFilter.apply(this) && !$(this).parent().is(excludeParent);
};
}
// Prepare the functions
var functions = {
@@ -1735,10 +1744,12 @@ var SnippetsMenu = Widget.extend({
this.templateOptions = [];
var selectors = [];
var $styles = $html.find('[data-selector]');
const snippetAdditionDropIn = $styles.filter('#so_snippet_addition').data('drop-in');
$styles.each(function () {
var $style = $(this);
var selector = $style.data('selector');
var exclude = $style.data('exclude') || '';
const excludeParent = $style.attr('id') === "so_content_addition" ? snippetAdditionDropIn : '';
var target = $style.data('target');
var noCheck = $style.data('no-check');
var optionID = $style.data('js') || $style.data('option-name'); // used in tour js as selector
@@ -1749,7 +1760,7 @@ var SnippetsMenu = Widget.extend({
'base_target': target,
'selector': self._computeSelectorFunctions(selector, exclude, target, noCheck),
'$el': $style,
'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', false, noCheck, true),
'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', false, noCheck, true, excludeParent),
'drop-in': $style.data('drop-in') && self._computeSelectorFunctions($style.data('drop-in'), '', false, noCheck),
'data': _.extend({string: $style.attr('string')}, $style.data()),
};
+1 -1
View File
@@ -173,7 +173,7 @@ class Slide(models.Model):
slide_resource_ids = fields.One2many('slide.slide.resource', 'slide_id', string="Additional Resource for this slide")
slide_resource_downloadable = fields.Boolean('Allow Download', default=True, help="Allow the user to download the content of the slide.")
mime_type = fields.Char('Mime-type')
html_content = fields.Html("HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True, sanitize_form=False)
html_content = fields.Html("HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True, sanitize_attributes=False, sanitize_form=False)
# website
website_id = fields.Many2one(related='channel_id.website_id', readonly=True)
date_published = fields.Datetime('Publish Date', readonly=True, tracking=1)