From e7df30a565804d2130ecbbcd201c068073dbc16f Mon Sep 17 00:00:00 2001 From: william Date: Thu, 24 Nov 2022 12:01:04 +0000 Subject: [PATCH] [IMP] account{,_sequence}: merge This module was added in stable but was never intended as a separate module. closes odoo/odoo#106413 Related: odoo/upgrade#4073 Related: odoo/enterprise#37844 Signed-off-by: Josse Colpaert --- addons/account/models/account_move.py | 59 +++++++-------- addons/account/models/sequence_mixin.py | 40 ++++++---- addons/account_sequence/__init__.py | 1 - addons/account_sequence/__manifest__.py | 16 ---- addons/account_sequence/models/__init__.py | 2 - .../account_sequence/models/account_move.py | 54 -------------- .../account_sequence/models/sequence_mixin.py | 45 ----------- .../l10n_latam_account_sequence/__init__.py | 1 - .../__manifest__.py | 16 ---- .../models/__init__.py | 1 - .../models/account_move.py | 74 ------------------- .../models/account_move.py | 48 +++++------- 12 files changed, 69 insertions(+), 288 deletions(-) delete mode 100644 addons/account_sequence/__init__.py delete mode 100644 addons/account_sequence/__manifest__.py delete mode 100644 addons/account_sequence/models/__init__.py delete mode 100644 addons/account_sequence/models/account_move.py delete mode 100644 addons/account_sequence/models/sequence_mixin.py delete mode 100644 addons/l10n_latam_account_sequence/__init__.py delete mode 100644 addons/l10n_latam_account_sequence/__manifest__.py delete mode 100644 addons/l10n_latam_account_sequence/models/__init__.py delete mode 100644 addons/l10n_latam_account_sequence/models/account_move.py diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 8b2cfacfd0d..6cf6ed74e03 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -28,8 +28,8 @@ from odoo.tools import ( formatLang, frozendict, get_lang, + index_exists, is_html_empty, - sql ) _logger = logging.getLogger(__name__) @@ -575,14 +575,29 @@ class AccountMove(models.Model): show_payment_term_details = fields.Boolean(compute="_compute_show_payment_term_details") show_discount_details = fields.Boolean(compute="_compute_show_payment_term_details") + _sql_constraints = [( + 'unique_name', "", "Another entry with the same name already exists.", + )] + def _auto_init(self): super()._auto_init() - self.env.cr.execute(""" - CREATE INDEX IF NOT EXISTS account_move_to_check_idx - ON account_move(journal_id) WHERE to_check = true; - CREATE INDEX IF NOT EXISTS account_move_payment_idx - ON account_move(journal_id, state, payment_state, move_type, date); - """) + if not index_exists(self.env.cr, 'account_move_to_check_idx'): + self.env.cr.execute(""" + CREATE INDEX account_move_to_check_idx + ON account_move(journal_id) + WHERE to_check = true + """) + if not index_exists(self.env.cr, 'account_move_payment_idx'): + self.env.cr.execute(""" + CREATE INDEX account_move_payment_idx + ON account_move(journal_id, state, payment_state, move_type, date) + """) + if not index_exists(self.env.cr, 'account_move_unique_name'): + self.env.cr.execute(""" + CREATE UNIQUE INDEX account_move_unique_name + ON account_move(name, journal_id) + WHERE (state = 'posted' AND name != '/') + """) # ------------------------------------------------------------------------- # COMPUTE METHODS @@ -723,7 +738,7 @@ class AccountMove(models.Model): @api.depends('journal_id', 'date') def _compute_highest_name(self): for record in self: - record.highest_name = record._get_last_sequence(lock=False) + record.highest_name = record._get_last_sequence() @api.depends('name', 'journal_id') def _compute_made_sequence_hole(self): @@ -1671,7 +1686,7 @@ class AccountMove(models.Model): @api.onchange('journal_id') def _onchange_journal_id(self): - if not self.quick_edit_mode and self._get_last_sequence(lock=False): + if not self.quick_edit_mode and self._get_last_sequence(): self.name = '/' self._compute_name() @@ -1679,30 +1694,6 @@ class AccountMove(models.Model): # CONSTRAINT METHODS # ------------------------------------------------------------------------- - @api.constrains('name', 'journal_id', 'state') - def _check_unique_sequence_number(self): - moves = self.filtered(lambda move: move.state == 'posted') - if not moves: - return - - self.flush_model(['name', 'journal_id', 'move_type', 'state']) - - # /!\ Computed stored fields are not yet inside the database. - self._cr.execute(''' - SELECT move2.id, move2.name - FROM account_move move - INNER JOIN account_move move2 ON - move2.name = move.name - AND move2.journal_id = move.journal_id - AND move2.move_type = move.move_type - AND move2.id != move.id - WHERE move.id IN %s AND move2.state = 'posted' - ''', [tuple(moves.ids)]) - res = self._cr.fetchall() - if res: - raise ValidationError(_('Posted journal entry must have an unique sequence number per company.\n' - 'Problematic numbers: %s\n') % ', '.join(r[1] for r in res)) - @contextmanager def _check_balanced(self, container): ''' Assert the move is fully balanced debit = credit. @@ -3854,7 +3845,7 @@ class AccountMove(models.Model): """ lock_dates = self._get_violated_lock_dates(invoice_date, has_tax) today = fields.Date.today() - highest_name = self.highest_name or self._get_last_sequence(relaxed=True, lock=False) + highest_name = self.highest_name or self._get_last_sequence(relaxed=True) number_reset = self._deduce_sequence_number_reset(highest_name) if lock_dates: invoice_date = lock_dates[-1][0] + timedelta(days=1) diff --git a/addons/account/models/sequence_mixin.py b/addons/account/models/sequence_mixin.py index 5bd725d241b..9f3dcc23284 100644 --- a/addons/account/models/sequence_mixin.py +++ b/addons/account/models/sequence_mixin.py @@ -3,11 +3,11 @@ from odoo import api, fields, models, _ from odoo.exceptions import ValidationError from odoo.tools.misc import format_date -from odoo.tools import frozendict +from odoo.tools import frozendict, mute_logger import re from collections import defaultdict -from psycopg2 import sql +from psycopg2 import sql, DatabaseError class SequenceMixin(models.AbstractModel): @@ -138,7 +138,7 @@ class SequenceMixin(models.AbstractModel): self.ensure_one() return "00000000" - def _get_last_sequence(self, relaxed=False, with_prefix=None, lock=True): + def _get_last_sequence(self, relaxed=False, with_prefix=None): """Retrieve the previous sequence. This is done by taking the number with the greatest alphabetical value within @@ -171,21 +171,12 @@ class SequenceMixin(models.AbstractModel): param['with_prefix'] = with_prefix query = f""" - SELECT {{field}} FROM {self._table} + SELECT {self._sequence_field} FROM {self._table} {where_string} AND sequence_prefix = (SELECT sequence_prefix FROM {self._table} {where_string} ORDER BY id DESC LIMIT 1) ORDER BY sequence_number DESC LIMIT 1 """ - if lock: - query = f""" - UPDATE {self._table} SET write_date = write_date WHERE id = ( - {query.format(field='id')} - ) - RETURNING {self._sequence_field}; - """ - else: - query = query.format(field=self._sequence_field) self.flush_model([self._sequence_field, 'sequence_number', 'sequence_prefix']) self.env.cr.execute(query, param) @@ -242,15 +233,32 @@ class SequenceMixin(models.AbstractModel): if new: last_sequence = self._get_last_sequence(relaxed=True) or self._get_starting_sequence() - format, format_values = self._get_sequence_format_param(last_sequence) + format_string, format_values = self._get_sequence_format_param(last_sequence) if new: format_values['seq'] = 0 format_values['year'] = self[self._sequence_date_field].year % (10 ** format_values['year_length']) format_values['month'] = self[self._sequence_date_field].month - format_values['seq'] = format_values['seq'] + 1 - self[self._sequence_field] = format.format(**format_values) + # before flushing inside the savepoint (which may be rolled back!), make sure everything + # is already flushed, otherwise we could lose non-sequence fields values, as the ORM believes + # them to be flushed. + self.flush_recordset() + while True: + format_values['seq'] = format_values['seq'] + 1 + sequence = format_string.format(**format_values) + try: + with self.env.cr.savepoint(flush=False), mute_logger('odoo.sql_db'): + self[self._sequence_field] = sequence + self.flush_recordset([self._sequence_field]) + break + except DatabaseError as e: + # 23P01 ExclusionViolation + # 23505 UniqueViolation + if e.pgcode not in ('23P01', '23505'): + raise e self._compute_split_sequence() + self.flush_recordset(['sequence_prefix', 'sequence_number']) + def _is_last_from_seq_chain(self): """Tells whether or not this element is the last one of the sequence chain. diff --git a/addons/account_sequence/__init__.py b/addons/account_sequence/__init__.py deleted file mode 100644 index 0650744f6bc..00000000000 --- a/addons/account_sequence/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import models diff --git a/addons/account_sequence/__manifest__.py b/addons/account_sequence/__manifest__.py deleted file mode 100644 index 30ad2e97c86..00000000000 --- a/addons/account_sequence/__manifest__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -# Copyright (c) 2011 CCI Connect asbl (http://www.cciconnect.be) All Rights Reserved. -# Philmer - -{ - 'name': 'Accounting Sequence', - 'version': '1.0', - 'category': 'Hidden', - 'description': "Change the way `sequence.mixin` works to reduce concurrency errors", - 'depends': ['account'], - 'installable': True, - 'auto_install': True, - 'license': 'LGPL-3', -} diff --git a/addons/account_sequence/models/__init__.py b/addons/account_sequence/models/__init__.py deleted file mode 100644 index a23717850bb..00000000000 --- a/addons/account_sequence/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import account_move -from . import sequence_mixin diff --git a/addons/account_sequence/models/account_move.py b/addons/account_sequence/models/account_move.py deleted file mode 100644 index f9b16a25b65..00000000000 --- a/addons/account_sequence/models/account_move.py +++ /dev/null @@ -1,54 +0,0 @@ -from odoo import models -from odoo.tools import index_exists - - -class AccountMove(models.Model): - _inherit = 'account.move' - - _sql_constraints = [( - 'unique_name', "", "Another entry with the same name already exists.", - )] - - def _auto_init(self): - super()._auto_init() - if not index_exists(self.env.cr, 'account_move_unique_name'): - # Make all values of `name` different (naming them `name (1)`, `name (2)`...) so that we can add the following UNIQUE INDEX - self.env.cr.execute(""" - WITH duplicated_sequence AS ( - SELECT name, journal_id, state - FROM account_move - WHERE state = 'posted' - AND name != '/' - GROUP BY journal_id, name, state - HAVING COUNT(*) > 1 - ), - to_update AS ( - SELECT move.id, - move.name, - move.journal_id, - move.state, - move.date, - row_number() OVER(PARTITION BY move.name, move.journal_id ORDER BY move.name, move.journal_id, move.date) AS row_seq - FROM duplicated_sequence - JOIN account_move move ON move.name = duplicated_sequence.name - AND move.journal_id = duplicated_sequence.journal_id - AND move.state = duplicated_sequence.state - ), - new_vals AS ( - SELECT id, - name || ' (' || (row_seq-1)::text || ')' AS name - FROM to_update - WHERE row_seq > 1 - ) - UPDATE account_move - SET name = new_vals.name - FROM new_vals - WHERE account_move.id = new_vals.id; - """) - self.env.cr.execute(""" - CREATE UNIQUE INDEX account_move_unique_name - ON account_move(name, journal_id) WHERE (state = 'posted' AND name != '/'); - """) - - def _check_unique_sequence_number(self): - return diff --git a/addons/account_sequence/models/sequence_mixin.py b/addons/account_sequence/models/sequence_mixin.py deleted file mode 100644 index bb6a25ab99e..00000000000 --- a/addons/account_sequence/models/sequence_mixin.py +++ /dev/null @@ -1,45 +0,0 @@ -from psycopg2 import DatabaseError - -from odoo import models -from odoo.tools import mute_logger - - -class SequenceMixin(models.AbstractModel): - _inherit = 'sequence.mixin' - - def _get_last_sequence(self, relaxed=False, with_prefix=None, lock=True): - return super()._get_last_sequence(relaxed, with_prefix, False) - - def _set_next_sequence(self): - # OVERRIDE - self.ensure_one() - last_sequence = self._get_last_sequence() - new = not last_sequence - if new: - last_sequence = self._get_last_sequence(relaxed=True) or self._get_starting_sequence() - - format_string, format_values = self._get_sequence_format_param(last_sequence) - if new: - format_values['seq'] = 0 - format_values['year'] = self[self._sequence_date_field].year % (10 ** format_values['year_length']) - format_values['month'] = self[self._sequence_date_field].month - - # before flushing inside the savepoint (which may be rolled back!), make sure everything - # is already flushed, otherwise we could lose non-sequence fields values, as the ORM believes - # them to be flushed. - self.flush_recordset() - while True: - format_values['seq'] = format_values['seq'] + 1 - sequence = format_string.format(**format_values) - try: - with self.env.cr.savepoint(flush=False), mute_logger('odoo.sql_db'): - self[self._sequence_field] = sequence - self.flush_recordset([self._sequence_field]) - break - except DatabaseError as e: - # 23P01 ExclusionViolation - # 23505 UniqueViolation - if e.pgcode not in ('23P01', '23505'): - raise e - self._compute_split_sequence() - self.flush_recordset(['sequence_prefix', 'sequence_number']) diff --git a/addons/l10n_latam_account_sequence/__init__.py b/addons/l10n_latam_account_sequence/__init__.py deleted file mode 100644 index 0650744f6bc..00000000000 --- a/addons/l10n_latam_account_sequence/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import models diff --git a/addons/l10n_latam_account_sequence/__manifest__.py b/addons/l10n_latam_account_sequence/__manifest__.py deleted file mode 100644 index 4737446d1cd..00000000000 --- a/addons/l10n_latam_account_sequence/__manifest__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -# Copyright (c) 2011 CCI Connect asbl (http://www.cciconnect.be) All Rights Reserved. -# Philmer - -{ - 'name': 'Accounting Sequence - Latam Documents', - 'version': '1.0', - 'category': 'Hidden', - 'description': "Change the way `sequence.mixin` works to reduce concurrency errors", - 'depends': ['l10n_latam_invoice_document', 'account_sequence'], - 'installable': True, - 'auto_install': True, - 'license': 'LGPL-3', -} diff --git a/addons/l10n_latam_account_sequence/models/__init__.py b/addons/l10n_latam_account_sequence/models/__init__.py deleted file mode 100644 index 9c0a4213854..00000000000 --- a/addons/l10n_latam_account_sequence/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import account_move diff --git a/addons/l10n_latam_account_sequence/models/account_move.py b/addons/l10n_latam_account_sequence/models/account_move.py deleted file mode 100644 index a12fd8963c6..00000000000 --- a/addons/l10n_latam_account_sequence/models/account_move.py +++ /dev/null @@ -1,74 +0,0 @@ -from odoo import models -from odoo.tools.sql import index_exists, drop_index - - -class AccountMove(models.Model): - _inherit = 'account.move' - - _sql_constraints = [( - 'unique_name', "", "Another entry with the same name already exists.", - ), ( - 'unique_name_latam', "", "Another entry with the same name already exists.", - )] - - def _auto_init(self): - super()._auto_init() - # Update the generic unique name constraint to not consider the purchases in latam companies. - # The name should be unique by partner for those documents. - if not index_exists(self.env.cr, "account_move_unique_name_latam"): - drop_index(self.env.cr, "account_move_unique_name", self._table) - - # Fixup the account.move names like "sequence (N)" removing the "(N)" part - self.env.cr.execute(""" - UPDATE account_move SET name = SUBSTRING(account_move.name, 1, strpos(account_move.name::varchar, ' ('::varchar) -1 ) - WHERE l10n_latam_document_type_id IS NOT NULL AND account_move.name LIKE '%(%)' - AND move_type IN ('in_invoice', 'in_refund', 'in_receipt');""") - - # Make all values of `name` different (naming them `name (1)`, `name (2)`...) so that we can add the following UNIQUE INDEX - self.env.cr.execute(""" - WITH duplicated_sequence AS ( - SELECT name, commercial_partner_id, l10n_latam_document_type_id, state - FROM account_move - WHERE state = 'posted' - AND name != '/' - AND (l10n_latam_document_type_id IS NOT NULL AND move_type IN ('in_invoice', 'in_refund', 'in_receipt')) - GROUP BY commercial_partner_id, l10n_latam_document_type_id, name, state - HAVING COUNT(*) > 1 - ), - to_update AS ( - SELECT move.id, - move.name, - move.state, - move.date, - row_number() OVER(PARTITION BY move.name, move.commercial_partner_id, move.l10n_latam_document_type_id ORDER BY move.name, move.commercial_partner_id, move.l10n_latam_document_type_id, move.date) AS row_seq - FROM duplicated_sequence - JOIN account_move move ON move.name = duplicated_sequence.name - AND move.commercial_partner_id = duplicated_sequence.commercial_partner_id - AND move.l10n_latam_document_type_id = duplicated_sequence.l10n_latam_document_type_id - AND move.state = duplicated_sequence.state - ), - new_vals AS ( - SELECT id, - name || ' (' || (row_seq-1)::text || ')' AS name - FROM to_update - WHERE row_seq > 1 - ) - UPDATE account_move - SET name = new_vals.name - FROM new_vals - WHERE account_move.id = new_vals.id; - """) - - self.env.cr.execute(""" - CREATE UNIQUE INDEX account_move_unique_name - ON account_move(name, journal_id) - WHERE (state = 'posted' AND name != '/' - AND (l10n_latam_document_type_id IS NULL OR move_type NOT IN ('in_invoice', 'in_refund', 'in_receipt'))); - CREATE UNIQUE INDEX account_move_unique_name_latam - ON account_move(name, commercial_partner_id, l10n_latam_document_type_id, company_id) - WHERE (state = 'posted' AND name != '/' - AND (l10n_latam_document_type_id IS NOT NULL AND move_type IN ('in_invoice', 'in_refund', 'in_receipt'))); - """) - - def _check_unique_vendor_number(self): - return diff --git a/addons/l10n_latam_invoice_document/models/account_move.py b/addons/l10n_latam_invoice_document/models/account_move.py index f7fcd06da78..99a7ed74144 100644 --- a/addons/l10n_latam_invoice_document/models/account_move.py +++ b/addons/l10n_latam_invoice_document/models/account_move.py @@ -4,13 +4,19 @@ from collections import defaultdict from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError -from odoo.tools.sql import column_exists, create_column +from odoo.tools.sql import column_exists, create_column, drop_index, index_exists class AccountMove(models.Model): _inherit = "account.move" + _sql_constraints = [( + 'unique_name', "", "Another entry with the same name already exists.", + ), ( + 'unique_name_latam', "", "Another entry with the same name already exists.", + )] + def _auto_init(self): # Skip the computation of the field `l10n_latam_document_type_id` at the module installation # Without this, at the module installation, @@ -41,6 +47,19 @@ class AccountMove(models.Model): # for a Chilian or Argentian company (`res.company`) before installing `l10n_cl` or `l10n_ar`. if not column_exists(self.env.cr, "account_move", "l10n_latam_document_type_id"): create_column(self.env.cr, "account_move", "l10n_latam_document_type_id", "int4") + + if not index_exists(self.env.cr, "account_move_unique_name_latam"): + drop_index(self.env.cr, "account_move_unique_name", self._table) + self.env.cr.execute(""" + CREATE UNIQUE INDEX account_move_unique_name + ON account_move(name, journal_id) + WHERE (state = 'posted' AND name != '/' + AND (l10n_latam_document_type_id IS NULL OR move_type NOT IN ('in_invoice', 'in_refund', 'in_receipt'))); + CREATE UNIQUE INDEX account_move_unique_name_latam + ON account_move(name, commercial_partner_id, l10n_latam_document_type_id, company_id) + WHERE (state = 'posted' AND name != '/' + AND (l10n_latam_document_type_id IS NOT NULL AND move_type IN ('in_invoice', 'in_refund', 'in_receipt'))); + """) return super()._auto_init() l10n_latam_available_document_type_ids = fields.Many2many('l10n_latam.document.type', compute='_compute_l10n_latam_available_document_types') @@ -142,13 +161,6 @@ class AccountMove(models.Model): raise UserError(_('We do not accept the usage of document types on receipts yet. ')) return super()._post(soft) - @api.constrains('name', 'journal_id', 'state') - def _check_unique_sequence_number(self): - """ This uniqueness verification is only valid for customer invoices, and vendor bills that does not use - documents. A new constraint method _check_unique_vendor_number has been created just for validate for this purpose """ - vendor = self.filtered(lambda x: x.is_purchase_document() and x.l10n_latam_use_documents) - return super(AccountMove, self - vendor)._check_unique_sequence_number() - @api.constrains('state', 'l10n_latam_document_type_id') def _check_l10n_latam_documents(self): """ This constraint checks that if a invoice is posted and does not have a document type configured will raise @@ -206,23 +218,3 @@ class AccountMove(models.Model): if rec.debit_origin_id: document_types = document_types.filtered(lambda x: x.internal_type == 'debit_note') rec.l10n_latam_document_type_id = document_types and document_types[0].id - - @api.constrains('name', 'partner_id', 'company_id', 'posted_before') - def _check_unique_vendor_number(self): - """ The constraint _check_unique_sequence_number is valid for customer bills but not valid for us on vendor - bills because the uniqueness must be per partner """ - for rec in self.filtered( - lambda x: x.name and x.name != '/' and x.is_purchase_document() and x.l10n_latam_use_documents - and x.commercial_partner_id): - domain = [ - ('move_type', '=', rec.move_type), - # by validating name we validate l10n_latam_document_type_id - ('name', '=', rec.name), - ('company_id', '=', rec.company_id.id), - ('id', '!=', rec.id), - ('commercial_partner_id', '=', rec.commercial_partner_id.id), - # allow to have to equal if they are cancelled - ('state', '!=', 'cancel'), - ] - if rec.search(domain): - raise ValidationError(_('Vendor bill number must be unique per vendor and company.'))