From 5db4bb63c8caf4bfba863f7203bba05c7ddfc474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 14 Mar 2026 00:46:13 +0100 Subject: [PATCH] Fill invoice source amounts instead of checking amounts from UBL Closes #14624 --- modules/edocument_ubl/CHANGELOG | 1 + modules/edocument_ubl/edocument.py | 148 +++++++++--------- modules/edocument_ubl/message.xml | 6 - .../scenario_ubl_2_credit_note_parse.rst | 5 + .../tests/scenario_ubl_2_invoice_parse.rst | 5 + .../scenario_ubl_2_invoice_parse_trivial.rst | 4 + 6 files changed, 85 insertions(+), 84 deletions(-) diff --git a/modules/edocument_ubl/CHANGELOG b/modules/edocument_ubl/CHANGELOG index f55ea67c83..315289c72f 100644 --- a/modules/edocument_ubl/CHANGELOG +++ b/modules/edocument_ubl/CHANGELOG @@ -1,3 +1,4 @@ +* Fill invoice source amounts instead of checking amounts * Filter the MIME type of the additional documents allowed by the specification * Use VATEX as tax exemption reason code * Add support for Python 3.14 diff --git a/modules/edocument_ubl/edocument.py b/modules/edocument_ubl/edocument.py index 3d9beaaca2..1e40205d92 100644 --- a/modules/edocument_ubl/edocument.py +++ b/modules/edocument_ubl/edocument.py @@ -282,7 +282,6 @@ class Invoice(Model): invoice, attachments = cls.parser(namespace)(root) invoice.save() invoice.update_taxes() - cls.checker(namespace)(root, invoice) attachments = list(attachments) for attachment in attachments: attachment.resource = invoice @@ -298,15 +297,6 @@ class Invoice(Model): cls._parse_credit_note_2), }.get(namespace) - @classmethod - def checker(cls, namespace): - return { - 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2': ( - cls._check_invoice_2), - 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2': ( - cls._check_credit_note_2), - }.get(namespace) - @classmethod def _parse_invoice_2(cls, root): pool = Pool() @@ -406,6 +396,41 @@ class Invoice(Model): './{*}LegalMonetaryTotal/{*}PayableRoundingAmount') is not None)): invoice.cash_rounding = True + + if tax_exlusive_amount := root.findtext( + './{*}LegalMonetaryTotal/{*}TaxExclusiveAmount'): + untaxed_amount = Decimal(tax_exlusive_amount) + + if allowance_total_amount := root.findtext( + './{*}LegalMonetaryTotal/{*}AllowanceTotalAmount'): + allowance_total_amount = Decimal(allowance_total_amount) + untaxed_amount -= allowance_total_amount + + if charge_total_amount := root.findtext( + './{*}LegalMonetaryTotal/{*}ChargeTotalAmount'): + charge_total_amount = Decimal(charge_total_amount) + untaxed_amount += charge_total_amount + + invoice.source_untaxed_amount = untaxed_amount + + tax_amount = sum(Decimal(amount.text) for amount in root.iterfind( + './{*}TaxTotal/{*}TaxAmount')) + invoice.source_tax_amount = tax_amount + + payable_amount = Decimal( + root.findtext('./{*}LegalMonetaryTotal/{*}PayableAmount')) + prepaid_amount = Decimal( + root.findtext('./{*}LegalMonetaryTotal/{*}PrepaidAmount') + or 0) + total_amount = payable_amount + prepaid_amount + if not getattr(invoice, 'cash_rounding', False): + payable_rounding_amount = Decimal( + root.findtext( + './{*}LegalMonetaryTotal/{*}PayableRoundingAmount') + or 0) + total_amount -= payable_rounding_amount + invoice.source_total_amount = total_amount + return invoice, cls._parse_2_attachments(root) @classmethod @@ -682,6 +707,41 @@ class Invoice(Model): './{*}LegalMonetaryTotal/{*}PayableRoundingAmount') is not None): invoice.cash_rounding = True + + if tax_exlusive_amount := root.findtext( + './{*}LegalMonetaryTotal/{*}TaxExclusiveAmount'): + untaxed_amount = Decimal(tax_exlusive_amount) + + if allowance_total_amount := root.findtext( + './{*}LegalMonetaryTotal/{*}AllowanceTotalAmount'): + allowance_total_amount = Decimal(allowance_total_amount) + untaxed_amount -= allowance_total_amount + + if charge_total_amount := root.findtext( + './{*}LegalMonetaryTotal/{*}ChargeTotalAmount'): + charge_total_amount = Decimal(charge_total_amount) + untaxed_amount += charge_total_amount + + invoice.source_untaxed_amount = -untaxed_amount + + tax_amount = sum(Decimal(amount.text) for amount in root.iterfind( + './{*}TaxTotal/{*}TaxAmount')) + invoice.source_tax_amount = -tax_amount + + payable_amount = Decimal( + root.findtext('./{*}LegalMonetaryTotal/{*}PayableAmount')) + prepaid_amount = Decimal( + root.findtext('./{*}LegalMonetaryTotal/{*}PrepaidAmount') + or 0) + total_amount = payable_amount + prepaid_amount + if not getattr(invoice, 'cash_rounding', False): + payable_rounding_amount = Decimal( + root.findtext( + './{*}LegalMonetaryTotal/{*}PayableRoundingAmount') + or 0) + total_amount -= payable_rounding_amount + invoice.source_total_amount = -total_amount + return invoice, cls._parse_2_attachments(root) @classmethod @@ -1259,74 +1319,6 @@ class Invoice(Model): attachment.name = name yield attachment - @classmethod - def _check_invoice_2(cls, root, invoice): - pool = Pool() - Lang = pool.get('ir.lang') - lang = Lang.get() - - payable_amount = Decimal( - root.findtext('./{*}LegalMonetaryTotal/{*}PayableAmount')) - prepaid_amount = Decimal( - root.findtext('./{*}LegalMonetaryTotal/{*}PrepaidAmount') - or 0) - amount = payable_amount + prepaid_amount - if not getattr(invoice, 'cash_rounding', False): - payable_rounding_amount = Decimal( - root.findtext( - './{*}LegalMonetaryTotal/{*}PayableRoundingAmount') - or 0) - amount -= payable_rounding_amount - if invoice.total_amount != amount: - raise InvoiceError(gettext( - 'edocument_ubl.msg_invoice_total_amount_different', - invoice=invoice.rec_name, - total_amount=lang.format_number(invoice.total_amount), - amount=lang.format_number(amount))) - - tax_total = sum(Decimal(amount.text) for amount in root.iterfind( - './{*}TaxTotal/{*}TaxAmount')) - if invoice.tax_amount != tax_total: - raise InvoiceError(gettext( - 'edocument_ubl.msg_invoice_tax_amount_different', - invoice=invoice.rec_name, - tax_amount=lang.format_number(invoice.tax_amount), - tax_total=lang.format_number(tax_total))) - - @classmethod - def _check_credit_note_2(cls, root, invoice): - pool = Pool() - Lang = pool.get('ir.lang') - lang = Lang.get() - - payable_amount = Decimal( - root.findtext('./{*}LegalMonetaryTotal/{*}PayableAmount')) - prepaid_amount = Decimal( - root.findtext('./{*}LegalMonetaryTotal/{*}PrepaidAmount') - or 0) - amount = payable_amount + prepaid_amount - if not getattr(invoice, 'cash_rounding', False): - payable_rounding_amount = Decimal( - root.findtext( - './{*}LegalMonetaryTotal/{*}PayableRoundingAmount') - or 0) - amount -= payable_rounding_amount - if -invoice.total_amount != amount: - raise InvoiceError(gettext( - 'edocument_ubl.msg_invoice_total_amount_different', - invoice=invoice.rec_name, - total_amount=lang.format_number(-invoice.total_amount), - amount=lang.format_number(amount))) - - tax_total = sum(Decimal(amount.text) for amount in root.iterfind( - './{*}TaxTotal/{*}TaxAmount')) - if -invoice.tax_amount != tax_total: - raise InvoiceError(gettext( - 'edocument_ubl.msg_invoice_tax_amount_different', - invoice=invoice.rec_name, - tax_amount=lang.format_number(-invoice.tax_amount), - tax_total=lang.format_number(tax_total))) - class Invoice_Bank(metaclass=PoolMeta): __name__ = 'edocument.ubl.invoice' diff --git a/modules/edocument_ubl/message.xml b/modules/edocument_ubl/message.xml index 531d66d632..77e6098eea 100644 --- a/modules/edocument_ubl/message.xml +++ b/modules/edocument_ubl/message.xml @@ -24,11 +24,5 @@ this repository contains the full copyright notices and license terms. --> Could not find tax for: %(tax_category)s - - The total amount %(total_amount)s of the invoice "%(invoice)s" is different from the amount %(amount)s. - - - The tax amount %(tax_amount)s of the invoice "%(invoice)s" is different from the tax total %(tax_total)s. - diff --git a/modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.rst b/modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.rst index d894612953..9b560a7396 100644 --- a/modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.rst +++ b/modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.rst @@ -79,6 +79,11 @@ Parse the UBL invoice:: ... Decimal('-1729.00') if cash_rounding else Decimal('-1728.70')) >>> invoice.tax_amount Decimal('-292.20') + >>> assertEqual(invoice.source_untaxed_amount, Decimal('-1436.50')) + >>> assertEqual(invoice.source_tax_amount, Decimal('-292.20')) + >>> assertEqual( + ... invoice.source_total_amount, + ... Decimal('-1729.00') if cash_rounding else Decimal('-1728.70')) >>> len(invoice.lines) 7 diff --git a/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst b/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst index 0be9b98964..db6a0c4920 100644 --- a/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst +++ b/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst @@ -79,6 +79,11 @@ Parse the UBL invoice:: ... Decimal('1729.00') if cash_rounding else Decimal('1728.70')) >>> invoice.tax_amount Decimal('292.20') + >>> assertEqual(invoice.source_untaxed_amount, Decimal('1436.50')) + >>> assertEqual(invoice.source_tax_amount, Decimal('292.20')) + >>> assertEqual( + ... invoice.source_total_amount, + ... Decimal('1729.00') if cash_rounding else Decimal('1728.70')) >>> len(invoice.lines) 7 diff --git a/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse_trivial.rst b/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse_trivial.rst index 036175f7f6..5dd72839dd 100644 --- a/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse_trivial.rst +++ b/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse_trivial.rst @@ -5,6 +5,7 @@ UBL 2 Invoice Parse Trivial Imports:: >>> import datetime as dt + >>> from decimal import Decimal >>> from proteus import Model >>> from trytond.modules.account.tests.tools import create_chart @@ -46,6 +47,9 @@ Parse the UBL invoice:: >>> assertEqual(invoice.company, company) >>> invoice.total_amount Decimal('100.00') + >>> invoice.source_untaxed_amount + >>> assertEqual(invoice.source_tax_amount, Decimal('0.00')) + >>> assertEqual(invoice.source_total_amount, Decimal('100.00')) >>> line, = invoice.lines >>> line.description 'Cotter pin, MIL-SPEC'