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()