diff --git a/CHANGELOG b/CHANGELOG
index 1fe906523c..185bb03e98 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Make product code unique
* Add code to template as variant prefix code
* Hide product fields inherited from template when empty
* Skip default products when template is created from product
diff --git a/doc/index.rst b/doc/index.rst
index 8ce7d3dffe..c06abe7157 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -61,10 +61,10 @@ The Product Template model contains the following fields:
- Active, allow to disable a product.
-The Product model extend the Product Template with two fields: Code
-and Description. If a sequence is set on product configuration the code will
-be readonly and will be filled in using the sequence. It's also possible to
-define a list of identifiers on product. An identifier is composed by a type
+The Product model extends the Product Template with two fields: Code (aka SKU_)
+and Description. If a sequence is set on product configuration the code will be
+read-only and will be filled in using the sequence. It's also possible to
+define a list of identifiers on product. An identifier is composed by a type
and a code. The following types are available:
* International Article Number (EAN)
@@ -74,6 +74,8 @@ and a code. The following types are available:
* International Securities Identification Number (ISIN)
* International Standard Music Number (ISMN)
+.. _SKU: https://en.wikipedia.org/wiki/Stock_keeping_unit
+
Configuration
*************
diff --git a/message.xml b/message.xml
index d77bc0e9f2..d8b1f2f10e 100644
--- a/message.xml
+++ b/message.xml
@@ -24,5 +24,8 @@ this repository contains the full copyright notices and license terms. -->
The %(type)s "%(code)s" for product "%(product)s" is not valid.
+
+ Code of active product must be unique.
+
diff --git a/product.py b/product.py
index 1927da7d90..e9f20e1f5f 100644
--- a/product.py
+++ b/product.py
@@ -7,12 +7,13 @@ from importlib import import_module
import stdnum
import stdnum.exceptions
-from sql import Null, Column
+from sql import Null, Column, Literal
+from sql.operators import Equal
from trytond.i18n import gettext
from trytond.model import (
ModelView, ModelSQL, Model, UnionMixin, DeactivableMixin, sequence_ordered,
- fields)
+ Exclude, fields)
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.pool import Pool
@@ -235,7 +236,8 @@ class Product(
states={
'readonly': Eval('code_readonly', False),
},
- depends=['code_readonly'])
+ depends=['code_readonly'],
+ help="The unique identifier for the product (aka SKU).")
code = fields.Char("Code", readonly=True, select=True)
identifiers = fields.One2Many(
'product.identifier', 'product', "Identifiers",
@@ -261,6 +263,14 @@ class Product(
super(Product, cls).__setup__()
+ t = cls.__table__()
+ cls._sql_constraints = [
+ ('code_exclude', Exclude(t, (t.code, Equal),
+ where=(t.active == Literal(True))
+ & (t.code != '')),
+ 'product.msg_product_code_unique'),
+ ]
+
for attr in dir(Template):
tfield = getattr(Template, attr)
if not isinstance(tfield, fields.Field):