Files
odoo_source/addons/account/models/sequence_mixin.py
T

131 lines
6.0 KiB
Python

# -*- 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_highest_query(self):
return "SELECT {field} FROM {table} {where_string} ORDER BY {field} DESC LIMIT 1 FOR UPDATE"
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 = self._get_highest_query().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])