[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:
@@ -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)
|
||||
|
||||
@@ -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 +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',
|
||||
'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.'))
|
||||
|
||||
Reference in New Issue
Block a user