details:   https://code.tryton.org/tryton/commit/5d5cf986829c
branch:    default
user:      Cédric Krier <[email protected]>
date:      Fri Mar 13 23:53:22 2026 +0100
description:
        Check the invoice amounts against the source amounts
diffstat:

 modules/account_invoice/CHANGELOG                               |   1 +
 modules/account_invoice/invoice.py                              |  37 ++++
 modules/account_invoice/message.xml                             |   3 +
 modules/account_invoice/tests/scenario_invoice_check_source.rst |  76 
++++++++++
 modules/account_invoice/view/invoice_form.xml                   |  10 +-
 5 files changed, 126 insertions(+), 1 deletions(-)

diffs (194 lines):

diff -r 7ce694dea057 -r 5d5cf986829c modules/account_invoice/CHANGELOG
--- a/modules/account_invoice/CHANGELOG Fri Mar 27 13:38:34 2026 +0100
+++ b/modules/account_invoice/CHANGELOG Fri Mar 13 23:53:22 2026 +0100
@@ -1,3 +1,4 @@
+* Check the invoice amounts against the source amounts
 * Add support for Python 3.14
 * Remove support for Python 3.9
 * Add payment means to invoices
diff -r 7ce694dea057 -r 5d5cf986829c modules/account_invoice/invoice.py
--- a/modules/account_invoice/invoice.py        Fri Mar 27 13:38:34 2026 +0100
+++ b/modules/account_invoice/invoice.py        Fri Mar 13 23:53:22 2026 +0100
@@ -310,16 +310,31 @@
     origin_invoices = fields.Function(fields.Many2Many(
             'account.invoice', None, None, "Origin Invoices"),
         'get_origin_invoices', searcher='search_origin_invoices')
+    source_untaxed_amount = Monetary(
+        "Source Untaxed", digits='currency', readonly=True,
+        states={
+            'invisible': Eval('source_untaxed_amount', None) == Null,
+            })
     untaxed_amount = fields.Function(Monetary(
             "Untaxed", currency='currency', digits='currency'),
         'get_amount', searcher='search_untaxed_amount')
     untaxed_amount_cache = fields.Numeric(
         "Untaxed Cache", digits='currency', readonly=True)
+    source_tax_amount = Monetary(
+        "Source Tax", digits='currency', readonly=True,
+        states={
+            'invisible': Eval('source_tax_amount', None) == Null,
+            })
     tax_amount = fields.Function(Monetary(
             "Tax", currency='currency', digits='currency'),
         'get_amount', searcher='search_tax_amount')
     tax_amount_cache = fields.Numeric(
         "Tax Cache", digits='currency', readonly=True)
+    source_total_amount = Monetary(
+        "Source Total", digits='currency', readonly=True,
+        states={
+            'invisible': Eval('source_total_amount', None) == Null,
+            })
     total_amount = fields.Function(Monetary(
             "Total", currency='currency', digits='currency'),
         'get_amount', searcher='search_total_amount')
@@ -1719,9 +1734,31 @@
     @classmethod
     def validate_fields(cls, invoices, field_names):
         super().validate_fields(invoices, field_names)
+        cls.check_source(invoices, field_names)
         cls.check_supplier_payment_reference(invoices, field_names)
 
     @classmethod
+    def check_source(cls, invoices, field_names):
+        pool = Pool()
+        Lang = pool.get('ir.lang')
+        if field_names and 'state' not in field_names:
+            return
+        for invoice in invoices:
+            if invoice.state not in {'draft', 'cancelled'}:
+                for field in ['untaxed_amount', 'tax_amount', 'total_amount']:
+                    source = getattr(invoice, f'source_{field}')
+                    value = getattr(invoice, field)
+                    if source not in {None, value}:
+                        lang = Lang.get()
+                        raise InvoiceValidationError(
+                            gettext('account_invoice'
+                                '.msg_invoice_source_mismatch',
+                                invoice=invoice.rec_name,
+                                source=lang.currency(source, invoice.currency),
+                                value=lang.currency(value, invoice.currency),
+                                **cls.__names__(field=field)))
+
+    @classmethod
     def check_supplier_payment_reference(cls, invoices, field_names):
         if field_names and not (
                 field_names & {
diff -r 7ce694dea057 -r 5d5cf986829c modules/account_invoice/message.xml
--- a/modules/account_invoice/message.xml       Fri Mar 27 13:38:34 2026 +0100
+++ b/modules/account_invoice/message.xml       Fri Mar 13 23:53:22 2026 +0100
@@ -42,6 +42,9 @@
         <record model="ir.message" 
id="msg_invoice_payment_lines_greater_amount">
             <field name="text">Payment lines amount on invoice "%(invoice)s" 
can not be greater than the invoice amount.</field>
         </record>
+        <record model="ir.message" id="msg_invoice_source_mismatch">
+            <field name="text">The "%(field)s" value of the invoice 
"%(invoice)s", %(value)s, must equal the source value, %(source)s.</field>
+        </record>
         <record model="ir.message" 
id="msg_invoice_supplier_payment_reference_invalid">
             <field name="text">The %(type)s "%(reference)s" on invoice 
"%(invoice)s" is not valid.</field>
         </record>
diff -r 7ce694dea057 -r 5d5cf986829c 
modules/account_invoice/tests/scenario_invoice_check_source.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/account_invoice/tests/scenario_invoice_check_source.rst   Fri Mar 
13 23:53:22 2026 +0100
@@ -0,0 +1,76 @@
+=============================
+Invoice Check Source Scenario
+=============================
+
+Imports::
+
+    >>> import datetime as dt
+    >>> from decimal import Decimal
+
+    >>> from proteus import Model
+    >>> from trytond.modules.account.tests.tools import (
+    ...     create_chart, create_fiscalyear, create_tax, get_accounts)
+    >>> from trytond.modules.account_invoice.tests.tools import (
+    ...     set_fiscalyear_invoice_sequences)
+    >>> from trytond.modules.company.tests.tools import create_company
+    >>> from trytond.tests.tools import activate_modules
+
+    >>> today = dt.date.today()
+
+Activate modules::
+
+    >>> config = activate_modules('account_invoice', create_company, 
create_chart)
+
+    >>> Invoice = Model.get('account.invoice')
+    >>> Party = Model.get('party.party')
+
+Create fiscal year::
+
+    >>> fiscalyear = set_fiscalyear_invoice_sequences(create_fiscalyear())
+    >>> fiscalyear.click('create_period')
+
+Get accounts::
+
+    >>> accounts = get_accounts()
+
+Create tax::
+
+    >>> tax = create_tax(Decimal('.10'))
+    >>> tax.save()
+
+Create party::
+
+    >>> supplier = Party(name="Supplier")
+    >>> supplier.save()
+
+Create invoice::
+
+    >>> invoice = Invoice(type='in', party=supplier)
+    >>> invoice.invoice_date = today
+    >>> line = invoice.lines.new()
+    >>> line.quantity = 1
+    >>> line.unit_price = Decimal(10)
+    >>> line.account = accounts['expense']
+    >>> line.taxes.append(tax)
+    >>> invoice.save()
+
+    >>> Invoice.write([invoice.id], {
+    ...         'source_untaxed_amount': Decimal('100.00'),
+    ...         'source_tax_amount': Decimal('10.00'),
+    ...         'source_total_amount': Decimal('110.00'),
+    ...         }, invoice._context)
+
+Try to validate::
+
+    >>> invoice.click('validate_invoice')
+    Traceback (most recent call last):
+        ...
+    InvoiceValidationError: ...
+
+Correct quantity::
+
+    >>> line, = invoice.lines
+    >>> line.quantity = 10
+    >>> invoice.click('validate_invoice')
+    >>> invoice.state
+    'validated'
diff -r 7ce694dea057 -r 5d5cf986829c 
modules/account_invoice/view/invoice_form.xml
--- a/modules/account_invoice/view/invoice_form.xml     Fri Mar 27 13:38:34 
2026 +0100
+++ b/modules/account_invoice/view/invoice_form.xml     Fri Mar 13 23:53:22 
2026 +0100
@@ -38,13 +38,21 @@
                         <label name="state"/>
                         <field name="state"/>
                     </group>
-                    <group col="2" colspan="2" id="amount" yfill="1" 
yalign="1">
+                    <group col="4" colspan="2" id="amount" yfill="1" 
yalign="1">
                         <label name="untaxed_amount" xalign="1.0" xexpand="1" 
xfill="0"/>
                         <field name="untaxed_amount" xalign="1.0" xexpand="0"/>
+                        <label name="source_untaxed_amount" string="expected" 
xalign="1.0" xexpand="1" xfill="0"/>
+                        <field name="source_untaxed_amount" xalign="1.0" 
xexpand="0"/>
+
                         <label name="tax_amount" xalign="1.0" xexpand="1" 
xfill="0"/>
                         <field name="tax_amount" xalign="1.0" xexpand="0"/>
+                        <label name="source_tax_amount" string="expected" 
xalign="1.0" xexpand="1" xfill="0"/>
+                        <field name="source_tax_amount" xalign="1.0" 
xexpand="0"/>
+
                         <label name="total_amount" xalign="1.0" xexpand="1" 
xfill="0"/>
                         <field name="total_amount" xalign="1.0" xexpand="0"/>
+                        <label name="source_total_amount" string="expected" 
xalign="1.0" xexpand="1" xfill="0"/>
+                        <field name="source_total_amount" xalign="1.0" 
xexpand="0"/>
                     </group>
                 </group>
             </group>

Reply via email to