From eb0c1a34ea8b950151f1e7d5d7157677f4818e10 Mon Sep 17 00:00:00 2001
From: Julien Van Roy <juvr@odoo.com>
Date: Fri, 5 May 2023 15:30:03 +0000
Subject: [PATCH] [FIX] account_edi_ubl_cii: unit prices should not be rounded

When unit prices have more than 2 digits, it is currently not reflected
in the UBL formats. Consequently, the line amounts are not equal to the
unit price * quantity (assume there is no discount, charges or
allowance) and it raises validation errors: "Invoice line net amount
MUST equal (Invoiced quantity * (Item net price/item price base
quantity) + Sum of invoice line charge amount - sum of invoice line
allowance amount".

To fix this, we no longer round the unit prices.

NB: the decimal accuracy should be set in the settings (otherwise, the
default is 2 digits for unit prices).

See https://docs.peppol.eu/poacc/billing/3.0/bis/#_rounding

opw-3290035
task-3302904

closes odoo/odoo#121559

X-original-commit: bd7955934994a949055b6f272e3426cd1bed39c1
Signed-off-by: Laurent Smet <las@odoo.com>
Signed-off-by: Julien Van Roy <juvr@odoo.com>
---
 .../data/ubl_20_templates.xml                 |   2 +-
 .../models/account_edi_xml_ubl_20.py          |   3 +-
 .../from_odoo/bis3_out_invoice_rounding.xml   | 126 ++++++++++++++++++
 .../tests/test_xml_ubl_be.py                  |  27 ++++
 4 files changed, 156 insertions(+), 2 deletions(-)
 create mode 100644 addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/bis3_out_invoice_rounding.xml

diff --git a/addons/account_edi_ubl_cii/data/ubl_20_templates.xml b/addons/account_edi_ubl_cii/data/ubl_20_templates.xml
index ae2a54445ca5..61c96f387431 100644
--- a/addons/account_edi_ubl_cii/data/ubl_20_templates.xml
+++ b/addons/account_edi_ubl_cii/data/ubl_20_templates.xml
@@ -248,7 +248,7 @@
                 <t t-set="vals" t-value="vals.get('price_vals', {})"/>
                 <cbc:PriceAmount
                         t-att-currencyID="vals['currency'].name"
-                        t-out="format_float(vals.get('price_amount'), vals.get('currency_dp'))"/>
+                        t-out="format_float(vals.get('price_amount'), vals.get('product_price_dp'))"/>
                 <!-- nbr of item units to which the price applies), i.e.: 1 Dozen = 12 units, not mandatory -->
                 <cbc:BaseQuantity
                         t-att="vals.get('base_quantity_attrs', {})"
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 158e6c91bf11..80af378a1e0b 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
@@ -289,7 +289,7 @@ class AccountEdiXmlUBL20(models.AbstractModel):
         else:
             gross_price_subtotal = net_price_subtotal / (1.0 - (line.discount or 0.0) / 100.0)
         # Price subtotal with discount / quantity:
-        gross_price_unit = line.currency_id.round((gross_price_subtotal / line.quantity) if line.quantity else 0.0)
+        gross_price_unit = gross_price_subtotal / line.quantity if line.quantity else 0.0
 
         uom = super()._get_uom_unece_code(line)
 
@@ -299,6 +299,7 @@ class AccountEdiXmlUBL20(models.AbstractModel):
 
             # The price of an item, exclusive of VAT, after subtracting item price discount.
             'price_amount': gross_price_unit,
+            'product_price_dp': self.env['decimal.precision'].precision_get('Product Price'),
 
             # The number of item units to which the price applies.
             # setting to None -> the xml will not comprise the BaseQuantity (it's not mandatory)
diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/bis3_out_invoice_rounding.xml b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/bis3_out_invoice_rounding.xml
new file mode 100644
index 000000000000..50c135d002f0
--- /dev/null
+++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/bis3_out_invoice_rounding.xml
@@ -0,0 +1,126 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<Invoice xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
+         xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
+         xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
+    <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
+    </cbc:CustomizationID>
+    <cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
+    <cbc:ID>___ignore___</cbc:ID>
+    <cbc:IssueDate>2017-01-01</cbc:IssueDate>
+    <cbc:DueDate>2017-02-28</cbc:DueDate>
+    <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
+    <cbc:Note>test narration</cbc:Note>
+    <cbc:DocumentCurrencyCode>USD</cbc:DocumentCurrencyCode>
+    <cbc:BuyerReference>ref_partner_2</cbc:BuyerReference>
+    <cac:OrderReference>
+        <cbc:ID>___ignore___</cbc:ID>
+    </cac:OrderReference>
+    <cac:AccountingSupplierParty>
+        <cac:Party>
+            <cbc:EndpointID schemeID="9925">BE0202239951</cbc:EndpointID>
+            <cac:PartyName>
+                <cbc:Name>partner_1</cbc:Name>
+            </cac:PartyName>
+            <cac:PostalAddress>
+                <cbc:StreetName>Chauss&#233;e de Namur 40</cbc:StreetName>
+                <cbc:CityName>Ramillies</cbc:CityName>
+                <cbc:PostalZone>1367</cbc:PostalZone>
+                <cac:Country>
+                    <cbc:IdentificationCode>BE</cbc:IdentificationCode>
+                </cac:Country>
+            </cac:PostalAddress>
+            <cac:PartyTaxScheme>
+                <cbc:CompanyID>BE0202239951</cbc:CompanyID>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:PartyTaxScheme>
+            <cac:PartyLegalEntity>
+                <cbc:RegistrationName>partner_1</cbc:RegistrationName>
+                <cbc:CompanyID>BE0202239951</cbc:CompanyID>
+            </cac:PartyLegalEntity>
+            <cac:Contact>
+                <cbc:Name>partner_1</cbc:Name>
+            </cac:Contact>
+        </cac:Party>
+    </cac:AccountingSupplierParty>
+    <cac:AccountingCustomerParty>
+        <cac:Party>
+            <cbc:EndpointID schemeID="9925">BE0477472701</cbc:EndpointID>
+            <cac:PartyName>
+                <cbc:Name>partner_2</cbc:Name>
+            </cac:PartyName>
+            <cac:PostalAddress>
+                <cbc:StreetName>Rue des Bourlottes 9</cbc:StreetName>
+                <cbc:CityName>Ramillies</cbc:CityName>
+                <cbc:PostalZone>1367</cbc:PostalZone>
+                <cac:Country>
+                    <cbc:IdentificationCode>BE</cbc:IdentificationCode>
+                </cac:Country>
+            </cac:PostalAddress>
+            <cac:PartyTaxScheme>
+                <cbc:CompanyID>BE0477472701</cbc:CompanyID>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:PartyTaxScheme>
+            <cac:PartyLegalEntity>
+                <cbc:RegistrationName>partner_2</cbc:RegistrationName>
+                <cbc:CompanyID>BE0477472701</cbc:CompanyID>
+            </cac:PartyLegalEntity>
+            <cac:Contact>
+                <cbc:Name>partner_2</cbc:Name>
+            </cac:Contact>
+        </cac:Party>
+    </cac:AccountingCustomerParty>
+    <cac:PaymentMeans>
+        <cbc:PaymentMeansCode name="credit transfer">30</cbc:PaymentMeansCode>
+        <cbc:PaymentID>___ignore___</cbc:PaymentID>
+        <cac:PayeeFinancialAccount>
+            <cbc:ID>BE15001559627230</cbc:ID>
+        </cac:PayeeFinancialAccount>
+    </cac:PaymentMeans>
+    <cac:PaymentTerms>
+        <cbc:Note>30% Advance End of Following Month</cbc:Note>
+    </cac:PaymentTerms>
+    <cac:TaxTotal>
+        <cbc:TaxAmount currencyID="USD">959.07</cbc:TaxAmount>
+        <cac:TaxSubtotal>
+            <cbc:TaxableAmount currencyID="USD">4567.00</cbc:TaxableAmount>
+            <cbc:TaxAmount currencyID="USD">959.07</cbc:TaxAmount>
+            <cac:TaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>21.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:TaxCategory>
+        </cac:TaxSubtotal>
+    </cac:TaxTotal>
+    <cac:LegalMonetaryTotal>
+        <cbc:LineExtensionAmount currencyID="USD">4567.00</cbc:LineExtensionAmount>
+        <cbc:TaxExclusiveAmount currencyID="USD">4567.00</cbc:TaxExclusiveAmount>
+        <cbc:TaxInclusiveAmount currencyID="USD">5526.07</cbc:TaxInclusiveAmount>
+        <cbc:PrepaidAmount currencyID="USD">0.00</cbc:PrepaidAmount>
+        <cbc:PayableAmount currencyID="USD">5526.07</cbc:PayableAmount>
+    </cac:LegalMonetaryTotal>
+    <cac:InvoiceLine>
+        <cbc:ID>___ignore___</cbc:ID>
+        <cbc:InvoicedQuantity unitCode="C62">10000.0</cbc:InvoicedQuantity>
+        <cbc:LineExtensionAmount currencyID="USD">4567.00</cbc:LineExtensionAmount>
+        <cac:Item>
+            <cbc:Description>product_a</cbc:Description>
+            <cbc:Name>product_a</cbc:Name>
+            <cac:ClassifiedTaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>21.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:ClassifiedTaxCategory>
+        </cac:Item>
+        <cac:Price>
+            <cbc:PriceAmount currencyID="USD">0.4567</cbc:PriceAmount>
+        </cac:Price>
+    </cac:InvoiceLine>
+</Invoice>
diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_ubl_be.py b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_ubl_be.py
index 2f976df64051..56cdd1bf96e5 100644
--- a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_ubl_be.py
+++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_ubl_be.py
@@ -256,6 +256,33 @@ class TestUBLBE(TestUBLCommon):
             expected_file='from_odoo/bis3_out_invoice_public_admin.xml',
         )
 
+    def test_rounding_price_unit(self):
+        """ OpenPeppol states that:
+        * All document level amounts shall be rounded to two decimals for accounting
+        * Invoice line net amount shall be rounded to two decimals
+        See: https://docs.peppol.eu/poacc/billing/3.0/bis/#_rounding
+        Do not round the unit prices. It allows to obtain the correct line amounts when prices have more than 2
+        digits.
+        """
+        # Set the allowed number of digits for the price_unit
+        decimal_precision = self.env['decimal.precision'].search([('name', '=', 'Product Price')], limit=1)
+        self.assertTrue(bool(decimal_precision), "The decimal precision for Product Price is required for this test")
+        decimal_precision.digits = 4
+
+        invoice = self._generate_move(
+            self.partner_1,
+            self.partner_2,
+            move_type='out_invoice',
+            invoice_line_ids=[
+                {
+                    'product_id': self.product_a.id,
+                    'quantity': 10000,
+                    'price_unit': 0.4567,
+                    'tax_ids': [(6, 0, self.tax_21.ids)],
+                }
+            ],
+        )
+        self._assert_invoice_attachment(invoice, xpaths=None, expected_file='from_odoo/bis3_out_invoice_rounding.xml')
 
     ####################################################
     # Test import
-- 
GitLab