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: ...