From 1348fb8ba34fb3b8cc807de92e2b7559bd94d6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 28 Sep 2019 23:42:07 +0200 Subject: [PATCH] Prevent using same sequence on earlier customer invoice issue5205 review267921002 --- CHANGELOG | 1 + invoice.py | 39 +++++++- message.xml | 3 + .../scenario_invoice_customer_sequential.rst | 91 +++++++++++++++++++ tests/test_account_invoice.py | 5 + 5 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 tests/scenario_invoice_customer_sequential.rst diff --git a/CHANGELOG b/CHANGELOG index 9553bfe4c3..bd655b5426 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Prevent using same sequence on earlier customer invoice * Update invoice sequence name in renew fiscal year * Add option to cancel customer invoice diff --git a/invoice.py b/invoice.py index 04d93f382c..22002321ea 100644 --- a/invoice.py +++ b/invoice.py @@ -109,6 +109,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): depends=['state']) accounting_date = fields.Date('Accounting Date', states=_STATES, depends=_DEPENDS) + sequence = fields.Many2One('ir.sequence.strict', "Sequence", readonly=True) party = fields.Many2One('party.party', 'Party', required=True, states=_STATES, depends=_DEPENDS) party_tax_identifier = fields.Many2One( @@ -981,6 +982,14 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): ''' pool = Pool() Date = pool.get('ir.date') + Lang = pool.get('ir.lang') + today = Date.today() + + def accounting_date(invoice): + return invoice.accounting_date or invoice.invoice_date or today + + invoices = sorted(invoices, key=accounting_date) + sequences = set() for invoice in invoices: # Posted and paid invoices are tested by check_modify so we can @@ -997,11 +1006,35 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): continue if not invoice.invoice_date and invoice.type == 'out': - invoice.invoice_date = Date.today() - invoice.number = invoice.get_next_number() + invoice.invoice_date = today + invoice.number, invoice.sequence = invoice.get_next_number() + if invoice.type == 'out' and invoice.sequence not in sequences: + date = accounting_date(invoice) + # Do not need to lock the table + # because sequence.get_id is sequential + invoices = cls.search([ + ('sequence', '=', invoice.sequence.id), + ['OR', + ('accounting_date', '>', date), + [ + ('accounting_date', '=', None), + ('invoice_date', '>', date), + ], + ], + ], limit=1) + if invoices: + after_invoice, = invoices + raise InvoiceNumberError( + gettext('account_invoice.msg_invoice_number_after', + invoice=invoice.rec_name, + sequence=invoice.sequence.rec_name, + date=Lang.get().strftime(date), + after_invoice=after_invoice.rec_name)) + sequences.add(invoice.sequence) cls.save(invoices) def get_next_number(self, pattern=None): + "Return invoice number and sequence used" pool = Pool() Sequence = pool.get('ir.sequence.strict') Period = pool.get('account.period') @@ -1039,7 +1072,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): invoice=self.rec_name, fiscalyear=fiscalyear.rec_name)) with Transaction().set_context(date=accounting_date): - return Sequence.get_id(sequence.id) + return Sequence.get_id(sequence.id), sequence @classmethod def tax_identifier_types(cls): diff --git a/message.xml b/message.xml index 5295c961fa..2f76fe2409 100644 --- a/message.xml +++ b/message.xml @@ -21,6 +21,9 @@ this repository contains the full copyright notices and license terms. --> To post invoice "%(invoice)s", you must define a sequence on fiscal year "%(fiscalyear)s". + + To number the invoice "%(invoice)s", you must set an accounting date after "%(date)s" because the sequence "%(sequence)s" has already been used for invoice "%(after_invoice)s". + You cannot modify invoice "%(invoice)s" because it is posted, paid or cancelled. diff --git a/tests/scenario_invoice_customer_sequential.rst b/tests/scenario_invoice_customer_sequential.rst new file mode 100644 index 0000000000..9213af237d --- /dev/null +++ b/tests/scenario_invoice_customer_sequential.rst @@ -0,0 +1,91 @@ +=========================== +Customer Invoice Sequential +=========================== + +Imports:: + + >>> from decimal import Decimal + >>> import datetime as dt + >>> from dateutil.relativedelta import relativedelta + >>> from proteus import Model + >>> 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.tests.tools import \ + ... set_fiscalyear_invoice_sequences + >>> today = dt.date.today() + >>> next_year = today + relativedelta(years=1) + +Install account_invoice:: + + >>> config = activate_modules('account_invoice') + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create fiscal years:: + + >>> fiscalyear = set_fiscalyear_invoice_sequences( + ... create_fiscalyear(company, today=today)) + >>> fiscalyear.click('create_period') + >>> next_fiscalyear = set_fiscalyear_invoice_sequences( + ... create_fiscalyear(company, today=next_year)) + >>> next_fiscalyear.click('create_period') + +Create chart of accounts:: + + >>> _ = create_chart(company) + >>> accounts = get_accounts(company) + +Create party:: + + >>> Party = Model.get('party.party') + >>> party = Party(name='Party') + >>> party.save() + +Create invoice invoice second period and next year:: + + >>> Invoice = Model.get('account.invoice') + + >>> invoice = Invoice(type='out') + >>> invoice.party = party + >>> invoice.accounting_date = fiscalyear.periods[1].start_date + >>> line = invoice.lines.new() + >>> line.quantity = 1 + >>> line.unit_price = Decimal('10') + >>> line.account = accounts['revenue'] + >>> invoice.click('post') + + >>> invoice = Invoice(type='out') + >>> invoice.party = party + >>> invoice.accounting_date = next_fiscalyear.periods[0].start_date + >>> line = invoice.lines.new() + >>> line.quantity = 1 + >>> line.unit_price = Decimal('20') + >>> line.account = accounts['revenue'] + >>> invoice.click('post') + +Try to post invoice on first period:: + + >>> invoice = Invoice(type='out') + >>> invoice.party = party + >>> invoice.accounting_date = fiscalyear.periods[0].start_date + >>> line = invoice.lines.new() + >>> line.quantity = 1 + >>> line.unit_price = Decimal('5') + >>> line.account = accounts['revenue'] + >>> invoice.save() + + >>> invoice.click('post') # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvoiceNumberError: ... + +Post invoice on the third period:: + + >>> invoice.accounting_date = fiscalyear.periods[2].start_date + >>> invoice.click('post') diff --git a/tests/test_account_invoice.py b/tests/test_account_invoice.py index 74fcbcf9e0..5c70e86f1d 100644 --- a/tests/test_account_invoice.py +++ b/tests/test_account_invoice.py @@ -225,6 +225,11 @@ def suite(): tearDown=doctest_teardown, encoding='utf-8', checker=doctest_checker, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) + suite.addTests(doctest.DocFileSuite( + 'scenario_invoice_customer_sequential.rst', + tearDown=doctest_teardown, encoding='utf-8', + checker=doctest_checker, + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) suite.addTests(doctest.DocFileSuite( 'scenario_invoice_alternate_currency.rst', tearDown=doctest_teardown, encoding='utf-8',