diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG
index 2f03a11256..12d5f13aa8 100644
--- a/trytond/CHANGELOG
+++ b/trytond/CHANGELOG
@@ -1,3 +1,4 @@
+* Allow date formating in sequences substitutions
* Make ModelSQL based on table query read only by default
* Add email validation tools
* Remove overlapping indexes
diff --git a/trytond/trytond/ir/sequence.py b/trytond/trytond/ir/sequence.py
index 7130909acb..03e364255a 100644
--- a/trytond/trytond/ir/sequence.py
+++ b/trytond/trytond/ir/sequence.py
@@ -60,8 +60,12 @@ class Sequence(DeactivableMixin, ModelSQL, ModelView):
'readonly': Eval('id', -1) >= 0,
},
depends=['id'])
- prefix = fields.Char('Prefix', strip='leading')
- suffix = fields.Char('Suffix', strip='trailing')
+ prefix = fields.Char('Prefix', strip='leading',
+ help="The current date can be used formatted using strftime format "
+ "suffixed with underscores: i.e: ${date_Y}")
+ suffix = fields.Char('Suffix', strip='trailing',
+ help="The current date can be used formatted using strftime format "
+ "suffixed with underscores: i.e: ${date_Y}")
type = fields.Selection([
('incremental', 'Incremental'),
('decimal timestamp', 'Decimal Timestamp'),
@@ -319,11 +323,14 @@ class Sequence(DeactivableMixin, ModelSQL, ModelView):
@classmethod
def _process(cls, string, date=None):
- return Template(string or '').substitute(
- **cls._get_substitutions(date))
+ if not string:
+ return ''
+
+ substitutions = cls._get_substitutions(date)
+ return Template(string or '').safe_substitute(substitutions)
@classmethod
- def _get_substitutions(cls, date):
+ def _get_substitutions(cls, date=None):
'''
Returns a dictionary with the keys and values of the substitutions
available to format the sequence
@@ -332,11 +339,39 @@ class Sequence(DeactivableMixin, ModelSQL, ModelView):
Date = pool.get('ir.date')
if not date:
date = Date.today()
- return {
- 'year': date.strftime('%Y'),
- 'month': date.strftime('%m'),
- 'day': date.strftime('%d'),
- }
+
+ class CustomFormatter(dict):
+
+ def __getitem__(self, name):
+ try:
+ value = super().__getitem__(name)
+ except KeyError:
+ value = cls._convert_substitution_key(self, name)
+ return value
+
+ return CustomFormatter({
+ 'date': date,
+ })
+
+ @classmethod
+ def _convert_substitution_key(cls, substitutions, key):
+ """
+ Converts a substitution key into a different (i.e: formated) value
+ Returns the updated value
+ """
+ # Compatibilty with previous keywords
+ key = {
+ 'year': 'date_Y',
+ 'month': 'date_m',
+ 'day': 'date_d',
+ }.get(key, key)
+ if key.startswith('date_'):
+ format_ = key[len('date'):].replace('_', '%')
+ value = substitutions['date'].strftime(format_)
+ if value == format_:
+ raise ValueError(
+ f"Unknown substitution format {format_}")
+ return value
@staticmethod
def _timestamp(sequence):
diff --git a/trytond/trytond/ir/view/sequence_form.xml b/trytond/trytond/ir/view/sequence_form.xml
index 4dabddd7fd..4bb404e69f 100644
--- a/trytond/trytond/ir/view/sequence_form.xml
+++ b/trytond/trytond/ir/view/sequence_form.xml
@@ -30,12 +30,4 @@ this repository contains the full copyright notices and license terms. -->
-
-
-
-
-
-
-
diff --git a/trytond/trytond/tests/test_ir.py b/trytond/trytond/tests/test_ir.py
index 239555f6c4..ed03294ad5 100644
--- a/trytond/trytond/tests/test_ir.py
+++ b/trytond/trytond/tests/test_ir.py
@@ -13,6 +13,7 @@ except ImportError:
from dateutil.relativedelta import relativedelta
from trytond.config import config
+from trytond.ir.exceptions import SequenceAffixError
from trytond.ir.lang import _replace
from trytond.pool import Pool
from trytond.pyson import Eval, If, PYSONEncoder
@@ -118,6 +119,51 @@ class IrTestCase(ModuleTestCase):
with Transaction().set_context(date=next_year):
self.assertEqual(sequence.get(), '%s3' % str(next_year.year))
+ @with_transaction()
+ def test_sequence_format(self):
+ 'Test Sequence Format'
+ 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()
+ sequence.prefix = '${date_y}-'
+ sequence.save()
+
+ today = datetime.date(2023, 1, 1)
+
+ with Transaction().set_context(date=today):
+ self.assertEqual(sequence.get(), '23-1')
+
+ @with_transaction()
+ def test_sequence_wrong_format(self):
+ 'Test Sequence Wrong Format'
+ 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()
+
+ with self.assertRaises(SequenceAffixError):
+ sequence.prefix = '${date_K}'
+ sequence.save()
+
@with_transaction()
def test_global_search(self):
'Test Global Search'