diff --git a/modules/product/CHANGELOG b/modules/product/CHANGELOG
index 7d0657182d..4d8eb49430 100644
--- a/modules/product/CHANGELOG
+++ b/modules/product/CHANGELOG
@@ -1,3 +1,4 @@
+* Add code on product category
* Add product deactivable mixins
Version 7.0.0 - 2023-10-30
diff --git a/modules/product/category.py b/modules/product/category.py
index d3c0512864..7c4406b82d 100644
--- a/modules/product/category.py
+++ b/modules/product/category.py
@@ -1,13 +1,27 @@
# 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 trytond.model import ModelSQL, ModelView, fields, tree
-from trytond.pyson import PYSONEncoder
+
+from sql.conditionals import NullIf
+from sql.functions import CharLength
+from sql.operators import Equal
+
+from trytond.model import Exclude, ModelSQL, ModelView, fields, tree
+from trytond.pool import Pool
+from trytond.pyson import Eval, PYSONEncoder
+from trytond.tools import is_full_text, lstrip_wildcard
class Category(tree(separator=' / '), ModelSQL, ModelView):
"Product Category"
__name__ = "product.category"
name = fields.Char('Name', required=True, translate=True)
+ code = fields.Char(
+ "Code",
+ states={
+ 'readonly': Eval('code_readonly', False),
+ })
+ code_readonly = fields.Function(
+ fields.Boolean("Code Readonly"), 'get_code_readonly')
parent = fields.Many2One(
'product.category', "Parent",
help="Used to add structure above the category.")
@@ -20,7 +34,13 @@ class Category(tree(separator=' / '), ModelSQL, ModelView):
@classmethod
def __setup__(cls):
+ cls.code.search_unaccented = False
super(Category, cls).__setup__()
+ t = cls.__table__()
+ cls._sql_constraints += [
+ ('code_unique', Exclude(t, (NullIf(t.code, ''), Equal)),
+ 'product.msg_category_code_unique'),
+ ]
cls._order.insert(0, ('name', 'ASC'))
cls._buttons.update({
'add_products': {
@@ -28,6 +48,39 @@ class Category(tree(separator=' / '), ModelSQL, ModelView):
},
})
+ @classmethod
+ def default_code_readonly(cls):
+ pool = Pool()
+ Configuration = pool.get('product.configuration')
+ config = Configuration(1)
+ return bool(config.category_sequence)
+
+ def get_code_readonly(self, name):
+ return self.default_code_readonly()
+
+ @classmethod
+ def order_code(cls, tables):
+ table, _ = tables[None]
+ if cls.default_code_readonly():
+ return [CharLength(table.code), table.code]
+ else:
+ return [table.code]
+
+ @classmethod
+ def search_rec_name(cls, name, clause):
+ _, operator, operand, *extra = clause
+ if operator.startswith('!') or operator.startswith('not '):
+ bool_op = 'AND'
+ else:
+ bool_op = 'OR'
+ code_value = operand
+ if operator.endswith('like') and is_full_text(operand):
+ code_value = lstrip_wildcard(operand)
+ return [bool_op,
+ ('name', operator, operand, *extra),
+ ('code', operator, code_value, *extra),
+ ]
+
@classmethod
@ModelView.button_action('product.act_category_product')
def add_products(cls, categories):
@@ -37,8 +90,26 @@ class Category(tree(separator=' / '), ModelSQL, ModelView):
[('id', '=', categories[0].id)]),
}
+ @classmethod
+ def _new_code(cls):
+ pool = Pool()
+ Configuration = pool.get('product.configuration')
+ config = Configuration(1)
+ sequence = config.category_sequence
+ if sequence:
+ return sequence.get()
+
+ @classmethod
+ def create(cls, vlist):
+ vlist = [v.copy() for v in vlist]
+ for values in vlist:
+ if not values.get('code'):
+ values['code'] = cls._new_code()
+ return super().create(vlist)
+
@classmethod
def copy(cls, categories, default=None):
default = default.copy() if default is not None else {}
default.setdefault('templates')
+ default.setdefault('code', None)
return super().copy(categories, default=default)
diff --git a/modules/product/category.xml b/modules/product/category.xml
index b8854bfe3c..52c2fa294e 100644
--- a/modules/product/category.xml
+++ b/modules/product/category.xml
@@ -102,5 +102,17 @@ this repository contains the full copyright notices and license terms. -->