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