summaryrefslogtreecommitdiff
path: root/addons/account/models
diff options
context:
space:
mode:
authorwan <wan@odoo.com>2019-11-27 15:57:42 +0000
committerwan <wan@odoo.com>2020-02-05 13:59:43 +0000
commitdfd01b8c5c7e1177f37bf199790a0732a61eed78 (patch)
tree6ebb7f50ba554fb4138954c958f799836aae91a7 /addons/account/models
parenta6d7c7b04a48669037e6568849a26571602386f6 (diff)
[IMP] account: rework name sequences
Task 2146469 Rework the name computation: * It doesn't use ir.sequence anymore * It is an editable computed field * Add a renaming tool This allows the user to tweak the sequences more easily than having to get through the ir.sequence settings. The sequence depends on a parameter of the journal: continuous, montly and yearly restart of the sequence. Everytime a new period is started, try to make a pattern form another period. The incrementing is done by taking the previous name, ordered lexicographically, splitting it by taking the digits at the end, adding 1 and re contstruct with the prefix. ** This means there could be cases where the prefix has a big importance on the next number. For instance, if you have INVOICE/2019/0001 INVOICE/2019/0002 and then rename the last one to INV/2019/0002, don't expect the next number to be INV/2019/0003. It will be INVOICE/2019/0002 (again) because the highest number was INVOICE/2019/0001. You will then end up with INVOICE/2019/0001 INV/2019/0002 INVOICE/2019/0002 closes odoo/odoo#41485 Related: odoo/enterprise#7189 Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com>
Diffstat (limited to 'addons/account/models')
-rw-r--r--addons/account/models/__init__.py1
-rw-r--r--addons/account/models/account.py117
-rw-r--r--addons/account/models/account_bank_statement.py43
-rw-r--r--addons/account/models/account_move.py220
-rw-r--r--addons/account/models/sequence_mixin.py125
5 files changed, 280 insertions, 226 deletions
diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py
index ff544440e12..7ef7d6a36d6 100644
--- a/addons/account/models/__init__.py
+++ b/addons/account/models/__init__.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
+from . import sequence_mixin
from . import partner
from . import account
from . import account_reconcile_model
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 48385ebee96..787e67172df 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -921,7 +921,7 @@ class AccountJournal(models.Model):
return self.__get_bank_statements_available_sources()
name = fields.Char(string='Journal Name', required=True)
- code = fields.Char(string='Short Code', size=5, required=True, help="The journal entries of this journal will be named using this prefix.")
+ code = fields.Char(string='Short Code', size=5, required=True, help="Shorter name used for display. The journal entries of this journal will also be named using this prefix by default.")
active = fields.Boolean(default=True, help="Set active to false to hide the Journal without removing it.")
type = fields.Selection([
('sale', 'Sales'),
@@ -944,19 +944,7 @@ class AccountJournal(models.Model):
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", help="It acts as a default account for debit amount", ondelete='restrict')
restrict_mode_hash_table = fields.Boolean(string="Lock Posted Entries with Hash",
help="If ticked, the accounting entry or invoice receives a hash as soon as it is posted and cannot be modified anymore.")
- sequence_id = fields.Many2one('ir.sequence', string='Entry Sequence',
- help="This field contains the information related to the numbering of the journal entries of this journal.", required=True, copy=False)
- refund_sequence_id = fields.Many2one('ir.sequence', string='Credit Note Entry Sequence',
- help="This field contains the information related to the numbering of the credit note entries of this journal.", copy=False)
sequence = fields.Integer(help='Used to order Journals in the dashboard view', default=10)
- sequence_number_next = fields.Integer(string='Next Number',
- help='The next sequence number will be used for the next invoice.',
- compute='_compute_seq_number_next',
- inverse='_inverse_seq_number_next')
- refund_sequence_number_next = fields.Integer(string='Credit Notes Next Number',
- help='The next sequence number will be used for the next credit note.',
- compute='_compute_refund_seq_number_next',
- inverse='_inverse_refund_seq_number_next')
invoice_reference_type = fields.Selection(string='Communication Type', required=True, selection=[('none', 'Free'), ('partner', 'Based on Customer'), ('invoice', 'Based on Invoice')], default='invoice', help='You can set here the default communication that will appear on customer invoices, once validated, to help the customer to refer to that particular invoice when making the payment.')
invoice_reference_model = fields.Selection(string='Communication Standard', required=True, selection=[('odoo', 'Odoo'),('euro', 'European')], default='odoo', help="You can choose different models for each type of reference. The default one is the Odoo reference.")
@@ -967,6 +955,11 @@ class AccountJournal(models.Model):
help="Company related to this journal")
refund_sequence = fields.Boolean(string='Dedicated Credit Note Sequence', help="Check this box if you don't want to share the same sequence for invoices and credit notes made from this journal", default=False)
+ sequence_override_regex = fields.Text(help="Technical field used to enforce complex sequence composition that the system would normally misunderstand.\n"\
+ "This is a regex that can include all the following capture groups: prefix1, year, prefix2, month, prefix3, seq, suffix.\n"\
+ "The prefix* groups are the separators between the year, month and the actual increasing sequence number (seq).\n"\
+
+ "e.g: ^(?P<prefix1>.*?)(?P<year>\d{4})(?P<prefix2>\D*?)(?P<month>\d{2})(?P<prefix3>\D+?)(?P<seq>\d+)(?P<suffix>\D*?)$")
inbound_payment_method_ids = fields.Many2many('account.payment.method', 'account_journal_inbound_payment_method_rel', 'journal_id', 'inbound_payment_method',
domain=[('payment_type', '=', 'inbound')], string='For Incoming Payments', default=lambda self: self._default_inbound_payment_methods(),
@@ -1009,50 +1002,6 @@ class AccountJournal(models.Model):
for record in self:
record.alias_domain = alias_domain
- # do not depend on 'sequence_id.date_range_ids', because
- # sequence_id._get_current_sequence() may invalidate it!
- @api.depends('sequence_id.use_date_range', 'sequence_id.number_next_actual')
- def _compute_seq_number_next(self):
- '''Compute 'sequence_number_next' according to the current sequence in use,
- an ir.sequence or an ir.sequence.date_range.
- '''
- for journal in self:
- if journal.sequence_id:
- sequence = journal.sequence_id._get_current_sequence()
- journal.sequence_number_next = sequence.number_next_actual
- else:
- journal.sequence_number_next = 1
-
- def _inverse_seq_number_next(self):
- '''Inverse 'sequence_number_next' to edit the current sequence next number.
- '''
- for journal in self:
- if journal.sequence_id and journal.sequence_number_next:
- sequence = journal.sequence_id._get_current_sequence()
- sequence.sudo().number_next = journal.sequence_number_next
-
- # do not depend on 'refund_sequence_id.date_range_ids', because
- # refund_sequence_id._get_current_sequence() may invalidate it!
- @api.depends('refund_sequence_id.use_date_range', 'refund_sequence_id.number_next_actual')
- def _compute_refund_seq_number_next(self):
- '''Compute 'sequence_number_next' according to the current sequence in use,
- an ir.sequence or an ir.sequence.date_range.
- '''
- for journal in self:
- if journal.refund_sequence_id and journal.refund_sequence:
- sequence = journal.refund_sequence_id._get_current_sequence()
- journal.refund_sequence_number_next = sequence.number_next_actual
- else:
- journal.refund_sequence_number_next = 1
-
- def _inverse_refund_seq_number_next(self):
- '''Inverse 'refund_sequence_number_next' to edit the current sequence next number.
- '''
- for journal in self:
- if journal.refund_sequence_id and journal.refund_sequence and journal.refund_sequence_number_next:
- sequence = journal.refund_sequence_id._get_current_sequence()
- sequence.sudo().number_next = journal.refund_sequence_number_next
-
@api.constrains('type_control_ids')
def _constrains_type_control_ids(self):
self.env['account.move.line'].flush(['account_id', 'journal_id'])
@@ -1195,14 +1144,6 @@ class AccountJournal(models.Model):
'company_id': company.id,
'partner_id': company.partner_id.id,
})
- if ('code' in vals and journal.code != vals['code']):
- if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
- raise UserError(_('This journal already contains items, therefore you cannot modify its short name.'))
- new_prefix = self._get_sequence_prefix(vals['code'], refund=False)
- journal.sequence_id.write({'prefix': new_prefix})
- if journal.refund_sequence_id:
- new_prefix = self._get_sequence_prefix(vals['code'], refund=True)
- journal.refund_sequence_id.write({'prefix': new_prefix})
if 'currency_id' in vals:
if not 'default_debit_account_id' in vals and journal.default_debit_account_id:
journal.default_debit_account_id.currency_id = vals['currency_id']
@@ -1230,16 +1171,6 @@ class AccountJournal(models.Model):
if 'bank_acc_number' in vals:
for journal in self.filtered(lambda r: r.type == 'bank' and not r.bank_account_id):
journal.set_bank_account(vals.get('bank_acc_number'), vals.get('bank_id'))
- # create the relevant refund sequence
- if vals.get('refund_sequence'):
- for journal in self.filtered(lambda j: j.type in ('sale', 'purchase') and not j.refund_sequence_id):
- journal_vals = {
- 'name': journal.name,
- 'company_id': journal.company_id.id,
- 'code': journal.code,
- 'refund_sequence_number_next': vals.get('refund_sequence_number_next', journal.refund_sequence_number_next),
- }
- journal.refund_sequence_id = self.sudo()._create_sequence(journal_vals, refund=True).id
# Changing the 'post_at' option will post the draft payment moves and change the related invoices' state.
if 'post_at' in vals and vals['post_at'] != 'bank_rec':
draft_moves = self.env['account.move'].search([('journal_id', 'in', self.ids), ('state', '=', 'draft')])
@@ -1253,33 +1184,6 @@ class AccountJournal(models.Model):
return result
@api.model
- def _get_sequence_prefix(self, code, refund=False):
- prefix = code.upper()
- if refund:
- prefix = 'R' + prefix
- return prefix + '/%(range_year)s/'
-
- @api.model
- def _create_sequence(self, vals, refund=False):
- """ Create new no_gap entry sequence for every new Journal"""
- prefix = self._get_sequence_prefix(vals['code'], refund)
- seq_name = refund and vals['code'] + _(': Refund') or vals['code']
- seq = {
- 'name': _('%s Sequence') % seq_name,
- 'implementation': 'no_gap',
- 'prefix': prefix,
- 'padding': 4,
- 'number_increment': 1,
- 'use_date_range': True,
- }
- if 'company_id' in vals:
- seq['company_id'] = vals['company_id']
- seq = self.env['ir.sequence'].create(seq)
- seq_date_range = seq._get_current_sequence()
- seq_date_range.number_next = refund and vals.get('refund_sequence_number_next', 1) or vals.get('sequence_number_next', 1)
- return seq
-
- @api.model
def _prepare_liquidity_account(self, name, company, currency_id, type):
'''
This function prepares the value to use for the creation of the default debit and credit accounts of a
@@ -1353,11 +1257,6 @@ class AccountJournal(models.Model):
if 'refund_sequence' not in vals:
vals['refund_sequence'] = vals['type'] in ('sale', 'purchase')
- # We just need to create the relevant sequences according to the chosen options
- if not vals.get('sequence_id'):
- vals.update({'sequence_id': self.sudo()._create_sequence(vals).id})
- if vals.get('type') in ('sale', 'purchase') and vals.get('refund_sequence') and not vals.get('refund_sequence_id'):
- vals.update({'refund_sequence_id': self.sudo()._create_sequence(vals, refund=True).id})
journal = super(AccountJournal, self.with_context(mail_create_nolog=True)).create(vals)
if 'alias_name' in vals:
journal._update_mail_alias(vals)
@@ -1622,9 +1521,9 @@ class AccountTax(models.Model):
JOIN account_tax tax ON tax.id = line.tax_line_id
WHERE line.tax_line_id IN %s
AND line.company_id != tax.company_id
-
+
UNION ALL
-
+
SELECT line.id
FROM account_move_line_account_tax_rel tax_rel
JOIN account_tax tax ON tax.id = tax_rel.account_tax_id
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 416f6b18493..124484fc9ce 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -10,6 +10,7 @@ from odoo.exceptions import UserError, ValidationError
import time
import math
import base64
+import re
class AccountCashboxLine(models.Model):
@@ -220,7 +221,7 @@ class AccountBankStatement(models.Model):
_name = "account.bank.statement"
_description = "Bank Statement"
_order = "date desc, name desc, id desc"
- _inherit = ['mail.thread']
+ _inherit = ['mail.thread', 'sequence.mixin']
name = fields.Char(string='Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True)
reference = fields.Char(string='External Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True, help="Used to hold the reference of the external mean that created this statement (name of imported file, reference of online synchronization...)")
@@ -410,18 +411,44 @@ class AccountBankStatement(models.Model):
""" Changes statement state to Running."""
for statement in self:
if not statement.name:
- context = {'ir_sequence_date': statement.date}
- if statement.journal_id.sequence_id:
- st_number = statement.journal_id.sequence_id.with_context(**context).next_by_id()
- else:
- SequenceObj = self.env['ir.sequence']
- st_number = SequenceObj.with_context(**context).next_by_code('account.bank.statement')
- statement.name = st_number
+ statement._set_next_sequence()
statement.state = 'open'
def button_reopen(self):
self.state = 'open'
+ def _get_last_sequence_domain(self, relaxed=False):
+ self.ensure_one()
+ where_string = "WHERE journal_id = %(journal_id)s AND name != '/'"
+ param = {'journal_id': self.journal_id.id}
+
+ sequence_number_reset = self._deduce_sequence_number_reset(self.search([('date', '<', self.date)], order='date desc', limit=1).name)
+ if not relaxed:
+ if sequence_number_reset == 'year':
+ where_string += " AND date_trunc('year', date) = date_trunc('year', %(date)s) "
+ param['date'] = self.date
+ elif sequence_number_reset == 'month':
+ where_string += " AND date_trunc('month', date) = date_trunc('month', %(date)s) "
+ param['date'] = self.date
+ return where_string, param
+
+ def _get_starting_sequence(self):
+ self.ensure_one()
+ last_sequence = self._get_last_sequence(relaxed=True)
+ if last_sequence:
+ sequence_number_reset = self._deduce_sequence_number_reset(self.search([('date', '<', self.date)], order='date desc', limit=1).name)
+ if sequence_number_reset == 'year':
+ sequence = re.match(self._sequence_yearly_regex, last_sequence)
+ if sequence:
+ return '%s%04d%s%s%s' % (sequence.group('prefix1'), self.date.year, sequence.group('prefix2'), "0" * len(sequence.group('seq')), sequence.group('suffix'))
+ elif sequence_number_reset == 'month':
+ sequence = re.match(self._sequence_monthly_regex, last_sequence)
+ if sequence:
+ return '%s%04d%s%02d%s%s%s' % (sequence.group('prefix1'), self.date.year, sequence.group('prefix2'), self.date.month, sequence.group('prefix3'), "0" * len(sequence.group('seq')), sequence.group('suffix'))
+
+ # There was no pattern found, propose one
+ return "%s/%04d/%02d/0000" % (self.journal_id.code, self.date.year, self.date.month)
+
class AccountBankStatementLine(models.Model):
_name = "account.bank.statement.line"
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index d9f67d13b78..7a0d18c2209 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -31,10 +31,22 @@ def calc_check_digits(number):
class AccountMove(models.Model):
_name = "account.move"
- _inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin']
+ _inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin', 'sequence.mixin']
_description = "Journal Entries"
_order = 'date desc, name desc, id desc'
+ @property
+ def _sequence_monthly_regex(self):
+ return self.journal_id.sequence_override_regex or super()._sequence_monthly_regex
+
+ @property
+ def _sequence_yearly_regex(self):
+ return self.journal_id.sequence_override_regex or super()._sequence_yearly_regex
+
+ @property
+ def _sequence_fixed_regex(self):
+ return self.journal_id.sequence_override_regex or super()._sequence_fixed_regex
+
@api.model
def _get_default_journal(self):
''' Get the default journal.
@@ -90,7 +102,9 @@ class AccountMove(models.Model):
return self.env.company.incoterm_id
# ==== Business fields ====
- name = fields.Char(string='Number', required=True, readonly=True, copy=False, default='/')
+ name = fields.Char(string='Number', copy=False, compute='_compute_name', readonly=False, store=True, index=True, tracking=True)
+ highest_name = fields.Char(compute='_compute_highest_name')
+ show_name_warning = fields.Boolean(store=False)
date = fields.Date(string='Date', required=True, index=True, readonly=True,
states={'draft': [('readonly', False)]},
default=fields.Date.context_today)
@@ -102,6 +116,7 @@ class AccountMove(models.Model):
('cancel', 'Cancelled'),
], string='Status', required=True, readonly=True, copy=False, tracking=True,
default='draft')
+ posted_before = fields.Boolean(help="Technical field for knowing if the move has been posted before", copy=False)
type = fields.Selection(selection=[
('entry', 'Journal Entry'),
('out_invoice', 'Customer Invoice'),
@@ -243,13 +258,6 @@ class AccountMove(models.Model):
readonly=True, states={'draft': [('readonly', False)]},
help='Defines the smallest coinage of the currency that can be used to pay by cash.')
- # ==== Fields to set the sequence, on the first invoice of the journal ====
- invoice_sequence_number_next = fields.Char(string='Next Number',
- compute='_compute_invoice_sequence_number_next',
- inverse='_inverse_invoice_sequence_number_next')
- invoice_sequence_number_next_prefix = fields.Char(string='Next Number Prefix',
- compute="_compute_invoice_sequence_number_next")
-
# ==== Display purpose fields ====
invoice_filter_type_domain = fields.Char(compute='_compute_invoice_filter_type_domain',
help="Technical field used to have a dynamic domain on journal / taxes in the form view.")
@@ -909,6 +917,79 @@ class AccountMove(models.Model):
# COMPUTE METHODS
# -------------------------------------------------------------------------
+ @api.depends('journal_id', 'date', 'state', 'highest_name')
+ def _compute_name(self):
+ for record in self.sorted(lambda m: (m.date, m.ref or '', m.id)):
+ if not record.name or record.name == '/':
+ if record.state == 'draft' and not record.posted_before and not record.highest_name:
+ # First name of the period for the journal, no name yet
+ record._set_next_sequence()
+ elif record.state == 'posted':
+ # No name yet but has been posted
+ record._set_next_sequence()
+ if record.name and record.state == 'draft' and not record.posted_before and record.highest_name:
+ # Not the first name of the period for the journal, but had a name set
+ record.name = '/'
+ record.name = record.name or '/'
+
+ @api.depends('journal_id', 'date', 'state')
+ def _compute_highest_name(self):
+ for record in self:
+ record.highest_name = record._get_last_sequence()
+
+ @api.onchange('name', 'highest_name')
+ def _onchange_name_warning(self):
+ if self.name and self.name != '/' and self.name <= (self.highest_name or ''):
+ self.show_name_warning = True
+ else:
+ self.show_name_warning = False
+
+ def _get_last_sequence_domain(self, relaxed=False):
+ self.ensure_one()
+ if not self.date or not self.journal_id:
+ return "WHERE FALSE", {}
+ where_string = "WHERE journal_id = %(journal_id)s AND name != '/'"
+ param = {'journal_id': self.journal_id.id}
+
+ if not relaxed:
+ reference_move = self.search([('journal_id', '=', self.journal_id.id), ('date', '<=', self.date), ('id', '!=', self.id or self._origin.id)], order='date desc', limit=1) or self.search([('journal_id', '=', self.journal_id.id), ('id', '!=', self.id or self._origin.id)], order='date asc', limit=1)
+ sequence_number_reset = self._deduce_sequence_number_reset(reference_move.name)
+ if sequence_number_reset == 'year':
+ where_string += " AND date_trunc('year', date) = date_trunc('year', %(date)s) "
+ param['date'] = self.date
+ elif sequence_number_reset == 'month':
+ where_string += " AND date_trunc('month', date) = date_trunc('month', %(date)s) "
+ param['date'] = self.date
+
+ if self.journal_id.refund_sequence:
+ if self.type in ('out_refund', 'in_refund'):
+ where_string += " AND type IN ('out_refund', 'in_refund') "
+ else:
+ where_string += " AND type NOT IN ('out_refund', 'in_refund') "
+
+ return where_string, param
+
+ def _get_starting_sequence(self):
+ self.ensure_one()
+ # Try to find a pattern already used by relaxing a domain. If we are here, the domain non relaxed should return nothing.
+ last_sequence = self._get_last_sequence(relaxed=True)
+ if last_sequence:
+ reference_move = self.search([('journal_id', '=', self.journal_id.id), ('date', '<=', self.date), ('id', '!=', self.id or self._origin.id)], order='date asc', limit=1) or self.search([('journal_id', '=', self.journal_id.id), ('id', '!=', self.id or self._origin.id)], order='date desc', limit=1)
+ sequence_number_reset = self._deduce_sequence_number_reset(reference_move.name)
+ if sequence_number_reset == 'year':
+ sequence = re.match(self._sequence_yearly_regex, last_sequence)
+ if sequence:
+ return '%s%04d%s%s%s' % (sequence.group('prefix1'), self.date.year, sequence.group('prefix2'), "0" * len(sequence.group('seq')), sequence.group('suffix'))
+ elif sequence_number_reset == 'month':
+ sequence = re.match(self._sequence_monthly_regex, last_sequence)
+ if sequence:
+ return '%s%04d%s%02d%s%s%s' % (sequence.group('prefix1'), self.date.year, sequence.group('prefix2'), self.date.month, sequence.group('prefix3'), "0" * len(sequence.group('seq')), sequence.group('suffix'))
+
+ starting_sequence = "%s/%04d/%02d/0000" % (self.journal_id.code, self.date.year, self.date.month)
+ if self.journal_id.refund_sequence and self.type in ('out_refund', 'in_refund'):
+ starting_sequence = "R" + starting_sequence
+ return starting_sequence
+
@api.depends('type')
def _compute_type_name(self):
type_name_mapping = {k: v for k, v in
@@ -1128,69 +1209,6 @@ class AccountMove(models.Model):
vendor_display_name = _('#Created by: %s') % (move.sudo().create_uid.name or self.env.user.name)
move.invoice_partner_display_name = vendor_display_name
- @api.depends('state', 'journal_id', 'date', 'invoice_date')
- def _compute_invoice_sequence_number_next(self):
- """ computes the prefix of the number that will be assigned to the first invoice/bill/refund of a journal, in order to
- let the user manually change it.
- """
- # Check user group.
- system_user = self.env.is_system()
- if not system_user:
- self.invoice_sequence_number_next_prefix = False
- self.invoice_sequence_number_next = False
- return
-
- # Check moves being candidates to set a custom number next.
- moves = self.filtered(lambda move: move.is_invoice() and move.name == '/')
- if not moves:
- self.invoice_sequence_number_next_prefix = False
- self.invoice_sequence_number_next = False
- return
-
- treated = self.browse()
- for key, group in groupby(moves, key=lambda move: (move.journal_id, move._get_sequence())):
- journal, sequence = key
- domain = [('journal_id', '=', journal.id), ('state', '=', 'posted')]
- if self.ids:
- domain.append(('id', 'not in', self.ids))
- if journal.type == 'sale':
- domain.append(('type', 'in', ('out_invoice', 'out_refund')))
- elif journal.type == 'purchase':
- domain.append(('type', 'in', ('in_invoice', 'in_refund')))
- else:
- continue
- if self.search_count(domain):
- continue
-
- for move in group:
- sequence_date = move.date or move.invoice_date
- prefix, dummy = sequence._get_prefix_suffix(date=sequence_date, date_range=sequence_date)
- number_next = sequence._get_current_sequence(sequence_date=sequence_date).number_next_actual
- move.invoice_sequence_number_next_prefix = prefix
- move.invoice_sequence_number_next = '%%0%sd' % sequence.padding % number_next
- treated |= move
- remaining = (self - treated)
- remaining.invoice_sequence_number_next_prefix = False
- remaining.invoice_sequence_number_next = False
-
- def _inverse_invoice_sequence_number_next(self):
- ''' Set the number_next on the sequence related to the invoice/bill/refund'''
- # Check user group.
- if not self.env.is_admin():
- return
-
- # Set the next number in the sequence.
- for move in self:
- if not move.invoice_sequence_number_next:
- continue
- sequence = move._get_sequence()
- nxt = re.sub("[^0-9]", '', move.invoice_sequence_number_next)
- result = re.match("(0*)([0-9]+)", nxt)
- if result and sequence:
- sequence_date = move.date or move.invoice_date
- date_sequence = sequence._get_current_sequence(sequence_date=sequence_date)
- date_sequence.number_next_actual = int(result.group(2))
-
def _compute_payments_widget_to_reconcile_info(self):
for move in self:
move.invoice_outstanding_credits_debits_widget = json.dumps(False)
@@ -1367,7 +1385,7 @@ class AccountMove(models.Model):
# /!\ Computed stored fields are not yet inside the database.
self._cr.execute('''
- SELECT move2.id
+ SELECT move2.id, move2.name
FROM account_move move
INNER JOIN account_move move2 ON
move2.name = move.name
@@ -1376,9 +1394,10 @@ class AccountMove(models.Model):
AND move2.id != move.id
WHERE move.id IN %s AND move2.state = 'posted'
''', [tuple(moves.ids)])
- res = self._cr.fetchone()
+ res = self._cr.fetchall()
if res:
- raise ValidationError(_('Posted journal entry must have an unique sequence number per company.'))
+ 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))
@api.constrains('ref', 'type', 'partner_id', 'journal_id', 'invoice_date')
def _check_duplicate_supplier_reference(self):
@@ -1587,7 +1606,7 @@ class AccountMove(models.Model):
raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_MOVE_FIELDS))
if (move.restrict_mode_hash_table and move.inalterable_hash and 'inalterable_hash' in vals) or (move.secure_sequence_number and 'secure_sequence_number' in vals):
raise UserError(_('You cannot overwrite the values ensuring the inalterability of the accounting.'))
- if (move.name != '/' and 'journal_id' in vals and move.journal_id.id != vals['journal_id']):
+ if (move.posted_before and 'journal_id' in vals and move.journal_id.id != vals['journal_id']):
raise UserError(_('You cannot edit the journal of an account move if it has been posted once.'))
# You can't change the date of a move being inside a locked period.
@@ -1600,6 +1619,11 @@ class AccountMove(models.Model):
move._check_fiscalyear_lock_date()
move.line_ids._check_tax_lock_date()
+ if move.journal_id.sequence_override_regex and vals.get('name') and vals['name'] != '/' and not re.match(move.journal_id.sequence_override_regex, vals['name']):
+ if not self.env.user.has_group('account.group_account_manager'):
+ raise UserError(_('The Journal Entry sequence is not conform to the current format. Only the Advisor can change it.'))
+ move.journal_id.sequence_override_regex = False
+
if self._move_autocomplete_invoice_lines_write(vals):
res = True
else:
@@ -1612,8 +1636,8 @@ class AccountMove(models.Model):
self._check_fiscalyear_lock_date()
self.mapped('line_ids')._check_tax_lock_date()
- if ('state' in vals and vals.get('state') == 'posted') and self.restrict_mode_hash_table:
- for move in self.filtered(lambda m: not(m.secure_sequence_number or m.inalterable_hash)):
+ if ('state' in vals and vals.get('state') == 'posted'):
+ for move in self.filtered(lambda m: m.restrict_mode_hash_table and not(m.secure_sequence_number or m.inalterable_hash)):
new_number = move.journal_id.secure_sequence_id.next_by_id()
vals_hashing = {'secure_sequence_number': new_number,
'inalterable_hash': move._get_new_hash(new_number)}
@@ -1627,7 +1651,7 @@ class AccountMove(models.Model):
def unlink(self):
for move in self:
- if move.name != '/' and not self._context.get('force_delete'):
+ if move.posted_before and not self._context.get('force_delete'):
raise UserError(_("You cannot delete an entry which has been posted once."))
move.line_ids.unlink()
return super(AccountMove, self).unlink()
@@ -1782,19 +1806,6 @@ class AccountMove(models.Model):
else:
raise UserError(_('The combination of reference model and reference type on the journal is not implemented'))
- def _get_sequence(self):
- ''' Return the sequence to be used during the post of the current move.
- :return: An ir.sequence record or False.
- '''
- self.ensure_one()
-
- journal = self.journal_id
- if self.type in ('entry', 'out_invoice', 'in_invoice', 'out_receipt', 'in_receipt') or not journal.refund_sequence:
- return journal.sequence_id
- if not journal.refund_sequence_id:
- return
- return journal.refund_sequence_id
-
def _get_move_display_name(self, show_ref=False):
''' Helper to get the display name of an invoice depending of its type.
:param show_ref: A flag indicating of the display name must include or not the journal entry reference.
@@ -2082,7 +2093,7 @@ class AccountMove(models.Model):
raise UserError(_('You need to add a line before posting.'))
if move.auto_post and move.date > fields.Date.today():
date_msg = move.date.strftime(get_lang(self.env).date_format)
- raise UserError(_("This move is configured to be auto-posted on %s" % date_msg))
+ raise UserError(_("This move is configured to be auto-posted on %s") % date_msg)
if not move.partner_id:
if move.is_sale_document():
@@ -2110,25 +2121,14 @@ class AccountMove(models.Model):
# Create the analytic lines in batch is faster as it leads to less cache invalidation.
self.mapped('line_ids').create_analytic_lines()
+ self.state = 'posted'
+ self.posted_before = True
for move in self:
if move.auto_post and move.date > fields.Date.today():
raise UserError(_("This move is configured to be auto-posted on {}".format(move.date.strftime(get_lang(self.env).date_format))))
move.message_subscribe([p.id for p in [move.partner_id] if p not in move.sudo().message_partner_ids])
- to_write = {'state': 'posted'}
-
- if move.name == '/':
- # Get the journal's sequence.
- sequence = move._get_sequence()
- if not sequence:
- raise UserError(_('Please define a sequence on your journal.'))
-
- # Consume a new number.
- to_write['name'] = sequence.next_by_id(sequence_date=move.date)
-
- move.write(to_write)
-
# Compute 'ref' for 'out_invoice'.
if move._auto_compute_invoice_reference():
to_write = {
@@ -2147,7 +2147,8 @@ class AccountMove(models.Model):
move.company_id.account_bank_reconciliation_start = move.date
for move in self:
- if not move.partner_id: continue
+ if not move.partner_id:
+ continue
if move.type.startswith('out_'):
move.partner_id._increase_rank('customer_rank')
elif move.type.startswith('in_'):
@@ -3321,13 +3322,13 @@ class AccountMoveLine(models.Model):
if account_to_write and account_to_write.deprecated:
raise UserError(_('You cannot use a deprecated account.'))
- # when making a reconciliation on an existing liquidity journal item, mark the payment as reconciled
for line in self:
if line.parent_state == 'posted':
if line.move_id.restrict_mode_hash_table and set(vals).intersection(INTEGRITY_HASH_LINE_FIELDS):
raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_LINE_FIELDS))
if any(key in vals for key in ('tax_ids', 'tax_line_ids')):
raise UserError(_('You cannot modify the taxes related to a posted journal item, you should reset the journal entry to draft to do so.'))
+ # When making a reconciliation on an existing liquidity journal item, mark the payment as reconciled
if 'statement_line_id' in vals and line.payment_id:
# In case of an internal transfer, there are 2 liquidity move lines to match with a bank statement
if all(line.statement_id for line in line.payment_id.move_line_ids.filtered(
@@ -3370,7 +3371,7 @@ class AccountMoveLine(models.Model):
# Get initial values for each line
move_initial_values = {}
- for line in self.filtered(lambda l: l.move_id.name != '/'): # Only lines with posted once move.
+ for line in self.filtered(lambda l: l.move_id.posted_before): # Only lines with posted once move.
for field in tracking_fields:
# Group initial values by move_id
if line.move_id.id not in move_initial_values:
@@ -4398,6 +4399,7 @@ class AccountPartialReconcile(models.Model):
# recorded before the period lock date as the tax statement for this period is
# probably already sent to the estate.
newly_created_move.write({'date': move_date})
+ newly_created_move.recompute(['name'])
# post move
newly_created_move.post()
diff --git a/addons/account/models/sequence_mixin.py b/addons/account/models/sequence_mixin.py
new file mode 100644
index 00000000000..ca0c9cde80d
--- /dev/null
+++ b/addons/account/models/sequence_mixin.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+
+from odoo import api, fields, models, _
+from odoo.exceptions import ValidationError
+import re
+
+
+class SequenceMixin(models.AbstractModel):
+ """Mechanism used to have an editable sequence number.
+
+ Be careful of how you use this regarding the prefixes. More info in the
+ docstring of _get_last_sequence.
+ """
+
+ _name = 'sequence.mixin'
+ _description = "Automatic sequence"
+
+ _sequence_field = "name"
+ _sequence_monthly_regex = r'^(?P<prefix1>.*?)(?P<year>\d{4})(?P<prefix2>\D*?)(?P<month>\d{2})(?P<prefix3>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
+ _sequence_yearly_regex = r'^(?P<prefix1>.*?)(?P<year>\d{4})(?P<prefix2>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
+ _sequence_fixed_regex = r'^(?P<prefix1>.*?)(?P<seq>\d*)(?P<suffix>\D*?)$'
+
+ @api.model
+ def _deduce_sequence_number_reset(self, name):
+ """Detect if the used sequence resets yearly, montly or never.
+
+ :param name: the sequence that is used as a reference to detect the resetting
+ periodicity. Typically, it is the last before the one you want to give a
+ sequence.
+ """
+ def _check_grouping(grouping, optional=None, required=None):
+ sequence_dict = sequence.groupdict()
+ return all(key in sequence_dict for key in (optional or [])) and all(sequence_dict.get(key) for key in (required or []))
+
+ if not name:
+ return False
+ sequence = re.match(self._sequence_monthly_regex, name)
+ if sequence and _check_grouping(sequence, ['prefix1', 'prefix2', 'prefix3', 'seq', 'suffix'], ['year', 'month']) and 2000 <= int(sequence.group('year')) <= 2100 and 0 < int(sequence.group('month')) <= 12:
+ return 'month'
+ sequence = re.match(self._sequence_yearly_regex, name)
+ if sequence and _check_grouping(sequence, ['prefix1', 'prefix2', 'seq', 'suffix'], ['year']) and 2000 <= int(sequence.group('year')) <= 2100:
+ return 'year'
+ sequence = re.match(self._sequence_fixed_regex, name)
+ if sequence and _check_grouping(sequence, ['prefix1', 'seq', 'suffix']):
+ return 'never'
+ raise ValidationError(_('The sequence regex should at least contain the prefix1, seq and suffix grouping keys. For instance:\n^(?P<prefix1>.*?)(?P<seq>\d*)(?P<suffix>\D*?)$'))
+
+ def _get_last_sequence_domain(self, relaxed=False):
+ """Get the sql domain to retreive the previous sequence number.
+
+ This function should be overriden by models heriting from this mixin.
+
+ :param relaxed: see _get_last_sequence.
+
+ :returns: tuple(where_string, where_params): with
+ where_string: the entire SQL WHERE clause as a string.
+ where_params: a dictionary containing the parameters to substitute
+ at the execution of the query.
+ """
+ self.ensure_one()
+ return "", {}
+
+ def _get_starting_sequence(self):
+ """Get a default sequence number.
+
+ This function should be overriden by models heriting from this mixin
+ This number will be incremented so you probably want to start the sequence at 0.
+
+ :return: string to use as the default sequence to increment
+ """
+ self.ensure_one()
+ return "00000000"
+
+ def _get_last_sequence(self, relaxed=False):
+ """Retrieve the previous sequence.
+
+ This is done by taking the number with the greatest alphabetical value within
+ the domain of _get_last_sequence_domain. This means that the prefix has a
+ huge importance.
+ For instance, if you have INV/2019/0001 and INV/2019/0002, when you rename the
+ last one to FACT/2019/0001, one might expect the next number to be
+ FACT/2019/0002 but it will be INV/2019/0002 (again) because INV > FACT.
+ Therefore, changing the prefix might not be convenient during a period, and
+ would only work when the numbering makes a new start (domain returns by
+ _get_last_sequence_domain is [], i.e: a new year).
+
+ :param field_name: the field that contains the sequence.
+ :param relaxed: this should be set to True when a previous request didn't find
+ something without. This allows to find a pattern from a previous period, and
+ try to adapt it for the new period.
+
+ :return: the string of the previous sequence or None if there wasn't any.
+ """
+ self.ensure_one()
+ if self._sequence_field not in self._fields or not self._fields[self._sequence_field].store:
+ raise ValidationError(_('%s is not a stored field') % self._sequence_field)
+ where_string, param = self._get_last_sequence_domain(relaxed)
+ if self.id or self.id.origin:
+ where_string += " AND id != %(id)s "
+ param['id'] = self.id or self.id.origin
+ query = "SELECT {field} FROM {table} {where_string} ORDER BY {field} DESC LIMIT 1 FOR UPDATE".format(table=self._table, where_string=where_string, field=self._sequence_field)
+ self.flush([self._sequence_field])
+ self.env.cr.execute(query, param)
+ return (self.env.cr.fetchone() or [None])[0]
+
+ def _set_next_sequence(self):
+ """Set the next sequence.
+
+ This method ensures that the field is set both in the ORM and in the database.
+ This is necessary because we use a database query to get the previous sequence,
+ and we need that query to always be executed on the latest data.
+
+ :param field_name: the field that contains the sequence.
+ """
+ self.ensure_one()
+ last_sequence = self._get_last_sequence() or self._get_starting_sequence()
+
+ sequence = re.match(self._sequence_fixed_regex, last_sequence)
+ value = ("{prefix}{seq:0%sd}{suffix}" % len(sequence.group('seq'))).format(
+ prefix=sequence.group('prefix1'),
+ seq=int(sequence.group('seq') or 0) + 1,
+ suffix=sequence.group('suffix'),
+ )
+ self[self._sequence_field] = value
+ self.flush([self._sequence_field])