diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index ae1571f89d..15d9b0611a 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Support getting many sequences at once * Add sequence_nextval function to Database * Retrieve groups of actions without checking access (#13506) * Check read access of report records (#13505) diff --git a/trytond/doc/ref/backend.rst b/trytond/doc/ref/backend.rst index a8044e8be0..abc839394b 100644 --- a/trytond/doc/ref/backend.rst +++ b/trytond/doc/ref/backend.rst @@ -192,6 +192,10 @@ Database Advances the named sequence and returns its next value. +.. method:: Database.sequence_nextvals(connection, name, n) + + Advances the named sequence n times and yields the next values. + .. method:: Database.sequence_next_number(connection, name) Return the next number fo the named sequence using the ``connection``. diff --git a/trytond/trytond/backend/database.py b/trytond/trytond/backend/database.py index 9eb65ac093..623f5742ef 100644 --- a/trytond/trytond/backend/database.py +++ b/trytond/trytond/backend/database.py @@ -161,6 +161,11 @@ class DatabaseInterface(object): return raise NotImplementedError + def sequence_nextvals(self, connection, name, n): + if not self.has_sequence(): + return + raise NotImplementedError + def sequence_next_number(self, connection, name): if not self.has_sequence(): return diff --git a/trytond/trytond/backend/postgresql/database.py b/trytond/trytond/backend/postgresql/database.py index 90cd16dcf9..fe816e228c 100644 --- a/trytond/trytond/backend/postgresql/database.py +++ b/trytond/trytond/backend/postgresql/database.py @@ -738,6 +738,13 @@ class Database(DatabaseInterface): cursor.execute('SELECT NEXTVAL(%s)', (name,)) return cursor.fetchone()[0] + def sequence_nextvals(self, connection, name, n): + cursor = connection.cursor() + cursor.execute( + 'SELECT NEXTVAL(%s) FROM generate_series(1, %s)', (name, n)) + for val, in cursor: + yield val + def sequence_next_number(self, connection, name): cursor = connection.cursor() version = self.get_version(connection) diff --git a/trytond/trytond/ir/message.xml b/trytond/trytond/ir/message.xml index 0d816f54fb..02a8d5427a 100644 --- a/trytond/trytond/ir/message.xml +++ b/trytond/trytond/ir/message.xml @@ -285,9 +285,6 @@ this repository contains the full copyright notices and license terms. --> The condition "%(condition)s" is not a valid PYSON expression for button rule "%(rule)s". - - Missing sequence. - Invalid prefix "%(affix)s" for sequence "%(sequence)s". diff --git a/trytond/trytond/ir/sequence.py b/trytond/trytond/ir/sequence.py index a36eb7e5ce..d8a01dc4d2 100644 --- a/trytond/trytond/ir/sequence.py +++ b/trytond/trytond/ir/sequence.py @@ -380,37 +380,40 @@ class Sequence(DeactivableMixin, ModelSQL, ModelView): return int( (time.time() - self.timestamp_offset) / self.timestamp_rounding) - @classmethod - def _get_sequence(cls, sequence): - if sequence.type == 'incremental': - if sql_sequence and not cls._strict: + def _get_many(self, n=1): + if self.type == 'incremental': + if sql_sequence and not self._strict: transaction = Transaction() - number_next = transaction.database.sequence_nextval( - transaction.connection, sequence._sql_sequence_name) + numbers = transaction.database.sequence_nextvals( + transaction.connection, self._sql_sequence_name, n) # clean cache transaction.counter += 1 - sequence._local_cache.pop(sequence.id, None) + self._local_cache.pop(self.id, None) else: - # Pre-fetch number_next - number_next = sequence.number_next_internal - - cls.write([sequence], { - 'number_next_internal': (number_next - + sequence.number_increment), - }) - return f'{number_next:0>{sequence.padding}d}' - elif sequence.type in ('decimal timestamp', 'hexadecimal timestamp'): - timestamp = sequence.last_timestamp - while timestamp == sequence.last_timestamp: - timestamp = sequence._timestamp() - cls.write([sequence], { - 'last_timestamp': timestamp, - }) - if sequence.type == 'decimal timestamp': - return f'{timestamp:d}' + # pre-fetch number_next + start = self.number_next_internal + end = start + n * self.number_increment + numbers = list(range(start, end, self.number_increment)) + self.number_next_internal = end + self.save() + for number in numbers: + yield f'{number:0>{self.padding}d}' + elif self.type in {'decimal timestamp', 'hexadecimal timestamp'}: + timestamps = [] + last_timestamp = timestamp = self.last_timestamp + for _ in range(n): + while timestamp == last_timestamp: + timestamp = self._timestamp() + timestamps.append(timestamp) + last_timestamp = timestamp + self.last_timestamp = last_timestamp + self.save() + if self.type == 'decimal timestamp': + for timestamp in timestamps: + yield f'{timestamp:d}' else: - return hex(timestamp)[2:].upper() - return '' + for timestamp in timestamps: + yield hex(timestamp)[2:].upper() @fields.depends('type', 'padding', 'number_next', methods=['_timestamp']) def _get_preview_sequence(self): @@ -426,23 +429,20 @@ class Sequence(DeactivableMixin, ModelSQL, ModelView): return hex(timestamp)[2:].upper() return '' + def get(self): + "Returns the next sequence value" + return next(self.get_many(n=1)) + @without_check_access - def get(self, _lock=False): - ''' - Return the next sequence value - ''' - cls = self.__class__ - try: - sequence = cls(self.id) - except TypeError: - raise MissingError(gettext('ir.msg_sequence_missing')) + def get_many(self, n=1, _lock=False): + "Yield the n next sequence values" + assert n > 0, n if _lock: self.lock() - return '%s%s%s' % ( - cls._process(sequence.prefix), - cls._get_sequence(sequence), - cls._process(sequence.suffix), - ) + for value in self._get_many(n=n): + prefix = self._process(self.prefix) + suffix = self._process(self.suffix) + yield f'{prefix}{value}{suffix}' @fields.depends('prefix', 'suffix', methods=['_get_preview_sequence']) def on_change_with_preview(self, name=None): @@ -459,5 +459,5 @@ class SequenceStrict(Sequence): _table = None # Needed to reset Sequence._table _strict = True - def get(self, _lock=True): - return super().get(_lock=True) + def get_many(self, n=1, _lock=True): + yield from super().get_many(n=n, _lock=True) diff --git a/trytond/trytond/tests/test_ir.py b/trytond/trytond/tests/test_ir.py index b7fb6ac011..c507e6e903 100644 --- a/trytond/trytond/tests/test_ir.py +++ b/trytond/trytond/tests/test_ir.py @@ -168,6 +168,45 @@ class IrTestCase(ModuleTestCase): sequence.prefix = '${date_K}' sequence.save() + @with_transaction() + def test_sequence_get(self): + "Test get sequences" + pool = Pool() + Sequence = pool.get('ir.sequence') + SequenceType = pool.get('ir.sequence.type') + try: + Group = pool.get('res.group') + groups = Group.search([]) + except KeyError: + groups = [] + + sequence_type = SequenceType(name='Test', groups=groups) + sequence_type.save() + sequence = Sequence(name='Test Sequence', sequence_type=sequence_type) + sequence.save() + + self.assertEqual(sequence.get(), '1') + + @with_transaction() + def test_sequence_get_many(self): + "Test get many sequences" + pool = Pool() + Sequence = pool.get('ir.sequence') + SequenceType = pool.get('ir.sequence.type') + try: + Group = pool.get('res.group') + groups = Group.search([]) + except KeyError: + groups = [] + + sequence_type = SequenceType(name='Test', groups=groups) + sequence_type.save() + sequence = Sequence(name='Test Sequence', sequence_type=sequence_type) + sequence.save() + + self.assertEqual( + list(sequence.get_many(10)), list(map(str, range(1, 11)))) + @with_transaction() def test_global_search(self): 'Test Global Search'