diff --git a/modules/account/exceptions.py b/modules/account/exceptions.py
index ff34ffd5e1..51d02fcfd9 100644
--- a/modules/account/exceptions.py
+++ b/modules/account/exceptions.py
@@ -97,9 +97,21 @@ class GroupLineError(UserError):
pass
+class GroupLineWarning(UserWarning):
+ pass
+
+
class RescheduleLineError(UserError):
pass
+class RescheduleLineWarning(UserWarning):
+ pass
+
+
class DelegateLineError(UserError):
pass
+
+
+class DelegateLineWarning(UserWarning):
+ pass
diff --git a/modules/account_payment/CHANGELOG b/modules/account_payment/CHANGELOG
index 1377cd75c0..e821381551 100644
--- a/modules/account_payment/CHANGELOG
+++ b/modules/account_payment/CHANGELOG
@@ -1,3 +1,4 @@
+* Warn when cancelling, grouping, rescheduling or delegating move line with payment
* Mark Payment as succeeded or failed when imported in statement
* Add pay buttons on invoice lines to pay
diff --git a/modules/account_payment/__init__.py b/modules/account_payment/__init__.py
index 36848feed5..7f55a36d70 100644
--- a/modules/account_payment/__init__.py
+++ b/modules/account_payment/__init__.py
@@ -34,6 +34,10 @@ def register():
payment.ProcessPayment,
account.CreateDirectDebit,
account.PayLine,
+ account.MoveCancel,
+ account.MoveLineGroup,
+ account.MoveLineReschedule,
+ account.MoveLineDelegate,
party.Replace,
party.Erase,
module='account_payment', type_='wizard')
diff --git a/modules/account_payment/account.py b/modules/account_payment/account.py
index e947718c8e..d8162ce4a7 100644
--- a/modules/account_payment/account.py
+++ b/modules/account_payment/account.py
@@ -12,6 +12,9 @@ from sql.functions import Abs
from trytond.i18n import gettext
from trytond.model import ModelSQL, ModelView, fields
+from trytond.modules.account.exceptions import (
+ CancelWarning, DelegateLineWarning, GroupLineWarning,
+ RescheduleLineWarning)
from trytond.modules.company.model import CompanyValueMixin
from trytond.modules.currency.fields import Monetary
from trytond.pool import Pool, PoolMeta
@@ -487,6 +490,104 @@ class ConfigurationPaymentGroupSequence(ModelSQL, CompanyValueMixin):
return None
+class MoveCancel(metaclass=PoolMeta):
+ __name__ = 'account.move.cancel'
+
+ def transition_cancel(self):
+ pool = Pool()
+ Warning = pool.get('res.user.warning')
+ moves_w_payments = []
+ for move in self.records:
+ for line in move.lines:
+ if any(p.state != 'failed' for p in line.payments):
+ moves_w_payments.append(move)
+ break
+ if moves_w_payments:
+ names = ', '.join(
+ m.rec_name for m in moves_w_payments[:5])
+ if len(moves_w_payments) > 5:
+ names += '...'
+ key = Warning.format('cancel_payments', moves_w_payments)
+ if Warning.check(key):
+ raise CancelWarning(
+ key, gettext(
+ 'account_payment.msg_move_cancel_payments',
+ moves=names))
+ return super().transition_cancel()
+
+
+class MoveLineGroup(metaclass=PoolMeta):
+ __name__ = 'account.move.line.group'
+
+ def do_group(self, action):
+ pool = Pool()
+ Warning = pool.get('res.user.warning')
+ lines_w_payments = []
+ for line in self.records:
+ if any(p.state != 'failed' for p in line.payments):
+ lines_w_payments.append(line)
+ if lines_w_payments:
+ names = ', '.join(
+ m.rec_name for m in lines_w_payments[:5])
+ if len(lines_w_payments) > 5:
+ names += '...'
+ key = Warning.format('group_payments', lines_w_payments)
+ if Warning.check(key):
+ raise GroupLineWarning(
+ key, gettext(
+ 'account_payment.msg_move_line_group_payments',
+ lines=names))
+ return super().do_group(action)
+
+
+class MoveLineReschedule(metaclass=PoolMeta):
+ __name__ = 'account.move.line.reschedule'
+
+ def do_reschedule(self, action):
+ pool = Pool()
+ Warning = pool.get('res.user.warning')
+ lines_w_payments = []
+ for line in self.records:
+ if any(p.state != 'failed' for p in line.payments):
+ lines_w_payments.append(line)
+ if lines_w_payments:
+ names = ', '.join(
+ m.rec_name for m in lines_w_payments[:5])
+ if len(lines_w_payments) > 5:
+ names += '...'
+ key = Warning.format('reschedule_payments', lines_w_payments)
+ if Warning.check(key):
+ raise RescheduleLineWarning(
+ key, gettext(
+ 'account_payment.msg_move_line_reschedule_payments',
+ lines=names))
+ return super().do_reschedule(action)
+
+
+class MoveLineDelegate(metaclass=PoolMeta):
+ __name__ = 'account.move.line.delegate'
+
+ def do_delegate(self, action):
+ pool = Pool()
+ Warning = pool.get('res.user.warning')
+ lines_w_payments = []
+ for line in self.records:
+ if any(p.state != 'failed' for p in line.payments):
+ lines_w_payments.append(line)
+ if lines_w_payments:
+ names = ', '.join(
+ m.rec_name for m in lines_w_payments[:5])
+ if len(lines_w_payments) > 5:
+ names += '...'
+ key = Warning.format('delegate_payments', lines_w_payments)
+ if Warning.check(key):
+ raise DelegateLineWarning(
+ key, gettext(
+ 'account_payment.msg_move_line_delegate_payments',
+ lines=names))
+ return super().do_delegate(action)
+
+
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
diff --git a/modules/account_payment/message.xml b/modules/account_payment/message.xml
index 69fabc95fe..7e1210778c 100644
--- a/modules/account_payment/message.xml
+++ b/modules/account_payment/message.xml
@@ -18,5 +18,17 @@ 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 moves "%(moves)s" contain lines with payments, you may want to cancel them before cancelling.
+
+
+ The lines "%(lines)s" have payments, you may want to cancel them before grouping.
+
+
+ The lines "%(lines)s" have payments, you may want to cancel them before rescheduling.
+
+
+ The lines "%(lines)s" have payments, you may want to cancel them before delegating.
+
diff --git a/modules/account_payment/tests/scenario_account_payment_warnings.rst b/modules/account_payment/tests/scenario_account_payment_warnings.rst
new file mode 100644
index 0000000000..82d388a2a2
--- /dev/null
+++ b/modules/account_payment/tests/scenario_account_payment_warnings.rst
@@ -0,0 +1,121 @@
+=================================
+Account Payment Warnings Scenario
+=================================
+
+Imports::
+
+ >>> from decimal import Decimal
+ >>> import datetime as dt
+
+ >>> from proteus import Model, Wizard
+ >>> from trytond.tests.tools import activate_modules
+ >>> from trytond.modules.company.tests.tools import (
+ ... create_company, get_company)
+ >>> from trytond.modules.account.tests.tools import (
+ ... create_fiscalyear, create_chart, get_accounts)
+
+ >>> today = dt.date.today()
+
+Activate modules::
+
+ >>> config = activate_modules('account_payment')
+
+ >>> 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 company::
+
+ >>> _ = create_company()
+ >>> company = get_company()
+
+Create fiscal year::
+
+ >>> fiscalyear = create_fiscalyear(company)
+ >>> fiscalyear.click('create_period')
+
+Create chart of accounts::
+
+ >>> _ = create_chart(company)
+ >>> accounts = get_accounts(company)
+
+ >>> expense_journal, = Journal.find([('code', '=', 'EXP')])
+
+Create payment journal::
+
+ >>> payment_journal = PaymentJournal(
+ ... name="Manual", process_method='manual')
+ >>> payment_journal.save()
+
+Create parties::
+
+ >>> supplier = Party(name="Supplier")
+ >>> supplier.save()
+ >>> supplier2 = Party(name="Supplier 2")
+ >>> supplier2.save()
+
+Create receivable moves::
+
+ >>> 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'
+ >>> move2, = move.duplicate()
+ >>> move2.click('post')
+
+Pay line::
+
+ >>> line, = [l for l in move.lines if l.account == accounts['payable']]
+ >>> pay_line = Wizard('account.move.line.pay', [line])
+ >>> pay_line.form.date = today
+ >>> pay_line.execute('next_')
+ >>> pay_line.execute('next_')
+ >>> payment, = Payment.find()
+
+Try to cancel move::
+
+ >>> cancel_move = Wizard('account.move.cancel', [move])
+ >>> cancel_move.execute('cancel') # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ CancelWarning: ...
+
+Try to group lines::
+
+ >>> line2, = [l for l in move2.lines if l.account == accounts['payable']]
+ >>> group_line = Wizard('account.move.line.group', [line, line2])
+ >>> group_line.execute('group') # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ GroupLineWarning: ...
+
+Try to reschedule line::
+
+ >>> reschedule_line = Wizard('account.move.line.reschedule', [line])
+ >>> reschedule_line.form.start_date = today
+ >>> reschedule_line.form.frequency = 'monthly'
+ >>> reschedule_line.form.interval = 1
+ >>> reschedule_line.form.amount = Decimal('50.00')
+ >>> reschedule_line.execute('preview')
+ >>> reschedule_line.execute('reschedule') # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ RescheduleLineWarning: ...
+
+Try to delegate line::
+
+ >>> delegate_line = Wizard('account.move.line.delegate', [line])
+ >>> delegate_line.form.party = supplier2
+ >>> delegate_line.execute('delegate') # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ DelegateLineWarning: ...