From 33e7892bca6793e3ad5eae5b64b2e2365b0b4e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 14 Feb 2022 00:11:38 +0100 Subject: [PATCH] Raise a warning when posting supplier invoice with same reference issue8244 review265201002 --- CHANGELOG | 1 + exceptions.py | 4 ++ invoice.py | 59 ++++++++++++++++++++++++++++- message.xml | 3 ++ tests/scenario_invoice_supplier.rst | 22 +++++++++++ 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 811d8147cc..5c24a6e697 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Raise a warning when validating supplier invoice with same reference * Add support for Python 3.10 * Remove support for Python 3.6 diff --git a/exceptions.py b/exceptions.py index b0c038f1ac..8e540c69fd 100644 --- a/exceptions.py +++ b/exceptions.py @@ -39,3 +39,7 @@ class InvoicePaymentTermDateWarning(UserWarning): class InvoiceFutureWarning(UserWarning): pass + + +class InvoiceSimilarWarning(UserWarning): + pass diff --git a/invoice.py b/invoice.py index 415092df38..e7f652a527 100644 --- a/invoice.py +++ b/invoice.py @@ -1,5 +1,6 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. +import datetime as dt from collections import defaultdict, namedtuple from decimal import Decimal from itertools import chain, combinations, groupby @@ -30,8 +31,8 @@ from trytond.wizard import ( from .exceptions import ( InvoiceFutureWarning, InvoiceLineValidationError, InvoiceNumberError, - InvoicePaymentTermDateWarning, InvoiceTaxValidationError, - InvoiceValidationError, PayInvoiceError) + InvoicePaymentTermDateWarning, InvoiceSimilarWarning, + InvoiceTaxValidationError, InvoiceValidationError, PayInvoiceError) if config.getboolean('account_invoice', 'filestore', default=False): file_id = 'invoice_report_cache_id' @@ -1524,6 +1525,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): invoices_in = cls.browse([i for i in invoices if i.type == 'in']) cls.set_number(invoices_in) + cls._check_similar(invoices) moves = [] for invoice in invoices_in: move = invoice.get_move() @@ -1576,6 +1578,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): raise InvoiceFutureWarning(warning_key, gettext('account_invoice.msg_invoice_date_future', invoices=names)) + cls._check_similar([i for i in invoices if i.state != 'validated']) cls._post(invoices) @classmethod @@ -1605,6 +1608,58 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): if reconciled: cls.__queue__.process(reconciled) + @classmethod + def _check_similar(cls, invoices, type='in'): + pool = Pool() + Warning = pool.get('res.user.warning') + for sub_invoices in grouped_slice(invoices): + sub_invoices = list(sub_invoices) + domain = list(filter(None, + (i._similar_domain() for i in sub_invoices + if i.type == type))) + if not domain: + continue + if cls.search(['OR'] + domain, order=[]): + for invoice in sub_invoices: + domain = invoice._similar_domain() + if not domain: + continue + try: + similar, = cls.search(domain, limit=1) + except ValueError: + continue + warning_key = Warning.format( + 'invoice_similar', [invoice]) + if Warning.check(warning_key): + raise InvoiceSimilarWarning(warning_key, + gettext('account_invoice.msg_invoice_similar', + similar=similar.rec_name, + invoice=invoice.rec_name)) + + def _similar_domain(self, delay=None): + pool = Pool() + Date = pool.get('ir.date') + if not self.reference: + return + with Transaction().set_context(company=self.company.id): + invoice_date = self.invoice_date or Date.today() + if delay is None: + delay = dt.timedelta(days=60) + return [ + ('company', '=', self.company.id), + ('type', '=', self.type), + ('party', '=', self.party.id), + ('reference', '=', self.reference), + ('id', '!=', self.id), + ['OR', + ('invoice_date', '=', None), + [ + ('invoice_date', '>=', invoice_date - delay), + ('invoice_date', '<=', invoice_date + delay), + ], + ], + ] + @classmethod @ModelView.button_action('account_invoice.wizard_pay') def pay(cls, invoices): diff --git a/message.xml b/message.xml index 95c49632b1..e96332a25a 100644 --- a/message.xml +++ b/message.xml @@ -81,6 +81,9 @@ this repository contains the full copyright notices and license terms. --> The invoices "%(invoices)s" have an invoice date in the future. + + The invoice "%(invoice)s" is similar to invoice "%(similar)s". + diff --git a/tests/scenario_invoice_supplier.rst b/tests/scenario_invoice_supplier.rst index c7a8ae4b53..a4c9c0bd72 100644 --- a/tests/scenario_invoice_supplier.rst +++ b/tests/scenario_invoice_supplier.rst @@ -3,6 +3,7 @@ Invoice Supplier Scenario ========================= Imports:: + >>> import datetime >>> from dateutil.relativedelta import relativedelta >>> from decimal import Decimal @@ -102,6 +103,7 @@ Create invoice:: >>> invoice.party = party >>> invoice.payment_term = payment_term >>> invoice.invoice_date = today + >>> invoice.reference = 'FAC001' >>> line = InvoiceLine() >>> invoice.lines.append(line) >>> line.product = product @@ -192,6 +194,26 @@ Credit invoice:: >>> credit_note.total_amount == -invoice.total_amount True +A warning is raised when creating an invoice with same reference:: + + >>> invoice = Invoice() + >>> invoice.type = 'in' + >>> invoice.party = party + >>> invoice.invoice_date = today + >>> invoice.reference = 'FAC001' + >>> line = invoice.lines.new() + >>> line.product = product + >>> line.quantity = 1 + >>> line.unit_price = Decimal('20') + >>> invoice.click('post') # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvoiceSimilarWarning: ... + >>> invoice.reference = 'FAC002' + >>> invoice.click('post') + >>> invoice.state + 'posted' + Create a posted and a draft invoice to cancel:: >>> invoice = Invoice()