diff --git a/product_barcode_sequence/README.rst b/product_barcode_sequence/README.rst new file mode 100644 index 00000000000..ac6fa71ca18 --- /dev/null +++ b/product_barcode_sequence/README.rst @@ -0,0 +1,99 @@ +======================== +Product Barcode Sequence +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:14df7d0f61557716727fca1494e708b4c6cf3c08e75ff3681712f14c2372c871 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github + :target: https://github.com/OCA/product-attribute/tree/17.0/product_barcode_sequence + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-17-0/product-attribute-17-0-product_barcode_sequence + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows automatic assignment of EAN barcodes to products +based on their category. It uses sequences to generate unique barcode +numbers and adds the correct GTIN check digit. + +Features: + +- Configure barcode generation per product category +- Automatic barcode generation when creating products +- Support for EAN-13 format with GTIN check digit +- Configurable barcode prefixes per category +- Manual barcode generation action for existing products + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Automatic Generation: + +When you create a new product in a category configured for barcode +generation, the system will automatically assign a unique EAN-13 +barcode. + +Manual Generation: + +For existing products without barcodes: + +1. Select the products from the product list +2. Use the **Generate Barcode** action +3. The system will generate barcodes based on their category + configuration + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Open Source Integrators + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_barcode_sequence/__init__.py b/product_barcode_sequence/__init__.py new file mode 100644 index 00000000000..63c7baa357f --- /dev/null +++ b/product_barcode_sequence/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Open Source Integrators +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/product_barcode_sequence/__manifest__.py b/product_barcode_sequence/__manifest__.py new file mode 100644 index 00000000000..19db293d5e1 --- /dev/null +++ b/product_barcode_sequence/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2026 Open Source Integrators +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Product Barcode Sequence", + "version": "17.0.1.0.0", + "author": "Open Source Integrators, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-attribute", + "license": "AGPL-3", + "category": "Product", + "summary": "Automatically assign EAN barcodes to products by category", + "depends": ["product"], + "data": [ + "views/product_category.xml", + "data/ir_actions_server.xml", + ], + "installable": True, + "auto_install": False, + "application": False, + "demo": [], +} diff --git a/product_barcode_sequence/data/ir_actions_server.xml b/product_barcode_sequence/data/ir_actions_server.xml new file mode 100644 index 00000000000..b701e31ef25 --- /dev/null +++ b/product_barcode_sequence/data/ir_actions_server.xml @@ -0,0 +1,15 @@ + + + + + Generate Barcode + + code + +records.action_generate_barcode() + + + list + + diff --git a/product_barcode_sequence/i18n/fr.po b/product_barcode_sequence/i18n/fr.po new file mode 100644 index 00000000000..3002b2bb9d7 --- /dev/null +++ b/product_barcode_sequence/i18n/fr.po @@ -0,0 +1,136 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_barcode_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-19 13:07+0000\n" +"PO-Revision-Date: 2026-02-19 13:07+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_barcode_sequence +#: model:ir.model.fields,field_description:product_barcode_sequence.field_product_category__auto_generate_barcode +msgid "Auto Generate Barcode" +msgstr "Générer automatiquement le code-barres" + +#. module: product_barcode_sequence +#: model:ir.model.fields,help:product_barcode_sequence.field_product_category__auto_generate_barcode +msgid "" +"Automatically generate EAN barcode when creating products in this category." +msgstr "" +"Générer automatiquement le code-barres EAN lors de la création de produits " +"dans cette catégorie." + +#. module: product_barcode_sequence +#: model_terms:ir.ui.view,arch_db:product_barcode_sequence.product_category_form_view +msgid "Barcode Configuration" +msgstr "Configuration du code-barres" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_product.py:0 +#, python-format +msgid "Barcode Generation" +msgstr "Génération du code-barres" + +#. module: product_barcode_sequence +#: model:ir.model.fields,field_description:product_barcode_sequence.field_product_category__barcode_prefix +msgid "Barcode Prefix" +msgstr "Préfixe du code-barres" + +#. module: product_barcode_sequence +#: model:ir.model.fields,field_description:product_barcode_sequence.field_product_category__barcode_sequence_id +msgid "Barcode Sequence" +msgstr "Séquence du code-barres" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_product.py:0 +#, python-format +msgid "Barcode must be 12 digits to calculate GTIN check digit" +msgstr "" +"Le code-barres doit contenir 12 chiffres pour calculer le chiffre de contrôle " +"GTIN" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_category.py:0 +#, python-format +msgid "Barcode prefix cannot be empty" +msgstr "Le préfixe du code-barres ne peut pas être vide" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_category.py:0 +#, python-format +msgid "Barcode prefix cannot exceed 12 digits" +msgstr "Le préfixe du code-barres ne peut pas dépasser 12 chiffres" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_category.py:0 +#, python-format +msgid "Barcode prefix must contain only digits" +msgstr "Le préfixe du code-barres doit contenir uniquement des chiffres" + +#. module: product_barcode_sequence +#: model:ir.model.fields,field_description:product_barcode_sequence.field_product_product__can_generate_barcode +msgid "Can Generate Barcode" +msgstr "Peut générer un code-barres" + +#. module: product_barcode_sequence +#: model:ir.actions.server,name:product_barcode_sequence.action_generate_barcode +msgid "Generate Barcode" +msgstr "Générer le code-barres" + +#. module: product_barcode_sequence +#: model:ir.model.fields,help:product_barcode_sequence.field_product_product__can_generate_barcode +msgid "" +"Indicates if this product can have a barcode automatically generated based " +"on its category configuration" +msgstr "" +"Indique si ce produit peut avoir un code-barres généré automatiquement en " +"fonction de la configuration de sa catégorie" + +#. module: product_barcode_sequence +#: model:ir.model.fields,help:product_barcode_sequence.field_product_category__barcode_prefix +msgid "" +"Prefix used to generate EAN barcodes for products created with this " +"category. Should be 1-12 digits." +msgstr "" +"Préfixe utilisé pour générer les codes-barres EAN des produits créés avec " +"cette catégorie. Doit contenir 1-12 chiffres." + +#. module: product_barcode_sequence +#: model:ir.model,name:product_barcode_sequence.model_product_category +msgid "Product Category" +msgstr "Catégorie de produit" + +#. module: product_barcode_sequence +#: model:ir.model,name:product_barcode_sequence.model_product_product +msgid "Product Variant" +msgstr "Variante de produit" + +#. module: product_barcode_sequence +#: model:ir.model.fields,help:product_barcode_sequence.field_product_category__barcode_sequence_id +msgid "" +"Sequence used to generate unique barcode numbers for products in this " +"category." +msgstr "" +"Séquence utilisée pour générer des numéros de code-barres uniques pour les " +"produits de cette catégorie." + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_product.py:0 +#, python-format +msgid "Successfully generated barcodes for %d products" +msgstr "Code-barres générés avec succès pour %d produits" diff --git a/product_barcode_sequence/i18n/product_barcode_sequence.pot b/product_barcode_sequence/i18n/product_barcode_sequence.pot new file mode 100644 index 00000000000..274eacba179 --- /dev/null +++ b/product_barcode_sequence/i18n/product_barcode_sequence.pot @@ -0,0 +1,125 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_barcode_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-19 13:07+0000\n" +"PO-Revision-Date: 2026-02-19 13:07+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_barcode_sequence +#: model:ir.model.fields,field_description:product_barcode_sequence.field_product_category__auto_generate_barcode +msgid "Auto Generate Barcode" +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.model.fields,help:product_barcode_sequence.field_product_category__auto_generate_barcode +msgid "" +"Automatically generate EAN barcode when creating products in this category." +msgstr "" + +#. module: product_barcode_sequence +#: model_terms:ir.ui.view,arch_db:product_barcode_sequence.product_category_form_view +msgid "Barcode Configuration" +msgstr "" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_product.py:0 +#, python-format +msgid "Barcode Generation" +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.model.fields,field_description:product_barcode_sequence.field_product_category__barcode_prefix +msgid "Barcode Prefix" +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.model.fields,field_description:product_barcode_sequence.field_product_category__barcode_sequence_id +msgid "Barcode Sequence" +msgstr "" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_product.py:0 +#, python-format +msgid "Barcode must be 12 digits to calculate GTIN check digit" +msgstr "" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_category.py:0 +#, python-format +msgid "Barcode prefix cannot be empty" +msgstr "" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_category.py:0 +#, python-format +msgid "Barcode prefix cannot exceed 12 digits" +msgstr "" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_category.py:0 +#, python-format +msgid "Barcode prefix must contain only digits" +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.model.fields,field_description:product_barcode_sequence.field_product_product__can_generate_barcode +msgid "Can Generate Barcode" +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.actions.server,name:product_barcode_sequence.action_generate_barcode +msgid "Generate Barcode" +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.model.fields,help:product_barcode_sequence.field_product_product__can_generate_barcode +msgid "" +"Indicates if this product can have a barcode automatically generated based " +"on its category configuration" +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.model.fields,help:product_barcode_sequence.field_product_category__barcode_prefix +msgid "" +"Prefix used to generate EAN barcodes for products created with this " +"category. Should be 1-12 digits." +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.model,name:product_barcode_sequence.model_product_category +msgid "Product Category" +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.model,name:product_barcode_sequence.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: product_barcode_sequence +#: model:ir.model.fields,help:product_barcode_sequence.field_product_category__barcode_sequence_id +msgid "" +"Sequence used to generate unique barcode numbers for products in this " +"category." +msgstr "" + +#. module: product_barcode_sequence +#. odoo-python +#: code:addons/product_barcode_sequence/models/product_product.py:0 +#, python-format +msgid "Successfully generated barcodes for %d products" +msgstr "" diff --git a/product_barcode_sequence/models/__init__.py b/product_barcode_sequence/models/__init__.py new file mode 100644 index 00000000000..c45272fd95f --- /dev/null +++ b/product_barcode_sequence/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2026 Open Source Integrators +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import product_category +from . import product_product diff --git a/product_barcode_sequence/models/product_category.py b/product_barcode_sequence/models/product_category.py new file mode 100644 index 00000000000..e752a44a885 --- /dev/null +++ b/product_barcode_sequence/models/product_category.py @@ -0,0 +1,87 @@ +# Copyright 2026 Open Source Integrators +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class ProductCategory(models.Model): + _inherit = "product.category" + + barcode_prefix = fields.Char( + help="Prefix used to generate EAN barcodes for products " + "created with this category. Should be 1-12 digits.", + size=12, + ) + barcode_sequence_id = fields.Many2one( + comodel_name="ir.sequence", + help="Sequence used to generate unique barcode numbers for products " + "in this category.", + copy=False, + readonly=True, + ) + auto_generate_barcode = fields.Boolean( + help="Automatically generate EAN barcode when creating products " + "in this category.", + default=False, + ) + + @api.constrains("barcode_prefix") + def _check_barcode_prefix(self): + """Validate barcode prefix contains only digits and correct length.""" + for category in self.filtered("barcode_prefix"): + if not category.barcode_prefix.isdigit(): + raise UserError(_("Barcode prefix must contain only digits")) + if len(category.barcode_prefix) > 12: + raise UserError(_("Barcode prefix cannot exceed 12 digits")) + if len(category.barcode_prefix) == 0: + raise UserError(_("Barcode prefix cannot be empty")) + + @api.model + def _prepare_barcode_sequence(self, prefix): + """Prepare the vals for creating the barcode sequence + :param prefix: a string with the prefix of the barcode. + :return: a dict with the values. + """ + vals = { + "name": "Barcode " + prefix, + "code": "product.barcode - " + prefix, + "padding": 12 - len(prefix), # Total 13 digits for EAN-13 + "prefix": prefix, + "company_id": False, + "implementation": "no_gap", + } + return vals + + def _create_or_update_barcode_sequence(self, prefix): + """Create or update barcode sequence for given prefix""" + seq_vals = self._prepare_barcode_sequence(prefix) + if self.barcode_sequence_id: + self.sudo().barcode_sequence_id.write( + { + "prefix": prefix, + "padding": 12 - len(prefix), + } + ) + else: + self.barcode_sequence_id = self.env["ir.sequence"].create(seq_vals) + + def write(self, vals): + prefix = vals.get("barcode_prefix", False) + if prefix: + for rec in self: + rec._create_or_update_barcode_sequence(prefix) + return super().write(vals) + + @api.model_create_multi + def create(self, vals_list): + vals_list_updated = [] + for vals in vals_list: + prefix = vals.get("barcode_prefix", False) + if prefix: + seq_vals = self._prepare_barcode_sequence(prefix) + sequence = self.env["ir.sequence"].create(seq_vals) + vals_list_updated.append(dict(vals, barcode_sequence_id=sequence.id)) + else: + vals_list_updated.append(vals) + return super().create(vals_list_updated) diff --git a/product_barcode_sequence/models/product_product.py b/product_barcode_sequence/models/product_product.py new file mode 100644 index 00000000000..2defdca46dc --- /dev/null +++ b/product_barcode_sequence/models/product_product.py @@ -0,0 +1,85 @@ +# Copyright 2026 Open Source Integrators +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class ProductProduct(models.Model): + _inherit = "product.product" + + can_generate_barcode = fields.Boolean( + compute="_compute_can_generate_barcode", + help="Indicates if this product can have a barcode automatically generated" + " based on its category configuration", + ) + + @api.depends( + "barcode", "categ_id.auto_generate_barcode", "categ_id.barcode_sequence_id" + ) + def _compute_can_generate_barcode(self): + """Compute if barcode generation is possible for this product""" + for product in self: + product.can_generate_barcode = ( + product.categ_id.auto_generate_barcode + and product.categ_id.barcode_sequence_id + ) + + def _calculate_gtin_check_digit(self, barcode_without_check): + """ + Calculate GTIN check digit for EAN-13 barcode. + :param barcode_without_check: 12-digit string without check digit + :return: check digit (0-9) + """ + if len(barcode_without_check) != 12 or not barcode_without_check.isdigit(): + raise UserError( + _("Barcode must be 12 digits to calculate GTIN check digit") + ) + # GS1 GTIN-13 algorithm: multiply digits in odd positions (1,3,5,7,9,11) by 1 + # and digits in even positions (2,4,6,8,10,12) by 3, then sum + odd_sum = sum(int(barcode_without_check[i]) for i in range(0, 12, 2)) + even_sum = sum(int(barcode_without_check[i]) for i in range(1, 12, 2)) + total = odd_sum + (even_sum * 3) + # Calculate check digit: (10 - (sum % 10)) % 10 + check_digit = (10 - (total % 10)) % 10 + return check_digit + + def _generate_barcodes(self, force=False): + """Generate barcodes for products in recordset""" + products_to_generate = self.filtered( + lambda x: (force or not x.barcode) and x.can_generate_barcode + ) + for product in products_to_generate: + # Generate next sequence number (prefix is already encoded in sequence) + barcode_without_check = product.categ_id.barcode_sequence_id.next_by_id() + # Ensure we have exactly 12 digits for check digit calculation + barcode_without_check = str(barcode_without_check).zfill(12) + # Calculate GTIN check digit + check_digit = product._calculate_gtin_check_digit(barcode_without_check) + barcode = barcode_without_check + str(check_digit) + # Update product with generated barcode + product.barcode = barcode + return products_to_generate + + @api.model_create_multi + def create(self, vals_list): + products = super().create(vals_list) + products._generate_barcodes() + return products + + def action_generate_barcode(self, force=False): + """ + Action to manually generate barcode for selected products. + Can be called from server actions. + """ + generated_products = self._generate_barcodes(force=force) + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Barcode Generation"), + "message": _("Successfully generated barcodes for %d products") + % len(generated_products), + "type": "success", + }, + } diff --git a/product_barcode_sequence/pyproject.toml b/product_barcode_sequence/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/product_barcode_sequence/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_barcode_sequence/readme/CONFIGURATION.md b/product_barcode_sequence/readme/CONFIGURATION.md new file mode 100644 index 00000000000..261293dd9b6 --- /dev/null +++ b/product_barcode_sequence/readme/CONFIGURATION.md @@ -0,0 +1,24 @@ +To configure barcode generation for a product category: + +1. Go to **Products > Configuration > Product Categories** +2. Select or create a category +3. In the **Barcode Configuration** section: + * Enable **Auto Generate Barcode** + * Set a **Barcode Prefix** (1-12 digits) + * The system will automatically create a barcode sequence + +Barcode Format: + +* EAN-13 format (13 digits total) +* First digits: Category prefix (configurable) +* Middle digits: Sequential number +* Last digit: GTIN check digit (automatically calculated) + +Example: + +If a category has prefix "123456": +* First product: 1234560000018 +* Second product: 1234560000025 +* Third product: 1234560000032 + +The check digit (last number) is calculated using the GTIN algorithm. diff --git a/product_barcode_sequence/readme/DESCRIPTION.md b/product_barcode_sequence/readme/DESCRIPTION.md new file mode 100644 index 00000000000..dc96c1d7c0d --- /dev/null +++ b/product_barcode_sequence/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +This module allows automatic assignment of EAN barcodes to products based on their category. +It uses sequences to generate unique barcode numbers and adds the correct GTIN check digit. + +Features: +* Configure barcode generation per product category +* Automatic barcode generation when creating products +* Support for EAN-13 format with GTIN check digit +* Configurable barcode prefixes per category +* Manual barcode generation action for existing products diff --git a/product_barcode_sequence/readme/USAGE.md b/product_barcode_sequence/readme/USAGE.md new file mode 100644 index 00000000000..367df95f7f0 --- /dev/null +++ b/product_barcode_sequence/readme/USAGE.md @@ -0,0 +1,12 @@ +Automatic Generation: + +When you create a new product in a category configured for barcode generation, +the system will automatically assign a unique EAN-13 barcode. + +Manual Generation: + +For existing products without barcodes: + +1. Select the products from the product list +2. Use the **Generate Barcode** action +3. The system will generate barcodes based on their category configuration diff --git a/product_barcode_sequence/static/description/index.html b/product_barcode_sequence/static/description/index.html new file mode 100644 index 00000000000..6ea960d53cf --- /dev/null +++ b/product_barcode_sequence/static/description/index.html @@ -0,0 +1,57 @@ +# Product Barcode Sequence + +This module allows automatic assignment of EAN barcodes to products based on their category. +It uses sequences to generate unique barcode numbers and adds the correct GTIN check digit. + +## Features + +* Configure barcode generation per product category +* Automatic barcode generation when creating products +* Support for EAN-13 format with GTIN check digit +* Configurable barcode prefixes per category +* Manual barcode generation action for existing products + +## Configuration + +1. Go to **Products > Configuration > Product Categories** +2. Select or create a category +3. In the **Barcode Configuration** section: + * Enable **Auto Generate Barcode** + * Set a **Barcode Prefix** (1-12 digits) + * The system will automatically create a barcode sequence + +## Usage + +### Automatic Generation +When you create a new product in a category configured for barcode generation, +the system will automatically assign a unique EAN-13 barcode. + +### Manual Generation +For existing products without barcodes: +1. Select the products from the product list +2. Use the **Generate Barcode** action +3. The system will generate barcodes based on their category configuration + +### Barcode Format +* EAN-13 format (13 digits total) +* First digits: Category prefix (configurable) +* Middle digits: Sequential number +* Last digit: GTIN check digit (automatically calculated) + +### Example +If a category has prefix "123456": +* First product: 1234560000018 +* Second product: 1234560000025 +* Third product: 1234560000032 + +## Dependencies + +* `product` - Base product module + +## License + +AGPL-3 + +## Author + +Open Source Integrators diff --git a/product_barcode_sequence/tests/__init__.py b/product_barcode_sequence/tests/__init__.py new file mode 100644 index 00000000000..5a636837416 --- /dev/null +++ b/product_barcode_sequence/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Open Source Integrators +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_product_barcode_sequence diff --git a/product_barcode_sequence/tests/test_product_barcode_sequence.py b/product_barcode_sequence/tests/test_product_barcode_sequence.py new file mode 100644 index 00000000000..6e1c42b1f0c --- /dev/null +++ b/product_barcode_sequence/tests/test_product_barcode_sequence.py @@ -0,0 +1,333 @@ +# Copyright 2026 Open Source Integrators +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase + + +class TestProductBarcodeSequence(TransactionCase): + """Test essential workflows for product barcode sequence module.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create test category with barcode configuration + cls.category_electronics = cls.env["product.category"].create( + { + "name": "Electronics", + "auto_generate_barcode": True, + "barcode_prefix": "123456", + } + ) + + # Create test category without barcode configuration + cls.category_general = cls.env["product.category"].create( + { + "name": "General", + } + ) + + # Create existing product with barcode in disabled category to avoid + # auto-generation + cls.existing_product = cls.env["product.product"].create( + { + "name": "Existing Product", + "categ_id": cls.category_general.id, + "barcode": "1234560000018", + } + ) + + # ==================== HELPER FUNCTIONS ==================== + + def _create_category_with_barcode_config(self, name, prefix, auto_generate=True): + """Helper: Create category with barcode configuration.""" + return self.env["product.category"].create( + { + "name": name, + "auto_generate_barcode": auto_generate, + "barcode_prefix": prefix, + } + ) + + def _create_product_in_category(self, name, category, default_code=None): + """Helper: Create product in specific category.""" + vals = { + "name": name, + "categ_id": category.id, + "default_code": default_code or "", + } + return self.env["product.product"].create(vals) + + def _verify_barcode_format(self, barcode, expected_prefix): + """Helper: Verify barcode has correct EAN-13 format.""" + self.assertEqual(len(barcode), 13, "Barcode must be 13 digits") + self.assertTrue(barcode.isdigit(), "Barcode must contain only digits") + self.assertTrue( + barcode.startswith(expected_prefix), + f"Barcode must start with prefix {expected_prefix}", + ) + + # Verify GTIN check digit + barcode_without_check = barcode[:12] + expected_check_digit = self._calculate_gtin_check_digit(barcode_without_check) + self.assertEqual( + int(barcode[-1]), expected_check_digit, "GTIN check digit is incorrect" + ) + + def _calculate_gtin_check_digit(self, barcode_without_check): + """Helper: Calculate GTIN check digit (same as product method).""" + odd_sum = sum(int(barcode_without_check[i]) for i in range(0, 12, 2)) + even_sum = sum(int(barcode_without_check[i]) for i in range(1, 12, 2)) + total = odd_sum + (even_sum * 3) + check_digit = (10 - (total % 10)) % 10 + return check_digit + + def _get_sequence_next_number(self, category): + """Helper: Get next sequence number for category.""" + return category.barcode_sequence_id.number_next_actual + + # ==================== WORKFLOW TESTS ==================== + + def test_workflow_01_category_sequence_creation(self): + """Workflow: Category sequence creation when setting barcode prefix.""" + # Step 1: Create category without prefix + category = self._create_category_with_barcode_config("Test Category", "", False) + self.assertFalse( + category.barcode_sequence_id, "No sequence should exist without prefix" + ) + + # Step 2: Set barcode prefix + category.write({"barcode_prefix": "789012"}) + + # Step 3: Verify sequence was created + self.assertTrue(category.barcode_sequence_id, "Sequence should be created") + self.assertEqual(category.barcode_sequence_id.prefix, "789012") + self.assertEqual(category.barcode_sequence_id.padding, 6) # 12 - 6 = 6 + + def test_workflow_02_automatic_barcode_generation_on_product_creation(self): + """Workflow: Automatic barcode generation when creating product.""" + # Step 1: Verify sequence starting point + initial_seq_num = self._get_sequence_next_number(self.category_electronics) + + # Step 2: Create product in barcode-enabled category + product = self._create_product_in_category( + "Test Product", self.category_electronics + ) + + # Step 3: Verify barcode was generated + self.assertTrue(product.barcode, "Barcode should be generated automatically") + self._verify_barcode_format(product.barcode, "123456") + + # Step 4: Verify sequence was incremented + final_seq_num = self._get_sequence_next_number(self.category_electronics) + self.assertEqual( + final_seq_num, initial_seq_num + 1, "Sequence should be incremented" + ) + + def test_workflow_03_no_barcode_generation_for_disabled_category(self): + """Workflow: No barcode generation for category without auto-generate.""" + # Step 1: Create category with prefix but disabled auto-generation + category = self._create_category_with_barcode_config( + "Disabled Category", "111111", False + ) + + # Step 2: Create product in disabled category + product = self._create_product_in_category("Test Product", category) + + # Step 3: Verify no barcode was generated + self.assertFalse(product.barcode, "No barcode should be generated") + self.assertFalse( + product.can_generate_barcode, + "Product should not be eligible for barcode generation", + ) + + def test_workflow_04_manual_barcode_generation_action(self): + """Workflow: Manual barcode generation using action.""" + # Step 1: Create product in disabled category first, then move to enabled + product = self._create_product_in_category( + "Manual Test Product", self.category_general + ) + self.assertFalse(product.barcode, "Product should start without barcode") + + # Move to enabled category + product.write({"categ_id": self.category_electronics.id}) + self.assertFalse( + product.barcode, + "Product should still not have barcode after category change", + ) + + # Step 2: Execute manual barcode generation action + action_result = product.action_generate_barcode() + + # Step 3: Verify barcode was generated + self.assertTrue(product.barcode, "Barcode should be generated manually") + self._verify_barcode_format(product.barcode, "123456") + + # Step 4: Verify action returned success notification + self.assertEqual(action_result["type"], "ir.actions.client") + self.assertEqual(action_result["tag"], "display_notification") + self.assertIn("Successfully", action_result["params"]["message"]) + + def test_workflow_05_bulk_barcode_generation(self): + """Workflow: Bulk barcode generation for multiple products.""" + # Step 1: Create multiple products in disabled category first + products = self.env["product.product"] + for i in range(3): + product = self._create_product_in_category( + f"Bulk Product {i+1}", self.category_general + ) + products += product + self.assertFalse( + product.barcode, f"Product {i+1} should start without barcode" + ) + + # Move all products to enabled category + products.write({"categ_id": self.category_electronics.id}) + + # Verify they still don't have barcodes + for i, product in enumerate(products): + self.assertFalse( + product.barcode, + f"Product {i+1} should still not have barcode after category change", + ) + + # Step 2: Execute bulk barcode generation + generated_products = products._generate_barcodes() + + # Step 3: Verify all products got barcodes + self.assertEqual(len(generated_products), 3, "All products should be generated") + for product in products: + self.assertTrue(product.barcode, "Product should have barcode") + self._verify_barcode_format(product.barcode, "123456") + + # Step 4: Verify barcodes are unique + barcodes = products.mapped("barcode") + self.assertEqual( + len(barcodes), len(set(barcodes)), "All barcodes should be unique" + ) + + def test_workflow_06_category_sequence_update(self): + """Workflow: Update category barcode prefix and sequence.""" + # Step 1: Create category with initial prefix + category = self._create_category_with_barcode_config("Update Test", "111111") + initial_sequence_id = category.barcode_sequence_id.id + + # Step 2: Update prefix + category.write({"barcode_prefix": "222222"}) + + # Step 3: Verify sequence was updated (not recreated) + self.assertEqual( + category.barcode_sequence_id.id, + initial_sequence_id, + "Same sequence should be updated, not recreated", + ) + self.assertEqual( + category.barcode_sequence_id.prefix, + "222222", + "Sequence prefix should be updated", + ) + self.assertEqual( + category.barcode_sequence_id.padding, + 6, + "Sequence padding should be updated", + ) + + def test_workflow_07_error_handling_invalid_prefix(self): + """Workflow: Error handling for invalid barcode prefix.""" + # Step 1: Try to create category with non-digit prefix + with self.assertRaises(UserError) as context: + self.env["product.category"].create( + { + "name": "Invalid Category", + "auto_generate_barcode": True, + "barcode_prefix": "ABC123", + } + ) + + # Step 2: Verify error message + self.assertIn("must contain only digits", str(context.exception)) + + def test_workflow_08_gtin_check_digit_calculation(self): + """Workflow: GTIN check digit calculation accuracy.""" + # Step 1: Test known barcode examples + test_cases = [ + ("123456000001", 2), # Correct GTIN check digit + ("123456000002", 9), # Correct GTIN check digit + ("123456000003", 6), # Correct GTIN check digit + ] + + for barcode_without_check, expected_check_digit in test_cases: + # Step 2: Calculate check digit + calculated = self._calculate_gtin_check_digit(barcode_without_check) + + # Step 3: Verify calculation + self.assertEqual( + calculated, + expected_check_digit, + f"Check digit for {barcode_without_check} should be " + f"{expected_check_digit}", + ) + + def test_workflow_09_computed_field_can_generate_barcode(self): + """Workflow: Computed field can_generate_barcode behavior.""" + # Step 1: Test product in enabled category without barcode + product = self._create_product_in_category( + "Test Product", self.category_general + ) + # Move to enabled category but don't generate barcode + product.write({"categ_id": self.category_electronics.id}) + self.assertTrue( + product.can_generate_barcode, + "Product in enabled category without barcode should be generatable", + ) + + # Step 2: Test product with existing barcode (use unique barcode) + product.write({"barcode": "9999999999999"}) + self.assertFalse( + product.can_generate_barcode, + "Product with existing barcode should not be generatable", + ) + + # Step 3: Test product in disabled category + product.write({"barcode": False, "categ_id": self.category_general.id}) + self.assertFalse( + product.can_generate_barcode, + "Product in disabled category should not be generatable", + ) + + # Step 4: Test product in category without sequence + category_no_seq = self._create_category_with_barcode_config("No Seq", "", True) + product.write({"categ_id": category_no_seq.id}) + self.assertFalse( + product.can_generate_barcode, + "Product in category without sequence should not be generatable", + ) + + def test_workflow_10_force_barcode_regeneration(self): + """Workflow: Force barcode regeneration for existing barcodes.""" + # Step 1: Create product with existing barcode + # (in disabled category to avoid auto-generation) + product = self._create_product_in_category( + "Force Test Product", self.category_general + ) + product.write({"barcode": "9999999999999"}) + original_barcode = product.barcode + self.assertTrue(product.barcode, "Product should start with barcode") + + # Move to enabled category + product.write({"categ_id": self.category_electronics.id}) + + # Step 2: Force regenerate barcode + generated_products = product._generate_barcodes(force=True) + + # Step 3: Verify barcode was changed + self.assertNotEqual( + product.barcode, + original_barcode, + "Barcode should be different after force regeneration", + ) + self._verify_barcode_format(product.barcode, "123456") + self.assertIn( + product, generated_products, "Product should be in generated products" + ) diff --git a/product_barcode_sequence/views/product_category.xml b/product_barcode_sequence/views/product_category.xml new file mode 100644 index 00000000000..b9dea8e4756 --- /dev/null +++ b/product_barcode_sequence/views/product_category.xml @@ -0,0 +1,25 @@ + + + + product.category.form - product_barcode_sequence + product.category + + + + + + + + + +