[IMP] account_edi_ubl_cii: improve import of XML invoice with errors

Sometimes the user needs to import XML invoices (e.g. factur-x)
containing lines with the following problem:
The billed quantity of the line is set to 0 but the total of the line
(plus/minus allowances) is not 0.

The imported version (in odoo) of this line will have quantity 0 and thus a
total of 0 (since the total will be computed in odoo and not stored
directly in the database).

After this commit the quantity of these line will be calculated
such that the total computation yields the correct total.

To do this an already existing similar "exception" in the parsing
logic was extended to deal with this problem.

closes odoo/odoo#137833

Signed-off-by: Laurent Smet (las) <las@odoo.com>
This commit is contained in:
Sven Fuehr
2023-11-19 23:55:06 +00:00
parent f6fe982853
commit 22eb8e7b6a
4 changed files with 206 additions and 3 deletions
@@ -609,11 +609,20 @@ class AccountEdiCommon(models.AbstractModel):
if billed_qty * price_unit != 0 and price_subtotal is not None:
discount = 100 * (1 - (price_subtotal - amount_fixed_taxes) / (billed_qty * price_unit))
# Sometimes, the xml received is very bad: unit price = 0, qty = 1, but price_subtotal = -200
# Sometimes, the xml received is very bad; e.g.:
# * unit price = 0, qty = 0, but price_subtotal = -200
# * unit price = 0, qty = 1, but price_subtotal = -200
# * unit price = 1, qty = 0, 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:
price_unit = price_subtotal / (billed_qty or 1)
if net_price_unit is not None and price_subtotal != net_price_unit * (billed_qty / basis_qty) - allow_charge_amount:
if net_price_unit == 0 and billed_qty == 0:
quantity = 1
price_unit = price_subtotal
elif net_price_unit == 0:
price_unit = price_subtotal / billed_qty
elif billed_qty == 0:
quantity = price_subtotal / price_unit
return {
'quantity': quantity,
@@ -129,6 +129,7 @@ class TestUBLCommon(AccountTestInvoicingCommon):
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,
list_line_quantity=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
@@ -178,6 +179,8 @@ class TestUBLCommon(AccountTestInvoicingCommon):
if list_line_taxes:
for line, taxes in zip(invoice.invoice_line_ids, list_line_taxes):
self.assertEqual(line.tax_ids, taxes)
if list_line_quantity:
self.assertEqual(invoice.invoice_line_ids.mapped('quantity'), list_line_quantity)
# -------------------------------------------------------------------------
# EXPORT HELPERS
@@ -0,0 +1,179 @@
<?xml version='1.0' encoding='UTF-8'?>
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-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>INV/2017/01/0002</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:AdditionalDocumentReference>
<cbc:ID>___ignore___</cbc:ID>
<cbc:Attachment>
<cbc:EmbeddedDocumentBinaryObject>___ignore___</cbc:EmbeddedDocumentBinaryObject>
</cbc:Attachment>
</cac:AdditionalDocumentReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>partner_1</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Chaussé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="0208">0477472701</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:Delivery>
<cac:DeliveryLocation>
<cac:Address>
<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:Address>
</cac:DeliveryLocation>
</cac:Delivery>
<cac:PaymentMeans>
<cbc:PaymentMeansCode name="credit transfer">30</cbc:PaymentMeansCode>
<cbc:PaymentID>+++000/0000/26268+++</cbc:PaymentID>
<cac:PayeeFinancialAccount>
<cbc:ID>BE15001559627230</cbc:ID>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">630.00</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="USD">3000.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="USD">630</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">3000.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="USD">3000.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="USD">3630.00</cbc:TaxInclusiveAmount>
<cbc:PrepaidAmount currencyID="USD">0.00</cbc:PrepaidAmount>
<cbc:PayableAmount currencyID="USD">3630.00</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>901</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="USD">1000.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>product_b</cbc:Description>
<cbc:Name>product_b</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</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>902</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="USD">1000.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>product_b</cbc:Description>
<cbc:Name>product_b</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">100.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>903</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">100</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="USD">1000.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>product_b</cbc:Description>
<cbc:Name>product_b</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</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>
@@ -687,3 +687,15 @@ class TestUBLBE(TestUBLCommon, TestAccountMoveSendCommon):
('res_field', 'in', ('invoice_pdf_report_file', 'ubl_cii_xml_file')),
])
self.assertEqual(len(invoice_attachments), 2)
def test_import_quantity_and_or_unit_price_zero(self):
""" Tests some special handling cases in which the quantity or unit_price are missing.
"""
subfolder = "tests/test_files/from_odoo"
# The tax 21% from l10n_be is retrieved since it's a duplicate of self.tax_21
tax_21 = self.env.ref(f'account.{self.env.company.id}_attn_VAT-OUT-21-L')
self._assert_imported_invoice_from_file(
subfolder=subfolder, filename='bis3_out_invoice_quantity_and_or_unit_price_zero.xml', amount_total=3630.00, amount_tax=630.00,
list_line_subtotals=[1000, 1000, 1000], currency_id=self.currency_data['currency'].id, list_line_price_unit=[1000, 100, 10],
list_line_discount=[0, 0, 0], list_line_taxes=[tax_21, tax_21, tax_21], list_line_quantity=[1, 10, 100], move_type='out_invoice',
)