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',