diff --git a/modules/account_payment/CHANGELOG b/modules/account_payment/CHANGELOG index caa2619c5c..5bc2378f46 100644 --- a/modules/account_payment/CHANGELOG +++ b/modules/account_payment/CHANGELOG @@ -1,3 +1,4 @@ +* Warn against creating an overpayment Version 7.8.0 - 2025-12-15 -------------------------- diff --git a/modules/account_payment/account.py b/modules/account_payment/account.py index fcaab9a4d0..cdf923982f 100644 --- a/modules/account_payment/account.py +++ b/modules/account_payment/account.py @@ -24,7 +24,7 @@ from trytond.transaction import Transaction, check_access, without_check_access 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 @@ class PayLine(Wizard): 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 @@ class PayLine(Wizard): ('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 --git a/modules/account_payment/message.xml b/modules/account_payment/message.xml index afc1b836bb..a59affb6a4 100644 --- a/modules/account_payment/message.xml +++ b/modules/account_payment/message.xml @@ -24,6 +24,9 @@ this repository contains the full copyright notices and license terms. --> The lines "%(names)s" for %(party)s could be grouped with the line "%(line)s". + + The payment of %(payment_amount)s for lines "%(names)s" exceeds the amount for %(party)s, which is %(amount)s. + The moves "%(moves)s" contain lines with payments, you may want to cancel them before cancelling. diff --git a/modules/account_payment/tests/scenario_account_payment_overpay.rst b/modules/account_payment/tests/scenario_account_payment_overpay.rst new file mode 100644 index 0000000000..a8a0969f76 --- /dev/null +++ b/modules/account_payment/tests/scenario_account_payment_overpay.rst @@ -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: ...