[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 <jco@odoo.com>
This commit is contained in:
william
2023-03-07 18:02:19 +01:00
committed by william-andre
parent 0cc3b3e6d3
commit e7df30a565
12 changed files with 69 additions and 288 deletions
+25 -34
View File
@@ -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)
+24 -16
View File
@@ -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.
-1
View File
@@ -1 +0,0 @@
from . import models
-16
View File
@@ -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 <philmer@cciconnect.be>
{
'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',
}
@@ -1,2 +0,0 @@
from . import account_move
from . import sequence_mixin
@@ -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
@@ -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'])
@@ -1 +0,0 @@
from . import models
@@ -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 <philmer@cciconnect.be>
{
'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',
}
@@ -1 +0,0 @@
from . import account_move
@@ -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
@@ -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.'))