details:   https://code.tryton.org/tryton/commit/5b605ac1dc43
branch:    default
user:      Cédric Krier <[email protected]>
date:      Wed Dec 31 15:57:40 2025 +0100
description:
        Warn against creating an overpayment

        Closes #14457
diffstat:

 modules/account_payment/CHANGELOG                                  |    1 +
 modules/account_payment/account.py                                 |   65 ++++-
 modules/account_payment/message.xml                                |    3 +
 modules/account_payment/tests/scenario_account_payment_overpay.rst |  101 
++++++++++
 4 files changed, 153 insertions(+), 17 deletions(-)

diffs (218 lines):

diff -r d9af1c8b229d -r 5b605ac1dc43 modules/account_payment/CHANGELOG
--- a/modules/account_payment/CHANGELOG Tue Dec 30 17:41:36 2025 +0100
+++ b/modules/account_payment/CHANGELOG Wed Dec 31 15:57:40 2025 +0100
@@ -1,3 +1,4 @@
+* Warn against creating an overpayment
 
 Version 7.8.0 - 2025-12-15
 --------------------------
diff -r d9af1c8b229d -r 5b605ac1dc43 modules/account_payment/account.py
--- a/modules/account_payment/account.py        Tue Dec 30 17:41:36 2025 +0100
+++ b/modules/account_payment/account.py        Wed Dec 31 15:57:40 2025 +0100
@@ -24,7 +24,7 @@
 from trytond.wizard import (
     Button, StateAction, StateTransition, StateView, Wizard)
 
-from .exceptions import BlockedWarning, GroupWarning
+from .exceptions import BlockedWarning, GroupWarning, OverpayWarning
 from .payment import KINDS
 
 
@@ -412,9 +412,12 @@
 
     def default_start(self, fields):
         pool = Pool()
+        Currency = pool.get('currency.currency')
+        Lang = pool.get('ir.lang')
         Line = pool.get('account.move.line')
         Warning = pool.get('res.user.warning')
 
+        lang = Lang.get()
         reverse = {'receivable': 'payable', 'payable': 'receivable'}
         companies = {}
         lines = self.records
@@ -442,22 +445,50 @@
                         ('move_state', '=', 'posted'),
                         ])
                 for party in parties:
-                    party_lines = [l for l in others if l.party == party]
-                    if not party_lines:
-                        continue
-                    lines = [l for l in types[kind]['lines']
-                        if l.party == party]
-                    warning_name = Warning.format(
-                        '%s:%s' % (reverse[kind], party), lines)
-                    if Warning.check(warning_name):
-                        names = ', '.join(l.rec_name for l in lines[:5])
-                        if len(lines) > 5:
-                            names += '...'
-                        raise GroupWarning(warning_name,
-                            gettext('account_payment.msg_pay_line_group',
-                                names=names,
-                                party=party.rec_name,
-                                line=party_lines[0].rec_name))
+                    lines = [
+                        l for l in types[kind]['lines'] if l.party == party]
+
+                    if party_lines := [l for l in others if l.party == party]:
+                        warning_name = Warning.format(
+                            '%s:%s' % (reverse[kind], party), lines)
+                        if Warning.check(warning_name):
+                            names = ', '.join(l.rec_name for l in lines[:5])
+                            if len(lines) > 5:
+                                names += '...'
+                            raise GroupWarning(warning_name,
+                                gettext('account_payment.msg_pay_line_group',
+                                    names=names,
+                                    party=party.rec_name,
+                                    line=party_lines[0].rec_name))
+
+                    payment_amount = 0
+                    for line in lines:
+                        payment_amount += Currency.compute(
+                            line.payment_currency, line.payment_amount,
+                            company.currency).copy_sign(
+                                line.debit - line.credit)
+                    payment_amount_sign = payment_amount.as_tuple().sign
+                    amount = getattr(party, kind)
+                    amount_sign = amount.as_tuple().sign
+                    if (abs(payment_amount) > abs(amount)
+                            and (payment_amount_sign == amount_sign
+                                or payment_amount.is_zero()
+                                or amount.is_zero())):
+                        warning_name = Warning.format(
+                            '%s:%s' % (kind, party), lines)
+                        if Warning.check(warning_name):
+                            names = ', '.join(
+                                l.rec_name for l in lines[:5])
+                            if len(lines) > 5:
+                                names += '...'
+                            raise OverpayWarning(warning_name,
+                                gettext('account_payment.msg_pay_line_overpay',
+                                    names=names,
+                                    party=party.rec_name,
+                                    payment_amount=lang.currency(
+                                        payment_amount, company.currency),
+                                    amount=lang.currency(
+                                        amount, company.currency)))
         return {}
 
     def _get_journals(self):
diff -r d9af1c8b229d -r 5b605ac1dc43 modules/account_payment/message.xml
--- a/modules/account_payment/message.xml       Tue Dec 30 17:41:36 2025 +0100
+++ b/modules/account_payment/message.xml       Wed Dec 31 15:57:40 2025 +0100
@@ -24,6 +24,9 @@
         <record model="ir.message" id="msg_pay_line_group">
             <field name="text">The lines "%(names)s" for %(party)s could be 
grouped with the line "%(line)s".</field>
         </record>
+        <record model="ir.message" id="msg_pay_line_overpay">
+            <field name="text">The payment of %(payment_amount)s for lines 
"%(names)s" exceeds the amount for %(party)s, which is %(amount)s.</field>
+        </record>
         <record model="ir.message" id="msg_move_cancel_payments">
             <field name="text">The moves "%(moves)s" contain lines with 
payments, you may want to cancel them before cancelling.</field>
         </record>
diff -r d9af1c8b229d -r 5b605ac1dc43 
modules/account_payment/tests/scenario_account_payment_overpay.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/account_payment/tests/scenario_account_payment_overpay.rst        
Wed Dec 31 15:57:40 2025 +0100
@@ -0,0 +1,101 @@
+================================
+Account Payment Overpay Scenario
+================================
+
+Imports::
+
+    >>> import datetime as dt
+    >>> from decimal import Decimal
+
+    >>> from proteus import Model, Wizard
+    >>> from trytond.modules.account.tests.tools import (
+    ...     create_chart, create_fiscalyear, get_accounts)
+    >>> 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_payment', create_company, 
create_chart)
+
+    >>> Journal = Model.get('account.journal')
+    >>> Move = Model.get('account.move')
+    >>> Party = Model.get('party.party')
+    >>> Payment = Model.get('account.payment')
+    >>> PaymentJournal = Model.get('account.payment.journal')
+
+Create fiscal year::
+
+    >>> fiscalyear = create_fiscalyear()
+    >>> fiscalyear.click('create_period')
+
+Get accounts::
+
+    >>> accounts = get_accounts()
+
+    >>> expense_journal, = Journal.find([('code', '=', 'EXP')])
+    >>> cash_journal, = Journal.find([('code', '=', 'CASH')])
+
+Create parties::
+
+    >>> supplier = Party(name="Supplier")
+    >>> supplier.save()
+
+Create payable line::
+
+    >>> move = Move()
+    >>> move.journal = expense_journal
+    >>> line = move.lines.new(
+    ...     account=accounts['payable'], party=supplier, maturity_date=today,
+    ...     credit=Decimal('100.00'))
+    >>> line = move.lines.new(
+    ...     account=accounts['expense'],
+    ...     debit=Decimal('100.00'))
+    >>> move.click('post')
+    >>> move.state
+    'posted'
+
+Make partial payment::
+
+    >>> payment_move = Move()
+    >>> payment_move.journal = cash_journal
+    >>> _ = payment_move.lines.new(
+    ...     account=accounts['payable'], party=supplier,
+    ...     debit=Decimal('50.00'))
+    >>> _ = payment_move.lines.new(
+    ...     account=accounts['cash'],
+    ...     credit=Decimal('50.00'))
+    >>> payment_move.click('post')
+    >>> payment_move.state
+    'posted'
+
+Try to overpay the line::
+
+    >>> line, = [l for l in move.lines if l.account == accounts['payable']]
+    >>> pay_line = Wizard('account.move.line.pay', [line])
+    Traceback (most recent call last):
+        ...
+    OverpayWarning: ...
+
+Make full payment::
+
+    >>> payment_move2 = Move()
+    >>> payment_move2.journal = cash_journal
+    >>> _ = payment_move2.lines.new(
+    ...     account=accounts['payable'], party=supplier,
+    ...     debit=Decimal('50.00'))
+    >>> _ = payment_move2.lines.new(
+    ...     account=accounts['cash'],
+    ...     credit=Decimal('50.00'))
+    >>> payment_move2.click('post')
+    >>> payment_move2.state
+    'posted'
+
+Try to overpay the line::
+
+    >>> line, = [l for l in move.lines if l.account == accounts['payable']]
+    >>> pay_line = Wizard('account.move.line.pay', [line])
+    Traceback (most recent call last):
+        ...
+    OverpayWarning: ...

Reply via email to