From 7f93f72a4f874e7073c0fec8ab0a89ebbd13e029 Mon Sep 17 00:00:00 2001 From: Julien Van Roy <juvr@odoo.com> Date: Fri, 25 Nov 2022 09:03:07 +0000 Subject: [PATCH] [FIX] {l10n_}account_edi_ubl_cii{_tests}: handle price_include taxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Factur-X, there is no way to represent a tax "price_include" because every amounts should be tax excluded. Currently in Factur-X, a line with a tax price_include = True will be incorrectly exported. Indeed, the Factur-X.xml is generated by setting the GrossPriceProduct as the price_unit. In Factur-X, this amount (and the others in the line details) should be tax excluded. Thus, it's wrong to set the GrossPriceProduct as the price_unit. To fix this, the GrossPriceProduct should be the price_unit if no tax price_include = True is set, otherwise, the gross price = price_unit/(1+tax/100). This way, the Factur-X file will be consistent with the norm. Note that the import of a Factur-X xml will thus try to pick taxes with price_include = False, and the price_unit will be tax excluded. If no matching tax with price_include = False is retrieved, a tax with price_include = True is searched, if found, the price_unit is recomputed accordingly. In both cases, the lines subtotals are the same. opw-3032382 closes odoo/odoo#106563 X-original-commit: 9a5ba15cb308a55cb9bf6f885b72c12eaf647170 Signed-off-by: William André (wan) <wan@odoo.com> Signed-off-by: Julien Van Roy <juvr@odoo.com> --- .../data/cii_22_templates.xml | 18 +- .../models/account_edi_common.py | 65 ++++- .../models/account_edi_xml_cii_facturx.py | 26 +- .../models/account_edi_xml_ubl_20.py | 25 +- .../tests/common.py | 11 + .../from_odoo/facturx_out_invoice.xml | 7 - .../facturx_out_invoice_tax_incl.xml | 239 ++++++++++++++++++ .../from_odoo/facturx_out_refund.xml | 7 - .../tests/test_xml_cii_fr.py | 118 +++++++++ .../tests/test_xml_cii_us.py | 6 + 10 files changed, 439 insertions(+), 83 deletions(-) create mode 100644 addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice_tax_incl.xml diff --git a/addons/account_edi_ubl_cii/data/cii_22_templates.xml b/addons/account_edi_ubl_cii/data/cii_22_templates.xml index 768dab9c8420..2bc3c38a11cc 100644 --- a/addons/account_edi_ubl_cii/data/cii_22_templates.xml +++ b/addons/account_edi_ubl_cii/data/cii_22_templates.xml @@ -31,9 +31,12 @@ <!-- Amounts. --> <ram:SpecifiedLineTradeAgreement> - <!-- Line information: unit_price (discount is below the taxes) --> + <!-- Line information: unit_price + NB: the gross_price_unit should be the price tax excluded ! + if price_unit = 100 and tax 10% price_include -> then the gross_price_unit is 91 + --> <ram:GrossPriceProductTradePrice> - <ram:ChargeAmount t-out="format_monetary(line.price_unit, 2)"/> + <ram:ChargeAmount t-out="format_monetary(line_vals['gross_price_total_unit'], 2)"/> <!-- Discount. --> <ram:AppliedTradeAllowanceCharge t-if="line.discount"> <ram:ChargeIndicator> @@ -66,17 +69,6 @@ </ram:ApplicableTradeTax> </t> - <!-- Discount. --> - <!-- A discount in percent can only be on the whole document (CalculationPercent), not on a line... --> - <ram:SpecifiedTradeAllowanceCharge t-if="line.discount"> - <ram:ChargeIndicator> - <udt:Indicator>false</udt:Indicator> - </ram:ChargeIndicator> - <ram:ActualAmount t-out="format_monetary(line.price_unit * line.discount/100, 2)"/> - <!-- https://unece.org/fileadmin/DAM/trade/untdid/d16b/tred/tred5189.htm --> - <ram:ReasonCode>95</ram:ReasonCode> - </ram:SpecifiedTradeAllowanceCharge> - <!-- Subtotal. --> <ram:SpecifiedTradeSettlementLineMonetarySummation> <ram:LineTotalAmount t-out="format_monetary(line.price_subtotal, 2)"/> diff --git a/addons/account_edi_ubl_cii/models/account_edi_common.py b/addons/account_edi_ubl_cii/models/account_edi_common.py index 0b304296e0c6..46e49f1283a4 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_common.py +++ b/addons/account_edi_ubl_cii/models/account_edi_common.py @@ -416,7 +416,7 @@ class AccountEdiCommon(models.AbstractModel): with (UBL | CII): * net_unit_price = 'Price/PriceAmount' | 'NetPriceProductTradePrice' (mandatory) (BT-146) - * gross_unit_price = 'GrossPriceProductTradePrice' | 'GrossPriceProductTradePrice' (optional) (BT-148) + * gross_unit_price = 'Price/AllowanceCharge/BaseAmount' | 'GrossPriceProductTradePrice' (optional) (BT-148) * basis_qty = 'Price/BaseQuantity' | 'BasisQuantity' (optional, either below net_price node or gross_price node) (BT-149) * billed_qty = 'InvoicedQuantity' | 'BilledQuantity' (mandatory) (BT-129) @@ -458,6 +458,12 @@ class AccountEdiCommon(models.AbstractModel): } :params: invoice_line :params: qty_factor + :returns: { + 'quantity': float, + 'product_uom_id': (optional) uom.uom, + 'price_unit': float, + 'discount': float, + } """ # basis_qty (optional) basis_qty = 1 @@ -479,10 +485,9 @@ class AccountEdiCommon(models.AbstractModel): rebate_node = tree.find(xpath_dict['rebate']) net_price_unit_node = tree.find(xpath_dict['net_price_unit']) if rebate_node is not None: - if net_price_unit_node is not None and gross_price_unit_node is not None: - rebate = float(gross_price_unit_node.text) - float(net_price_unit_node.text) - else: - rebate = float(rebate_node.text) + rebate = float(rebate_node.text) + elif net_price_unit_node is not None and gross_price_unit_node is not None: + rebate = float(gross_price_unit_node.text) - float(net_price_unit_node.text) # net_price_unit (mandatory) net_price_unit = None @@ -527,9 +532,7 @@ class AccountEdiCommon(models.AbstractModel): #################################################### # quantity - invoice_line.quantity = billed_qty * qty_factor - if product_uom_id is not None: - invoice_line.product_uom_id = product_uom_id + quantity = billed_qty * qty_factor # price_unit if gross_price_unit is not None: @@ -538,17 +541,57 @@ class AccountEdiCommon(models.AbstractModel): price_unit = (net_price_unit + rebate) / basis_qty else: raise UserError(_("No gross price nor net price found for line in xml")) - invoice_line.price_unit = price_unit # discount + discount = 0 if billed_qty * price_unit != 0 and price_subtotal is not None: - invoice_line.discount = 100 * (1 - price_subtotal / (billed_qty * price_unit)) + discount = 100 * (1 - price_subtotal / (billed_qty * price_unit)) # Sometimes, the xml received is very bad: unit price = 0, qty = 1, but price_subtotal = -200 # for instance, when filling a down payment as an invoice line. The equation in the docstring is not # respected, and the result will not be correct, so we just follow the simple rule below: if net_price_unit == 0 and price_subtotal != net_price_unit * (billed_qty / basis_qty) - allow_charge_amount: - invoice_line.price_unit = price_subtotal / billed_qty + price_unit = price_subtotal / billed_qty + + return { + 'quantity': quantity, + 'price_unit': price_unit, + 'discount': discount, + 'product_uom_id': product_uom_id, + } + + def _import_fill_invoice_line_taxes(self, journal, tax_nodes, invoice_line_form, inv_line_vals, logs): + # Taxes: all amounts are tax excluded, so first try to fetch price_include=False taxes, + # if no results, try to fetch the price_include=True taxes. If results, need to adapt the price_unit. + inv_line_vals['taxes'] = [] + for tax_node in tax_nodes: + amount = float(tax_node.text) + domain = [ + ('company_id', '=', journal.company_id.id), + ('amount_type', '=', 'percent'), + ('type_tax_use', '=', journal.type), + ('amount', '=', amount), + ] + tax_excl = self.env['account.tax'].search(domain + [('price_include', '=', False)], limit=1) + tax_incl = self.env['account.tax'].search(domain + [('price_include', '=', True)], limit=1) + if tax_excl: + inv_line_vals['taxes'].append(tax_excl.id) + elif tax_incl: + inv_line_vals['taxes'].append(tax_incl.id) + inv_line_vals['price_unit'] *= (1 + tax_incl.amount / 100) + else: + logs.append(_("Could not retrieve the tax: %s %% for line '%s'.", amount, invoice_line_form.name)) + # Set the values on the line_form + invoice_line_form.quantity = inv_line_vals['quantity'] + if inv_line_vals.get('product_uom_id'): + invoice_line_form.product_uom_id = inv_line_vals['product_uom_id'] + else: + logs.append( + _("Could not retrieve the unit of measure for line with label '%s'.", invoice_line_form.name)) + invoice_line_form.price_unit = inv_line_vals['price_unit'] + invoice_line_form.discount = inv_line_vals['discount'] + invoice_line_form.tax_ids = inv_line_vals['taxes'] + return logs # ------------------------------------------------------------------------- # Check xml using the free API from Ph. Helger, don't abuse it ! diff --git a/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py b/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py index 99cb536eb087..7544ba5a1530 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py +++ b/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py @@ -332,30 +332,10 @@ class AccountEdiXmlCII(models.AbstractModel): 'allowance_charge_amount': './{*}ActualAmount', # below allowance_charge node 'line_total_amount': './{*}SpecifiedLineTradeSettlement/{*}SpecifiedTradeSettlementLineMonetarySummation/{*}LineTotalAmount', } - self._import_fill_invoice_line_values(tree, xpath_dict, invoice_line, qty_factor) - - if not invoice_line.product_uom_id: - logs.append( - _("Could not retrieve the unit of measure for line with label '%s'. Did you install the inventory " - "app and enabled the 'Units of Measure' option ?", invoice_line.name)) - - # Taxes - taxes = self.env['account.tax'] + inv_line_vals = self._import_fill_invoice_line_values(tree, xpath_dict, invoice_line, qty_factor) + # retrieve tax nodes tax_nodes = tree.findall('.//{*}ApplicableTradeTax/{*}RateApplicablePercent') - for tax_node in tax_nodes: - tax = self.env['account.tax'].search([ - ('company_id', '=', journal.company_id.id), - ('amount', '=', float(tax_node.text)), - ('amount_type', '=', 'percent'), - ('type_tax_use', '=', journal.type), - ], limit=1) - if tax: - taxes += tax - else: - logs.append(_("Could not retrieve the tax: %s %% for line '%s'.", float(tax_node.text), invoice_line.name)) - - invoice_line.tax_ids = taxes - return logs + return self._import_fill_invoice_line_taxes(journal, tax_nodes, invoice_line, inv_line_vals, logs) # ------------------------------------------------------------------------- # IMPORT : helpers diff --git a/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py b/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py index e4e7bb7c1bec..cb227c7860b7 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py +++ b/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py @@ -586,33 +586,14 @@ class AccountEdiXmlUBL20(models.AbstractModel): } self._import_fill_invoice_line_values(tree, xpath_dict, invoice_line, qty_factor) - if not invoice_line.product_uom_id: - logs.append( - _("Could not retrieve the unit of measure for line with label '%s'. Did you install the inventory " - "app and enabled the 'Units of Measure' option ?", invoice_line.name)) - # Taxes - taxes = self.env['account.tax'] - + inv_line_vals = self._import_fill_invoice_line_values(tree, xpath_dict, invoice_line, qty_factor) + # retrieve tax nodes tax_nodes = tree.findall('.//{*}Item/{*}ClassifiedTaxCategory/{*}Percent') if not tax_nodes: for elem in tree.findall('.//{*}TaxTotal'): tax_nodes += elem.findall('.//{*}TaxSubtotal/{*}Percent') - - for tax_node in tax_nodes: - tax = self.env['account.tax'].search([ - ('company_id', '=', journal.company_id.id), - ('amount', '=', float(tax_node.text)), - ('amount_type', '=', 'percent'), - ('type_tax_use', '=', journal.type), - ], limit=1) - if tax: - taxes += tax - else: - logs.append(_("Could not retrieve the tax: %s %% for line '%s'.", float(tax_node.text), invoice_line.name)) - - invoice_line.tax_ids = taxes - return logs + return self._import_fill_invoice_line_taxes(journal, tax_nodes, invoice_line, inv_line_vals, logs) # ------------------------------------------------------------------------- # IMPORT : helpers diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/common.py b/addons/l10n_account_edi_ubl_cii_tests/tests/common.py index ec7d36be2eca..79b50aaff32d 100644 --- a/addons/l10n_account_edi_ubl_cii_tests/tests/common.py +++ b/addons/l10n_account_edi_ubl_cii_tests/tests/common.py @@ -82,6 +82,7 @@ class TestUBLCommon(AccountEdiTestCommon): self.assert_same_invoice(invoice, new_invoice) def _assert_imported_invoice_from_file(self, subfolder, filename, amount_total, amount_tax, list_line_subtotals, + list_line_price_unit=None, list_line_discount=None, list_line_taxes=None, move_type='in_invoice', currency_id=None): """ Create an empty account.move, update the file to fill its fields, asserts the currency, total and tax amounts @@ -114,6 +115,16 @@ class TestUBLCommon(AccountEdiTestCommon): Counter(invoice.invoice_line_ids.mapped('price_subtotal')), Counter(list_line_subtotals), ) + if list_line_price_unit: + self.assertEqual(invoice.invoice_line_ids.mapped('price_unit'), list_line_price_unit) + if list_line_discount: + # See test_import_tax_included: sometimes, it's impossible to retrieve the exact discount at import because + # of rounding during export. The obtained discount might be 10.001 while the expected is 10. + dp = self.env.ref('product.decimal_discount').precision_get("Discount") + self.assertEqual([round(d, dp) for d in invoice.invoice_line_ids.mapped('discount')], list_line_discount) + if list_line_taxes: + for line, taxes in zip(invoice.invoice_line_ids, list_line_taxes): + self.assertEqual(line.tax_ids, taxes) # ------------------------------------------------------------------------- # EXPORT HELPERS diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice.xml b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice.xml index 8c7f1a242ba6..7fa151c8f3e6 100644 --- a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice.xml +++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice.xml @@ -45,13 +45,6 @@ <ram:CategoryCode>S</ram:CategoryCode> <ram:RateApplicablePercent>21.0</ram:RateApplicablePercent> </ram:ApplicableTradeTax> - <ram:SpecifiedTradeAllowanceCharge> - <ram:ChargeIndicator> - <udt:Indicator>false</udt:Indicator> - </ram:ChargeIndicator> - <ram:ActualAmount>99.00</ram:ActualAmount> - <ram:ReasonCode>95</ram:ReasonCode> - </ram:SpecifiedTradeAllowanceCharge> <ram:SpecifiedTradeSettlementLineMonetarySummation> <ram:LineTotalAmount>1782.00</ram:LineTotalAmount> </ram:SpecifiedTradeSettlementLineMonetarySummation> diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice_tax_incl.xml b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice_tax_incl.xml new file mode 100644 index 000000000000..ae2eef40989f --- /dev/null +++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice_tax_incl.xml @@ -0,0 +1,239 @@ +<rsm:CrossIndustryInvoice xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"> + <rsm:ExchangedDocumentContext> + <ram:GuidelineSpecifiedDocumentContextParameter> + <ram:ID>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</ram:ID> + </ram:GuidelineSpecifiedDocumentContextParameter> + </rsm:ExchangedDocumentContext> + <rsm:ExchangedDocument> + <ram:ID>INV/2017/01/0001</ram:ID> + <ram:TypeCode>380</ram:TypeCode> + <ram:IssueDateTime> + <udt:DateTimeString format="102">20170101</udt:DateTimeString> + </ram:IssueDateTime> + <ram:IncludedNote> + <ram:Content>test narration</ram:Content> + </ram:IncludedNote> + </rsm:ExchangedDocument> + <rsm:SupplyChainTradeTransaction> + <ram:IncludedSupplyChainTradeLineItem> + <ram:AssociatedDocumentLineDocument> + <ram:LineID>1</ram:LineID> + </ram:AssociatedDocumentLineDocument> + <ram:SpecifiedTradeProduct> + <ram:Name>product_a</ram:Name> + </ram:SpecifiedTradeProduct> + <ram:SpecifiedLineTradeAgreement> + <ram:GrossPriceProductTradePrice> + <ram:ChargeAmount>95.24</ram:ChargeAmount> + </ram:GrossPriceProductTradePrice> + <ram:NetPriceProductTradePrice> + <ram:ChargeAmount>95.24</ram:ChargeAmount> + </ram:NetPriceProductTradePrice> + </ram:SpecifiedLineTradeAgreement> + <ram:SpecifiedLineTradeDelivery> + <ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity> + </ram:SpecifiedLineTradeDelivery> + <ram:SpecifiedLineTradeSettlement> + <ram:ApplicableTradeTax> + <ram:TypeCode>VAT</ram:TypeCode> + <ram:CategoryCode>S</ram:CategoryCode> + <ram:RateApplicablePercent>5.0</ram:RateApplicablePercent> + </ram:ApplicableTradeTax> + <ram:SpecifiedTradeSettlementLineMonetarySummation> + <ram:LineTotalAmount>95.24</ram:LineTotalAmount> + </ram:SpecifiedTradeSettlementLineMonetarySummation> + </ram:SpecifiedLineTradeSettlement> + </ram:IncludedSupplyChainTradeLineItem> + <ram:IncludedSupplyChainTradeLineItem> + <ram:AssociatedDocumentLineDocument> + <ram:LineID>2</ram:LineID> + </ram:AssociatedDocumentLineDocument> + <ram:SpecifiedTradeProduct> + <ram:Name>product_a</ram:Name> + </ram:SpecifiedTradeProduct> + <ram:SpecifiedLineTradeAgreement> + <ram:GrossPriceProductTradePrice> + <ram:ChargeAmount>100.00</ram:ChargeAmount> + </ram:GrossPriceProductTradePrice> + <ram:NetPriceProductTradePrice> + <ram:ChargeAmount>100.00</ram:ChargeAmount> + </ram:NetPriceProductTradePrice> + </ram:SpecifiedLineTradeAgreement> + <ram:SpecifiedLineTradeDelivery> + <ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity> + </ram:SpecifiedLineTradeDelivery> + <ram:SpecifiedLineTradeSettlement> + <ram:ApplicableTradeTax> + <ram:TypeCode>VAT</ram:TypeCode> + <ram:CategoryCode>S</ram:CategoryCode> + <ram:RateApplicablePercent>5.0</ram:RateApplicablePercent> + </ram:ApplicableTradeTax> + <ram:SpecifiedTradeSettlementLineMonetarySummation> + <ram:LineTotalAmount>100.00</ram:LineTotalAmount> + </ram:SpecifiedTradeSettlementLineMonetarySummation> + </ram:SpecifiedLineTradeSettlement> + </ram:IncludedSupplyChainTradeLineItem> + <ram:IncludedSupplyChainTradeLineItem> + <ram:AssociatedDocumentLineDocument> + <ram:LineID>3</ram:LineID> + </ram:AssociatedDocumentLineDocument> + <ram:SpecifiedTradeProduct> + <ram:Name>product_a</ram:Name> + </ram:SpecifiedTradeProduct> + <ram:SpecifiedLineTradeAgreement> + <ram:GrossPriceProductTradePrice> + <ram:ChargeAmount>190.48</ram:ChargeAmount> + <ram:AppliedTradeAllowanceCharge> + <ram:ChargeIndicator> + <udt:Indicator>false</udt:Indicator> + </ram:ChargeIndicator> + <ram:ActualAmount>19.05</ram:ActualAmount> + </ram:AppliedTradeAllowanceCharge> + </ram:GrossPriceProductTradePrice> + <ram:NetPriceProductTradePrice> + <ram:ChargeAmount>171.43</ram:ChargeAmount> + </ram:NetPriceProductTradePrice> + </ram:SpecifiedLineTradeAgreement> + <ram:SpecifiedLineTradeDelivery> + <ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity> + </ram:SpecifiedLineTradeDelivery> + <ram:SpecifiedLineTradeSettlement> + <ram:ApplicableTradeTax> + <ram:TypeCode>VAT</ram:TypeCode> + <ram:CategoryCode>S</ram:CategoryCode> + <ram:RateApplicablePercent>5.0</ram:RateApplicablePercent> + </ram:ApplicableTradeTax> + <ram:SpecifiedTradeSettlementLineMonetarySummation> + <ram:LineTotalAmount>171.43</ram:LineTotalAmount> + </ram:SpecifiedTradeSettlementLineMonetarySummation> + </ram:SpecifiedLineTradeSettlement> + </ram:IncludedSupplyChainTradeLineItem> + <ram:IncludedSupplyChainTradeLineItem> + <ram:AssociatedDocumentLineDocument> + <ram:LineID>4</ram:LineID> + </ram:AssociatedDocumentLineDocument> + <ram:SpecifiedTradeProduct> + <ram:Name>product_a</ram:Name> + </ram:SpecifiedTradeProduct> + <ram:SpecifiedLineTradeAgreement> + <ram:GrossPriceProductTradePrice> + <ram:ChargeAmount>200.00</ram:ChargeAmount> + <ram:AppliedTradeAllowanceCharge> + <ram:ChargeIndicator> + <udt:Indicator>false</udt:Indicator> + </ram:ChargeIndicator> + <ram:ActualAmount>20.00</ram:ActualAmount> + </ram:AppliedTradeAllowanceCharge> + </ram:GrossPriceProductTradePrice> + <ram:NetPriceProductTradePrice> + <ram:ChargeAmount>180.00</ram:ChargeAmount> + </ram:NetPriceProductTradePrice> + </ram:SpecifiedLineTradeAgreement> + <ram:SpecifiedLineTradeDelivery> + <ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity> + </ram:SpecifiedLineTradeDelivery> + <ram:SpecifiedLineTradeSettlement> + <ram:ApplicableTradeTax> + <ram:TypeCode>VAT</ram:TypeCode> + <ram:CategoryCode>S</ram:CategoryCode> + <ram:RateApplicablePercent>5.0</ram:RateApplicablePercent> + </ram:ApplicableTradeTax> + <ram:SpecifiedTradeSettlementLineMonetarySummation> + <ram:LineTotalAmount>180.00</ram:LineTotalAmount> + </ram:SpecifiedTradeSettlementLineMonetarySummation> + </ram:SpecifiedLineTradeSettlement> + </ram:IncludedSupplyChainTradeLineItem> + <ram:ApplicableHeaderTradeAgreement> + <ram:SellerTradeParty> + <ram:Name>partner_1</ram:Name> + <ram:DefinedTradeContact> + <ram:PersonName>partner_1</ram:PersonName> + <ram:TelephoneUniversalCommunication> + <ram:CompleteNumber>+1 (650) 555-0111</ram:CompleteNumber> + </ram:TelephoneUniversalCommunication> + <ram:EmailURIUniversalCommunication> + <ram:URIID schemeID="SMTP">partner1@yourcompany.com</ram:URIID> + </ram:EmailURIUniversalCommunication> + </ram:DefinedTradeContact> + <ram:PostalTradeAddress> + <ram:PostcodeCode>75000</ram:PostcodeCode> + <ram:LineOne>Rue Jean Jaurès, 42</ram:LineOne> + <ram:CityName>Paris</ram:CityName> + <ram:CountryID>FR</ram:CountryID> + </ram:PostalTradeAddress> + <ram:SpecifiedTaxRegistration> + <ram:ID schemeID="VA">FR05677404089</ram:ID> + </ram:SpecifiedTaxRegistration> + </ram:SellerTradeParty> + <ram:BuyerTradeParty> + <ram:Name>partner_2</ram:Name> + <ram:DefinedTradeContact> + <ram:PersonName>partner_2</ram:PersonName> + </ram:DefinedTradeContact> + <ram:PostalTradeAddress> + <ram:PostcodeCode>52330</ram:PostcodeCode> + <ram:LineOne>Rue Charles de Gaulle</ram:LineOne> + <ram:CityName>Colombey-les-Deux-Églises</ram:CityName> + <ram:CountryID>FR</ram:CountryID> + </ram:PostalTradeAddress> + <ram:SpecifiedTaxRegistration> + <ram:ID schemeID="VA">FR35562153452</ram:ID> + </ram:SpecifiedTaxRegistration> + </ram:BuyerTradeParty> + <ram:BuyerOrderReferencedDocument> + <ram:IssuerAssignedID>INV/2017/01/0001</ram:IssuerAssignedID> + </ram:BuyerOrderReferencedDocument> + </ram:ApplicableHeaderTradeAgreement> + <ram:ApplicableHeaderTradeDelivery> + <ram:ShipToTradeParty> + <ram:Name>partner_2</ram:Name> + <ram:DefinedTradeContact> + <ram:PersonName>partner_2</ram:PersonName> + </ram:DefinedTradeContact> + <ram:PostalTradeAddress> + <ram:PostcodeCode>52330</ram:PostcodeCode> + <ram:LineOne>Rue Charles de Gaulle</ram:LineOne> + <ram:CityName>Colombey-les-Deux-Églises</ram:CityName> + <ram:CountryID>FR</ram:CountryID> + </ram:PostalTradeAddress> + </ram:ShipToTradeParty> + <ram:ActualDeliverySupplyChainEvent> + <ram:OccurrenceDateTime> + <udt:DateTimeString format="102">20170101</udt:DateTimeString> + </ram:OccurrenceDateTime> + </ram:ActualDeliverySupplyChainEvent> + </ram:ApplicableHeaderTradeDelivery> + <ram:ApplicableHeaderTradeSettlement> + <ram:PaymentReference>INV/2017/00001</ram:PaymentReference> + <ram:InvoiceCurrencyCode>USD</ram:InvoiceCurrencyCode> + <ram:SpecifiedTradeSettlementPaymentMeans> + <ram:TypeCode>42</ram:TypeCode> + <ram:PayeePartyCreditorFinancialAccount> + <ram:ProprietaryID>FR15001559627230</ram:ProprietaryID> + </ram:PayeePartyCreditorFinancialAccount> + </ram:SpecifiedTradeSettlementPaymentMeans> + <ram:ApplicableTradeTax> + <ram:CalculatedAmount>27.33</ram:CalculatedAmount> + <ram:TypeCode>VAT</ram:TypeCode> + <ram:BasisAmount>546.67</ram:BasisAmount> + <ram:CategoryCode>S</ram:CategoryCode> + <ram:DueDateTypeCode>5</ram:DueDateTypeCode> + <ram:RateApplicablePercent>5.0</ram:RateApplicablePercent> + </ram:ApplicableTradeTax> + <ram:SpecifiedTradePaymentTerms> + <ram:Description>30% Advance End of Following Month</ram:Description> + <ram:DueDateDateTime> + <udt:DateTimeString format="102">20170228</udt:DateTimeString> + </ram:DueDateDateTime> + </ram:SpecifiedTradePaymentTerms> + <ram:SpecifiedTradeSettlementHeaderMonetarySummation> + <ram:LineTotalAmount>546.67</ram:LineTotalAmount> + <ram:TaxBasisTotalAmount>546.67</ram:TaxBasisTotalAmount> + <ram:TaxTotalAmount currencyID="USD">27.33</ram:TaxTotalAmount> + <ram:GrandTotalAmount>574.00</ram:GrandTotalAmount> + <ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount> + <ram:DuePayableAmount>574.00</ram:DuePayableAmount> + </ram:SpecifiedTradeSettlementHeaderMonetarySummation> + </ram:ApplicableHeaderTradeSettlement> + </rsm:SupplyChainTradeTransaction> +</rsm:CrossIndustryInvoice> diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_refund.xml b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_refund.xml index d1ff9a334700..1c4ed46bcbd0 100644 --- a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_refund.xml +++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_refund.xml @@ -45,13 +45,6 @@ <ram:CategoryCode>S</ram:CategoryCode> <ram:RateApplicablePercent>21.0</ram:RateApplicablePercent> </ram:ApplicableTradeTax> - <ram:SpecifiedTradeAllowanceCharge> - <ram:ChargeIndicator> - <udt:Indicator>false</udt:Indicator> - </ram:ChargeIndicator> - <ram:ActualAmount>99.00</ram:ActualAmount> - <ram:ReasonCode>95</ram:ReasonCode> - </ram:SpecifiedTradeAllowanceCharge> <ram:SpecifiedTradeSettlementLineMonetarySummation> <ram:LineTotalAmount>1782.00</ram:LineTotalAmount> </ram:SpecifiedTradeSettlementLineMonetarySummation> diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_fr.py b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_fr.py index f7211cf84d4e..2795414946cc 100644 --- a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_fr.py +++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_fr.py @@ -69,6 +69,35 @@ class TestCIIFR(TestUBLCommon): 'country_id': cls.env.ref('base.fr').id, }) + cls.tax_5_purchase = cls.env['account.tax'].create({ + 'name': 'tax_5', + 'amount_type': 'percent', + 'amount': 5, + 'type_tax_use': 'purchase', + }) + + cls.tax_0_purchase = cls.env['account.tax'].create({ + 'name': 'tax_0', + 'amount_type': 'percent', + 'amount': 0, + 'type_tax_use': 'purchase', + }) + + cls.tax_5 = cls.env['account.tax'].create({ + 'name': 'tax_5', + 'amount_type': 'percent', + 'amount': 5, + 'type_tax_use': 'sale', + }) + + cls.tax_5_incl = cls.env['account.tax'].create({ + 'name': 'tax_5_incl', + 'amount_type': 'percent', + 'amount': 5, + 'type_tax_use': 'sale', + 'price_include': True, + }) + @classmethod def setup_company_data(cls, company_name, chart_template): # OVERRIDE @@ -205,10 +234,99 @@ class TestCIIFR(TestUBLCommon): self.assertEqual(attachment.name, "factur-x.xml") self._assert_imported_invoice_from_etree(refund, attachment) + def test_export_tax_included(self): + """ + Tests whether the tax included price_units are correctly converted to tax excluded + amounts in the exported xml + """ + invoice = self._generate_move( + self.partner_1, + self.partner_2, + move_type='out_invoice', + invoice_line_ids=[ + { + 'product_id': self.product_a.id, + 'quantity': 1, + 'price_unit': 100, + 'tax_ids': [(6, 0, self.tax_5_incl.ids)], + }, + { + 'product_id': self.product_a.id, + 'quantity': 1, + 'price_unit': 100, + 'tax_ids': [(6, 0, self.tax_5.ids)], + }, + { + 'product_id': self.product_a.id, + 'quantity': 1, + 'price_unit': 200, + 'discount': 10, + 'tax_ids': [(6, 0, self.tax_5_incl.ids)], + }, + { + 'product_id': self.product_a.id, + 'quantity': 1, + 'price_unit': 200, + 'discount': 10, + 'tax_ids': [(6, 0, self.tax_5.ids)], + }, + ], + ) + self._assert_invoice_attachment( + invoice, + xpaths=''' + <xpath expr="./*[local-name()='ExchangedDocument']/*[local-name()='ID']" position="replace"> + <ID>___ignore___</ID> + </xpath> + <xpath expr=".//*[local-name()='IssuerAssignedID']" position="replace"> + <IssuerAssignedID>___ignore___</IssuerAssignedID> + </xpath> + ''', + expected_file='from_odoo/facturx_out_invoice_tax_incl.xml' + ) + #################################################### # Test import #################################################### + def test_import_tax_included(self): + """ + Tests whether the tax included / tax excluded are correctly decoded when + importing a document. The imported xml represents the following invoice: + + Description Quantity Unit Price Disc (%) Taxes Amount + -------------------------------------------------------------------------------- + Product A 1 100 0 5% (incl) 95.24 + Product A 1 100 0 5% (not incl) 100 + Product A 2 200 10 5% (incl) 171.43 + Product A 2 200 10 5% (not incl) 180 + ----------------------- + Untaxed Amount: 546.67 + Taxes: 27.334 + ----------------------- + Total: 574.004 + """ + self._assert_imported_invoice_from_file( + subfolder='tests/test_files/from_odoo', + filename='facturx_out_invoice_tax_incl.xml', + amount_total=574.004, + amount_tax=27.334, + list_line_subtotals=[95.24, 100, 171.43, 180], + # /!\ The price_unit are different for taxes with price_include, because all amounts in Factur-X should be + # tax excluded. At import, the tax included amounts are thus converted into tax excluded ones. + # Yet, the line subtotals and total will be the same (if an equivalent tax exist with price_include = False) + list_line_price_unit=[95.24, 100, 190.48, 200], + # rounding error since for line 3: we round several times... + # when exporting the invoice, we compute the price tax excluded = 200/1.05 ~= 190.48 + # then, when computing the discount amount: 190.48 * 0.1 ~= 19.05 => price net amount = 171.43 + # Thus, at import: price_unit = 190.48, and discount = 100 * (1 - 171.43 / 190.48) = 10.001049979 + list_line_discount=[0, 0, 10, 10], + # Again, all taxes in the imported invoice are price_include = False + list_line_taxes=[self.tax_5_purchase]*4, + move_type='in_invoice', + currency_id=self.env['res.currency'].search([('name', '=', 'USD')], limit=1).id, + ) + def test_import_fnfe_examples(self): # Source: official documentation of the FNFE (subdirectory: "5. FACTUR-X 1.0.06 - Examples") subfolder = 'tests/test_files/from_factur-x_doc' diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_us.py b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_us.py index c90be3b22e4f..4eb6b1db1ba6 100644 --- a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_us.py +++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_us.py @@ -23,6 +23,12 @@ class TestCIIUS(TestUBLCommon): 'country_id': cls.env.ref('base.us').id, }) + cls.tax_0 = cls.env['account.tax'].create({ + 'name': "Tax 0%", + 'type_tax_use': 'purchase', + 'amount': 0, + }) + @classmethod def setup_company_data(cls, company_name, chart_template): # OVERRIDE -- GitLab