From 0d679effe2843b190cf839d052dfc0c0db055cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Thu, 2 Feb 2023 18:50:26 +0100 Subject: [PATCH] Warn when cancelling a move related to an invoice Closes #4981 --- modules/account_invoice/CHANGELOG | 2 + modules/account_invoice/__init__.py | 1 + modules/account_invoice/account.py | 34 ++++++ modules/account_invoice/exceptions.py | 4 + modules/account_invoice/message.xml | 6 + .../scenario_cancelling_invoice_move.rst | 106 ++++++++++++++++++ 6 files changed, 153 insertions(+) create mode 100644 modules/account_invoice/tests/scenario_cancelling_invoice_move.rst diff --git a/modules/account_invoice/CHANGELOG b/modules/account_invoice/CHANGELOG index 6bd07bd43a..04f8040402 100644 --- a/modules/account_invoice/CHANGELOG +++ b/modules/account_invoice/CHANGELOG @@ -1,3 +1,5 @@ +* Warn when cancelling a move related to an invoice + Version 6.6.0 - 2022-10-31 -------------------------- * Bug fixes (see mercurial logs for details) diff --git a/modules/account_invoice/__init__.py b/modules/account_invoice/__init__.py index 275fda6026..9920570489 100644 --- a/modules/account_invoice/__init__.py +++ b/modules/account_invoice/__init__.py @@ -51,6 +51,7 @@ def register(): account.RenewFiscalYear, account.RescheduleLines, account.DelegateLines, + account.CancelMoves, module='account_invoice', type_='wizard') Pool.register( invoice.InvoiceReport, diff --git a/modules/account_invoice/account.py b/modules/account_invoice/account.py index 35a55736b0..6b70818e53 100644 --- a/modules/account_invoice/account.py +++ b/modules/account_invoice/account.py @@ -1,6 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from collections import OrderedDict +from itertools import islice from sql import Literal from sql.conditionals import Coalesce @@ -15,6 +16,8 @@ from trytond.pyson import Bool, Eval, Id, If from trytond.tools import grouped_slice from trytond.transaction import Transaction +from .exceptions import CancelInvoiceMoveWarning + class Configuration(metaclass=PoolMeta): __name__ = 'account.configuration' @@ -461,3 +464,34 @@ class DelegateLines(metaclass=PoolMeta): 'additional_moves': [('add', [move.id])], }) return move + + +class CancelMoves(metaclass=PoolMeta): + __name__ = 'account.move.cancel' + + def transition_cancel(self): + pool = Pool() + Invoice = pool.get('account.invoice') + Warning = pool.get('res.user.warning') + + moves_w_invoices = { + m: m.origin for m in self.records + if (isinstance(m.origin, Invoice) + and m.origin.state not in {'paid', 'cancelled'})} + if moves_w_invoices: + move_names = ', '.join(m.rec_name + for m in islice(moves_w_invoices, None, 5)) + invoice_names = ', '.join(i.rec_name + for i in islice(moves_w_invoices.values(), None, 5)) + if len(moves_w_invoices) > 5: + move_names += '...' + invoice_names += '...' + key = Warning.format('cancel_invoice_move', moves_w_invoices) + if Warning.check(key): + raise CancelInvoiceMoveWarning(key, + gettext('account_invoice.msg_cancel_invoice_move', + moves=move_names, invoices=invoice_names), + gettext( + 'account_invoice.msg_cancel_invoice_move_description')) + + return super().transition_cancel() diff --git a/modules/account_invoice/exceptions.py b/modules/account_invoice/exceptions.py index 8e540c69fd..3aacd58b94 100644 --- a/modules/account_invoice/exceptions.py +++ b/modules/account_invoice/exceptions.py @@ -43,3 +43,7 @@ class InvoiceFutureWarning(UserWarning): class InvoiceSimilarWarning(UserWarning): pass + + +class CancelInvoiceMoveWarning(UserWarning): + pass diff --git a/modules/account_invoice/message.xml b/modules/account_invoice/message.xml index 3a74e9ea3e..ec98e0130e 100644 --- a/modules/account_invoice/message.xml +++ b/modules/account_invoice/message.xml @@ -84,6 +84,12 @@ this repository contains the full copyright notices and license terms. --> The invoice "%(invoice)s" is similar to invoice "%(similar)s". + + The moves "%(moves)s" have the posted invoices "%(invoices)s" as origin. + + + Cancelling them will reconcile the move lines thus paying the invoices. You might want to cancel the invoices first. + diff --git a/modules/account_invoice/tests/scenario_cancelling_invoice_move.rst b/modules/account_invoice/tests/scenario_cancelling_invoice_move.rst new file mode 100644 index 0000000000..67b3146dab --- /dev/null +++ b/modules/account_invoice/tests/scenario_cancelling_invoice_move.rst @@ -0,0 +1,106 @@ +================================ +Cancelling Invoice Move Scenario +================================ + +Imports:: + + >>> from decimal import Decimal + >>> 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) + >>> from trytond.modules.account_invoice.exceptions import ( + ... CancelInvoiceMoveWarning) + >>> from trytond.modules.account_invoice.tests.tools import ( + ... set_fiscalyear_invoice_sequences) + +Activate modules:: + + >>> config = activate_modules('account_invoice') + >>> Party = Model.get('party.party') + >>> ProductCategory = Model.get('product.category') + >>> ProductUom = Model.get('product.uom') + >>> ProductTemplate = Model.get('product.template') + >>> Invoice = Model.get('account.invoice') + >>> Warning = Model.get('res.user.warning') + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create fiscal year:: + + >>> fiscalyear = set_fiscalyear_invoice_sequences( + ... create_fiscalyear(company)) + >>> fiscalyear.click('create_period') + +Create chart of accounts:: + + >>> _ = create_chart(company) + >>> accounts = get_accounts(company) + +Create party:: + + >>> party = Party(name="Party") + >>> party.save() + +Create account category:: + + >>> account_category = ProductCategory(name="Account Category") + >>> account_category.accounting = True + >>> account_category.account_expense = accounts['expense'] + >>> account_category.account_revenue = accounts['revenue'] + >>> account_category.save() + +Create product:: + + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> template = ProductTemplate() + >>> template.name = 'product' + >>> template.default_uom = unit + >>> template.type = 'service' + >>> template.list_price = Decimal('40') + >>> template.account_category = account_category + >>> template.save() + >>> product, = template.products + +Create invoice:: + + >>> invoice = Invoice() + >>> invoice.party = party + >>> line = invoice.lines.new() + >>> line.product = product + >>> line.quantity = 5 + >>> line.unit_price = Decimal('40') + +Post invoice and cancel the created move:: + + >>> invoice.click('post') + >>> invoice.state + 'posted' + >>> cancel_move = Wizard('account.move.cancel', [invoice.move]) + >>> cancel_move.form.description = 'Cancel' + >>> cancel_move.execute('cancel') # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + CancelInvoiceMoveWarning: ... + +Bypass the warning and cancel the move:: + + >>> try: + ... cancel_move.execute('cancel') + ... except CancelInvoiceMoveWarning as e: + ... Warning(user=config.user, name=e.name).save() + >>> cancel_move.execute('cancel') + >>> cancel_move.state + 'end' + + >>> invoice.reload() + >>> [bool(l.reconciliation) for l in invoice.move.lines + ... if l.account == accounts['receivable']] + [True] + >>> invoice.state + 'paid'