From 63d7e39a09b7e2167f2ec673cffb9135ebc44346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nshimiyimana=20S=C3=A9na?= Date: Thu, 4 May 2023 06:54:01 +0000 Subject: [PATCH] [FIX] account: prevent sequence number gap in Accounting Firms mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When modifying the accounting date of an invoice in Accounting Firms mode, Odoo always generates a new invoice sequence number. This behavior can introduce gaps in the sequences. 1. Activate Accounting Firms mode. 2. Create a new Draft Invoice and save it. 3. Create a second Draft Invoice and save it. 4. Modify the accounting date of the first invoice and save it. 5. Observe that the first invoice's invoice sequence number is updated to the next sequence, creating a gap. Recompute the sequence only when the new date falls into a different fiscal year. opw-3164537 closes odoo/odoo#121215 X-original-commit: 28c5c37c420eda8f916dc0f031521313ac18ec08 Signed-off-by: Nshimiyimana Serge Séna (sesn) Signed-off-by: William André (wan) --- addons/account/models/account_move.py | 17 +++--- addons/account/models/sequence_mixin.py | 48 +++++++++++------ addons/account/tests/test_sequence_mixin.py | 57 ++++++++++++++++++++- 3 files changed, 95 insertions(+), 27 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index f431a621845..fcdade3e8eb 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -731,22 +731,19 @@ class AccountMove(models.Model): self = self.sorted(lambda m: (m.date, m.ref or '', m.id)) for move in self: - if not move.highest_name and not move.posted_before and move.date and (not move.name or move.name == '/'): + name_not_set = not move.name or move.name == '/' + if not move.highest_name and not move.posted_before and move.date and name_not_set: # In the form view, we need to compute a default sequence so that the user can edit # it. We only check the first move as an approximation (enough for new in form view) move._set_next_sequence() elif move.quick_edit_mode and not move.posted_before: # We always suggest the next sequence as the default name of the new move - move._set_next_sequence() - elif (move.name and move.name != '/') or move.state != 'posted': - try: - move._constrains_date_sequence() - # The name matches the date: we don't recompute - except ValidationError: - # Has never been posted and the name doesn't match the date: recompute it + if name_not_set or not move._sequence_matches_date(): move._set_next_sequence() - else: - # The name is not set yet and it is posted + elif not move.posted_before and not move._sequence_matches_date(): + # The date changed before posting on first move of period + move._set_next_sequence() + elif (name_not_set and move.state == 'posted'): move._set_next_sequence() self.filtered(lambda m: not m.name).name = '/' diff --git a/addons/account/models/sequence_mixin.py b/addons/account/models/sequence_mixin.py index e2c032fd71f..6b32b9d032a 100644 --- a/addons/account/models/sequence_mixin.py +++ b/addons/account/models/sequence_mixin.py @@ -50,6 +50,22 @@ class SequenceMixin(models.AbstractModel): def _must_check_constrains_date_sequence(self): return True + def _sequence_matches_date(self): + self.ensure_one() + date = fields.Date.to_date(self[self._sequence_date_field]) + sequence = self[self._sequence_field] + + if not sequence or not date: + return True + + format_values = self._get_sequence_format_param(sequence)[1] + year_match = ( + not format_values["year"] + or format_values["year"] == date.year % 10 ** len(str(format_values["year"])) + ) + month_match = not format_values['month'] or format_values['month'] == date.month + return year_match and month_match + @api.constrains(lambda self: (self._sequence_field, self._sequence_date_field)) def _constrains_date_sequence(self): # Make it possible to bypass the constraint to allow edition of already messed up documents. @@ -63,22 +79,22 @@ class SequenceMixin(models.AbstractModel): continue date = fields.Date.to_date(record[record._sequence_date_field]) sequence = record[record._sequence_field] - if sequence and date and date > constraint_date: - format_values = record._get_sequence_format_param(sequence)[1] - if ( - format_values['year'] and format_values['year'] != date.year % 10**len(str(format_values['year'])) - or format_values['month'] and format_values['month'] != date.month - ): - raise ValidationError(_( - "The %(date_field)s (%(date)s) doesn't match the sequence number of the related %(model)s (%(sequence)s)\n" - "You will need to clear the %(model)s's %(sequence_field)s to proceed.\n" - "In doing so, you might want to resequence your entries in order to maintain a continuous date-based sequence.", - date=format_date(self.env, date), - sequence=sequence, - date_field=record._fields[record._sequence_date_field]._description_string(self.env), - sequence_field=record._fields[record._sequence_field]._description_string(self.env), - model=self.env['ir.model']._get(record._name).display_name, - )) + if ( + sequence + and date + and date > constraint_date + and not record._sequence_matches_date() + ): + raise ValidationError(_( + "The %(date_field)s (%(date)s) doesn't match the sequence number of the related %(model)s (%(sequence)s)\n" + "You will need to clear the %(model)s's %(sequence_field)s to proceed.\n" + "In doing so, you might want to resequence your entries in order to maintain a continuous date-based sequence.", + date=format_date(self.env, date), + sequence=sequence, + date_field=record._fields[record._sequence_date_field]._description_string(self.env), + sequence_field=record._fields[record._sequence_field]._description_string(self.env), + model=self.env['ir.model']._get(record._name).display_name, + )) @api.depends(lambda self: [self._sequence_field]) def _compute_split_sequence(self): diff --git a/addons/account/tests/test_sequence_mixin.py b/addons/account/tests/test_sequence_mixin.py index 47ceddf30c4..7f91130191c 100644 --- a/addons/account/tests/test_sequence_mixin.py +++ b/addons/account/tests/test_sequence_mixin.py @@ -2,7 +2,7 @@ from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests import tagged from odoo.tests.common import Form, TransactionCase -from odoo import fields, api, SUPERUSER_ID +from odoo import fields, api, SUPERUSER_ID, Command from odoo.exceptions import ValidationError, UserError from odoo.tools import mute_logger @@ -66,6 +66,61 @@ class TestSequenceMixin(TestSequenceMixinCommon): self.test_move.action_post() self.assertEqual(self.test_move.name, 'MyMISC/2020/0000001') + def test_sequence_change_date_with_quick_edit_mode(self): + """ + Test the sequence update behavior when changing the date of a move in quick edit mode. + The sequence should only be recalculated if a value (year or month) utilized in the sequence is modified. + """ + self.env.company.quick_edit_mode = "out_and_in_invoices" + self.env.company.fiscalyear_last_day = 30 + self.env.company.fiscalyear_last_month = '12' + + bill = self.env['account.move'].create({ + 'partner_id': 1, + 'move_type': 'in_invoice', + 'date': '2016-01-01', + 'line_ids': [ + Command.create({ + 'name': 'line', + 'account_id': self.company_data['default_account_revenue'].id, + }), + ] + }) + bill = bill.copy({'date': '2016-01-01'}) + bill.copy({'date': '2016-01-01'}) + + self.assertEqual(bill.name, 'BILL/2016/01/0002') + with Form(bill) as bill_form: + bill_form.date = '2016-01-02' + self.assertEqual(bill_form.name, 'BILL/2016/01/0002') + bill_form.date = '2016-02-02' + self.assertEqual(bill_form.name, 'BILL/2016/02/0001') + bill_form.date = '2017-01-01' + self.assertEqual(bill_form.name, 'BILL/2017/01/0001') + + invoice = self.env['account.move'].create({ + 'partner_id': 1, + 'move_type': 'out_invoice', + 'date': '2016-01-01', + 'line_ids': [ + Command.create({ + 'name': 'line', + 'account_id': self.company_data['default_account_revenue'].id, + }), + ] + }) + invoice = invoice.copy({'date': '2016-01-01'}) + invoice.copy({'date': '2016-01-01'}) + + self.assertEqual(invoice.name, 'INV/2016/00002') + with Form(invoice) as invoice_form: + invoice_form.date = '2016-01-02' + self.assertEqual(invoice_form.name, 'INV/2016/00002') + invoice_form.date = '2016-02-02' + self.assertEqual(invoice_form.name, 'INV/2016/00002') + invoice_form.date = '2017-01-01' + self.assertEqual(invoice_form.name, 'INV/2017/00001') + def test_journal_sequence(self): self.assertEqual(self.test_move.name, 'MISC/2016/01/0001') self.test_move.action_post()