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>