#This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. from string import Template import time from ..model import ModelView, ModelSQL, fields from ..tools import datetime_strftime from ..pyson import Eval, And from ..transaction import Transaction from ..pool import Pool from ..config import CONFIG from ..backend import TableHandler __all__ = [ 'SequenceType', 'Sequence', 'SequenceStrict', ] sql_sequence = CONFIG.options['db_type'] == 'postgresql' class SequenceType(ModelSQL, ModelView): "Sequence type" __name__ = 'ir.sequence.type' name = fields.Char('Sequence Name', required=True, translate=True) code = fields.Char('Sequence Code', required=True) class Sequence(ModelSQL, ModelView): "Sequence" __name__ = 'ir.sequence' _strict = False name = fields.Char('Sequence Name', required=True, translate=True) code = fields.Selection('code_get', 'Sequence Code', required=True, states={ 'readonly': Eval('context', {}).contains('code'), }) active = fields.Boolean('Active') prefix = fields.Char('Prefix') suffix = fields.Char('Suffix') type = fields.Selection([ ('incremental', 'Incremental'), ('decimal timestamp', 'Decimal Timestamp'), ('hexadecimal timestamp', 'Hexadecimal Timestamp'), ], 'Type') number_next_internal = fields.Integer('Next Number', states={ 'invisible': ~Eval('type').in_(['incremental']), 'required': And(Eval('type').in_(['incremental']), not sql_sequence), }, depends=['type']) number_next = fields.Function(number_next_internal, 'get_number_next', 'set_number_next') number_increment = fields.Integer('Increment Number', states={ 'invisible': ~Eval('type').in_(['incremental']), 'required': Eval('type').in_(['incremental']), }, depends=['type']) padding = fields.Integer('Number padding', states={ 'invisible': ~Eval('type').in_(['incremental']), 'required': Eval('type').in_(['incremental']), }, depends=['type']) timestamp_rounding = fields.Float('Timestamp Rounding', required=True, states={ 'invisible': ~Eval('type').in_( ['decimal timestamp', 'hexadecimal timestamp']), }, depends=['type']) timestamp_offset = fields.Float('Timestamp Offset', required=True, states={ 'invisible': ~Eval('type').in_( ['decimal timestamp', 'hexadecimal timestamp']), }, depends=['type']) last_timestamp = fields.Integer('Last Timestamp', states={ 'invisible': ~Eval('type').in_( ['decimal timestamp', 'hexadecimal timestamp']), 'required': Eval('type').in_( ['decimal timestamp', 'hexadecimal timestamp']), }, depends=['type']) @classmethod def __setup__(cls): super(Sequence, cls).__setup__() cls._constraints += [ ('check_prefix_suffix', 'invalid_prefix_suffix'), ('check_last_timestamp', 'future_last_timestamp'), ] cls._sql_constraints += [ ('check_timestamp_rounding', 'CHECK(timestamp_rounding > 0)', 'Timestamp rounding should be greater than 0'), ] cls._error_messages.update({ 'missing': 'Missing sequence!', 'invalid_prefix_suffix': 'Invalid prefix/suffix!', 'future_last_timestamp': 'Last Timestamp could not be in future!', }) @classmethod def __register__(cls, module_name): cursor = Transaction().cursor table = TableHandler(cursor, cls, module_name) # Migration from 2.0 rename number_next into number_next_internal table.column_rename('number_next', 'number_next_internal') super(Sequence, cls).__register__(module_name) # Migration from 2.0 create sql_sequence if sql_sequence and not cls._strict: sequences = cls.search([]) for sequence in sequences: if sequence.type != 'incremental': continue if not TableHandler.sequence_exist(cursor, sequence._sql_sequence_name): sequence.create_sql_sequence(sequence.number_next_internal) @staticmethod def default_active(): return True @staticmethod def default_type(): return 'incremental' @staticmethod def default_number_increment(): return 1 @staticmethod def default_number_next(): return 1 @staticmethod def default_padding(): return 0 @staticmethod def default_timestamp_rounding(): return 1.0 @staticmethod def default_timestamp_offset(): return 946681200.0 # Offset for 2000-01-01 @staticmethod def default_last_timestamp(): return 0.0 @staticmethod def default_code(): return Transaction().context.get('code') def get_number_next(self, name): cursor = Transaction().cursor sql_name = self._sql_sequence_name if sql_sequence and not self._strict: cursor.execute('SELECT ' 'CASE WHEN NOT is_called THEN last_value ' 'ELSE last_value + increment_by ' 'END FROM "%s"' % sql_name) return cursor.fetchone()[0] else: return self.number_next_internal @classmethod def set_number_next(cls, sequences, name, value): super(Sequence, cls).write(sequences, { 'number_next_internal': value, }) @classmethod def create(cls, values): sequence_id = super(Sequence, cls).create(values) if sql_sequence and not cls._strict: sequence = cls(sequence_id) sequence.update_sql_sequence(values.get('number_next', cls.default_number_next())) return sequence_id @classmethod def write(cls, sequences, values): result = super(Sequence, cls).write(sequences, values) if sql_sequence and not cls._strict: for sequence in sequences: sequence.update_sql_sequence(values.get('number_next')) return result @classmethod def delete(cls, sequences): if sql_sequence and not cls._strict: for sequence in sequences: sequence.delete_sql_sequence() return super(Sequence, cls).delete(sequences) @classmethod def code_get(cls): pool = Pool() SequenceType = pool.get('ir.sequence.type') sequence_types = SequenceType.search([]) return [(x.code, x.name) for x in sequence_types] @classmethod def check_prefix_suffix(cls, sequences): "Check prefix and suffix" for sequence in sequences: try: cls._process(sequence.prefix) cls._process(sequence.suffix) except Exception: return False return True @classmethod def check_last_timestamp(cls, sequences): "Check last_timestamp" for sequence in sequences: next_timestamp = cls._timestamp(sequence) if sequence.last_timestamp > next_timestamp: return False return True @property def _sql_sequence_name(self): 'Return SQL sequence name' return '%s_%s' % (self._table, self.id) def create_sql_sequence(self, number_next=None): 'Create the SQL sequence' cursor = Transaction().cursor if self.type != 'incremental': return if number_next is None: number_next = self.number_next cursor.execute('CREATE SEQUENCE "' + self._sql_sequence_name + '" INCREMENT BY %s START WITH %s', (self.number_increment, number_next)) def update_sql_sequence(self, number_next=None): 'Update the SQL sequence' cursor = Transaction().cursor exist = TableHandler.sequence_exist(cursor, self._sql_sequence_name) if self.type != 'incremental': if exist: self.delete_sql_sequence() return if not exist: self.create_sql_sequence(number_next) return if number_next is None: number_next = self.number_next cursor.execute('ALTER SEQUENCE "' + self._sql_sequence_name + '" INCREMENT BY %s RESTART WITH %s', (self.number_increment, number_next)) def delete_sql_sequence(self): 'Delete the SQL sequence' cursor = Transaction().cursor if self.type != 'incremental': return cursor.execute('DROP SEQUENCE "%s"' % self._sql_sequence_name) @staticmethod def _process(string, date=None): pool = Pool() Date = pool.get('ir.date') if not date: date = Date.today() year = datetime_strftime(date, '%Y') month = datetime_strftime(date, '%m') day = datetime_strftime(date, '%d') return Template(string or '').substitute( year=year, month=month, day=day, ) @staticmethod def _timestamp(sequence): return int((time.time() - sequence.timestamp_offset) / sequence.timestamp_rounding) @classmethod def _get_sequence(cls, sequence): if sequence.type == 'incremental': if sql_sequence and not cls._strict: cursor = Transaction().cursor cursor.execute('SELECT nextval(\'"%s"\')' % sequence._sql_sequence_name) number_next, = cursor.fetchone() else: #Pre-fetch number_next number_next = sequence.number_next_internal with Transaction().set_user(0): cls.write([sequence], { 'number_next_internal': (number_next + sequence.number_increment), }) return '%%0%sd' % sequence.padding % number_next elif sequence.type in ('decimal timestamp', 'hexadecimal timestamp'): timestamp = sequence.last_timestamp while timestamp == sequence.last_timestamp: timestamp = cls._timestamp(sequence) with Transaction().set_user(0): cls.write([sequence], { 'last_timestamp': timestamp, }) if sequence.type == 'decimal timestamp': return '%d' % timestamp else: return hex(timestamp)[2:].upper() return '' @classmethod def get_id(cls, domain): ''' Return sequence value for the domain ''' if isinstance(domain, cls): domain = domain.id if isinstance(domain, (int, long)): domain = [('id', '=', domain)] # bypass rules on sequences with Transaction().set_context(user=False): with Transaction().set_user(0): try: sequence, = cls.search(domain, limit=1) except TypeError: cls.raise_user_error('missing') date = Transaction().context.get('date') return '%s%s%s' % ( cls._process(sequence.prefix, date=date), cls._get_sequence(sequence), cls._process(sequence.suffix, date=date), ) @classmethod def get(cls, code): return cls.get_id([('code', '=', code)]) class SequenceStrict(Sequence): "Sequence Strict" __name__ = 'ir.sequence.strict' _table = 'ir_sequence_strict' # Needed to override Sequence._table _strict = True @classmethod def get_id(cls, clause): Transaction().cursor.lock(cls._table) return super(SequenceStrict, cls).get_id(clause)