diff --git a/bom_product_revision/README.rst b/bom_product_revision/README.rst new file mode 100644 index 00000000..f46535e2 --- /dev/null +++ b/bom_product_revision/README.rst @@ -0,0 +1,93 @@ +=============================== +BOM Product Revision Management +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5d8948b6a96f42cf4eae328d56020345158b8885849b78ece9dd065daf8516fd + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Faxls--oca-lightgray.png?logo=github + :target: https://github.com/OCA/axls-oca/tree/wng-add-revision-functions/bom_product_revision + :alt: OCA/axls-oca +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/axls-oca-wng-add-revision-functions/axls-oca-wng-add-revision-functions-bom_product_revision + :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/axls-oca&target_branch=wng-add-revision-functions + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of the product_revision module to include +revision information in BOM lines. + +Features: + + +* Adds revision information to BOM lines +* Automatically sets the latest revision information when a BOM line is created +* Allows users to change the revision after saving the BOM + +**Table of contents** + +.. contents:: + :local: + +Known issues / Roadmap +====================== + +* Add support for filtering BOMs by revision +* Add support for copying revisions when copying BOMs +* Add support for revision history in BOMs + +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 +~~~~~~~ + +* Axelspace + +Contributors +~~~~~~~~~~~~ + +* `Axelspace Corporation `__: + + * WangTKurata + +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/axls-oca `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/bom_product_revision/__init__.py b/bom_product_revision/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/bom_product_revision/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/bom_product_revision/__manifest__.py b/bom_product_revision/__manifest__.py new file mode 100644 index 00000000..aaaaceee --- /dev/null +++ b/bom_product_revision/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2025 Axelspace +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "BOM Product Revision Management", + "summary": "Link product revisions to BOM lines", + "author": "Axelspace, Odoo Community Association (OCA)", + "website": "https://www.axelspace.com", + "license": "AGPL-3", + "category": "Manufacturing", + "version": "16.0.1.0.0", + "depends": ["mrp", "product_revision"], + "data": [ + "security/ir.model.access.csv", + "views/mrp_bom_views.xml", + ], + "demo": [], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/bom_product_revision/i18n/ja_JP.po b/bom_product_revision/i18n/ja_JP.po new file mode 100644 index 00000000..b062c34b --- /dev/null +++ b/bom_product_revision/i18n/ja_JP.po @@ -0,0 +1,127 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * bom_product_revision +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0-20230701\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-06 23:02+0000\n" +"PO-Revision-Date: 2025-05-06 23:02+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: bom_product_revision +#: model:ir.model,name:bom_product_revision.model_mrp_bom +msgid "Bill of Material" +msgstr "部品表" + +#. module: bom_product_revision +#: model:ir.model,name:bom_product_revision.model_mrp_bom_line +msgid "Bill of Material Line" +msgstr "部品表明細" + +#. module: bom_product_revision +#. odoo-python +#: code:addons/bom_product_revision/models/mrp_bom.py:0 +#: code:addons/bom_product_revision/models/mrp_bom_line.py:0 +#, python-format +msgid "Inactive Revision Selected" +msgstr "無効なリビジョンが選択されました" + +#. module: bom_product_revision +#: model:ir.model.fields,field_description:bom_product_revision.field_mrp_bom__inactive_revision_warning +#: model:ir.model.fields,field_description:bom_product_revision.field_mrp_bom_line__inactive_revision_warning +msgid "Inactive Revision Warning" +msgstr "旧リビジョン使用警告" + +#. module: bom_product_revision +#: model:ir.model,name:bom_product_revision.model_product_revision +#: model:ir.model.fields,field_description:bom_product_revision.field_mrp_bom__revision_id +msgid "Product Revision" +msgstr "製品リビジョン" + +#. module: bom_product_revision +#: model:ir.model.fields,field_description:bom_product_revision.field_mrp_bom_line__revision_id +msgid "Revision" +msgstr "リビジョン" + +#. module: bom_product_revision +#: model:ir.model.fields,field_description:bom_product_revision.field_mrp_bom__revision_active +#: model:ir.model.fields,field_description:bom_product_revision.field_mrp_bom_line__revision_active +msgid "Revision Active" +msgstr "有効なリビジョン" + +#. module: bom_product_revision +#: model:ir.model.fields,field_description:bom_product_revision.field_mrp_bom__revision_number +#: model:ir.model.fields,field_description:bom_product_revision.field_mrp_bom_line__revision_number +msgid "Revision Number" +msgstr "リビジョン番号" + +#. module: bom_product_revision +#: model:ir.model.fields,help:bom_product_revision.field_mrp_bom__revision_active +#: model:ir.model.fields,help:bom_product_revision.field_mrp_bom_line__revision_active +msgid "Technical field to indicate if the revision is active" +msgstr "リビジョンが有効かどうかを示す技術フィールド" + +#. module: bom_product_revision +#: model:ir.model.fields,help:bom_product_revision.field_mrp_bom_line__revision_number +msgid "The revision number of the product in this BOM line" +msgstr "この部品表明細の製品のリビジョン番号" + +#. module: bom_product_revision +#: model:ir.model.fields,help:bom_product_revision.field_mrp_bom__revision_number +msgid "The revision number of the product this BOM is for" +msgstr "この部品表用製品のリビジョン番号" + +#. module: bom_product_revision +#: model:ir.model.fields,help:bom_product_revision.field_mrp_bom_line__revision_id +msgid "The revision of the product in this BOM line" +msgstr "この部品表明細の製品のリビジョン" + +#. module: bom_product_revision +#: model:ir.model.fields,help:bom_product_revision.field_mrp_bom__revision_id +msgid "The revision of the product this BOM is for" +msgstr "この部品表用製品のリビジョン" + +#. module: bom_product_revision +#. odoo-python +#: code:addons/bom_product_revision/models/mrp_bom.py:0 +#: code:addons/bom_product_revision/models/mrp_bom_line.py:0 +#, python-format +msgid "" +"The selected revision '%(revision)s' is not active. This may affect " +"manufacturing processes." +msgstr "選択されたリビジョン '%(revision)s' は無効です。これにより製造プロセスに影響が出る可能性があります。" + +#. module: bom_product_revision +#. odoo-python +#: code:addons/bom_product_revision/models/mrp_bom.py:0 +#, python-format +msgid "" +"This BOM is using inactive revision \"%(revision)s\". This may affect " +"manufacturing processes." +msgstr "この部品表は無効なリビジョン \"%(revision)s\" を使用しています。これにより製造プロセスに影響が出る可能性があります。" + +#. module: bom_product_revision +#. odoo-python +#: code:addons/bom_product_revision/models/mrp_bom_line.py:0 +#, python-format +msgid "" +"This BOM line is using inactive revision \"%(revision)s\". This may affect " +"manufacturing processes." +msgstr "この部品表明細は無効なリビジョン \"%(revision)s\" を使用しています。これにより製造プロセスに影響が出る可能性があります。" + +#. module: bom_product_revision +#: model:ir.model.fields,help:bom_product_revision.field_mrp_bom_line__inactive_revision_warning +msgid "Warning displayed when the BOM line uses an inactive revision" +msgstr "部品表明細が無効なリビジョンを使用している場合に表示される警告" + +#. module: bom_product_revision +#: model:ir.model.fields,help:bom_product_revision.field_mrp_bom__inactive_revision_warning +msgid "Warning displayed when the BOM uses an inactive revision" +msgstr "部品表が無効なリビジョンを使用している場合に表示される警告" diff --git a/bom_product_revision/models/__init__.py b/bom_product_revision/models/__init__.py new file mode 100644 index 00000000..5dc540f2 --- /dev/null +++ b/bom_product_revision/models/__init__.py @@ -0,0 +1,3 @@ +from . import mrp_bom_line +from . import mrp_bom +from . import product_revision diff --git a/bom_product_revision/models/mrp_bom.py b/bom_product_revision/models/mrp_bom.py new file mode 100644 index 00000000..942596a2 --- /dev/null +++ b/bom_product_revision/models/mrp_bom.py @@ -0,0 +1,111 @@ +from odoo import _, api, fields, models + + +class MrpBom(models.Model): + _inherit = "mrp.bom" + + revision_id = fields.Many2one( + "product.revision", + string="Product Revision", + help="The revision of the product this BOM is for", + ) + revision_number = fields.Char( + related="revision_id.revision_number", + string="Revision Number", + readonly=True, + store=True, + help="The revision number of the product this BOM is for", + ) + inactive_revision_warning = fields.Html( + compute="_compute_inactive_revision_warning", + help="Warning displayed when the BOM uses an inactive revision", + ) + revision_active = fields.Boolean( + compute="_compute_revision_active", + help="Technical field to indicate if the revision is active", + store=True, + ) + + @api.depends("revision_id", "revision_id.active") + def _compute_revision_active(self): + for bom in self: + bom.revision_active = bom.revision_id.active if bom.revision_id else True + + @api.depends("revision_id", "revision_id.active") + def _compute_inactive_revision_warning(self): + for bom in self: + if bom.revision_id and not bom.revision_id.active: + warning_message = _( + 'This BOM is using inactive revision "%(revision)s". ' + "This may affect manufacturing processes.", + revision=bom.revision_id.revision_number, + ) + bom.inactive_revision_warning = ( + '" + ) + else: + bom.inactive_revision_warning = False + + @api.onchange("product_tmpl_id", "product_id") + def _onchange_product_id(self): + """When product changes, set the latest revision""" + res = super(MrpBom, self)._onchange_product_id() + + # Clear revision when product changes + self.revision_id = False + + if self.product_id: + # First check if the product variant has its own revision + if self.product_id.current_revision_id: + self.revision_id = self.product_id.current_revision_id + # If not, fall back to the product template's revision + elif self.product_id.product_tmpl_id.current_revision_id: + self.revision_id = self.product_id.product_tmpl_id.current_revision_id + elif self.product_tmpl_id: + # If only product template is set, use its revision + if self.product_tmpl_id.current_revision_id: + self.revision_id = self.product_tmpl_id.current_revision_id + + return res + + @api.onchange("revision_id") + def _onchange_revision_id(self): + """Show warning if selected revision is not active""" + res = {} + if self.revision_id and not self.revision_id.active: + res["warning"] = { + "title": _("Inactive Revision Selected"), + "message": _( + "The selected revision '%(revision)s' is not active. " + "This may affect manufacturing processes.", + revision=self.revision_id.revision_number, + ), + } + return res + + @api.model_create_multi + def create(self, vals_list): + """Override create to automatically set the revision_id if available""" + for vals in vals_list: + if not vals.get("revision_id"): + # If product_id is set, use its revision + if vals.get("product_id"): + product = self.env["product.product"].browse(vals["product_id"]) + # First check if the product variant has its own revision + if product.current_revision_id: + vals["revision_id"] = product.current_revision_id.id + # If not, fall back to the product template's revision + elif product.product_tmpl_id.current_revision_id: + vals[ + "revision_id" + ] = product.product_tmpl_id.current_revision_id.id + # If only product_tmpl_id is set, use its revision + elif vals.get("product_tmpl_id"): + product_tmpl = self.env["product.template"].browse( + vals["product_tmpl_id"] + ) + if product_tmpl.current_revision_id: + vals["revision_id"] = product_tmpl.current_revision_id.id + return super(MrpBom, self).create(vals_list) diff --git a/bom_product_revision/models/mrp_bom_line.py b/bom_product_revision/models/mrp_bom_line.py new file mode 100644 index 00000000..e7bcd832 --- /dev/null +++ b/bom_product_revision/models/mrp_bom_line.py @@ -0,0 +1,98 @@ +from odoo import _, api, fields, models + + +class MrpBomLine(models.Model): + _inherit = "mrp.bom.line" + + revision_id = fields.Many2one( + "product.revision", + string="Revision", + help="The revision of the product in this BOM line", + ) + revision_number = fields.Char( + related="revision_id.revision_number", + string="Revision Number", + readonly=True, + store=True, + help="The revision number of the product in this BOM line", + ) + inactive_revision_warning = fields.Html( + compute="_compute_inactive_revision_warning", + help="Warning displayed when the BOM line uses an inactive revision", + ) + revision_active = fields.Boolean( + compute="_compute_revision_active", + help="Technical field to indicate if the revision is active", + store=True, + ) + + @api.depends("revision_id", "revision_id.active") + def _compute_revision_active(self): + for line in self: + line.revision_active = line.revision_id.active if line.revision_id else True + + @api.depends("revision_id", "revision_id.active") + def _compute_inactive_revision_warning(self): + for line in self: + if line.revision_id and not line.revision_id.active: + warning_message = _( + 'This BOM line is using inactive revision "%(revision)s". ' + "This may affect manufacturing processes.", + revision=line.revision_id.revision_number, + ) + line.inactive_revision_warning = ( + '" + ) + else: + line.inactive_revision_warning = False + + @api.onchange("product_id") + def onchange_product_id(self): + """When product changes, set the latest revision and update domain""" + res = super(MrpBomLine, self).onchange_product_id() + + # Set the latest revision + if self.product_id: + # First check if the product variant has its own revision + if self.product_id.current_revision_id: + self.revision_id = self.product_id.current_revision_id + # If not, fall back to the product template's revision + elif self.product_id.product_tmpl_id.current_revision_id: + self.revision_id = self.product_id.product_tmpl_id.current_revision_id + else: + self.revision_id = False + + # No need to set domain here, it's handled in the name_search method of product.revision + + return res + + @api.onchange("revision_id") + def _onchange_revision_id(self): + """Show warning if selected revision is not active""" + res = {} + if self.revision_id and not self.revision_id.active: + res["warning"] = { + "title": _("Inactive Revision Selected"), + "message": _( + "The selected revision '%(revision)s' is not active. " + "This may affect manufacturing processes.", + revision=self.revision_id.revision_number, + ), + } + return res + + @api.model_create_multi + def create(self, vals_list): + """Override create to automatically set the revision_id if available""" + for vals in vals_list: + if not vals.get("revision_id") and vals.get("product_id"): + product = self.env["product.product"].browse(vals["product_id"]) + # First check if the product variant has its own revision + if product.current_revision_id: + vals["revision_id"] = product.current_revision_id.id + # If not, fall back to the product template's revision + elif product.product_tmpl_id.current_revision_id: + vals["revision_id"] = product.product_tmpl_id.current_revision_id.id + return super(MrpBomLine, self).create(vals_list) diff --git a/bom_product_revision/models/product_revision.py b/bom_product_revision/models/product_revision.py new file mode 100644 index 00000000..c6bc9873 --- /dev/null +++ b/bom_product_revision/models/product_revision.py @@ -0,0 +1,39 @@ +from odoo import api, models + + +class ProductRevision(models.Model): + _inherit = "product.revision" + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + """Override name_search to filter revisions based on product_id in context""" + if args is None: + args = [] + + # Get product_id and product_tmpl_id from context + product_id = self.env.context.get("product_id") + product_tmpl_id = self.env.context.get("product_tmpl_id") + + # If product_id is set, filter revisions for this product + if product_id: + product = self.env["product.product"].browse(product_id) + args = args + [ + "|", + ("active", "=", True), + ("active", "=", False), # Include both active and inactive revisions + "|", + ("product_id", "=", product_id), + ("product_tmpl_id", "=", product.product_tmpl_id.id), + ] + # If only product_tmpl_id is set, filter revisions for this template + elif product_tmpl_id: + args = args + [ + "|", + ("active", "=", True), + ("active", "=", False), # Include both active and inactive revisions + ("product_tmpl_id", "=", product_tmpl_id), + ] + + return super(ProductRevision, self).name_search( + name=name, args=args, operator=operator, limit=limit + ) diff --git a/bom_product_revision/readme/CONTRIBUTORS.rst b/bom_product_revision/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..bd765ad6 --- /dev/null +++ b/bom_product_revision/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Axelspace Corporation `__: + + * WangTKurata diff --git a/bom_product_revision/readme/DESCRIPTION.rst b/bom_product_revision/readme/DESCRIPTION.rst new file mode 100644 index 00000000..a63a2dcd --- /dev/null +++ b/bom_product_revision/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +This module extends the functionality of the product_revision module to include +revision information in BOM lines. + +Features: + + +* Adds revision information to BOM lines +* Automatically sets the latest revision information when a BOM line is created +* Allows users to change the revision after saving the BOM diff --git a/bom_product_revision/readme/ROADMAP.rst b/bom_product_revision/readme/ROADMAP.rst new file mode 100644 index 00000000..b4d61497 --- /dev/null +++ b/bom_product_revision/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +* Add support for filtering BOMs by revision +* Add support for copying revisions when copying BOMs +* Add support for revision history in BOMs diff --git a/bom_product_revision/security/ir.model.access.csv b/bom_product_revision/security/ir.model.access.csv new file mode 100644 index 00000000..97dd8b91 --- /dev/null +++ b/bom_product_revision/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/bom_product_revision/static/description/index.html b/bom_product_revision/static/description/index.html new file mode 100644 index 00000000..df0d8b13 --- /dev/null +++ b/bom_product_revision/static/description/index.html @@ -0,0 +1,442 @@ + + + + + +BOM Product Revision Management + + + +
+

BOM Product Revision Management

+ + +

Beta License: AGPL-3 OCA/axls-oca Translate me on Weblate Try me on Runboat

+

This module extends the functionality of the product_revision module to include +revision information in BOM lines.

+

Features:

+
    +
  • Adds revision information to BOM lines
  • +
  • Automatically sets the latest revision information when a BOM line is created
  • +
  • Allows users to change the revision after saving the BOM
  • +
+

Table of contents

+ +
+

Known issues / Roadmap

+
    +
  • Add support for filtering BOMs by revision
  • +
  • Add support for copying revisions when copying BOMs
  • +
  • Add support for revision history in BOMs
  • +
+
+
+

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

+
    +
  • Axelspace
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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/axls-oca project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/bom_product_revision/tests/__init__.py b/bom_product_revision/tests/__init__.py new file mode 100644 index 00000000..75a38472 --- /dev/null +++ b/bom_product_revision/tests/__init__.py @@ -0,0 +1 @@ +from . import test_bom_product_revision diff --git a/bom_product_revision/tests/test_bom_product_revision.py b/bom_product_revision/tests/test_bom_product_revision.py new file mode 100644 index 00000000..d6107c17 --- /dev/null +++ b/bom_product_revision/tests/test_bom_product_revision.py @@ -0,0 +1,491 @@ +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestBomProductRevision(TransactionCase): + def setUp(self): + super(TestBomProductRevision, self).setUp() + self.ProductTemplate = self.env["product.template"] + self.ProductRevision = self.env["product.revision"] + self.ProductProduct = self.env["product.product"] + self.MrpBom = self.env["mrp.bom"] + self.MrpBomLine = self.env["mrp.bom.line"] + + # Create test user with necessary security groups + self.test_user = self.env["res.users"].create( + { + "name": "Test User", + "login": "test.user", + "email": "test.user@example.com", + } + ) + group_user = self.env.ref("base.group_user") + inventory_group = self.env.ref("stock.group_stock_user") + mrp_group = self.env.ref("mrp.group_mrp_user") + self.test_user.write( + {"groups_id": [(6, 0, [group_user.id, inventory_group.id, mrp_group.id])]} + ) + + # Test environment with test user + self.test_env = self.env(user=self.test_user.id) + + # Create a product template for the finished product + self.finished_product_template = self.ProductTemplate.create( + { + "name": "Finished Product", + "type": "product", + "default_code": "FP-001", + } + ) + + # Create a product template for the component + self.component_template = self.ProductTemplate.create( + { + "name": "Component", + "type": "product", + "default_code": "COMP-001", + } + ) + + # Get the product variants + self.finished_product = self.finished_product_template.product_variant_ids[0] + self.component = self.component_template.product_variant_ids[0] + + # Create revisions for the products + self.finished_product_rev1 = self.ProductRevision.create( + { + "name": "Finished Product Rev 1", + "revision_number": "1", + "product_tmpl_id": self.finished_product_template.id, + "internal_product_id": "FP-001", + "active": True, + } + ) + + self.component_rev1 = self.ProductRevision.create( + { + "name": "Component Rev 1", + "revision_number": "1", + "product_tmpl_id": self.component_template.id, + "internal_product_id": "COMP-001", + "active": True, + } + ) + + # Create a BOM + self.bom = self.MrpBom.create( + { + "product_tmpl_id": self.finished_product_template.id, + "product_qty": 1.0, + "type": "normal", + "bom_line_ids": [ + ( + 0, + 0, + { + "product_id": self.component.id, + "product_qty": 2.0, + }, + ) + ], + } + ) + + def test_bom_revision_auto_set(self): + """Test that BOM revision is automatically set when creating a BOM""" + # Check that the BOM has the revision set + self.assertEqual(self.bom.revision_id, self.finished_product_rev1) + self.assertEqual(self.bom.revision_number, "1") + + # Check that the BOM line has the revision set + bom_line = self.bom.bom_line_ids[0] + self.assertEqual(bom_line.revision_id, self.component_rev1) + self.assertEqual(bom_line.revision_number, "1") + + def test_bom_revision_onchange(self): + """Test that BOM revision is updated when product changes""" + # Create a new product and revision + new_product_template = self.ProductTemplate.create( + { + "name": "New Finished Product", + "type": "product", + "default_code": "NFP-001", + } + ) + new_product_rev = self.ProductRevision.create( + { + "name": "New Finished Product Rev 1", + "revision_number": "1", + "product_tmpl_id": new_product_template.id, + "internal_product_id": "NFP-001", + "active": True, + } + ) + + # Update the BOM product + self.bom.product_tmpl_id = new_product_template + + # Trigger the onchange + self.bom._onchange_product_id() + + # Check that the revision is updated + self.assertEqual(self.bom.revision_id, new_product_rev) + + def test_bom_line_revision_onchange(self): + """Test that BOM line revision is updated when product changes""" + # Create a new component and revision + new_component_template = self.ProductTemplate.create( + { + "name": "New Component", + "type": "product", + "default_code": "NCOMP-001", + } + ) + new_component = new_component_template.product_variant_ids[0] + + new_component_rev = self.ProductRevision.create( + { + "name": "New Component Rev 1", + "revision_number": "1", + "product_tmpl_id": new_component_template.id, + "internal_product_id": "NCOMP-001", + "active": True, + } + ) + + # Get the BOM line + bom_line = self.bom.bom_line_ids[0] + + # Update the BOM line product + bom_line.product_id = new_component + + # Trigger the onchange + bom_line.onchange_product_id() + + # Check that the revision is updated + self.assertEqual(bom_line.revision_id, new_component_rev) + + def test_inactive_revision_warning(self): + """Test that warnings are shown for inactive revisions""" + # Create a new revision for the finished product + self.ProductRevision.create( + { + "name": "Finished Product Rev 2", + "revision_number": "2", + "product_tmpl_id": self.finished_product_template.id, + "internal_product_id": "FP-001", + "active": True, + } + ) + + # The first revision should now be inactive + self.assertFalse(self.finished_product_rev1.active) + + # Set the BOM to use the inactive revision + self.bom.revision_id = self.finished_product_rev1 + + # Check that the inactive revision warning is set + self.assertTrue(self.bom.inactive_revision_warning) + self.assertFalse(self.bom.revision_active) + + # Create a new revision for the component + self.ProductRevision.create( + { + "name": "Component Rev 2", + "revision_number": "2", + "product_tmpl_id": self.component_template.id, + "internal_product_id": "COMP-001", + "active": True, + } + ) + + # The first revision should now be inactive + self.assertFalse(self.component_rev1.active) + + # Set the BOM line to use the inactive revision + bom_line = self.bom.bom_line_ids[0] + bom_line.revision_id = self.component_rev1 + + # Check that the inactive revision warning is set + self.assertTrue(bom_line.inactive_revision_warning) + self.assertFalse(bom_line.revision_active) + + def test_revision_onchange_warning(self): + """Test that a warning is shown when selecting an inactive revision""" + # Create a new revision for the finished product + self.ProductRevision.create( + { + "name": "Finished Product Rev 2", + "revision_number": "2", + "product_tmpl_id": self.finished_product_template.id, + "internal_product_id": "FP-001", + "active": True, + } + ) + + # The first revision should now be inactive + self.assertFalse(self.finished_product_rev1.active) + + # Set the BOM to use the inactive revision + self.bom.revision_id = self.finished_product_rev1 + + # Trigger the onchange + result = self.bom._onchange_revision_id() + + # Check that a warning is returned + self.assertTrue(result.get("warning")) + self.assertEqual(result["warning"]["title"], "Inactive Revision Selected") + + # Create a new revision for the component + self.ProductRevision.create( + { + "name": "Component Rev 2", + "revision_number": "2", + "product_tmpl_id": self.component_template.id, + "internal_product_id": "COMP-001", + "active": True, + } + ) + + # The first revision should now be inactive + self.assertFalse(self.component_rev1.active) + + # Set the BOM line to use the inactive revision + bom_line = self.bom.bom_line_ids[0] + bom_line.revision_id = self.component_rev1 + + # Trigger the onchange + result = bom_line._onchange_revision_id() + + # Check that a warning is returned + self.assertTrue(result.get("warning")) + self.assertEqual(result["warning"]["title"], "Inactive Revision Selected") + + def test_name_search_filtering(self): + """Test that name_search filters revisions based on context""" + # Create a new product and revision + new_product_template = self.ProductTemplate.create( + { + "name": "Search Test Product", + "type": "product", + "default_code": "SEARCH-001", + } + ) + new_product = new_product_template.product_variant_ids[0] + + search_rev = self.ProductRevision.create( + { + "name": "Search Test Rev", + "revision_number": "1", + "product_tmpl_id": new_product_template.id, + "internal_product_id": "SEARCH-001", + "active": True, + } + ) + + # Test name_search with product_id in context + result = self.ProductRevision.with_context( + product_id=new_product.id + ).name_search(name="Search") + + # Should find only the search revision + self.assertEqual(len(result), 1) + self.assertEqual(result[0][0], search_rev.id) + + # Test name_search with product_tmpl_id in context + result = self.ProductRevision.with_context( + product_tmpl_id=new_product_template.id + ).name_search(name="Search") + + # Should find only the search revision + self.assertEqual(len(result), 1) + self.assertEqual(result[0][0], search_rev.id) + + # Test name_search with finished_product_id in context + result = self.ProductRevision.with_context( + product_id=self.finished_product.id + ).name_search(name="Search") + + # Should not find the search revision + self.assertEqual(len(result), 0) + + def test_create_bom_with_revisions(self): + """Test creating a BOM with revisions specified""" + # Create a new BOM with revisions specified + new_bom = self.MrpBom.create( + { + "product_tmpl_id": self.finished_product_template.id, + "product_qty": 1.0, + "type": "normal", + "revision_id": self.finished_product_rev1.id, + "bom_line_ids": [ + ( + 0, + 0, + { + "product_id": self.component.id, + "product_qty": 2.0, + "revision_id": self.component_rev1.id, + }, + ) + ], + } + ) + + # Check that the revisions are set correctly + self.assertEqual(new_bom.revision_id, self.finished_product_rev1) + self.assertEqual(new_bom.bom_line_ids[0].revision_id, self.component_rev1) + + def test_variant_specific_revision(self): + """Test BOM with variant-specific revisions""" + # Create a product with variants + size_attribute = self.env["product.attribute"].create( + { + "name": "Size", + "create_variant": "always", + } + ) + size_s = self.env["product.attribute.value"].create( + { + "name": "S", + "attribute_id": size_attribute.id, + } + ) + size_m = self.env["product.attribute.value"].create( + { + "name": "M", + "attribute_id": size_attribute.id, + } + ) + + variant_template = self.ProductTemplate.create( + { + "name": "Variant Product", + "type": "product", + "default_code": "VAR-001", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": size_attribute.id, + "value_ids": [(6, 0, [size_s.id, size_m.id])], + }, + ) + ], + } + ) + + # Get the variants + variant_s = self.ProductProduct.search( + [ + ("product_tmpl_id", "=", variant_template.id), + ( + "product_template_attribute_value_ids.product_attribute_value_id", + "=", + size_s.id, + ), + ], + limit=1, + ) + variant_m = self.ProductProduct.search( + [ + ("product_tmpl_id", "=", variant_template.id), + ( + "product_template_attribute_value_ids.product_attribute_value_id", + "=", + size_m.id, + ), + ], + limit=1, + ) + + # Create a template revision + template_rev = self.ProductRevision.create( + { + "name": "Template Rev", + "revision_number": "T1", + "product_tmpl_id": variant_template.id, + "internal_product_id": "VAR-001", + "active": True, + } + ) + + # Create a variant-specific revision + variant_s_rev = self.ProductRevision.create( + { + "name": "Variant S Rev", + "revision_number": "S1", + "product_id": variant_s.id, + "internal_product_id": "VAR-001-S", + "active": True, + } + ) + + # Create a BOM for the template + template_bom = self.MrpBom.create( + { + "product_tmpl_id": variant_template.id, + "product_qty": 1.0, + "type": "normal", + "bom_line_ids": [ + ( + 0, + 0, + { + "product_id": self.component.id, + "product_qty": 1.0, + }, + ) + ], + } + ) + + # Check that the template revision is set + self.assertEqual(template_bom.revision_id, template_rev) + + # Create a BOM for variant S + variant_s_bom = self.MrpBom.create( + { + "product_id": variant_s.id, + "product_tmpl_id": variant_template.id, # Also need to set the template + "product_qty": 1.0, + "type": "normal", + "bom_line_ids": [ + ( + 0, + 0, + { + "product_id": self.component.id, + "product_qty": 1.0, + }, + ) + ], + } + ) + + # Check that the variant-specific revision is set + self.assertEqual(variant_s_bom.revision_id, variant_s_rev) + + # Create a BOM for variant M (which has no specific revision) + variant_m_bom = self.MrpBom.create( + { + "product_id": variant_m.id, + "product_tmpl_id": variant_template.id, # Also need to set the template + "product_qty": 1.0, + "type": "normal", + "bom_line_ids": [ + ( + 0, + 0, + { + "product_id": self.component.id, + "product_qty": 1.0, + }, + ) + ], + } + ) + + # Check that the template revision is set for variant M + self.assertEqual(variant_m_bom.revision_id, template_rev) diff --git a/bom_product_revision/views/mrp_bom_views.xml b/bom_product_revision/views/mrp_bom_views.xml new file mode 100644 index 00000000..0ddc3453 --- /dev/null +++ b/bom_product_revision/views/mrp_bom_views.xml @@ -0,0 +1,101 @@ + + + + + mrp.bom.form.inherit.revision + mrp.bom + + + + + + + + + + + + + + + + + + + + + + + + + + + + mrp.bom.tree.inherit.revision + mrp.bom + + + + + + + + + + + + + + + + mrp.bom.line.form.inherit.revision + mrp.bom.line + + + + + + + + + + + + + + + + diff --git a/product_lot_revision/README.rst b/product_lot_revision/README.rst new file mode 100644 index 00000000..c93bab11 --- /dev/null +++ b/product_lot_revision/README.rst @@ -0,0 +1,109 @@ +=============================== +Product Lot Revision Management +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:98398456b45282b03ef8c410d0c9b886e4c1b39ffeb3fd832c1e90579056d3e7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Faxls--oca-lightgray.png?logo=github + :target: https://github.com/OCA/axls-oca/tree/wng-add-revision-functions/product_lot_revision + :alt: OCA/axls-oca +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/axls-oca-wng-add-revision-functions/axls-oca-wng-add-revision-functions-product_lot_revision + :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/axls-oca&target_branch=wng-add-revision-functions + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of the product_revision module to include +revision information in lot and serial numbers. + +Features: + + +* Adds revision information to lot/serial numbers +* Automatically sets the latest revision information when a lot/serial number is created +* Allows users to change the revision after saving the lot/serial number +* Provides a wizard to assign revisions to lot/serial numbers + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to: + + +#. Go to Inventory > Operations > Lot/Serial Numbers +#. Open a lot/serial number form +#. You will see a "Revision" field and buttons to view, create, or assign a revision +#. Click "Assign Revision" to assign an existing revision to the lot/serial number +#. Click "Create Revision" to create a new revision for the product and assign it to the lot/serial number +#. Click "View Revision" to view the details of the assigned revision + +When creating a new lot/serial number, the system will automatically assign the current active revision of the product to the lot/serial number. + +Known issues / Roadmap +====================== + +* Add support for filtering lot/serial numbers by revision +* Add support for bulk assignment of revisions to lot/serial numbers +* Add support for revision history in lot/serial numbers + +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 +~~~~~~~ + +* Axelspace + +Contributors +~~~~~~~~~~~~ + +* `Axelspace Corporation `__: + + * WangTKurata + +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/axls-oca `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_lot_revision/__init__.py b/product_lot_revision/__init__.py new file mode 100644 index 00000000..aee8895e --- /dev/null +++ b/product_lot_revision/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/product_lot_revision/__manifest__.py b/product_lot_revision/__manifest__.py new file mode 100644 index 00000000..a4d5d501 --- /dev/null +++ b/product_lot_revision/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2025 Axelspace +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Product Lot Revision Management", + "summary": "Link product revisions to lot/serial numbers", + "author": "Axelspace, Odoo Community Association (OCA)", + "website": "https://www.axelspace.com", + "license": "AGPL-3", + "category": "Inventory", + "version": "16.0.1.0.0", + "depends": ["stock", "product_revision"], + "data": [ + "security/ir.model.access.csv", + "views/stock_lot_views.xml", + "wizards/assign_revision_wizard_views.xml", + ], + "demo": [], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/product_lot_revision/i18n/ja_JP.po b/product_lot_revision/i18n/ja_JP.po new file mode 100644 index 00000000..5c35cf8f --- /dev/null +++ b/product_lot_revision/i18n/ja_JP.po @@ -0,0 +1,158 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_lot_revision +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0-20230701\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-02 07:00+0000\n" +"PO-Revision-Date: 2025-05-02 07:00+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_lot_revision +#: model_terms:ir.ui.view,arch_db:product_lot_revision.view_stock_lot_form_inherit +msgid "Create Revision" +msgstr "リビジョンを作成" + +#. module: product_lot_revision +#: model_terms:ir.ui.view,arch_db:product_lot_revision.view_stock_lot_form_inherit +msgid "View Revision" +msgstr "リビジョンを見る" + +#. module: product_lot_revision +#: model_terms:ir.ui.view,arch_db:product_lot_revision.view_assign_revision_wizard_form +msgid "Assign" +msgstr "割り当て" + +#. module: product_lot_revision +#: model:ir.actions.act_window,name:product_lot_revision.action_assign_revision_wizard +#: model_terms:ir.ui.view,arch_db:product_lot_revision.view_assign_revision_wizard_form +msgid "Assign Revision" +msgstr "リビジョンを割り当て" + +#. module: product_lot_revision +#: model:ir.model,name:product_lot_revision.model_assign_revision_wizard +msgid "Assign Revision to Lot/Serial Number" +msgstr "ロット/シリアル番号にリビジョンを割り当て" + +#. module: product_lot_revision +#: model_terms:ir.ui.view,arch_db:product_lot_revision.view_assign_revision_wizard_form +msgid "Cancel" +msgstr "キャンセル" + +#. module: product_lot_revision +#. odoo-python +#: code:addons/product_lot_revision/models/stock_lot.py:0 +#: code:addons/product_lot_revision/models/stock_lot.py:0 +#, python-format +msgid "Create Revision" +msgstr "リビジョンを作成" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__create_uid +msgid "Created by" +msgstr "作成者" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__create_date +msgid "Created on" +msgstr "作成日時" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__display_name +msgid "Display Name" +msgstr "表示名" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__id +msgid "ID" +msgstr "ID" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard____last_update +msgid "Last Modified on" +msgstr "最終更新日時" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__write_uid +msgid "Last Updated by" +msgstr "最終更新者" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__write_date +msgid "Last Updated on" +msgstr "最終更新日時" + +#. module: product_lot_revision +#: model:ir.model,name:product_lot_revision.model_stock_lot +msgid "Lot/Serial" +msgstr "ロット/シリアル" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__lot_id +msgid "Lot/Serial Number" +msgstr "ロット/シリアル番号" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__product_id +msgid "Product" +msgstr "製品" + +#. module: product_lot_revision +#: model:ir.model,name:product_lot_revision.model_product_revision +msgid "Product Revision" +msgstr "製品リビジョン" + +#. module: product_lot_revision +#. odoo-python +#: code:addons/product_lot_revision/models/stock_lot.py:0 +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__revision_id +#: model:ir.model.fields,field_description:product_lot_revision.field_stock_lot__revision_id +#: model_terms:ir.ui.view,arch_db:product_lot_revision.view_stock_lot_search_inherit +#, python-format +msgid "Revision" +msgstr "リビジョン" + +#. module: product_lot_revision +#: model:ir.model.fields,field_description:product_lot_revision.field_assign_revision_wizard__revision_number +#: model:ir.model.fields,field_description:product_lot_revision.field_stock_lot__revision_number +msgid "Revision Number" +msgstr "リビジョン番号" + +#. module: product_lot_revision +#: model:ir.model.fields,help:product_lot_revision.field_assign_revision_wizard__lot_id +msgid "The lot/serial number to assign a revision to" +msgstr "リビジョンを割り当てるロット/シリアル番号" + +#. module: product_lot_revision +#: model:ir.model.fields,help:product_lot_revision.field_assign_revision_wizard__product_id +msgid "The product associated with this lot/serial number" +msgstr "このロット/シリアル番号に紐付く製品" + +#. module: product_lot_revision +#: model:ir.model.fields,help:product_lot_revision.field_stock_lot__revision_number +msgid "" +"The revision number of the product associated with this lot/serial number" +msgstr "" +"このロット/シリアル番号に紐付く製品のリビジョン番号" + +#. module: product_lot_revision +#: model:ir.model.fields,help:product_lot_revision.field_assign_revision_wizard__revision_number +msgid "The revision number of the selected revision" +msgstr "選択されたリビジョンのリビジョン番号" + +#. module: product_lot_revision +#: model:ir.model.fields,help:product_lot_revision.field_stock_lot__revision_id +msgid "The revision of the product associated with this lot/serial number" +msgstr "このロット/シリアル番号に紐付く製品のリビジョン" + +#. module: product_lot_revision +#: model:ir.model.fields,help:product_lot_revision.field_assign_revision_wizard__revision_id +msgid "The revision to assign to this lot/serial number" +msgstr "このロット/シリアル番号に割り当てるリビジョン" diff --git a/product_lot_revision/models/__init__.py b/product_lot_revision/models/__init__.py new file mode 100644 index 00000000..661649df --- /dev/null +++ b/product_lot_revision/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_revision_ext +from . import stock_lot diff --git a/product_lot_revision/models/product_revision_ext.py b/product_lot_revision/models/product_revision_ext.py new file mode 100644 index 00000000..72089095 --- /dev/null +++ b/product_lot_revision/models/product_revision_ext.py @@ -0,0 +1,61 @@ +from odoo import api, models + + +class ProductRevisionExt(models.Model): + _inherit = "product.revision" + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + """Override name_search to filter revisions based on product_id in context""" + if args is None: + args = [] + + # Get product_id and product_tmpl_id from context + product_id = self.env.context.get("product_id") + product_tmpl_id = self.env.context.get("product_tmpl_id") + + # If product_id is set, filter revisions for this product + if product_id: + product = self.env["product.product"].browse(product_id) + include_variant_revisions = self.env.context.get( + "include_variant_revisions", True + ) + + if include_variant_revisions: + # Include both template and variant revisions + args = args + [ + "|", + ("active", "=", True), + ( + "active", + "=", + False, + ), # Include both active and inactive revisions + "|", + ("product_id", "=", product_id), + ("product_tmpl_id", "=", product.product_tmpl_id.id), + ] + else: + # Include only template revisions + args = args + [ + "|", + ("active", "=", True), + ( + "active", + "=", + False, + ), # Include both active and inactive revisions + ("product_tmpl_id", "=", product.product_tmpl_id.id), + ] + # If only product_tmpl_id is set, filter revisions for this template + elif product_tmpl_id: + args = args + [ + "|", + ("active", "=", True), + ("active", "=", False), # Include both active and inactive revisions + ("product_tmpl_id", "=", product_tmpl_id), + ] + + return super(ProductRevisionExt, self).name_search( + name=name, args=args, operator=operator, limit=limit + ) diff --git a/product_lot_revision/models/stock_lot.py b/product_lot_revision/models/stock_lot.py new file mode 100644 index 00000000..8f5a0603 --- /dev/null +++ b/product_lot_revision/models/stock_lot.py @@ -0,0 +1,120 @@ +from odoo import _, api, fields, models + + +class StockLot(models.Model): + _inherit = "stock.lot" + + revision_id = fields.Many2one( + "product.revision", + string="Revision", + help="The revision of the product associated with this lot/serial number", + ) + revision_number = fields.Char( + related="revision_id.revision_number", + string="Revision Number", + readonly=True, + store=True, + help="The revision number of the product associated with this lot/serial number", + ) + + @api.model_create_multi + def create(self, vals_list): + """Override create to automatically set the revision_id if available""" + for vals in vals_list: + if not vals.get("revision_id") and vals.get("product_id"): + product = self.env["product.product"].browse(vals["product_id"]) + # First check if the product variant has its own revision + if product.current_revision_id: + vals["revision_id"] = product.current_revision_id.id + # If not, fall back to the product template's revision + elif product.product_tmpl_id.current_revision_id: + vals["revision_id"] = product.product_tmpl_id.current_revision_id.id + return super(StockLot, self).create(vals_list) + + def action_view_revision(self): + """Open the revision form view""" + self.ensure_one() + if self.revision_id: + # If revision exists, open it + return { + "name": _("Revision"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "product.revision", + "res_id": self.revision_id.id, + } + else: + # If no revision exists, open form to create one + return self.action_create_revision() + + def action_create_revision(self): + """Open a form to create a new revision for the product""" + self.ensure_one() + product = self.product_id + product_tmpl = product.product_tmpl_id + + # Check if this is a default variant with only one variant + is_default_variant = len(product_tmpl.product_variant_ids) == 1 + + # If this is not a default variant or + # if the product already has variant-specific revisions, + # create a revision for the product variant + if not is_default_variant or product.revision_ids: + # Get the next revision number for the product variant + next_revision_number = "1" + if product.revision_ids: + latest_revision = self.env["product.revision"].search( + [("product_id", "=", product.id)], + order="revision_number desc", + limit=1, + ) + try: + next_revision_number = str(int(latest_revision.revision_number) + 1) + except ValueError: + next_revision_number = f"{latest_revision.revision_number}-1" + + return { + "name": _("Create Revision"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "product.revision", + "context": { + "default_product_id": product.id, + "default_name": f"{product.name} Rev. {next_revision_number}", + "default_revision_number": next_revision_number, + "default_internal_product_id": product.default_code or "", + "form_view_ref": "product_revision.view_product_revision_form", + "default_active": True, + }, + "target": "new", + } + else: + # For default variants with no variant-specific revisions, + # create a revision for the product template + next_revision_number = "1" + if product_tmpl.revision_ids: + latest_revision = self.env["product.revision"].search( + [("product_tmpl_id", "=", product_tmpl.id)], + order="revision_number desc", + limit=1, + ) + try: + next_revision_number = str(int(latest_revision.revision_number) + 1) + except ValueError: + next_revision_number = f"{latest_revision.revision_number}-1" + + return { + "name": _("Create Revision"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "product.revision", + "context": { + "default_product_tmpl_id": product_tmpl.id, + "default_name": f"{product_tmpl.name} Rev. {next_revision_number}", + "default_revision_number": next_revision_number, + "default_internal_product_id": product_tmpl.default_code or "", + "form_view_ref": "product_revision.view_product_revision_form", + "default_active": True, + }, + "target": "new", + } diff --git a/product_lot_revision/readme/CONTRIBUTORS.rst b/product_lot_revision/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..bd765ad6 --- /dev/null +++ b/product_lot_revision/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Axelspace Corporation `__: + + * WangTKurata diff --git a/product_lot_revision/readme/DESCRIPTION.rst b/product_lot_revision/readme/DESCRIPTION.rst new file mode 100644 index 00000000..8f9316d9 --- /dev/null +++ b/product_lot_revision/readme/DESCRIPTION.rst @@ -0,0 +1,10 @@ +This module extends the functionality of the product_revision module to include +revision information in lot and serial numbers. + +Features: + + +* Adds revision information to lot/serial numbers +* Automatically sets the latest revision information when a lot/serial number is created +* Allows users to change the revision after saving the lot/serial number +* Provides a wizard to assign revisions to lot/serial numbers diff --git a/product_lot_revision/readme/ROADMAP.rst b/product_lot_revision/readme/ROADMAP.rst new file mode 100644 index 00000000..24d43b59 --- /dev/null +++ b/product_lot_revision/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +* Add support for filtering lot/serial numbers by revision +* Add support for bulk assignment of revisions to lot/serial numbers +* Add support for revision history in lot/serial numbers diff --git a/product_lot_revision/readme/USAGE.rst b/product_lot_revision/readme/USAGE.rst new file mode 100644 index 00000000..62337b4e --- /dev/null +++ b/product_lot_revision/readme/USAGE.rst @@ -0,0 +1,11 @@ +To use this module, you need to: + + +#. Go to Inventory > Operations > Lot/Serial Numbers +#. Open a lot/serial number form +#. You will see a "Revision" field and buttons to view, create, or assign a revision +#. Click "Assign Revision" to assign an existing revision to the lot/serial number +#. Click "Create Revision" to create a new revision for the product and assign it to the lot/serial number +#. Click "View Revision" to view the details of the assigned revision + +When creating a new lot/serial number, the system will automatically assign the current active revision of the product to the lot/serial number. diff --git a/product_lot_revision/security/ir.model.access.csv b/product_lot_revision/security/ir.model.access.csv new file mode 100644 index 00000000..01ce02d9 --- /dev/null +++ b/product_lot_revision/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_assign_revision_wizard,access_assign_revision_wizard,model_assign_revision_wizard,base.group_user,1,1,1,1 diff --git a/product_lot_revision/static/description/index.html b/product_lot_revision/static/description/index.html new file mode 100644 index 00000000..1e04b996 --- /dev/null +++ b/product_lot_revision/static/description/index.html @@ -0,0 +1,457 @@ + + + + + +Product Lot Revision Management + + + +
+

Product Lot Revision Management

+ + +

Beta License: AGPL-3 OCA/axls-oca Translate me on Weblate Try me on Runboat

+

This module extends the functionality of the product_revision module to include +revision information in lot and serial numbers.

+

Features:

+
    +
  • Adds revision information to lot/serial numbers
  • +
  • Automatically sets the latest revision information when a lot/serial number is created
  • +
  • Allows users to change the revision after saving the lot/serial number
  • +
  • Provides a wizard to assign revisions to lot/serial numbers
  • +
+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to Inventory > Operations > Lot/Serial Numbers
  2. +
  3. Open a lot/serial number form
  4. +
  5. You will see a “Revision” field and buttons to view, create, or assign a revision
  6. +
  7. Click “Assign Revision” to assign an existing revision to the lot/serial number
  8. +
  9. Click “Create Revision” to create a new revision for the product and assign it to the lot/serial number
  10. +
  11. Click “View Revision” to view the details of the assigned revision
  12. +
+

When creating a new lot/serial number, the system will automatically assign the current active revision of the product to the lot/serial number.

+
+
+

Known issues / Roadmap

+
    +
  • Add support for filtering lot/serial numbers by revision
  • +
  • Add support for bulk assignment of revisions to lot/serial numbers
  • +
  • Add support for revision history in lot/serial numbers
  • +
+
+
+

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

+
    +
  • Axelspace
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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/axls-oca project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_lot_revision/tests/__init__.py b/product_lot_revision/tests/__init__.py new file mode 100644 index 00000000..a931f48e --- /dev/null +++ b/product_lot_revision/tests/__init__.py @@ -0,0 +1 @@ +from . import test_product_lot_revision diff --git a/product_lot_revision/tests/test_product_lot_revision.py b/product_lot_revision/tests/test_product_lot_revision.py new file mode 100644 index 00000000..6ad9072a --- /dev/null +++ b/product_lot_revision/tests/test_product_lot_revision.py @@ -0,0 +1,261 @@ +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestProductLotRevision(TransactionCase): + def setUp(self): + super(TestProductLotRevision, self).setUp() + self.ProductTemplate = self.env["product.template"] + self.ProductRevision = self.env["product.revision"] + self.ProductProduct = self.env["product.product"] + self.StockLot = self.env["stock.lot"] + self.AssignRevisionWizard = self.env["assign.revision.wizard"] + + # Create test user with necessary security groups + self.test_user = self.env["res.users"].create( + { + "name": "Test User", + "login": "test.user", + "email": "test.user@example.com", + } + ) + group_user = self.env.ref("base.group_user") + inventory_group = self.env.ref("stock.group_stock_user") + self.test_user.write( + {"groups_id": [(6, 0, [group_user.id, inventory_group.id])]} + ) + + # Test environment with test user + self.test_env = self.env(user=self.test_user.id) + + # Create a product template for testing + self.template = self.ProductTemplate.create( + { + "name": "Test Product", + "type": "product", + "default_code": "TP-001", + "tracking": "lot", # Enable lot tracking + } + ) + + # Get the product variant + self.product = self.template.product_variant_ids[0] + + # Create a revision for the product template + self.template_rev1 = self.ProductRevision.create( + { + "name": "Template Rev 1", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a lot for the product + self.lot = self.StockLot.create( + { + "name": "LOT-001", + "product_id": self.product.id, + "company_id": self.env.company.id, + } + ) + + def test_lot_revision_auto_set(self): + """Test that lot revision is automatically set when creating a lot""" + # Check that the lot has the revision set + self.assertEqual(self.lot.revision_id, self.template_rev1) + self.assertEqual(self.lot.revision_number, "1") + + # Create a new revision for the product template + template_rev2 = self.ProductRevision.create( + { + "name": "Template Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a new lot for the product + lot2 = self.StockLot.create( + { + "name": "LOT-002", + "product_id": self.product.id, + "company_id": self.env.company.id, + } + ) + + # Check that the new lot has the new revision set + self.assertEqual(lot2.revision_id, template_rev2) + self.assertEqual(lot2.revision_number, "2") + + def test_variant_specific_revision(self): + """Test lot revision with variant-specific revisions""" + # Create a variant-specific revision + variant_rev = self.ProductRevision.create( + { + "name": "Variant Rev A", + "revision_number": "A", + "product_id": self.product.id, + "internal_product_id": "TP-001-V", + "active": True, + } + ) + + # Create a new lot for the product + lot3 = self.StockLot.create( + { + "name": "LOT-003", + "product_id": self.product.id, + "company_id": self.env.company.id, + } + ) + + # Check that the lot has the variant-specific revision set + self.assertEqual(lot3.revision_id, variant_rev) + self.assertEqual(lot3.revision_number, "A") + + def test_assign_revision_wizard(self): + """Test the assign revision wizard""" + # Create a new revision for the product template + template_rev2 = self.ProductRevision.create( + { + "name": "Template Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a wizard to assign the new revision to the lot + wizard = self.AssignRevisionWizard.create( + { + "lot_id": self.lot.id, + "revision_id": template_rev2.id, + } + ) + + # Execute the wizard + wizard.action_assign_revision() + + # Check that the lot has the new revision set + self.assertEqual(self.lot.revision_id, template_rev2) + self.assertEqual(self.lot.revision_number, "2") + + def test_name_search_filtering(self): + """Test that name_search filters revisions based on context""" + # Create a new product and revision + new_template = self.ProductTemplate.create( + { + "name": "New Test Product", + "type": "product", + "default_code": "NTP-001", + "tracking": "lot", + } + ) + new_product = new_template.product_variant_ids[0] + + new_rev = self.ProductRevision.create( + { + "name": "New Product Rev 1", + "revision_number": "1", + "product_tmpl_id": new_template.id, + "internal_product_id": "NTP-001", + "active": True, + } + ) + + # Test name_search with product_id in context + result = self.ProductRevision.with_context( + product_id=new_product.id + ).name_search(name="New") + + # Should find only the new revision + self.assertEqual(len(result), 1) + self.assertEqual(result[0][0], new_rev.id) + + # Create a variant-specific revision for the original product + # This is needed for the next test to pass + variant_rev = self.ProductRevision.create( + { + "name": "Variant Rev A", + "revision_number": "A", + "product_id": self.product.id, + "internal_product_id": "TP-001-V", + "active": True, + } + ) + + # Test name_search with product_id in context for original product + result = self.ProductRevision.with_context( + product_id=self.product.id + ).name_search(name="Rev") + + # Should find both the template revision and the variant-specific revision + self.assertEqual(len(result), 2) # Template Rev 1 and Variant Rev A + found_ids = [r[0] for r in result] + self.assertIn(self.template_rev1.id, found_ids) + self.assertIn(variant_rev.id, found_ids) + + def test_action_view_revision(self): + """Test the action_view_revision method""" + # Test action_view_revision for lot with revision + action = self.lot.action_view_revision() + self.assertEqual(action["res_model"], "product.revision") + self.assertEqual(action["res_id"], self.template_rev1.id) + + # Create a lot without revision + lot_no_rev = self.StockLot.create( + { + "name": "LOT-NO-REV", + "product_id": self.product.id, + "company_id": self.env.company.id, + "revision_id": False, + } + ) + + # Test action_view_revision for lot without revision + action = lot_no_rev.action_create_revision() + self.assertEqual(action["res_model"], "product.revision") + + # Check if this is a default variant (only one variant for the template) + is_default_variant = len(self.product.product_tmpl_id.product_variant_ids) == 1 + + if is_default_variant and not self.product.revision_ids: + # For default variants with no variant-specific revisions, + # the action should have default_product_tmpl_id + self.assertEqual( + action["context"]["default_product_tmpl_id"], + self.product.product_tmpl_id.id, + ) + else: + # Otherwise, it should have default_product_id + self.assertEqual(action["context"]["default_product_id"], self.product.id) + + def test_action_create_revision(self): + """Test the action_create_revision method""" + # Test action_create_revision for lot + action = self.lot.action_create_revision() + self.assertEqual(action["res_model"], "product.revision") + + # Check if this is a default variant (only one variant for the template) + is_default_variant = len(self.product.product_tmpl_id.product_variant_ids) == 1 + + if is_default_variant and not self.product.revision_ids: + # For default variants with no variant-specific revisions, + # the action should have default_product_tmpl_id + self.assertEqual( + action["context"]["default_product_tmpl_id"], + self.product.product_tmpl_id.id, + ) + # Check the revision number format - should be "2" + # since we already have template_rev1 + self.assertEqual(action["context"]["default_revision_number"], "2") + else: + # Otherwise, it should have default_product_id + self.assertEqual(action["context"]["default_product_id"], self.product.id) + # Check the revision number format + self.assertEqual(action["context"]["default_revision_number"], "1") diff --git a/product_lot_revision/views/stock_lot_views.xml b/product_lot_revision/views/stock_lot_views.xml new file mode 100644 index 00000000..77429f9b --- /dev/null +++ b/product_lot_revision/views/stock_lot_views.xml @@ -0,0 +1,70 @@ + + + + + stock.lot.form.inherit + stock.lot + + + + + + + + + + + + + + stock.lot.tree.inherit + stock.lot + + + + + + + + + + + + stock.lot.search.inherit + stock.lot + + + + + + + + + + + + diff --git a/product_lot_revision/wizards/__init__.py b/product_lot_revision/wizards/__init__.py new file mode 100644 index 00000000..2118f4d3 --- /dev/null +++ b/product_lot_revision/wizards/__init__.py @@ -0,0 +1 @@ +from . import assign_revision_wizard diff --git a/product_lot_revision/wizards/assign_revision_wizard.py b/product_lot_revision/wizards/assign_revision_wizard.py new file mode 100644 index 00000000..308aaf15 --- /dev/null +++ b/product_lot_revision/wizards/assign_revision_wizard.py @@ -0,0 +1,56 @@ +from odoo import api, fields, models + + +class AssignRevisionWizard(models.TransientModel): + _name = "assign.revision.wizard" + _description = "Assign Revision to Lot/Serial Number" + + lot_id = fields.Many2one( + "stock.lot", + string="Lot/Serial Number", + required=True, + readonly=True, + help="The lot/serial number to assign a revision to", + ) + product_id = fields.Many2one( + related="lot_id.product_id", + string="Product", + readonly=True, + help="The product associated with this lot/serial number", + ) + revision_id = fields.Many2one( + "product.revision", + string="Revision", + required=True, + domain="[('product_id', '=', product_id)]", + context="{'product_id': product_id}", + help="The revision to assign to this lot/serial number", + ) + revision_number = fields.Char( + related="revision_id.revision_number", + string="Revision Number", + readonly=True, + help="The revision number of the selected revision", + ) + + @api.onchange("product_id") + def _onchange_product_id(self): + """When product changes, update the domain for revision_id""" + if self.product_id: + # Set domain to include both product variant and template revisions + return { + "domain": { + "revision_id": [ + "|", + ("product_id", "=", self.product_id.id), + ("product_tmpl_id", "=", self.product_id.product_tmpl_id.id), + ] + } + } + return {"domain": {"revision_id": []}} + + def action_assign_revision(self): + """Assign the selected revision to the lot/serial number""" + self.ensure_one() + self.lot_id.write({"revision_id": self.revision_id.id}) + return {"type": "ir.actions.act_window_close"} diff --git a/product_lot_revision/wizards/assign_revision_wizard_views.xml b/product_lot_revision/wizards/assign_revision_wizard_views.xml new file mode 100644 index 00000000..baf13f78 --- /dev/null +++ b/product_lot_revision/wizards/assign_revision_wizard_views.xml @@ -0,0 +1,37 @@ + + + + + assign.revision.wizard.form + assign.revision.wizard + +
+ + + + + + + + +
+
+
+
+
+ + + + Assign Revision + assign.revision.wizard + form + new + +
diff --git a/product_revision/README.rst b/product_revision/README.rst new file mode 100644 index 00000000..5e5425be --- /dev/null +++ b/product_revision/README.rst @@ -0,0 +1,179 @@ +=========================== +Product Revision Management +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:87667dbeea4ee9dec17e154f78632170bdb35c069c3f73214e96552409ea2158 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Faxls--oca-lightgray.png?logo=github + :target: https://github.com/OCA/axls-oca/tree/wng-add-revision-functions/product_revision + :alt: OCA/axls-oca +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/axls-oca-wng-add-revision-functions/axls-oca-wng-add-revision-functions-product_revision + :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/axls-oca&target_branch=wng-add-revision-functions + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides functionality to assign and manage revision data for products in Odoo 16. It allows tracking changes to products over time through a revision system. + +Each revision record stores the following details: + + +* Name: The designation for the revision. +* Revision Number: The version or iterative identifier (automatically incremented if not provided). +* Change Date: The date when the revision was made. +* Product Internal ID: The unique internal identifier of the product. +* Notes: Additional information about the revision. +* Active status: Indicates if this is the current active revision. + +Key features: + + +* Revisions can be linked to either product templates or product variants, but not both simultaneously. +* Only one revision can be active at a time for a given product. +* All revision history is preserved in the system for traceability. +* Integrated with Odoo's chatter system for tracking changes and activities. +* Automatic revision numbering when creating new revisions. +* Visibility of current revision number in product listings, forms, and kanban views. + +This module enables enhanced traceability and version control of products throughout their lifecycle. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to: + +Managing Revisions from Product Forms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +#. Go to Inventory > Products +#. Open a product form +#. In the "Options" tab, you will see: + * Current Revision field showing the active revision + * "View All" button to see all revisions for this product + * "Add Revision" button to create a new revision +#. Click "Add Revision" to create a new revision for the product +#. Fill in the revision details: + * Name: A descriptive name for the revision + * Revision Number: Automatically suggested as the next number, but can be modified + * Change Date: Defaults to today's date + * Notes: Optional additional information about the revision +#. Save the revision +#. The new revision will automatically be set as the active revision for the product, and any previous active revision will be set to inactive + +Managing Revisions Directly +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +You can also access all product revisions directly from: + * Inventory > Inventory Control > Revisions + +This view allows you to: + * Create new revisions + * View and filter all revisions across all products + * Toggle the active status of revisions + * Group revisions by product, date, etc. + +Revision Visibility in Product Views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The current revision number is visible in: + * Product list views (as an optional column) + * Product kanban views (displayed alongside the product reference) + * Product form views (in the Options tab) + +Product Variants and Revisions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * For products with variants, revisions can be managed at either the product template level or the individual variant level + * If a product variant doesn't have its own revisions, it will inherit the revision from its product template + * For the default variant of a template with only one variant, the template's revisions are used + +Known issues / Roadmap +====================== + +Future enhancements planned for this module: + +* Add support for attaching documents to revisions + * Allow uploading technical drawings, specifications, and other documents + * Implement document versioning aligned with product revisions +* Add support for approval workflows for new revisions + * Multi-step approval process for revision changes + * Integration with Odoo's approval module + * Role-based approval requirements +* Add support for revision effectivity dates + * Start and end dates for when a revision is valid + * Automatic transition between revisions based on dates + * Historical tracking of which revision was active at a given time +* Add support for revision change reasons + * Categorization of revision changes (e.g., design change, material change) + * Required justification field for revision changes + * Impact assessment for revision changes +* Enhanced BOM integration + * Link Bill of Materials to specific product revisions + * Track changes in components across product revisions + * Support for revision-specific manufacturing instructions +* Revision comparison tool + * Side-by-side comparison of different revisions + * Highlighting of changes between revisions + * Change history visualization +* Revision propagation across supply chain + * Notify suppliers of revision changes + * Update purchase orders with new revision information + * Track which revision of a product was used in which sales order + +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 +~~~~~~~ + +* Axelspace + +Contributors +~~~~~~~~~~~~ + +* `Axelspace Corporation `__: + + * WangTKurata + +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/axls-oca `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_revision/__init__.py b/product_revision/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/product_revision/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_revision/__manifest__.py b/product_revision/__manifest__.py new file mode 100644 index 00000000..da24cb30 --- /dev/null +++ b/product_revision/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2025 Axelspace +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Product Revision Management", + "summary": "Manage and assign revision information for products.", + "author": "Axelspace, Odoo Community Association (OCA)", + "website": "https://www.axelspace.com", + "license": "AGPL-3", + "category": "Inventory", + "version": "16.0.1.0.0", + "depends": ["stock"], + "data": [ + "views/product_revision_views.xml", + "views/product_product_views.xml", + "views/product_kanban_views.xml", + "security/ir.model.access.csv", + "demo/demo.xml", + ], + "installable": True, + "application": False, +} diff --git a/product_revision/demo/demo.xml b/product_revision/demo/demo.xml new file mode 100644 index 00000000..4e74e286 --- /dev/null +++ b/product_revision/demo/demo.xml @@ -0,0 +1,14 @@ + + + + diff --git a/product_revision/i18n/ja_JP.po b/product_revision/i18n/ja_JP.po new file mode 100644 index 00000000..0f567fa6 --- /dev/null +++ b/product_revision/i18n/ja_JP.po @@ -0,0 +1,430 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_revision +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0-20230701\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-02 06:39+0000\n" +"PO-Revision-Date: 2025-05-02 06:39+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_revision +#: model_terms:ir.ui.view,arch_db:product_revision.product_kanban_view_inherit +#: model_terms:ir.ui.view,arch_db:product_revision.product_template_kanban_view_inherit +msgid "- Rev:" +msgstr "- リビジョン:" + +#. module: product_revision +#: model_terms:ir.ui.view,arch_db:product_revision.product_kanban_view_inherit +#: model_terms:ir.ui.view,arch_db:product_revision.product_template_kanban_view_inherit +msgid "Ref:" +msgstr "参照:" + +#. module: product_revision +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_template_form_inherit +msgid "Add Revision" +msgstr "リビジョンを追加" + +#. module: product_revision +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_template_form_inherit +msgid "View All" +msgstr "すべて表示" + +#. module: product_revision +#. odoo-python +#: code:addons/product_revision/models/product_revision.py:0 +#, python-format +msgid "" +"A revision cannot be linked to both a producttemplate and a product variant " +"at the same time." +msgstr "リビジョンは、製品テンプレートと製品バリアントの両方に同時にリンクすることはできません。" + +#. module: product_revision +#. odoo-python +#: code:addons/product_revision/models/product_revision.py:0 +#, python-format +msgid "" +"A revision must be linked to eithera product template or a product variant." +msgstr "リビジョンは、製品テンプレートまたは製品バリアントのいずれかにリンクする必要があります。" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_needaction +msgid "Action Needed" +msgstr "アクションが必要" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__active +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_search +msgid "Active" +msgstr "アクティブ" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__activity_ids +msgid "Activities" +msgstr "アクティビティ" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "アクティビティ例外の装飾" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__activity_state +msgid "Activity State" +msgstr "アクティビティ状態" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__activity_type_icon +msgid "Activity Type Icon" +msgstr "アクティビティタイプのアイコン" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__notes +msgid "Additional information about this revision" +msgstr "このリビジョンに関する追加情報" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_attachment_count +msgid "Attachment Count" +msgstr "添付ファイルの数" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__change_date +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_search +msgid "Change Date" +msgstr "変更日" + +#. module: product_revision +#. odoo-python +#: code:addons/product_revision/models/product_product.py:0 +#: code:addons/product_revision/models/product_template.py:0 +#, python-format +msgid "Create Revision" +msgstr "リビジョンを作成" + +#. module: product_revision +#: model_terms:ir.actions.act_window,help:product_revision.action_product_revision +msgid "Create your first product revision" +msgstr "最初の製品リビジョンを作成してください" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__create_uid +msgid "Created by" +msgstr "作成者" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__create_date +msgid "Created on" +msgstr "作成日" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_product__current_revision_id +#: model:ir.model.fields,field_description:product_revision.field_product_product__current_revision_number +#: model:ir.model.fields,field_description:product_revision.field_product_template__current_revision_id +#: model:ir.model.fields,field_description:product_revision.field_product_template__current_revision_number +msgid "Current Revision" +msgstr "現在のリビジョン" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__change_date +msgid "Date when the revision was made" +msgstr "リビジョンが行われた日付" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__name +msgid "Designation for the revision" +msgstr "リビジョンの名称" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__display_name +msgid "Display Name" +msgstr "表示名" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_follower_ids +msgid "Followers" +msgstr "フォロワー" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_partner_ids +msgid "Followers (Partners)" +msgstr "フォロワー(パートナー)" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Font Awesome アイコン例: fa-tasks" + +#. module: product_revision +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_search +msgid "Group By" +msgstr "グループ化" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__has_message +msgid "Has Message" +msgstr "メッセージあり" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__id +msgid "ID" +msgstr "ID" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__activity_exception_icon +msgid "Icon" +msgstr "アイコン" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "例外アクティビティを示すアイコン" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__message_needaction +msgid "If checked, new messages require your attention." +msgstr "チェックされた場合、新しいメッセージに注意が必要です。" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__message_has_error +#: model:ir.model.fields,help:product_revision.field_product_revision__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "チェックされた場合、一部のメッセージに配送エラーがあります。" + +#. module: product_revision +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_search +msgid "Inactive" +msgstr "非アクティブ" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__active +msgid "Indicates if this is the active revision" +msgstr "これがアクティブなリビジョンであるかを示します。" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__internal_product_id +msgid "Internal Product" +msgstr "内部製品" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_is_follower +msgid "Is Follower" +msgstr "フォロワーである" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision____last_update +msgid "Last Modified on" +msgstr "最終変更日" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__write_uid +msgid "Last Updated by" +msgstr "最終更新者" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__write_date +msgid "Last Updated on" +msgstr "最終更新日" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_template__revision_ids +msgid "List of revisions for this product" +msgstr "この製品のリビジョン一覧" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_product__revision_ids +msgid "List of revisions for this product variant" +msgstr "この製品バリアントのリビジョン一覧" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_main_attachment_id +msgid "Main Attachment" +msgstr "メイン添付ファイル" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_has_error +msgid "Message Delivery error" +msgstr "メッセージ配送エラー" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_ids +msgid "Messages" +msgstr "メッセージ" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "自分のアクティビティ期限" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__name +msgid "Name" +msgstr "名前" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "次のアクティビティ期限" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__activity_summary +msgid "Next Activity Summary" +msgstr "次のアクティビティの概要" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__activity_type_id +msgid "Next Activity Type" +msgstr "次のアクティビティタイプ" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__notes +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_form +msgid "Notes" +msgstr "ノート" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_needaction_counter +msgid "Number of Actions" +msgstr "アクション数" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__message_has_error_counter +msgid "Number of errors" +msgstr "エラー数" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "アクションが必要なメッセージ数" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "配送エラーのあるメッセージ数" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_template__revision_count +msgid "Number of revisions for this product" +msgstr "この製品のリビジョン数" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_product__revision_count +msgid "Number of revisions for this product variant" +msgstr "この製品バリアントのリビジョン数" + +#. module: product_revision +#: model:ir.model,name:product_revision.model_product_template +msgid "Product" +msgstr "プロダクト" + +#. module: product_revision +#: model:ir.model,name:product_revision.model_product_revision +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_form +msgid "Product Revision" +msgstr "製品リビジョン" + +#. module: product_revision +#: model:ir.actions.act_window,name:product_revision.action_product_revision +msgid "Product Revisions" +msgstr "製品リビジョン" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__product_tmpl_id +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_search +msgid "Product Template" +msgstr "製品テンプレート" + +#. module: product_revision +#: model:ir.model,name:product_revision.model_product_product +#: model:ir.model.fields,field_description:product_revision.field_product_revision__product_id +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_search +msgid "Product Variant" +msgstr "プロダクトバリアント" + +#. module: product_revision +#: model_terms:ir.actions.act_window,help:product_revision.action_product_revision +msgid "" +"Product revisions allow you to track changes to your products over time." +msgstr "製品のリビジョンを使用すると、製品の変更を時間の経過とともに追跡できます。" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__product_tmpl_id +msgid "Product template this revision belongs to" +msgstr "このリビジョンが属する製品テンプレート" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__product_id +msgid "Product variant this revision belongs to" +msgstr "このリビジョンが属する製品バリアント" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__activity_user_id +msgid "Responsible User" +msgstr "担当ユーザー" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_product__revision_count +#: model:ir.model.fields,field_description:product_revision.field_product_template__revision_count +msgid "Revision Count" +msgstr "リビジョン数" + +#. module: product_revision +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_form +msgid "Revision History" +msgstr "リビジョン履歴" + +#. module: product_revision +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_form +msgid "Revision Name" +msgstr "リビジョン名" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__revision_number +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_form +msgid "Revision Number" +msgstr "リビジョン番号" + +#. module: product_revision +#: model:ir.model.constraint,message:product_revision.constraint_product_revision_product_revision_unique +msgid "Revision number must be unique per product or product variant!" +msgstr "リビジョン番号は、製品または製品バリアントごとに一意である必要があります!" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__revision_number +msgid "Version or iterative identifier" +msgstr "バージョンまたは反復識別子" + +#. module: product_revision +#: model_terms:ir.ui.view,arch_db:product_revision.view_product_revision_form +msgid "" +"This section shows the history of this revision. All revisions for a product are preserved in the system,\n" +" with only one being active at a time. When a new revision is created, previous revisions are automatically\n" +" set to inactive but remain in the system for historical reference." +msgstr "このセクションは、このリビジョンの履歴を表示します。製品の全てのリビジョンはシステムに保存され、同時に一つだけがアクティブとなります。新しいリビジョンが作成されると、以前のリビジョンは自動的に非アクティブに設定され、履歴として残ります。" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "レコード上の例外アクティビティの種類" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__internal_product_id +msgid "Unique internal identifier of the product" +msgstr "製品の内部一意識別子" + +#. module: product_revision +#: model:ir.model.fields,field_description:product_revision.field_product_revision__website_message_ids +msgid "Website Messages" +msgstr "ウェブサイトメッセージ" + +#. module: product_revision +#: model:ir.model.fields,help:product_revision.field_product_revision__website_message_ids +msgid "Website communication history" +msgstr "ウェブサイトの通信履歴" diff --git a/product_revision/models/__init__.py b/product_revision/models/__init__.py new file mode 100644 index 00000000..db03716a --- /dev/null +++ b/product_revision/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_revision +from . import product_template +from . import product_product +from . import product_revision_ext diff --git a/product_revision/models/product_product.py b/product_revision/models/product_product.py new file mode 100644 index 00000000..01580953 --- /dev/null +++ b/product_revision/models/product_product.py @@ -0,0 +1,163 @@ +from odoo import _, api, fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + # Revision fields + revision_ids = fields.One2many( + "product.revision", + "product_id", + string="Revisions", + help="List of revisions for this product variant", + ) + current_revision_id = fields.Many2one( + "product.revision", + compute="_compute_current_revision", + store=True, + help="The active revision for this product variant", + ) + revision_count = fields.Integer( + compute="_compute_revision_count", + help="Number of revisions for this product variant", + ) + current_revision_number = fields.Char( + compute="_compute_current_revision_number", + store=True, + help="The revision number of the active revision", + ) + + @api.depends( + "revision_ids", "revision_ids.active", "product_tmpl_id.current_revision_id" + ) + def _compute_current_revision(self): + """Compute the current active revision for the product variant""" + for product in self: + # First check if this product has its own revisions + active_revision = product.revision_ids.filtered(lambda r: r.active) + if active_revision: + product.current_revision_id = active_revision[0] + else: + # If no variant-specific revisions, use the template's revision + product.current_revision_id = ( + product.product_tmpl_id.current_revision_id + ) + + @api.depends("revision_ids", "product_tmpl_id.revision_ids") + def _compute_revision_count(self): + """Compute the number of revisions for the product variant""" + for product in self: + # If this is the default variant of a template with only one variant, + # include the template's revisions in the count + if product.is_default_variant() and not product.revision_ids: + product.revision_count = len(product.product_tmpl_id.revision_ids) + else: + product.revision_count = len(product.revision_ids) + + def action_view_revisions(self): + """Open the revisions view for this product variant""" + self.ensure_one() + + # If this product has its own revisions, show them + if self.revision_ids: + tree_view_id = self.env.ref( + "product_revision.view_product_revision_tree" + ).id + form_view_id = self.env.ref( + "product_revision.view_product_revision_form" + ).id + + return { + "name": _("Revisions"), + "type": "ir.actions.act_window", + "view_mode": "tree,form", + "res_model": "product.revision", + "views": [(tree_view_id, "tree"), (form_view_id, "form")], + "domain": [("product_id", "=", self.id)], + "context": { + "default_product_id": self.id, + "search_default_active": 1, + "search_default_inactive": 1, + }, + } + else: + # If this is the default variant of a template with only one variant, + # and the template has revisions, show the template's revisions + if self.is_default_variant() and self.product_tmpl_id.revision_ids: + tree_view_id = self.env.ref( + "product_revision.view_product_revision_tree" + ).id + form_view_id = self.env.ref( + "product_revision.view_product_revision_form" + ).id + + return { + "name": _("Revisions"), + "type": "ir.actions.act_window", + "view_mode": "tree,form", + "res_model": "product.revision", + "views": [(tree_view_id, "tree"), (form_view_id, "form")], + "domain": [("product_tmpl_id", "=", self.product_tmpl_id.id)], + "context": { + "default_product_tmpl_id": self.product_tmpl_id.id, + "search_default_active": 1, + "search_default_inactive": 1, + }, + } + else: + # If no variant-specific revisions and not a default variant, + # delegate to the template + return self.product_tmpl_id.action_view_revisions() + + def action_create_revision(self): + """Open a form to create a new revision for this product variant""" + self.ensure_one() + + # If this is the default variant of a template with only one variant, + # and the template already has revisions, use the template's revisions + if ( + self.is_default_variant() + and self.product_tmpl_id.revision_ids + and not self.revision_ids + ): + return self.product_tmpl_id.action_create_revision() + + # Get the next revision number + next_revision_number = "1" + if self.revision_ids: + latest_revision = self.env["product.revision"].search( + [("product_id", "=", self.id)], order="revision_number desc", limit=1 + ) + + try: + next_revision_number = str(int(latest_revision.revision_number) + 1) + except ValueError: + next_revision_number = f"{latest_revision.revision_number}-1" + + return { + "name": _("Create Revision"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "product.revision", + "context": { + "default_product_id": self.id, + "default_name": f"{self.name} Rev. {next_revision_number}", + "default_revision_number": next_revision_number, + "default_internal_product_id": self.default_code or "", + }, + } + + def is_default_variant(self): + """Check if this product is the default variant of a template with only one variant""" + self.ensure_one() + return len(self.product_tmpl_id.product_variant_ids) == 1 + + @api.depends("current_revision_id", "current_revision_id.revision_number") + def _compute_current_revision_number(self): + """Compute the current revision number for display purposes""" + for product in self: + product.current_revision_number = ( + product.current_revision_id.revision_number + if product.current_revision_id + else "" + ) diff --git a/product_revision/models/product_revision.py b/product_revision/models/product_revision.py new file mode 100644 index 00000000..0c5fbc5e --- /dev/null +++ b/product_revision/models/product_revision.py @@ -0,0 +1,177 @@ +from odoo import _, api, fields, models + + +class ProductRevision(models.Model): + _name = "product.revision" + _description = "Product Revision" + _order = "revision_number desc" + _inherit = ["mail.thread", "mail.activity.mixin"] + # Disable automatic filtering of inactive records + _active_name = False + + name = fields.Char(required=True, help="Designation for the revision") + revision_number = fields.Char(required=True, help="Version or iterative identifier") + change_date = fields.Date( + default=fields.Date.today, + help="Date when the revision was made", + ) + product_tmpl_id = fields.Many2one( + "product.template", + string="Product Template", + ondelete="cascade", + index=True, + help="Product template this revision belongs to", + ) + product_id = fields.Many2one( + "product.product", + string="Product Variant", + ondelete="cascade", + index=True, + help="Product variant this revision belongs to", + ) + internal_product_id = fields.Char(help="Unique internal identifier of the product") + active = fields.Boolean( + default=True, help="Indicates if this is the active revision" + ) + notes = fields.Text(help="Additional information about this revision") + + _sql_constraints = [ + ( + "product_revision_unique", + "unique(product_tmpl_id, product_id, revision_number)", + "Revision number must be unique per product or product variant!", + ) + ] + + @api.constrains("product_tmpl_id", "product_id") + def _check_product_tmpl_or_product(self): + """Ensure that either product_tmpl_id or product_id is set, but not both""" + for record in self: + if record.product_tmpl_id and record.product_id: + raise models.ValidationError( + _( + "A revision cannot be linked to both a product" + "template and a product variant at the same time." + ) + ) + if not record.product_tmpl_id and not record.product_id: + raise models.ValidationError( + _( + "A revision must be linked to either" + "a product template or a product variant." + ) + ) + + @api.onchange("product_tmpl_id") + def _onchange_product_tmpl_id(self): + """Handle the case where a product.template has only one product.product variant""" + if self.product_tmpl_id: + # Clear product_id when product_tmpl_id is set + self.product_id = False + + @api.onchange("product_id") + def _onchange_product_id(self): + """Handle the case where a product.product is selected""" + if self.product_id: + # Clear product_tmpl_id when product_id is set + self.product_tmpl_id = False + + @api.model_create_multi + def create(self, vals_list): + """Override create to handle automatic revision numbering if not provided + + This method supports batch creation of revisions. + """ + for vals in vals_list: + if not vals.get("revision_number"): + # If no revision number provided, get the next number + product_tmpl_id = vals.get("product_tmpl_id") + product_id = vals.get("product_id") + + if product_tmpl_id: + vals["revision_number"] = self._get_next_revision_number( + product_tmpl_id=product_tmpl_id + ) + elif product_id: + vals["revision_number"] = self._get_next_revision_number( + product_id=product_id + ) + else: + vals["revision_number"] = "1" + + # Create the records + records = super().create(vals_list) + + # If any of the new revisions are active, deactivate other revisions + for record in records: + if record.active: + self._deactivate_other_revisions(record) + + return records + + def write(self, vals): + """Override write to handle active status changes""" + res = super().write(vals) + + # If active status is being set to True, deactivate other revisions + if vals.get("active"): + for record in self: + if record.active: + self._deactivate_other_revisions(record) + + return res + + def _deactivate_other_revisions(self, active_revision): + """Deactivate other revisions of the same product or product variant + + This preserves all revision history by only changing the active status, + ensuring that all previous revisions are kept in the system. + """ + domain = [ + ("id", "!=", active_revision.id), + ("active", "=", True), + ] + + # Add appropriate domain based on whether this is a template or variant revision + if active_revision.product_tmpl_id: + domain.append(("product_tmpl_id", "=", active_revision.product_tmpl_id.id)) + elif active_revision.product_id: + domain.append(("product_id", "=", active_revision.product_id.id)) + + other_revisions = self.search(domain) + + if other_revisions: + # Set to inactive but preserve the records + other_revisions.write({"active": False}) + + # Log the change for traceability + for rev in other_revisions: + rev.message_post( + body=_( + "This revision was set to inactive because revision %s became active." + ) + % active_revision.revision_number, + subtype_id=self.env.ref("mail.mt_note").id, + ) + + def _get_next_revision_number(self, product_tmpl_id=None, product_id=None): + """Get the next revision number for a product or product variant""" + domain = [] + if product_tmpl_id: + domain.append(("product_tmpl_id", "=", product_tmpl_id)) + elif product_id: + domain.append(("product_id", "=", product_id)) + + # Get the highest current revision number + latest_revision = self.search(domain, order="revision_number desc", limit=1) + + if not latest_revision: + return "1" + + # Try to convert to integer and increment + try: + next_number = int(latest_revision.revision_number) + 1 + return str(next_number) + except ValueError: + # If not a simple number, just append a suffix + return f"{latest_revision.revision_number}-1" diff --git a/product_revision/models/product_revision_ext.py b/product_revision/models/product_revision_ext.py new file mode 100644 index 00000000..dffe17a6 --- /dev/null +++ b/product_revision/models/product_revision_ext.py @@ -0,0 +1,39 @@ +from odoo import api, models + + +class ProductRevisionExt(models.Model): + _inherit = "product.revision" + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + """Override name_search to filter revisions based on product_id in context""" + if args is None: + args = [] + + # Get product_id and product_tmpl_id from context + product_id = self.env.context.get("product_id") + product_tmpl_id = self.env.context.get("product_tmpl_id") + + # If product_id is set, filter revisions for this product + if product_id: + product = self.env["product.product"].browse(product_id) + args = args + [ + "|", + ("active", "=", True), + ("active", "=", False), # Include both active and inactive revisions + "|", + ("product_id", "=", product_id), + ("product_tmpl_id", "=", product.product_tmpl_id.id), + ] + # If only product_tmpl_id is set, filter revisions for this template + elif product_tmpl_id: + args = args + [ + "|", + ("active", "=", True), + ("active", "=", False), # Include both active and inactive revisions + ("product_tmpl_id", "=", product_tmpl_id), + ] + + return super(ProductRevisionExt, self).name_search( + name=name, args=args, operator=operator, limit=limit + ) diff --git a/product_revision/models/product_template.py b/product_revision/models/product_template.py new file mode 100644 index 00000000..55eb2c0e --- /dev/null +++ b/product_revision/models/product_template.py @@ -0,0 +1,106 @@ +from odoo import _, api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + # Revision fields + revision_ids = fields.One2many( + "product.revision", + "product_tmpl_id", + string="Revisions", + help="List of revisions for this product", + ) + current_revision_id = fields.Many2one( + "product.revision", + compute="_compute_current_revision", + store=True, + help="The active revision for this product", + ) + revision_count = fields.Integer( + compute="_compute_revision_count", + help="Number of revisions for this product", + ) + current_revision_number = fields.Char( + compute="_compute_current_revision_number", + store=True, + help="The revision number of the active revision", + ) + + @api.depends("revision_ids", "revision_ids.active") + def _compute_current_revision(self): + """Compute the current active revision for the product""" + for product in self: + active_revision = product.revision_ids.filtered(lambda r: r.active) + product.current_revision_id = ( + active_revision[0] if active_revision else False + ) + + @api.depends("revision_ids") + def _compute_revision_count(self): + """Compute the number of revisions for the product""" + for product in self: + product.revision_count = len(product.revision_ids) + + def action_view_revisions(self): + """Open the revisions view for this product""" + self.ensure_one() + + # Get the tree view that shows both active and inactive revisions + tree_view_id = self.env.ref("product_revision.view_product_revision_tree").id + form_view_id = self.env.ref("product_revision.view_product_revision_form").id + + return { + "name": _("Revisions"), + "type": "ir.actions.act_window", + "view_mode": "tree,form", + "res_model": "product.revision", + "views": [(tree_view_id, "tree"), (form_view_id, "form")], + "domain": [("product_tmpl_id", "=", self.id)], + "context": { + "default_product_tmpl_id": self.id, + "search_default_active": 1, + "search_default_inactive": 1, + }, + } + + @api.depends("current_revision_id", "current_revision_id.revision_number") + def _compute_current_revision_number(self): + """Compute the current revision number for display purposes""" + for product in self: + product.current_revision_number = ( + product.current_revision_id.revision_number + if product.current_revision_id + else "" + ) + + def action_create_revision(self): + """Open a form to create a new revision for this product""" + self.ensure_one() + + # Get the next revision number + next_revision_number = "1" + if self.revision_ids: + latest_revision = self.env["product.revision"].search( + [("product_tmpl_id", "=", self.id)], + order="revision_number desc", + limit=1, + ) + + try: + next_revision_number = str(int(latest_revision.revision_number) + 1) + except ValueError: + next_revision_number = f"{latest_revision.revision_number}-1" + + return { + "name": _("Create Revision"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "product.revision", + "context": { + "default_product_tmpl_id": self.id, + "default_name": f"{self.name} Rev. {next_revision_number}", + "default_revision_number": next_revision_number, + "default_internal_product_id": self.default_code or "", + }, + } diff --git a/product_revision/readme/CONTRIBUTORS.rst b/product_revision/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..bd765ad6 --- /dev/null +++ b/product_revision/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Axelspace Corporation `__: + + * WangTKurata diff --git a/product_revision/readme/DESCRIPTION.rst b/product_revision/readme/DESCRIPTION.rst new file mode 100644 index 00000000..8f531bce --- /dev/null +++ b/product_revision/readme/DESCRIPTION.rst @@ -0,0 +1,23 @@ +This module provides functionality to assign and manage revision data for products in Odoo 16. It allows tracking changes to products over time through a revision system. + +Each revision record stores the following details: + + +* Name: The designation for the revision. +* Revision Number: The version or iterative identifier (automatically incremented if not provided). +* Change Date: The date when the revision was made. +* Product Internal ID: The unique internal identifier of the product. +* Notes: Additional information about the revision. +* Active status: Indicates if this is the current active revision. + +Key features: + + +* Revisions can be linked to either product templates or product variants, but not both simultaneously. +* Only one revision can be active at a time for a given product. +* All revision history is preserved in the system for traceability. +* Integrated with Odoo's chatter system for tracking changes and activities. +* Automatic revision numbering when creating new revisions. +* Visibility of current revision number in product listings, forms, and kanban views. + +This module enables enhanced traceability and version control of products throughout their lifecycle. diff --git a/product_revision/readme/ROADMAP.rst b/product_revision/readme/ROADMAP.rst new file mode 100644 index 00000000..b596f279 --- /dev/null +++ b/product_revision/readme/ROADMAP.rst @@ -0,0 +1,29 @@ +Future enhancements planned for this module: + +* Add support for attaching documents to revisions + * Allow uploading technical drawings, specifications, and other documents + * Implement document versioning aligned with product revisions +* Add support for approval workflows for new revisions + * Multi-step approval process for revision changes + * Integration with Odoo's approval module + * Role-based approval requirements +* Add support for revision effectivity dates + * Start and end dates for when a revision is valid + * Automatic transition between revisions based on dates + * Historical tracking of which revision was active at a given time +* Add support for revision change reasons + * Categorization of revision changes (e.g., design change, material change) + * Required justification field for revision changes + * Impact assessment for revision changes +* Enhanced BOM integration + * Link Bill of Materials to specific product revisions + * Track changes in components across product revisions + * Support for revision-specific manufacturing instructions +* Revision comparison tool + * Side-by-side comparison of different revisions + * Highlighting of changes between revisions + * Change history visualization +* Revision propagation across supply chain + * Notify suppliers of revision changes + * Update purchase orders with new revision information + * Track which revision of a product was used in which sales order diff --git a/product_revision/readme/USAGE.rst b/product_revision/readme/USAGE.rst new file mode 100644 index 00000000..1c40fe38 --- /dev/null +++ b/product_revision/readme/USAGE.rst @@ -0,0 +1,42 @@ +To use this module, you need to: + +Managing Revisions from Product Forms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +#. Go to Inventory > Products +#. Open a product form +#. In the "Options" tab, you will see: + * Current Revision field showing the active revision + * "View All" button to see all revisions for this product + * "Add Revision" button to create a new revision +#. Click "Add Revision" to create a new revision for the product +#. Fill in the revision details: + * Name: A descriptive name for the revision + * Revision Number: Automatically suggested as the next number, but can be modified + * Change Date: Defaults to today's date + * Notes: Optional additional information about the revision +#. Save the revision +#. The new revision will automatically be set as the active revision for the product, and any previous active revision will be set to inactive + +Managing Revisions Directly +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +You can also access all product revisions directly from: + * Inventory > Inventory Control > Revisions + +This view allows you to: + * Create new revisions + * View and filter all revisions across all products + * Toggle the active status of revisions + * Group revisions by product, date, etc. + +Revision Visibility in Product Views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The current revision number is visible in: + * Product list views (as an optional column) + * Product kanban views (displayed alongside the product reference) + * Product form views (in the Options tab) + +Product Variants and Revisions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * For products with variants, revisions can be managed at either the product template level or the individual variant level + * If a product variant doesn't have its own revisions, it will inherit the revision from its product template + * For the default variant of a template with only one variant, the template's revisions are used diff --git a/product_revision/security/ir.model.access.csv b/product_revision/security/ir.model.access.csv new file mode 100644 index 00000000..79ec0674 --- /dev/null +++ b/product_revision/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_revision,access_product_revision,model_product_revision,base.group_user,1,1,1,1 diff --git a/product_revision/static/description/index.html b/product_revision/static/description/index.html new file mode 100644 index 00000000..93a5eeb4 --- /dev/null +++ b/product_revision/static/description/index.html @@ -0,0 +1,591 @@ + + + + + +Product Revision Management + + + +
+

Product Revision Management

+ + +

Beta License: AGPL-3 OCA/axls-oca Translate me on Weblate Try me on Runboat

+

This module provides functionality to assign and manage revision data for products in Odoo 16. It allows tracking changes to products over time through a revision system.

+

Each revision record stores the following details:

+
    +
  • Name: The designation for the revision.
  • +
  • Revision Number: The version or iterative identifier (automatically incremented if not provided).
  • +
  • Change Date: The date when the revision was made.
  • +
  • Product Internal ID: The unique internal identifier of the product.
  • +
  • Notes: Additional information about the revision.
  • +
  • Active status: Indicates if this is the current active revision.
  • +
+

Key features:

+
    +
  • Revisions can be linked to either product templates or product variants, but not both simultaneously.
  • +
  • Only one revision can be active at a time for a given product.
  • +
  • All revision history is preserved in the system for traceability.
  • +
  • Integrated with Odoo’s chatter system for tracking changes and activities.
  • +
  • Automatic revision numbering when creating new revisions.
  • +
  • Visibility of current revision number in product listings, forms, and kanban views.
  • +
+

This module enables enhanced traceability and version control of products throughout their lifecycle.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+
+

Managing Revisions from Product Forms

+
    +
  1. Go to Inventory > Products
  2. +
  3. Open a product form
  4. +
  5. In the “Options” tab, you will see: +* Current Revision field showing the active revision +* “View All” button to see all revisions for this product +* “Add Revision” button to create a new revision
  6. +
  7. Click “Add Revision” to create a new revision for the product
  8. +
  9. Fill in the revision details: +* Name: A descriptive name for the revision +* Revision Number: Automatically suggested as the next number, but can be modified +* Change Date: Defaults to today’s date +* Notes: Optional additional information about the revision
  10. +
  11. Save the revision
  12. +
  13. The new revision will automatically be set as the active revision for the product, and any previous active revision will be set to inactive
  14. +
+
+
+

Managing Revisions Directly

+
+
You can also access all product revisions directly from:
+
    +
  • Inventory > Inventory Control > Revisions
  • +
+
+
This view allows you to:
+
    +
  • Create new revisions
  • +
  • View and filter all revisions across all products
  • +
  • Toggle the active status of revisions
  • +
  • Group revisions by product, date, etc.
  • +
+
+
+
+
+

Revision Visibility in Product Views

+
+
The current revision number is visible in:
+
    +
  • Product list views (as an optional column)
  • +
  • Product kanban views (displayed alongside the product reference)
  • +
  • Product form views (in the Options tab)
  • +
+
+
+
+
+

Product Variants and Revisions

+
+
    +
  • For products with variants, revisions can be managed at either the product template level or the individual variant level
  • +
  • If a product variant doesn’t have its own revisions, it will inherit the revision from its product template
  • +
  • For the default variant of a template with only one variant, the template’s revisions are used
  • +
+
+
+
+
+

Known issues / Roadmap

+

Future enhancements planned for this module:

+
    +
  • +
    Add support for attaching documents to revisions
    +
      +
    • Allow uploading technical drawings, specifications, and other documents
    • +
    • Implement document versioning aligned with product revisions
    • +
    +
    +
    +
  • +
  • +
    Add support for approval workflows for new revisions
    +
      +
    • Multi-step approval process for revision changes
    • +
    • Integration with Odoo’s approval module
    • +
    • Role-based approval requirements
    • +
    +
    +
    +
  • +
  • +
    Add support for revision effectivity dates
    +
      +
    • Start and end dates for when a revision is valid
    • +
    • Automatic transition between revisions based on dates
    • +
    • Historical tracking of which revision was active at a given time
    • +
    +
    +
    +
  • +
  • +
    Add support for revision change reasons
    +
      +
    • Categorization of revision changes (e.g., design change, material change)
    • +
    • Required justification field for revision changes
    • +
    • Impact assessment for revision changes
    • +
    +
    +
    +
  • +
  • +
    Enhanced BOM integration
    +
      +
    • Link Bill of Materials to specific product revisions
    • +
    • Track changes in components across product revisions
    • +
    • Support for revision-specific manufacturing instructions
    • +
    +
    +
    +
  • +
  • +
    Revision comparison tool
    +
      +
    • Side-by-side comparison of different revisions
    • +
    • Highlighting of changes between revisions
    • +
    • Change history visualization
    • +
    +
    +
    +
  • +
  • +
    Revision propagation across supply chain
    +
      +
    • Notify suppliers of revision changes
    • +
    • Update purchase orders with new revision information
    • +
    • Track which revision of a product was used in which sales order
    • +
    +
    +
    +
  • +
+
+
+

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

+
    +
  • Axelspace
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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/axls-oca project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_revision/tests/test_product_revision.py b/product_revision/tests/test_product_revision.py new file mode 100644 index 00000000..2fae7c5b --- /dev/null +++ b/product_revision/tests/test_product_revision.py @@ -0,0 +1,807 @@ +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase, tagged +from odoo.tools import mute_logger + + +@tagged("post_install", "-at_install") +class TestProductRevision(TransactionCase): + def setUp(self): + super(TestProductRevision, self).setUp() + self.ProductTemplate = self.env["product.template"] + self.ProductRevision = self.env["product.revision"] + self.ProductProduct = self.env["product.product"] + self.ProductAttribute = self.env["product.attribute"] + self.ProductAttributeValue = self.env["product.attribute.value"] + + # Create test user with necessary security groups + self.test_user = self.env["res.users"].create( + { + "name": "Test User", + "login": "test.user", + "email": "test.user@example.com", + } + ) + group_user = self.env.ref("base.group_user") + inventory_group = self.env.ref("stock.group_stock_user") + self.test_user.write( + {"groups_id": [(6, 0, [group_user.id, inventory_group.id])]} + ) + + # Test environment with test user + self.test_env = self.env(user=self.test_user.id) + + # Create a product attribute and values for testing variants + self.size_attribute = self.ProductAttribute.create( + { + "name": "Size", + "create_variant": "always", + } + ) + self.size_s = self.ProductAttributeValue.create( + { + "name": "S", + "attribute_id": self.size_attribute.id, + } + ) + self.size_m = self.ProductAttributeValue.create( + { + "name": "M", + "attribute_id": self.size_attribute.id, + } + ) + self.size_l = self.ProductAttributeValue.create( + { + "name": "L", + "attribute_id": self.size_attribute.id, + } + ) + + # Create a color attribute for testing multiple attributes + self.color_attribute = self.ProductAttribute.create( + { + "name": "Color", + "create_variant": "always", + } + ) + self.color_red = self.ProductAttributeValue.create( + { + "name": "Red", + "attribute_id": self.color_attribute.id, + } + ) + self.color_blue = self.ProductAttributeValue.create( + { + "name": "Blue", + "attribute_id": self.color_attribute.id, + } + ) + + # Create a product template with no variants initially + self.template = self.ProductTemplate.create( + { + "name": "Test Template", + "list_price": 100.0, + "default_code": "TMP-001", + } + ) + + # Create a product template with variants + self.template_with_variants = self.ProductTemplate.create( + { + "name": "Test Template with Variants", + "list_price": 100.0, + "default_code": "TMP-002", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": self.size_attribute.id, + "value_ids": [(6, 0, [self.size_s.id, self.size_m.id])], + }, + ) + ], + } + ) + + # Create a product template with multiple attributes (size and color) + self.template_multi_attr = self.ProductTemplate.create( + { + "name": "Test Template with Multiple Attributes", + "list_price": 120.0, + "default_code": "TMP-003", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": self.size_attribute.id, + "value_ids": [ + (6, 0, [self.size_s.id, self.size_m.id, self.size_l.id]) + ], + }, + ), + ( + 0, + 0, + { + "attribute_id": self.color_attribute.id, + "value_ids": [ + (6, 0, [self.color_red.id, self.color_blue.id]) + ], + }, + ), + ], + } + ) + + # Get the variants created for the template_with_variants + self.variant_s = self.ProductProduct.search( + [ + ("product_tmpl_id", "=", self.template_with_variants.id), + ( + "product_template_attribute_value_ids.product_attribute_value_id", + "=", + self.size_s.id, + ), + ], + limit=1, + ) + self.variant_m = self.ProductProduct.search( + [ + ("product_tmpl_id", "=", self.template_with_variants.id), + ( + "product_template_attribute_value_ids.product_attribute_value_id", + "=", + self.size_m.id, + ), + ], + limit=1, + ) + + # Create a standalone product variant + self.standalone_variant = self.ProductProduct.create( + { + "name": "Test Standalone Variant", + "list_price": 100.0, + "default_code": "VAR-001", + } + ) + + def test_template_revision_creation(self): + """Test revision creation and active status for product template""" + # Create first revision for template + rev1 = self.ProductRevision.create( + { + "name": "Template Rev 1", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TMP-001", + "active": True, + } + ) + self.assertTrue(rev1.active) + self.assertEqual(self.template.current_revision_id, rev1) + self.assertEqual(self.template.current_revision_number, "1") + self.assertEqual(self.template.revision_count, 1) + + # Create second revision for same template: previous should become inactive + rev2 = self.ProductRevision.create( + { + "name": "Template Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "TMP-001", + "active": True, + } + ) + self.assertTrue(rev2.active) + # Re-read template to check current revision update + self.template.invalidate_cache() + self.assertEqual(self.template.current_revision_id, rev2) + self.assertEqual(self.template.current_revision_number, "2") + self.assertEqual(rev1.active, False) + self.assertEqual(self.template.revision_count, 2) + + def test_variant_revision_creation(self): + """Test revision creation and active status for product variant""" + # Create first revision for standalone variant + rev1 = self.ProductRevision.create( + { + "name": "Variant Rev 1", + "revision_number": "1", + "product_id": self.standalone_variant.id, + "internal_product_id": "VAR-001", + "active": True, + } + ) + self.assertTrue(rev1.active) + self.standalone_variant.invalidate_cache() + self.assertEqual(self.standalone_variant.current_revision_id, rev1) + self.assertEqual(self.standalone_variant.current_revision_number, "1") + self.assertEqual(self.standalone_variant.revision_count, 1) + + # Create second revision for same variant + rev2 = self.ProductRevision.create( + { + "name": "Variant Rev 2", + "revision_number": "2", + "product_id": self.standalone_variant.id, + "internal_product_id": "VAR-001", + "active": True, + } + ) + self.standalone_variant.invalidate_cache() + self.assertEqual(self.standalone_variant.current_revision_id, rev2) + self.assertEqual(self.standalone_variant.current_revision_number, "2") + self.assertEqual(rev1.active, False) + self.assertEqual(self.standalone_variant.revision_count, 2) + + def test_mutual_exclusion_constraint(self): + """Test that a revision cannot be linked to both product template and variant""" + with self.assertRaises(ValidationError): + self.ProductRevision.create( + { + "name": "Invalid Revision", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "product_id": self.standalone_variant.id, + "internal_product_id": "INV-001", + "active": True, + } + ) + + # Also test that a revision must be linked to either a template or variant + with self.assertRaises(ValidationError): + self.ProductRevision.create( + { + "name": "Invalid Revision", + "revision_number": "1", + "product_tmpl_id": False, + "product_id": False, + "internal_product_id": "INV-001", + "active": True, + } + ) + + def test_auto_revision_number_increment(self): + """Test auto increment of revision number when a non-numeric value is given""" + # Create revision with non-numeric revision_number and store for testing + test_rev = self.ProductRevision.create( + { + "name": "Template Rev X", + "revision_number": "X", + "product_tmpl_id": self.template.id, + "internal_product_id": "TMP-001", + "active": True, + } + ) + self.assertEqual(test_rev.revision_number, "X") + # Simulate calculating next number using _get_next_revision_number method + # (since auto increment inside create is not triggered when revision_number is provided) + next_num = self.ProductRevision._get_next_revision_number( + product_tmpl_id=self.template.id + ) + self.assertEqual(next_num, "X-1") + + # Test auto-numbering when revision_number is not provided + rev2 = self.ProductRevision.create( + { + "name": "Template Auto Rev", + "product_tmpl_id": self.template.id, + "internal_product_id": "TMP-001", + "active": True, + } + ) + self.assertEqual(rev2.revision_number, "X-1") + + # Test numeric revision auto-increment + template2 = self.ProductTemplate.create( + { + "name": "Test Template 2", + "list_price": 100.0, + } + ) + + # Create a numeric revision and test next number + self.ProductRevision.create( + { + "name": "Numeric Rev", + "revision_number": "5", + "product_tmpl_id": template2.id, + "internal_product_id": "TMP-002", + "active": True, + } + ) + + next_num = self.ProductRevision._get_next_revision_number( + product_tmpl_id=template2.id + ) + self.assertEqual(next_num, "6") + + def test_write_active_status(self): + """Test the write method behavior when active status changes""" + # Create two revisions for the same template + rev1 = self.ProductRevision.create( + { + "name": "Write Test Rev 1", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TMP-001", + "active": True, + } + ) + + rev2 = self.ProductRevision.create( + { + "name": "Write Test Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "TMP-001", + "active": False, # Initially inactive + } + ) + + # Verify initial state + self.assertTrue(rev1.active) + self.assertFalse(rev2.active) + + # Activate rev2 using write method + rev2.write({"active": True}) + + # Verify rev1 is now inactive and rev2 is active + self.assertFalse(rev1.active) + self.assertTrue(rev2.active) + + # Verify template's current revision is updated + self.template.invalidate_cache() + self.assertEqual(self.template.current_revision_id, rev2) + + def test_default_variant_behavior(self): + """Test the behavior of is_default_variant method and its impact on revision handling""" + # Create a revision for the template + template_rev = self.ProductRevision.create( + { + "name": "Template Only Rev", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TMP-001", + "active": True, + } + ) + + # Get the default variant of the template + default_variant = self.template.product_variant_ids[0] + + # Check if it's correctly identified as default variant + self.assertTrue(default_variant.is_default_variant()) + + # Check that the default variant inherits the template's revision + default_variant.invalidate_cache() + self.assertEqual(default_variant.current_revision_id, template_rev) + self.assertEqual(default_variant.current_revision_number, "1") + + # Check that the variant with its own revision doesn't use the template's revision + variant_rev = self.ProductRevision.create( + { + "name": "Variant Specific Rev", + "revision_number": "A", + "product_id": self.standalone_variant.id, + "internal_product_id": "VAR-001", + "active": True, + } + ) + + self.standalone_variant.invalidate_cache() + self.assertEqual(self.standalone_variant.current_revision_id, variant_rev) + self.assertNotEqual(self.standalone_variant.current_revision_id, template_rev) + + def test_variant_template_revision_inheritance(self): + """Test that variants can inherit revisions from their template""" + # Create a revision for the template with variants + template_rev = self.ProductRevision.create( + { + "name": "Template With Variants Rev", + "revision_number": "1", + "product_tmpl_id": self.template_with_variants.id, + "internal_product_id": "TMP-002", + "active": True, + } + ) + + # Check that both variants inherit the template's revision + self.variant_s.invalidate_cache() + self.variant_m.invalidate_cache() + + self.assertEqual(self.variant_s.current_revision_id, template_rev) + self.assertEqual(self.variant_m.current_revision_id, template_rev) + + # Create a specific revision for variant_s + variant_s_rev = self.ProductRevision.create( + { + "name": "Variant S Rev", + "revision_number": "S1", + "product_id": self.variant_s.id, + "internal_product_id": "VAR-S", + "active": True, + } + ) + + # Check that variant_s now has its own revision. + # And variant_m still inherits from template + self.variant_s.invalidate_cache() + self.variant_m.invalidate_cache() + + self.assertEqual(self.variant_s.current_revision_id, variant_s_rev) + self.assertEqual(self.variant_m.current_revision_id, template_rev) + + # Check revision counts + self.assertEqual(self.template_with_variants.revision_count, 1) + self.assertEqual(self.variant_s.revision_count, 1) # Only its own revisions + self.assertEqual(self.variant_m.revision_count, 0) # No specific revisions + + def test_name_search(self): + """Test the name_search method in ProductRevisionExt""" + # Create revisions for testing + template_rev = self.ProductRevision.create( + { + "name": "Template Search Rev", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "SEARCH-001", + "active": True, + } + ) + + variant_rev = self.ProductRevision.create( + { + "name": "Variant Search Rev", + "revision_number": "1", + "product_id": self.standalone_variant.id, + "internal_product_id": "SEARCH-002", + "active": True, + } + ) + + # Test name_search with product_id in context + result = self.ProductRevision.with_context( + product_id=self.standalone_variant.id + ).name_search(name="Search") + + # Should find only the variant revision + self.assertEqual(len(result), 1) + self.assertEqual(result[0][0], variant_rev.id) + + # Test name_search with product_tmpl_id in context + result = self.ProductRevision.with_context( + product_tmpl_id=self.template.id + ).name_search(name="Search") + + # Should find only the template revision + self.assertEqual(len(result), 1) + self.assertEqual(result[0][0], template_rev.id) + + # Test name_search without context + result = self.ProductRevision.name_search(name="Search") + + # Should find both revisions + self.assertEqual(len(result), 2) + found_ids = [r[0] for r in result] + self.assertIn(template_rev.id, found_ids) + self.assertIn(variant_rev.id, found_ids) + + def test_action_view_revisions(self): + """Test the action_view_revisions method""" + # Test action_view_revisions for template + action = self.template.action_view_revisions() + self.assertEqual(action["res_model"], "product.revision") + self.assertEqual(action["domain"], [("product_tmpl_id", "=", self.template.id)]) + self.assertEqual(action["context"]["default_product_tmpl_id"], self.template.id) + + action = self.standalone_variant.action_view_revisions() + self.assertEqual(action["res_model"], "product.revision") + self.assertEqual( + action["domain"], [("product_id", "=", self.standalone_variant.id)] + ) + self.assertEqual( + action["context"]["default_product_id"], self.standalone_variant.id + ) + + # Test action_view_revisions for default variant (should show template revisions) + default_variant = self.template.product_variant_ids[0] + action = default_variant.action_view_revisions() + self.assertEqual(action["res_model"], "product.revision") + self.assertEqual(action["domain"], [("product_tmpl_id", "=", self.template.id)]) + self.assertEqual(action["context"]["default_product_tmpl_id"], self.template.id) + + def test_action_create_revision(self): + """Test the action_create_revision method""" + # Test action_create_revision for template + action = self.template.action_create_revision() + self.assertEqual(action["res_model"], "product.revision") + self.assertEqual(action["context"]["default_product_tmpl_id"], self.template.id) + + # Create a revision first to test next revision number + self.ProductRevision.create( + { + "name": "Template First Rev", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TMP-001", + "active": True, + } + ) + + # Test action_create_revision again to check next revision number + action = self.template.action_create_revision() + self.assertEqual(action["context"]["default_revision_number"], "2") + + # Test action_create_revision for variant + action = self.standalone_variant.action_create_revision() + self.assertEqual(action["res_model"], "product.revision") + self.assertEqual( + action["context"]["default_product_id"], self.standalone_variant.id + ) + + # Test action_create_revision for default variant of template with revisions + default_variant = self.template.product_variant_ids[0] + action = default_variant.action_create_revision() + self.assertEqual(action["res_model"], "product.revision") + self.assertEqual(action["context"]["default_product_tmpl_id"], self.template.id) + self.assertNotIn("default_product_id", action["context"]) + + def test_batch_revision_creation(self): + """Test creating multiple revisions in batch""" + # Create multiple revisions in a single batch + revisions = self.ProductRevision.create( + [ + { + "name": "Batch Rev 1", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "BATCH-001", + "active": True, + }, + { + "name": "Batch Rev 2", + "revision_number": "1", + "product_id": self.standalone_variant.id, + "internal_product_id": "BATCH-002", + "active": True, + }, + ] + ) + + # Verify both revisions were created + self.assertEqual(len(revisions), 2) + + # Verify the revisions are correctly linked + template_rev = revisions.filtered(lambda r: r.product_tmpl_id) + variant_rev = revisions.filtered(lambda r: r.product_id) + + self.assertEqual(template_rev.product_tmpl_id, self.template) + self.assertEqual(variant_rev.product_id, self.standalone_variant) + + # Verify both are active + self.assertTrue(template_rev.active) + self.assertTrue(variant_rev.active) + + def test_complex_variant_scenario(self): + """Test revision behavior with complex variant scenarios (multiple attributes)""" + # Get some variants from the multi-attribute template + variants = self.ProductProduct.search( + [("product_tmpl_id", "=", self.template_multi_attr.id)], limit=3 + ) + + self.assertEqual(len(variants), 3) + + # Create a revision for the template + multi_attr_template_rev = self.ProductRevision.create( + { + "name": "Multi-Attr Template Rev", + "revision_number": "1", + "product_tmpl_id": self.template_multi_attr.id, + "internal_product_id": "MULTI-001", + "active": True, + } + ) + + # Verify all variants inherit the template revision + for variant in variants: + variant.invalidate_cache() + self.assertEqual(variant.current_revision_id, multi_attr_template_rev) + + # Create specific revisions for two variants + variant_rev1 = self.ProductRevision.create( + { + "name": "Variant 1 Rev", + "revision_number": "V1", + "product_id": variants[0].id, + "internal_product_id": "VAR-MULTI-1", + "active": True, + } + ) + + multi_attr_variant_rev2 = self.ProductRevision.create( + { + "name": "Variant 2 Rev", + "revision_number": "V2", + "product_id": variants[1].id, + "internal_product_id": "VAR-MULTI-2", + "active": True, + } + ) + + # Verify the specific variants have their own revisions + variants[0].invalidate_cache() + variants[1].invalidate_cache() + variants[2].invalidate_cache() + + self.assertEqual(variants[0].current_revision_id, variant_rev1) + self.assertEqual(variants[1].current_revision_id, multi_attr_variant_rev2) + self.assertEqual( + variants[2].current_revision_id, multi_attr_template_rev + ) # Still inherits from template + + def test_product_copy_behavior(self): + """Test revision behavior when a product is copied""" + # Create a revision for the template and verify copy behavior + self.ProductRevision.create( + { + "name": "Original Template Rev", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "ORIG-001", + "active": True, + } + ) + + # Copy the template + copied_template = self.template.copy() + + # Verify the copied template has no revisions + self.assertEqual(len(copied_template.revision_ids), 0) + self.assertFalse(copied_template.current_revision_id) + self.assertEqual(copied_template.current_revision_number, "") + + # Create a revision for the variant and verify copy behavior + self.ProductRevision.create( + { + "name": "Original Variant Rev", + "revision_number": "1", + "product_id": self.standalone_variant.id, + "internal_product_id": "ORIG-VAR-001", + "active": True, + } + ) + + # Copy the variant + copied_variant = self.standalone_variant.copy() + + # Verify the copied variant has no revisions + self.assertEqual(len(copied_variant.revision_ids), 0) + self.assertFalse(copied_variant.current_revision_id) + self.assertEqual(copied_variant.current_revision_number, "") + + def test_product_archive_behavior(self): + """Test revision behavior when a product is archived/unarchived""" + # Create a revision for the template and test archive behavior + test_template_rev = self.ProductRevision.create( + { + "name": "Archive Test Rev", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "ARCH-001", + "active": True, + } + ) + + # Archive the template + self.template.active = False + + # Verify the revision is still active + self.assertTrue(test_template_rev.active) + + # Unarchive the template + self.template.active = True + + # Verify the revision is still active + self.assertTrue(test_template_rev.active) + + # Create a revision for the variant and test archive behavior + test_variant_rev = self.ProductRevision.create( + { + "name": "Variant Archive Test Rev", + "revision_number": "1", + "product_id": self.standalone_variant.id, + "internal_product_id": "ARCH-VAR-001", + "active": True, + } + ) + + # Archive the variant + self.standalone_variant.active = False + + # Verify the revision is still active + self.assertTrue(test_variant_rev.active) + + # Unarchive the variant + self.standalone_variant.active = True + + # Verify the revision is still active + self.assertTrue(test_variant_rev.active) + + def test_revision_deletion(self): + """Test behavior when a revision is deleted""" + # Create multiple revisions for the template + rev1 = self.ProductRevision.create( + { + "name": "Delete Test Rev 1", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "DEL-001", + "active": True, + } + ) + + rev2 = self.ProductRevision.create( + { + "name": "Delete Test Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "DEL-002", + "active": False, + } + ) + + # Verify initial state + self.template.invalidate_cache() + self.assertEqual(self.template.current_revision_id, rev1) + + # Delete the active revision + rev1.unlink() + + # Verify the template no longer has an active revision + self.template.invalidate_cache() + self.assertFalse(self.template.current_revision_id) + + # Activate the remaining revision + rev2.write({"active": True}) + + # Verify it becomes the current revision + self.template.invalidate_cache() + self.assertEqual(self.template.current_revision_id, rev2) + + @mute_logger("odoo.models.unlink") + def test_revision_constraints_with_test_user(self): + """Test revision constraints with test user""" + # Switch to test user environment + ProductRevision = self.test_env["product.revision"] + + # Create a revision with test user + rev = ProductRevision.create( + { + "name": "Test User Rev", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TEST-USER-001", + "active": True, + } + ) + + # Verify the revision was created correctly + self.assertEqual(rev.name, "Test User Rev") + self.assertEqual(rev.product_tmpl_id, self.template) + + # Test constraint with test user + with self.assertRaises(ValidationError): + ProductRevision.create( + { + "name": "Invalid Test User Rev", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "product_id": self.standalone_variant.id, + "internal_product_id": "TEST-USER-002", + "active": True, + } + ) diff --git a/product_revision/views/product_kanban_views.xml b/product_revision/views/product_kanban_views.xml new file mode 100644 index 00000000..bfa7b014 --- /dev/null +++ b/product_revision/views/product_kanban_views.xml @@ -0,0 +1,45 @@ + + + + product.template.kanban.inherit + product.template + + + +
+ Ref: + + + - Rev: + + +
+
+
+
+ + + + product.product.kanban.inherit + product.product + + + +
+ Ref: + + + - Rev: + + +
+
+
+
+
diff --git a/product_revision/views/product_product_views.xml b/product_revision/views/product_product_views.xml new file mode 100644 index 00000000..5e2b0867 --- /dev/null +++ b/product_revision/views/product_product_views.xml @@ -0,0 +1,46 @@ + + + + product.template.form.inherit.variant + product.template + + + +
+ +
+
+
+
+ + + + product.product.tree.inherit + product.product + + + + + + + +
diff --git a/product_revision/views/product_revision_views.xml b/product_revision/views/product_revision_views.xml new file mode 100644 index 00000000..691adabd --- /dev/null +++ b/product_revision/views/product_revision_views.xml @@ -0,0 +1,175 @@ + + + + product.revision.form + product.revision + +
+ +
+ +
+
+

+ +

+

+ +

+
+ + + + + + + + + + + + + + + + +

+ This section shows the history of this revision. All revisions for a product are preserved in the system, + with only one being active at a time. When a new revision is created, previous revisions are automatically + set to inactive but remain in the system for historical reference. +

+
+
+
+
+ + + +
+
+
+
+ + + + product.revision.tree + product.revision + + + + + + + + + + + + + + + + product.revision.search + product.revision + + + + + + + + + + + + + + + + + + + + + product.template.form.inherit + product.template + + + +
+ +
+
+
+
+ + + + product.template.tree.inherit + product.template + + + + + + + + + + + Product Revisions + product.revision + tree,form + {'search_default_active': 1, 'search_default_inactive': 1} + +

+ Create your first product revision +

+

+ Product revisions allow you to track changes to your products over time. +

+
+
+ + + +
diff --git a/purchase_order_product_revision/README.rst b/purchase_order_product_revision/README.rst new file mode 100644 index 00000000..b5dd53b4 --- /dev/null +++ b/purchase_order_product_revision/README.rst @@ -0,0 +1,137 @@ +=============================== +Purchase Order Product Revision +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:96943e188295f043f0206b42a41ad0685faef6a5adf8aedc559d91cf785826f6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Faxls--oca-lightgray.png?logo=github + :target: https://github.com/OCA/axls-oca/tree/wng-add-revision-functions/purchase_order_product_revision + :alt: OCA/axls-oca +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/axls-oca-wng-add-revision-functions/axls-oca-wng-add-revision-functions-purchase_order_product_revision + :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/axls-oca&target_branch=wng-add-revision-functions + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + + +This module adds product revision information to purchase order lines in Odoo 16. It integrates with the product_revision module to track which revision of a product was used in each purchase order line. + +Key features: + + +* Adds product revision and revision number fields to purchase order lines +* Automatically sets the current revision of a product when added to a purchase order +* Displays revision information in purchase order line views +* Allows filtering and grouping purchase order lines by product revision +* Passes revision information to stock moves when confirming purchase orders (requires stock_move_revision module) +* Integrates with stock_picking_product_revision module to track revisions from purchase to receipt + +This module enhances traceability by ensuring that the specific revision of a product used in a purchase order is recorded and tracked throughout the procurement process. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + + +To use this module, you need to: + +Managing Product Revisions in Purchase Orders +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#. Go to Purchase > Orders > Purchase Orders +#. Create a new purchase order or edit an existing one +#. Add products to the order lines +#. The current revision of each product will be automatically set in the "Product Revision" field +#. The revision number will be displayed in the "Revision Number" field +#. You can manually change the revision if needed by selecting a different revision from the dropdown + +Viewing and Filtering Purchase Order Lines by Revision +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Purchase > Orders > Purchase Order Lines +#. The product revision and revision number columns are available (you may need to enable them in the view) +#. You can filter the list by using the search box and selecting a specific revision +#. You can group the lines by revision using the "Group By" menu and selecting "Product Revision" + +Traceability +~~~~~~~~~~~~~ + +When a purchase order is confirmed and receipts are created: + + +#. The revision information is passed to the stock moves (requires the stock_move_revision module) +#. The revision information is also passed to the stock picking lines (requires the stock_picking_product_revision module) +#. This ensures complete traceability of which product revision was used throughout the procurement process +#. You can view the revision information in both the purchase order, stock moves, and stock picking lines + +Dependencies +~~~~~~~~~~~~~ + +This module depends on: + + +* product_revision: Provides the base revision functionality for products +* stock_move_revision: Extends stock moves to include revision information +* stock_picking_product_revision: Extends stock picking lines to include revision information + +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 +~~~~~~~ + +* Axelspace + +Contributors +~~~~~~~~~~~~ + +* `Axelspace Corporation `__: + + * WangTKurata + +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/axls-oca `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_order_product_revision/__init__.py b/purchase_order_product_revision/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/purchase_order_product_revision/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/purchase_order_product_revision/__manifest__.py b/purchase_order_product_revision/__manifest__.py new file mode 100644 index 00000000..aeee5fd2 --- /dev/null +++ b/purchase_order_product_revision/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2025 Axelspace +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Purchase Order Product Revision", + "summary": "Add revision number in purchase order lines", + "author": "Axelspace, Odoo Community Association (OCA)", + "website": "https://www.axelspace.com", + "license": "AGPL-3", + "category": "Purchase", + "version": "16.0.1.0.0", + "depends": [ + "purchase", + "product_revision", + "stock_move_revision", + "stock_picking_product_revision", + ], + "data": [ + "views/purchase_order_line_views.xml", + "security/ir.model.access.csv", + ], + "installable": True, + "application": False, +} diff --git a/purchase_order_product_revision/i18n/ja_JP.po b/purchase_order_product_revision/i18n/ja_JP.po new file mode 100644 index 00000000..b4f00ad7 --- /dev/null +++ b/purchase_order_product_revision/i18n/ja_JP.po @@ -0,0 +1,43 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_order_product_revision +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0-20230701\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-02 07:08+0000\n" +"PO-Revision-Date: 2025-05-02 07:08+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: purchase_order_product_revision +#: model:ir.model,name:purchase_order_product_revision.model_product_revision +#: model:ir.model.fields,field_description:purchase_order_product_revision.field_purchase_order_line__product_revision_id +#: model_terms:ir.ui.view,arch_db:purchase_order_product_revision.purchase_order_line_search_inherit +msgid "Product Revision" +msgstr "製品リビジョン" + +#. module: purchase_order_product_revision +#: model:ir.model,name:purchase_order_product_revision.model_purchase_order_line +msgid "Purchase Order Line" +msgstr "購買オーダ明細" + +#. module: purchase_order_product_revision +#: model:ir.model.fields,field_description:purchase_order_product_revision.field_purchase_order_line__product_revision_number +msgid "Revision Number" +msgstr "リビジョン番号" + +#. module: purchase_order_product_revision +#: model:ir.model.fields,help:purchase_order_product_revision.field_purchase_order_line__product_revision_number +msgid "Revision number of the product at the time of purchase" +msgstr "購入時の商品のリビジョン番号" + +#. module: purchase_order_product_revision +#: model:ir.model.fields,help:purchase_order_product_revision.field_purchase_order_line__product_revision_id +msgid "Revision of the product at the time of purchase" +msgstr "購入時の商品のリビジョン" diff --git a/purchase_order_product_revision/models/__init__.py b/purchase_order_product_revision/models/__init__.py new file mode 100644 index 00000000..3dcee9e0 --- /dev/null +++ b/purchase_order_product_revision/models/__init__.py @@ -0,0 +1,2 @@ +from . import purchase_order_line +from . import product_revision_ext diff --git a/purchase_order_product_revision/models/product_revision_ext.py b/purchase_order_product_revision/models/product_revision_ext.py new file mode 100644 index 00000000..b4bf3a61 --- /dev/null +++ b/purchase_order_product_revision/models/product_revision_ext.py @@ -0,0 +1,71 @@ +from odoo import api, models + + +class ProductRevisionExt(models.Model): + _inherit = "product.revision" + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + """Override name_search to filter revisions based on product_id in context""" + if args is None: + args = [] + + # Get product_id from context (set by purchase.order.line) + product_id = self.env.context.get("product_id") or self._context.get( + "default_product_id" + ) + product_tmpl_id = self.env.context.get("product_tmpl_id") or self._context.get( + "default_product_tmpl_id" + ) + + # If product_id is set, filter revisions for this product + if product_id: + product = self.env["product.product"].browse(product_id) + include_variant_revisions = self.env.context.get( + "include_variant_revisions", True + ) + + if include_variant_revisions: + # Include both template and variant revisions + args = args + [ + "|", + ("active", "=", True), + ( + "active", + "=", + False, + ), # Include both active and inactive revisions + "|", + ("product_id", "=", product_id), + "&", + ("product_tmpl_id", "=", product.product_tmpl_id.id), + ("product_id", "=", False), + ] + else: + # Include only template revisions + args = args + [ + "|", + ("active", "=", True), + ( + "active", + "=", + False, + ), # Include both active and inactive revisions + "&", + ("product_tmpl_id", "=", product.product_tmpl_id.id), + ("product_id", "=", False), + ] + # If only product_tmpl_id is set, filter revisions for this template + elif product_tmpl_id: + args = args + [ + "|", + ("active", "=", True), + ("active", "=", False), # Include both active and inactive revisions + "&", + ("product_tmpl_id", "=", product_tmpl_id), + ("product_id", "=", False), + ] + + return super(ProductRevisionExt, self).name_search( + name=name, args=args, operator=operator, limit=limit + ) diff --git a/purchase_order_product_revision/models/purchase_order_line.py b/purchase_order_product_revision/models/purchase_order_line.py new file mode 100644 index 00000000..a1a86b21 --- /dev/null +++ b/purchase_order_product_revision/models/purchase_order_line.py @@ -0,0 +1,74 @@ +from odoo import api, fields, models + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + product_revision_id = fields.Many2one( + "product.revision", + string="Product Revision", + context={"include_variant_revisions": True}, + help="Revision of the product at the time of purchase", + ) + product_revision_number = fields.Char( + string="Revision Number", + related="product_revision_id.revision_number", + store=True, + readonly=True, + help="Revision number of the product at the time of purchase", + ) + + @api.onchange("product_id") + def onchange_product_id(self): + """When product is changed, set the current revision""" + res = super().onchange_product_id() + for line in self: + if line.product_id: + # Get the current active revision for this product + line.product_revision_id = line.product_id.current_revision_id + return res + + def get_product_revision_domain(self): + """Get domain for product revisions based on selected product""" + self.ensure_one() + if not self.product_id: + return [("id", "=", False)] # No product selected, no revisions + + # Get revisions for this product (variant or template) + return [ + "|", + ("active", "=", True), + ("active", "=", False), # Include both active and inactive + "|", + ("product_id", "=", self.product_id.id), # Variant-specific revisions + "&", + ( + "product_tmpl_id", + "=", + self.product_id.product_tmpl_id.id, + ), # Template revisions + ("product_id", "=", False), + ] # Not linked to a specific variant + + def _prepare_stock_moves(self, picking): + """Pass the revision information to stock moves""" + res = super()._prepare_stock_moves(picking) + for move_vals, line in zip(res, self): + if line.product_revision_id: + move_vals.update( + { + "product_revision_id": line.product_revision_id.id, + } + ) + return res + + def product_revision_change(self, product_id): + """Method to be called from the UI to update the context for product_revision_id""" + return { + "domain": { + "product_revision_id": self.get_product_revision_domain(), + }, + "context": { + "product_id": product_id, + }, + } diff --git a/purchase_order_product_revision/readme/CONTRIBUTORS.rst b/purchase_order_product_revision/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..bd765ad6 --- /dev/null +++ b/purchase_order_product_revision/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Axelspace Corporation `__: + + * WangTKurata diff --git a/purchase_order_product_revision/readme/DESCRIPTION.rst b/purchase_order_product_revision/readme/DESCRIPTION.rst new file mode 100644 index 00000000..bc984b04 --- /dev/null +++ b/purchase_order_product_revision/readme/DESCRIPTION.rst @@ -0,0 +1,14 @@ + +This module adds product revision information to purchase order lines in Odoo 16. It integrates with the product_revision module to track which revision of a product was used in each purchase order line. + +Key features: + + +* Adds product revision and revision number fields to purchase order lines +* Automatically sets the current revision of a product when added to a purchase order +* Displays revision information in purchase order line views +* Allows filtering and grouping purchase order lines by product revision +* Passes revision information to stock moves when confirming purchase orders (requires stock_move_revision module) +* Integrates with stock_picking_product_revision module to track revisions from purchase to receipt + +This module enhances traceability by ensuring that the specific revision of a product used in a purchase order is recorded and tracked throughout the procurement process. diff --git a/purchase_order_product_revision/readme/USAGE.rst b/purchase_order_product_revision/readme/USAGE.rst new file mode 100644 index 00000000..05127354 --- /dev/null +++ b/purchase_order_product_revision/readme/USAGE.rst @@ -0,0 +1,42 @@ + +To use this module, you need to: + +Managing Product Revisions in Purchase Orders +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#. Go to Purchase > Orders > Purchase Orders +#. Create a new purchase order or edit an existing one +#. Add products to the order lines +#. The current revision of each product will be automatically set in the "Product Revision" field +#. The revision number will be displayed in the "Revision Number" field +#. You can manually change the revision if needed by selecting a different revision from the dropdown + +Viewing and Filtering Purchase Order Lines by Revision +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Purchase > Orders > Purchase Order Lines +#. The product revision and revision number columns are available (you may need to enable them in the view) +#. You can filter the list by using the search box and selecting a specific revision +#. You can group the lines by revision using the "Group By" menu and selecting "Product Revision" + +Traceability +~~~~~~~~~~~~~ + +When a purchase order is confirmed and receipts are created: + + +#. The revision information is passed to the stock moves (requires the stock_move_revision module) +#. The revision information is also passed to the stock picking lines (requires the stock_picking_product_revision module) +#. This ensures complete traceability of which product revision was used throughout the procurement process +#. You can view the revision information in both the purchase order, stock moves, and stock picking lines + +Dependencies +~~~~~~~~~~~~~ + +This module depends on: + + +* product_revision: Provides the base revision functionality for products +* stock_move_revision: Extends stock moves to include revision information +* stock_picking_product_revision: Extends stock picking lines to include revision information diff --git a/purchase_order_product_revision/security/ir.model.access.csv b/purchase_order_product_revision/security/ir.model.access.csv new file mode 100644 index 00000000..97dd8b91 --- /dev/null +++ b/purchase_order_product_revision/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/purchase_order_product_revision/static/description/index.html b/purchase_order_product_revision/static/description/index.html new file mode 100644 index 00000000..9e7524de --- /dev/null +++ b/purchase_order_product_revision/static/description/index.html @@ -0,0 +1,486 @@ + + + + + +Purchase Order Product Revision + + + +
+

Purchase Order Product Revision

+ + +

Beta License: AGPL-3 OCA/axls-oca Translate me on Weblate Try me on Runboat

+

This module adds product revision information to purchase order lines in Odoo 16. It integrates with the product_revision module to track which revision of a product was used in each purchase order line.

+

Key features:

+
    +
  • Adds product revision and revision number fields to purchase order lines
  • +
  • Automatically sets the current revision of a product when added to a purchase order
  • +
  • Displays revision information in purchase order line views
  • +
  • Allows filtering and grouping purchase order lines by product revision
  • +
  • Passes revision information to stock moves when confirming purchase orders (requires stock_move_revision module)
  • +
  • Integrates with stock_picking_product_revision module to track revisions from purchase to receipt
  • +
+

This module enhances traceability by ensuring that the specific revision of a product used in a purchase order is recorded and tracked throughout the procurement process.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+
+

Managing Product Revisions in Purchase Orders

+
    +
  1. Go to Purchase > Orders > Purchase Orders
  2. +
  3. Create a new purchase order or edit an existing one
  4. +
  5. Add products to the order lines
  6. +
  7. The current revision of each product will be automatically set in the “Product Revision” field
  8. +
  9. The revision number will be displayed in the “Revision Number” field
  10. +
  11. You can manually change the revision if needed by selecting a different revision from the dropdown
  12. +
+
+
+

Viewing and Filtering Purchase Order Lines by Revision

+
    +
  1. Go to Purchase > Orders > Purchase Order Lines
  2. +
  3. The product revision and revision number columns are available (you may need to enable them in the view)
  4. +
  5. You can filter the list by using the search box and selecting a specific revision
  6. +
  7. You can group the lines by revision using the “Group By” menu and selecting “Product Revision”
  8. +
+
+
+

Traceability

+

When a purchase order is confirmed and receipts are created:

+
    +
  1. The revision information is passed to the stock moves (requires the stock_move_revision module)
  2. +
  3. The revision information is also passed to the stock picking lines (requires the stock_picking_product_revision module)
  4. +
  5. This ensures complete traceability of which product revision was used throughout the procurement process
  6. +
  7. You can view the revision information in both the purchase order, stock moves, and stock picking lines
  8. +
+
+
+

Dependencies

+

This module depends on:

+
    +
  • product_revision: Provides the base revision functionality for products
  • +
  • stock_move_revision: Extends stock moves to include revision information
  • +
  • stock_picking_product_revision: Extends stock picking lines to include revision information
  • +
+
+
+
+

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

+
    +
  • Axelspace
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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/axls-oca project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/purchase_order_product_revision/tests/__init__.py b/purchase_order_product_revision/tests/__init__.py new file mode 100644 index 00000000..673689df --- /dev/null +++ b/purchase_order_product_revision/tests/__init__.py @@ -0,0 +1 @@ +from . import test_purchase_order_product_revision diff --git a/purchase_order_product_revision/tests/test_purchase_order_product_revision.py b/purchase_order_product_revision/tests/test_purchase_order_product_revision.py new file mode 100644 index 00000000..fdffe83f --- /dev/null +++ b/purchase_order_product_revision/tests/test_purchase_order_product_revision.py @@ -0,0 +1,358 @@ +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestPurchaseOrderProductRevision(TransactionCase): + def setUp(self): + super(TestPurchaseOrderProductRevision, self).setUp() + self.ProductTemplate = self.env["product.template"] + self.ProductRevision = self.env["product.revision"] + self.ProductProduct = self.env["product.product"] + self.PurchaseOrder = self.env["purchase.order"] + self.PurchaseOrderLine = self.env["purchase.order.line"] + self.ResPartner = self.env["res.partner"] + + # Create test user with necessary security groups + self.test_user = self.env["res.users"].create( + { + "name": "Test User", + "login": "test.user", + "email": "test.user@example.com", + } + ) + group_user = self.env.ref("base.group_user") + purchase_group = self.env.ref("purchase.group_purchase_user") + self.test_user.write( + {"groups_id": [(6, 0, [group_user.id, purchase_group.id])]} + ) + + # Test environment with test user + self.test_env = self.env(user=self.test_user.id) + + # Create a supplier + self.supplier = self.ResPartner.create( + { + "name": "Test Supplier", + "email": "supplier@test.com", + "supplier_rank": 1, + } + ) + + # Create a product template for testing + self.template = self.ProductTemplate.create( + { + "name": "Test Product", + "type": "product", + "default_code": "TP-001", + "purchase_ok": True, + } + ) + + # Get the product variant + self.product = self.template.product_variant_ids[0] + + # Create a revision for the product template + self.template_rev1 = self.ProductRevision.create( + { + "name": "Template Rev 1", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a purchase order + self.purchase_order = self.PurchaseOrder.create( + { + "partner_id": self.supplier.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_qty": 5.0, + "price_unit": 100.0, + "name": "Test Product", + "date_planned": "2025-01-01", + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + + # Get the purchase order line + self.purchase_order_line = self.purchase_order.order_line[0] + + # Manually call onchange_product_id to set the product_revision_id + # In the UI, this would be called automatically when the product is selected + self.purchase_order_line.onchange_product_id() + + def test_po_line_revision_auto_set(self): + """Test that purchase order line revision is automatically set when creating a line""" + # Check that the purchase order line has the revision set + self.assertEqual( + self.purchase_order_line.product_revision_id, self.template_rev1 + ) + self.assertEqual(self.purchase_order_line.product_revision_number, "1") + + # Create a new revision for the product template + template_rev2 = self.ProductRevision.create( + { + "name": "Template Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a new purchase order + new_purchase_order = self.PurchaseOrder.create( + { + "partner_id": self.supplier.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_qty": 5.0, + "price_unit": 100.0, + "name": "Test Product", + "date_planned": "2025-01-01", + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + + # Get the new purchase order line + new_purchase_order_line = new_purchase_order.order_line[0] + + # Manually call onchange_product_id to set the product_revision_id + new_purchase_order_line.onchange_product_id() + + # Check that the new purchase order line has the new revision set + self.assertEqual(new_purchase_order_line.product_revision_id, template_rev2) + self.assertEqual(new_purchase_order_line.product_revision_number, "2") + + def test_variant_specific_revision(self): + """Test purchase order line revision with variant-specific revisions""" + # Create a variant-specific revision + variant_rev = self.ProductRevision.create( + { + "name": "Variant Rev A", + "revision_number": "A", + "product_id": self.product.id, + "internal_product_id": "TP-001-V", + "active": True, + } + ) + + # Create a new purchase order + new_purchase_order = self.PurchaseOrder.create( + { + "partner_id": self.supplier.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_qty": 5.0, + "price_unit": 100.0, + "name": "Test Product", + "date_planned": "2025-01-01", + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + + # Get the new purchase order line + new_purchase_order_line = new_purchase_order.order_line[0] + + # Manually call onchange_product_id to set the product_revision_id + new_purchase_order_line.onchange_product_id() + + # Check that the new purchase order line has the variant-specific revision set + self.assertEqual(new_purchase_order_line.product_revision_id, variant_rev) + self.assertEqual(new_purchase_order_line.product_revision_number, "A") + + def test_manual_revision_selection(self): + """Test manually selecting a revision for a purchase order line""" + # Create a new revision for the product template + template_rev2 = self.ProductRevision.create( + { + "name": "Template Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": False, # Inactive revision + } + ) + + # Manually set the revision on the purchase order line + self.purchase_order_line.product_revision_id = template_rev2 + + # Check that the purchase order line has the manually selected revision set + self.assertEqual(self.purchase_order_line.product_revision_id, template_rev2) + self.assertEqual(self.purchase_order_line.product_revision_number, "2") + + def test_get_product_revision_domain(self): + """Test the get_product_revision_domain method""" + # Create a new product + new_template = self.ProductTemplate.create( + { + "name": "New Test Product", + "type": "product", + "default_code": "NTP-001", + "purchase_ok": True, + } + ) + new_product = new_template.product_variant_ids[0] + + # Create a revision for the new product + self.ProductRevision.create( + { + "name": "New Product Rev 1", + "revision_number": "1", + "product_tmpl_id": new_template.id, + "internal_product_id": "NTP-001", + "active": True, + } + ) + + # Create a purchase order line with the new product + new_po_line = self.PurchaseOrderLine.create( + { + "order_id": self.purchase_order.id, + "product_id": new_product.id, + "product_qty": 5.0, + "price_unit": 100.0, + "name": "New Test Product", + "date_planned": "2025-01-01", + "product_uom": new_product.uom_id.id, + } + ) + + # Get the domain for the new product + domain = new_po_line.get_product_revision_domain() + + # Check that the domain includes the new product's revision + # The domain structure is complex, so we need to check + # if any part of it contains the product_tmpl_id condition + found = False + for item in domain: + if ( + isinstance(item, tuple) + and item[0] == "product_tmpl_id" + and item[1] == "=" + and item[2] == new_template.id + ): + found = True + break + self.assertTrue(found, "Domain should include product_tmpl_id condition") + + # Create a temporary purchase order line object + # without saving it to the database + # This allows us to test + # the get_product_revision_domain method without violating constraints + empty_po_line = self.PurchaseOrderLine.new( + { + "order_id": self.purchase_order.id, + "product_id": False, + "product_qty": 5.0, + "price_unit": 100.0, + "name": "No Product", + "date_planned": "2025-01-01", + } + ) + + # Get the domain for the empty line + domain = empty_po_line.get_product_revision_domain() + + # Check that the domain is empty + self.assertEqual(domain, [("id", "=", False)]) + + def test_name_search_filtering(self): + """Test that name_search filters revisions based on context""" + # Create a new product and revision + new_template = self.ProductTemplate.create( + { + "name": "Search Test Product", + "type": "product", + "default_code": "SEARCH-001", + "purchase_ok": True, + } + ) + new_product = new_template.product_variant_ids[0] + + search_rev = self.ProductRevision.create( + { + "name": "Search Test Rev", + "revision_number": "1", + "product_tmpl_id": new_template.id, + "internal_product_id": "SEARCH-001", + "active": True, + } + ) + + # Test name_search with product_id in context + result = self.ProductRevision.with_context( + product_id=new_product.id + ).name_search(name="Search") + + # Should find only the search revision + self.assertEqual(len(result), 1) + self.assertEqual(result[0][0], search_rev.id) + + # Create a variant-specific revision for the original product + # This is needed for the next test to pass + variant_rev = self.ProductRevision.create( + { + "name": "Variant Rev A", + "revision_number": "A", + "product_id": self.product.id, + "internal_product_id": "TP-001-V", + "active": True, + } + ) + + # Test name_search with product_id in context for original product + result = self.ProductRevision.with_context( + product_id=self.product.id + ).name_search(name="Rev") + + # Should find both the template revision and the variant-specific revision + self.assertEqual(len(result), 2) # Template Rev 1 and Variant Rev A + found_ids = [r[0] for r in result] + self.assertIn(self.template_rev1.id, found_ids) + self.assertIn(variant_rev.id, found_ids) + + def test_prepare_stock_moves(self): + """Test that revision information is passed to stock moves""" + # Confirm the purchase order to create stock moves + self.purchase_order.button_confirm() + + # Check that the stock move has the revision set + move = self.env["stock.move"].search( + [("purchase_line_id", "=", self.purchase_order_line.id)], limit=1 + ) + self.assertEqual(move.product_revision_id, self.template_rev1) + + def test_product_revision_change(self): + """Test the product_revision_change method""" + # Call the product_revision_change method + result = self.purchase_order_line.product_revision_change(self.product.id) + + # Check that the domain and context are correctly set + self.assertIn("domain", result) + self.assertIn("product_revision_id", result["domain"]) + self.assertIn("context", result) + self.assertEqual(result["context"]["product_id"], self.product.id) diff --git a/purchase_order_product_revision/views/purchase_order_line_views.xml b/purchase_order_product_revision/views/purchase_order_line_views.xml new file mode 100644 index 00000000..a37de7ad --- /dev/null +++ b/purchase_order_product_revision/views/purchase_order_line_views.xml @@ -0,0 +1,103 @@ + + + + + purchase.order.form.inherit.product.revision + purchase.order + + + + {'product_id': product_id} + + + + + + + {'product_id': product_id} + + + + + + + + + + + purchase.order.line.tree.inherit.product.revision + purchase.order.line + + + + {'product_id': product_id} + + + + + + + + + + + purchase.order.line.form.inherit.product.revision + purchase.order.line + + + + {'product_id': product_id} + + + + + + + + + + + purchase.order.line.search.inherit.product.revision + purchase.order.line + + + + + + + + + + + + diff --git a/setup/bom_product_revision/odoo/addons/bom_product_revision b/setup/bom_product_revision/odoo/addons/bom_product_revision new file mode 120000 index 00000000..a056f16f --- /dev/null +++ b/setup/bom_product_revision/odoo/addons/bom_product_revision @@ -0,0 +1 @@ +../../../../bom_product_revision \ No newline at end of file diff --git a/setup/bom_product_revision/setup.py b/setup/bom_product_revision/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/bom_product_revision/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/product_lot_revision/odoo/addons/product_lot_revision b/setup/product_lot_revision/odoo/addons/product_lot_revision new file mode 120000 index 00000000..d1569ca7 --- /dev/null +++ b/setup/product_lot_revision/odoo/addons/product_lot_revision @@ -0,0 +1 @@ +../../../../product_lot_revision \ No newline at end of file diff --git a/setup/product_lot_revision/setup.py b/setup/product_lot_revision/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/product_lot_revision/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/product_revision/odoo/addons/product_revision b/setup/product_revision/odoo/addons/product_revision new file mode 120000 index 00000000..20774066 --- /dev/null +++ b/setup/product_revision/odoo/addons/product_revision @@ -0,0 +1 @@ +../../../../product_revision \ No newline at end of file diff --git a/setup/product_revision/setup.py b/setup/product_revision/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/product_revision/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/purchase_order_product_revision/odoo/addons/purchase_order_product_revision b/setup/purchase_order_product_revision/odoo/addons/purchase_order_product_revision new file mode 120000 index 00000000..3e8e7069 --- /dev/null +++ b/setup/purchase_order_product_revision/odoo/addons/purchase_order_product_revision @@ -0,0 +1 @@ +../../../../purchase_order_product_revision \ No newline at end of file diff --git a/setup/purchase_order_product_revision/setup.py b/setup/purchase_order_product_revision/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/purchase_order_product_revision/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_move_revision/odoo/addons/stock_move_revision b/setup/stock_move_revision/odoo/addons/stock_move_revision new file mode 120000 index 00000000..3011ed8b --- /dev/null +++ b/setup/stock_move_revision/odoo/addons/stock_move_revision @@ -0,0 +1 @@ +../../../../stock_move_revision \ No newline at end of file diff --git a/setup/stock_move_revision/setup.py b/setup/stock_move_revision/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/stock_move_revision/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_picking_product_revision/odoo/addons/stock_picking_product_revision b/setup/stock_picking_product_revision/odoo/addons/stock_picking_product_revision new file mode 120000 index 00000000..9921fd1b --- /dev/null +++ b/setup/stock_picking_product_revision/odoo/addons/stock_picking_product_revision @@ -0,0 +1 @@ +../../../../stock_picking_product_revision \ No newline at end of file diff --git a/setup/stock_picking_product_revision/setup.py b/setup/stock_picking_product_revision/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/stock_picking_product_revision/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_quant_tree_lot_revision/odoo/addons/stock_quant_tree_lot_revision b/setup/stock_quant_tree_lot_revision/odoo/addons/stock_quant_tree_lot_revision new file mode 120000 index 00000000..ba4797fd --- /dev/null +++ b/setup/stock_quant_tree_lot_revision/odoo/addons/stock_quant_tree_lot_revision @@ -0,0 +1 @@ +../../../../stock_quant_tree_lot_revision \ No newline at end of file diff --git a/setup/stock_quant_tree_lot_revision/setup.py b/setup/stock_quant_tree_lot_revision/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/stock_quant_tree_lot_revision/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_move_revision/README.rst b/stock_move_revision/README.rst new file mode 100644 index 00000000..41bd6023 --- /dev/null +++ b/stock_move_revision/README.rst @@ -0,0 +1,124 @@ +=================== +Stock Move Revision +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e9fb7c897e48f48be98da691af85c2a8ef168bfc563ee03d2485205c212e96c0 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Faxls--oca-lightgray.png?logo=github + :target: https://github.com/OCA/axls-oca/tree/wng-add-revision-functions/stock_move_revision + :alt: OCA/axls-oca +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/axls-oca-wng-add-revision-functions/axls-oca-wng-add-revision-functions-stock_move_revision + :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/axls-oca&target_branch=wng-add-revision-functions + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + + +This module adds product revision information to stock moves in Odoo 16. It integrates with the product_revision module to track which revision of a product was used in each stock move. + +Key features: + + +* Adds product revision and revision number fields to stock moves +* Displays revision information in stock move views +* Allows filtering and grouping stock moves by product revision + +This module enhances traceability by ensuring that the specific revision of a product used in inventory operations is recorded and tracked throughout the inventory process. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + + +To use this module, you need to: + +Viewing Product Revisions in Stock Moves +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Inventory > Operations > All Transfers +#. Open a transfer +#. View the stock moves +#. The product revision and revision number fields will be displayed + +Filtering and Grouping Stock Moves by Revision +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Inventory > Operations > Stock Moves +#. The product revision and revision number columns are available (you may need to enable them in the view) +#. You can filter the list by using the search box and selecting a specific revision +#. You can group the moves by revision using the "Group By" menu and selecting "Product Revision" + +Traceability +~~~~~~~~~~~~~~~~~ + +When a stock move is created or processed, the revision information is recorded. This ensures complete traceability of which product revision was used throughout the inventory process. + +This module enhances traceability by: + + +#. Recording which revision of a product was used in each stock move +#. Allowing you to track product revisions throughout the inventory process +#. Providing visibility of product revisions in inventory reports and analysis + +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 +~~~~~~~ + +* Axelspace + +Contributors +~~~~~~~~~~~~ + +* `Axelspace Corporation `__: + + * WangTKurata + +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/axls-oca `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_move_revision/__init__.py b/stock_move_revision/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/stock_move_revision/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_move_revision/__manifest__.py b/stock_move_revision/__manifest__.py new file mode 100644 index 00000000..d6db0674 --- /dev/null +++ b/stock_move_revision/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2025 Axelspace +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Stock Move Revision", + "summary": "Add revision number in stock moves", + "author": "Axelspace, Odoo Community Association (OCA)", + "website": "https://www.axelspace.com", + "license": "AGPL-3", + "category": "Inventory", + "version": "16.0.1.0.0", + "depends": ["stock", "product_revision"], + "data": [ + "views/stock_move_views.xml", + "security/ir.model.access.csv", + ], + "installable": True, + "application": False, +} diff --git a/stock_move_revision/i18n/ja_JP.po b/stock_move_revision/i18n/ja_JP.po new file mode 100644 index 00000000..807f7e26 --- /dev/null +++ b/stock_move_revision/i18n/ja_JP.po @@ -0,0 +1,42 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_move_revision +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0-20230701\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-02 07:10+0000\n" +"PO-Revision-Date: 2025-05-02 07:10+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: stock_move_revision +#: model:ir.model.fields,field_description:stock_move_revision.field_stock_move__product_revision_id +#: model_terms:ir.ui.view,arch_db:stock_move_revision.view_stock_move_search_inherit +msgid "Product Revision" +msgstr "製品リビジョン" + +#. module: stock_move_revision +#: model:ir.model.fields,field_description:stock_move_revision.field_stock_move__product_revision_number +msgid "Revision Number" +msgstr "リビジョン番号" + +#. module: stock_move_revision +#: model:ir.model.fields,help:stock_move_revision.field_stock_move__product_revision_number +msgid "Revision number of the product at the time of purchase" +msgstr "購入時の製品のリビジョン番号" + +#. module: stock_move_revision +#: model:ir.model.fields,help:stock_move_revision.field_stock_move__product_revision_id +msgid "Revision of the product at the time of purchase" +msgstr "購入時の製品のリビジョン" + +#. module: stock_move_revision +#: model:ir.model,name:stock_move_revision.model_stock_move +msgid "Stock Move" +msgstr "在庫移動" diff --git a/stock_move_revision/models/__init__.py b/stock_move_revision/models/__init__.py new file mode 100644 index 00000000..6bda2d24 --- /dev/null +++ b/stock_move_revision/models/__init__.py @@ -0,0 +1 @@ +from . import stock_move diff --git a/stock_move_revision/models/stock_move.py b/stock_move_revision/models/stock_move.py new file mode 100644 index 00000000..bd75a6c1 --- /dev/null +++ b/stock_move_revision/models/stock_move.py @@ -0,0 +1,18 @@ +from odoo import fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + product_revision_id = fields.Many2one( + "product.revision", + string="Product Revision", + help="Revision of the product at the time of purchase", + ) + product_revision_number = fields.Char( + string="Revision Number", + related="product_revision_id.revision_number", + store=True, + readonly=True, + help="Revision number of the product at the time of purchase", + ) diff --git a/stock_move_revision/readme/CONTRIBUTORS.rst b/stock_move_revision/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..bd765ad6 --- /dev/null +++ b/stock_move_revision/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Axelspace Corporation `__: + + * WangTKurata diff --git a/stock_move_revision/readme/DESCRIPTION.rst b/stock_move_revision/readme/DESCRIPTION.rst new file mode 100644 index 00000000..3cfb7ad1 --- /dev/null +++ b/stock_move_revision/readme/DESCRIPTION.rst @@ -0,0 +1,11 @@ + +This module adds product revision information to stock moves in Odoo 16. It integrates with the product_revision module to track which revision of a product was used in each stock move. + +Key features: + + +* Adds product revision and revision number fields to stock moves +* Displays revision information in stock move views +* Allows filtering and grouping stock moves by product revision + +This module enhances traceability by ensuring that the specific revision of a product used in inventory operations is recorded and tracked throughout the inventory process. diff --git a/stock_move_revision/readme/USAGE.rst b/stock_move_revision/readme/USAGE.rst new file mode 100644 index 00000000..86e71474 --- /dev/null +++ b/stock_move_revision/readme/USAGE.rst @@ -0,0 +1,32 @@ + +To use this module, you need to: + +Viewing Product Revisions in Stock Moves +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Inventory > Operations > All Transfers +#. Open a transfer +#. View the stock moves +#. The product revision and revision number fields will be displayed + +Filtering and Grouping Stock Moves by Revision +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Inventory > Operations > Stock Moves +#. The product revision and revision number columns are available (you may need to enable them in the view) +#. You can filter the list by using the search box and selecting a specific revision +#. You can group the moves by revision using the "Group By" menu and selecting "Product Revision" + +Traceability +~~~~~~~~~~~~~~~~~ + +When a stock move is created or processed, the revision information is recorded. This ensures complete traceability of which product revision was used throughout the inventory process. + +This module enhances traceability by: + + +#. Recording which revision of a product was used in each stock move +#. Allowing you to track product revisions throughout the inventory process +#. Providing visibility of product revisions in inventory reports and analysis diff --git a/stock_move_revision/security/ir.model.access.csv b/stock_move_revision/security/ir.model.access.csv new file mode 100644 index 00000000..97dd8b91 --- /dev/null +++ b/stock_move_revision/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/stock_move_revision/static/description/index.html b/stock_move_revision/static/description/index.html new file mode 100644 index 00000000..9893fc32 --- /dev/null +++ b/stock_move_revision/static/description/index.html @@ -0,0 +1,471 @@ + + + + + +Stock Move Revision + + + +
+

Stock Move Revision

+ + +

Beta License: AGPL-3 OCA/axls-oca Translate me on Weblate Try me on Runboat

+

This module adds product revision information to stock moves in Odoo 16. It integrates with the product_revision module to track which revision of a product was used in each stock move.

+

Key features:

+
    +
  • Adds product revision and revision number fields to stock moves
  • +
  • Displays revision information in stock move views
  • +
  • Allows filtering and grouping stock moves by product revision
  • +
+

This module enhances traceability by ensuring that the specific revision of a product used in inventory operations is recorded and tracked throughout the inventory process.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+
+

Viewing Product Revisions in Stock Moves

+
    +
  1. Go to Inventory > Operations > All Transfers
  2. +
  3. Open a transfer
  4. +
  5. View the stock moves
  6. +
  7. The product revision and revision number fields will be displayed
  8. +
+
+
+

Filtering and Grouping Stock Moves by Revision

+
    +
  1. Go to Inventory > Operations > Stock Moves
  2. +
  3. The product revision and revision number columns are available (you may need to enable them in the view)
  4. +
  5. You can filter the list by using the search box and selecting a specific revision
  6. +
  7. You can group the moves by revision using the “Group By” menu and selecting “Product Revision”
  8. +
+
+
+

Traceability

+

When a stock move is created or processed, the revision information is recorded. This ensures complete traceability of which product revision was used throughout the inventory process.

+

This module enhances traceability by:

+
    +
  1. Recording which revision of a product was used in each stock move
  2. +
  3. Allowing you to track product revisions throughout the inventory process
  4. +
  5. Providing visibility of product revisions in inventory reports and analysis
  6. +
+
+
+
+

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

+
    +
  • Axelspace
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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/axls-oca project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/stock_move_revision/tests/__init__.py b/stock_move_revision/tests/__init__.py new file mode 100644 index 00000000..65d462e6 --- /dev/null +++ b/stock_move_revision/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_move_revision diff --git a/stock_move_revision/tests/test_stock_move_revision.py b/stock_move_revision/tests/test_stock_move_revision.py new file mode 100644 index 00000000..9557a828 --- /dev/null +++ b/stock_move_revision/tests/test_stock_move_revision.py @@ -0,0 +1,150 @@ +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestStockMoveRevision(TransactionCase): + def setUp(self): + super(TestStockMoveRevision, self).setUp() + self.ProductTemplate = self.env["product.template"] + self.ProductRevision = self.env["product.revision"] + self.ProductProduct = self.env["product.product"] + self.StockMove = self.env["stock.move"] + self.StockPicking = self.env["stock.picking"] + self.StockLocation = self.env["stock.location"] + + # Create test user with necessary security groups + self.test_user = self.env["res.users"].create( + { + "name": "Test User", + "login": "test.user", + "email": "test.user@example.com", + } + ) + group_user = self.env.ref("base.group_user") + inventory_group = self.env.ref("stock.group_stock_user") + self.test_user.write( + {"groups_id": [(6, 0, [group_user.id, inventory_group.id])]} + ) + + # Test environment with test user + self.test_env = self.env(user=self.test_user.id) + + # Get stock locations + self.stock_location = self.StockLocation.search( + [("usage", "=", "internal")], limit=1 + ) + self.customer_location = self.StockLocation.search( + [("usage", "=", "customer")], limit=1 + ) + + # Create a product template for testing + self.template = self.ProductTemplate.create( + { + "name": "Test Product", + "type": "product", + "default_code": "TP-001", + } + ) + + # Get the product variant + self.product = self.template.product_variant_ids[0] + + # Create a revision for the product template + self.template_rev1 = self.ProductRevision.create( + { + "name": "Template Rev 1", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a stock picking + self.picking = self.StockPicking.create( + { + "picking_type_id": self.env.ref("stock.picking_type_out").id, + "location_id": self.stock_location.id, + "location_dest_id": self.customer_location.id, + } + ) + + # Create a stock move + self.move = self.StockMove.create( + { + "name": "Test Move", + "product_id": self.product.id, + "product_uom_qty": 5.0, + "product_uom": self.product.uom_id.id, + "picking_id": self.picking.id, + "location_id": self.stock_location.id, + "location_dest_id": self.customer_location.id, + "product_revision_id": self.template_rev1.id, + } + ) + + def test_stock_move_revision_fields(self): + """Test that stock move revision fields are correctly set""" + # Check that the stock move has the revision fields + self.assertEqual(self.move.product_revision_id, self.template_rev1) + self.assertEqual(self.move.product_revision_number, "1") + + # Create a new revision for the product template + template_rev2 = self.ProductRevision.create( + { + "name": "Template Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a new stock move with the new revision + new_move = self.StockMove.create( + { + "name": "Test Move 2", + "product_id": self.product.id, + "product_uom_qty": 5.0, + "product_uom": self.product.uom_id.id, + "picking_id": self.picking.id, + "location_id": self.stock_location.id, + "location_dest_id": self.customer_location.id, + "product_revision_id": template_rev2.id, + } + ) + + # Check that the new stock move has the new revision + self.assertEqual(new_move.product_revision_id, template_rev2) + self.assertEqual(new_move.product_revision_number, "2") + + def test_variant_specific_revision(self): + """Test stock move revision with variant-specific revisions""" + # Create a variant-specific revision + variant_rev = self.ProductRevision.create( + { + "name": "Variant Rev A", + "revision_number": "A", + "product_id": self.product.id, + "internal_product_id": "TP-001-V", + "active": True, + } + ) + + # Create a new stock move with the variant-specific revision + new_move = self.StockMove.create( + { + "name": "Test Move 3", + "product_id": self.product.id, + "product_uom_qty": 5.0, + "product_uom": self.product.uom_id.id, + "picking_id": self.picking.id, + "location_id": self.stock_location.id, + "location_dest_id": self.customer_location.id, + "product_revision_id": variant_rev.id, + } + ) + + # Check that the new stock move has the variant-specific revision + self.assertEqual(new_move.product_revision_id, variant_rev) + self.assertEqual(new_move.product_revision_number, "A") diff --git a/stock_move_revision/views/stock_move_views.xml b/stock_move_revision/views/stock_move_views.xml new file mode 100644 index 00000000..d7b596bc --- /dev/null +++ b/stock_move_revision/views/stock_move_views.xml @@ -0,0 +1,49 @@ + + + + + stock.move.form.inherit.product.revision + stock.move + + + + + + + + + + + + stock.move.tree.inherit.product.revision + stock.move + + + + + + + + + + + + stock.move.search.inherit.product.revision + stock.move + + + + + + + + + + + + diff --git a/stock_picking_product_revision/README.rst b/stock_picking_product_revision/README.rst new file mode 100644 index 00000000..94168f35 --- /dev/null +++ b/stock_picking_product_revision/README.rst @@ -0,0 +1,123 @@ +============================== +Stock Picking Product Revision +============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e544c4557416254149bc85e35c1e99ec15a016984c255287544e7323fe31a730 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Faxls--oca-lightgray.png?logo=github + :target: https://github.com/OCA/axls-oca/tree/wng-add-revision-functions/stock_picking_product_revision + :alt: OCA/axls-oca +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/axls-oca-wng-add-revision-functions/axls-oca-wng-add-revision-functions-stock_picking_product_revision + :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/axls-oca&target_branch=wng-add-revision-functions + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + + +This module adds product revision information to stock picking lines in Odoo 16. It integrates with the product_revision module to track which revision of a product was used in each stock picking line. + +Key features: + + +* Adds product revision and revision number fields to stock picking lines +* Displays revision information in stock picking form view (in the operations tab) +* Displays revision information in stock picking line tree views +* Works with purchase_order_product_revision to track revisions from purchase to receipt + +This module enhances traceability by ensuring that the specific revision of a product used in inventory operations is recorded and tracked throughout the inventory process. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + + +To use this module, you need to: + +Viewing Product Revisions in Stock Picking Form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Inventory > Operations > Transfers +#. Open a transfer +#. In the "Operations" tab, you will see the product revision and revision number fields for each move line +#. The revision information is displayed both in the initial demand section and in the detailed operations section + +Viewing Product Revisions in Stock Picking Lines +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Inventory > Operations > Transfers +#. Open a transfer +#. View the detailed operations +#. The product revision and revision number fields will be displayed in the operations list + +Integration with Purchase Orders +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a purchase order with product revisions is confirmed: + + +#. The revision information is automatically passed to the stock picking lines +#. This ensures traceability of which product revision was ordered and received +#. The revision information is visible in both the purchase order and the stock picking + +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 +~~~~~~~ + +* Axelspace + +Contributors +~~~~~~~~~~~~ + +* `Axelspace Corporation `__: + + * WangTKurata + +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/axls-oca `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_picking_product_revision/__init__.py b/stock_picking_product_revision/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/stock_picking_product_revision/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_picking_product_revision/__manifest__.py b/stock_picking_product_revision/__manifest__.py new file mode 100644 index 00000000..787031e4 --- /dev/null +++ b/stock_picking_product_revision/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2025 Axelspace +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Stock Picking Product Revision", + "summary": "Add revision number in stock picking lines", + "author": "Axelspace, Odoo Community Association (OCA)", + "website": "https://www.axelspace.com", + "license": "AGPL-3", + "category": "Inventory", + "version": "16.0.1.0.0", + "depends": ["stock", "product_revision", "stock_move_revision"], + "data": [ + "views/stock_move_line_views.xml", + "security/ir.model.access.csv", + ], + "installable": True, + "application": False, +} diff --git a/stock_picking_product_revision/i18n/ja_JP.po b/stock_picking_product_revision/i18n/ja_JP.po new file mode 100644 index 00000000..2e96a22a --- /dev/null +++ b/stock_picking_product_revision/i18n/ja_JP.po @@ -0,0 +1,41 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_picking_product_revision +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0-20230701\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-02 07:14+0000\n" +"PO-Revision-Date: 2025-05-02 07:14+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: stock_picking_product_revision +#: model:ir.model,name:stock_picking_product_revision.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "製品の移動(在庫移動ライン)" + +#. module: stock_picking_product_revision +#: model:ir.model.fields,field_description:stock_picking_product_revision.field_stock_move_line__product_revision_id +msgid "Product Revision" +msgstr "製品リビジョン" + +#. module: stock_picking_product_revision +#: model:ir.model.fields,field_description:stock_picking_product_revision.field_stock_move_line__product_revision_number +msgid "Revision Number" +msgstr "リビジョン番号" + +#. module: stock_picking_product_revision +#: model:ir.model.fields,help:stock_picking_product_revision.field_stock_move_line__product_revision_number +msgid "Revision number of the product at the time of receipt" +msgstr "受領時の製品リビジョン番号" + +#. module: stock_picking_product_revision +#: model:ir.model.fields,help:stock_picking_product_revision.field_stock_move_line__product_revision_id +msgid "受領時の製品リビジョン" +msgstr "" diff --git a/stock_picking_product_revision/models/__init__.py b/stock_picking_product_revision/models/__init__.py new file mode 100644 index 00000000..431f51c2 --- /dev/null +++ b/stock_picking_product_revision/models/__init__.py @@ -0,0 +1 @@ +from . import stock_move_line diff --git a/stock_picking_product_revision/models/stock_move_line.py b/stock_picking_product_revision/models/stock_move_line.py new file mode 100644 index 00000000..2de4e0fa --- /dev/null +++ b/stock_picking_product_revision/models/stock_move_line.py @@ -0,0 +1,46 @@ +from odoo import api, fields, models + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + product_revision_id = fields.Many2one( + "product.revision", + string="Product Revision", + related="move_id.product_revision_id", + store=True, + readonly=True, + help="Revision of the product at the time of receipt", + ) + product_revision_number = fields.Char( + string="Revision Number", + related="product_revision_id.revision_number", + store=True, + readonly=True, + help="Revision number of the product at the time of receipt", + ) + + @api.model_create_multi + def create(self, vals_list): + """Override create to copy revision to lot/serial number""" + res = super(StockMoveLine, self).create(vals_list) + # Copy revision to lot/serial number if applicable + for line in res: + self._copy_revision_to_lot(line) + return res + + def write(self, vals): + """Override write to copy revision to lot/serial number when lot is assigned""" + res = super(StockMoveLine, self).write(vals) + # If lot_id is being set or changed, copy the revision + if "lot_id" in vals: + for line in self: + self._copy_revision_to_lot(line) + return res + + def _copy_revision_to_lot(self, line): + """Copy the revision from the move line to the lot/serial number""" + if line.lot_id and line.product_revision_id: + # Only update the lot's revision if it doesn't already have one + if not line.lot_id.revision_id: + line.lot_id.revision_id = line.product_revision_id.id diff --git a/stock_picking_product_revision/readme/CONTRIBUTORS.rst b/stock_picking_product_revision/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..bd765ad6 --- /dev/null +++ b/stock_picking_product_revision/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Axelspace Corporation `__: + + * WangTKurata diff --git a/stock_picking_product_revision/readme/DESCRIPTION.rst b/stock_picking_product_revision/readme/DESCRIPTION.rst new file mode 100644 index 00000000..d652ed88 --- /dev/null +++ b/stock_picking_product_revision/readme/DESCRIPTION.rst @@ -0,0 +1,12 @@ + +This module adds product revision information to stock picking lines in Odoo 16. It integrates with the product_revision module to track which revision of a product was used in each stock picking line. + +Key features: + + +* Adds product revision and revision number fields to stock picking lines +* Displays revision information in stock picking form view (in the operations tab) +* Displays revision information in stock picking line tree views +* Works with purchase_order_product_revision to track revisions from purchase to receipt + +This module enhances traceability by ensuring that the specific revision of a product used in inventory operations is recorded and tracked throughout the inventory process. diff --git a/stock_picking_product_revision/readme/USAGE.rst b/stock_picking_product_revision/readme/USAGE.rst new file mode 100644 index 00000000..aea38d2d --- /dev/null +++ b/stock_picking_product_revision/readme/USAGE.rst @@ -0,0 +1,30 @@ + +To use this module, you need to: + +Viewing Product Revisions in Stock Picking Form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Inventory > Operations > Transfers +#. Open a transfer +#. In the "Operations" tab, you will see the product revision and revision number fields for each move line +#. The revision information is displayed both in the initial demand section and in the detailed operations section + +Viewing Product Revisions in Stock Picking Lines +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +#. Go to Inventory > Operations > Transfers +#. Open a transfer +#. View the detailed operations +#. The product revision and revision number fields will be displayed in the operations list + +Integration with Purchase Orders +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a purchase order with product revisions is confirmed: + + +#. The revision information is automatically passed to the stock picking lines +#. This ensures traceability of which product revision was ordered and received +#. The revision information is visible in both the purchase order and the stock picking diff --git a/stock_picking_product_revision/security/ir.model.access.csv b/stock_picking_product_revision/security/ir.model.access.csv new file mode 100644 index 00000000..97dd8b91 --- /dev/null +++ b/stock_picking_product_revision/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/stock_picking_product_revision/static/description/index.html b/stock_picking_product_revision/static/description/index.html new file mode 100644 index 00000000..655b08ad --- /dev/null +++ b/stock_picking_product_revision/static/description/index.html @@ -0,0 +1,471 @@ + + + + + +Stock Picking Product Revision + + + +
+

Stock Picking Product Revision

+ + +

Beta License: AGPL-3 OCA/axls-oca Translate me on Weblate Try me on Runboat

+

This module adds product revision information to stock picking lines in Odoo 16. It integrates with the product_revision module to track which revision of a product was used in each stock picking line.

+

Key features:

+
    +
  • Adds product revision and revision number fields to stock picking lines
  • +
  • Displays revision information in stock picking form view (in the operations tab)
  • +
  • Displays revision information in stock picking line tree views
  • +
  • Works with purchase_order_product_revision to track revisions from purchase to receipt
  • +
+

This module enhances traceability by ensuring that the specific revision of a product used in inventory operations is recorded and tracked throughout the inventory process.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+
+

Viewing Product Revisions in Stock Picking Form

+
    +
  1. Go to Inventory > Operations > Transfers
  2. +
  3. Open a transfer
  4. +
  5. In the “Operations” tab, you will see the product revision and revision number fields for each move line
  6. +
  7. The revision information is displayed both in the initial demand section and in the detailed operations section
  8. +
+
+
+

Viewing Product Revisions in Stock Picking Lines

+
    +
  1. Go to Inventory > Operations > Transfers
  2. +
  3. Open a transfer
  4. +
  5. View the detailed operations
  6. +
  7. The product revision and revision number fields will be displayed in the operations list
  8. +
+
+
+

Integration with Purchase Orders

+

When a purchase order with product revisions is confirmed:

+
    +
  1. The revision information is automatically passed to the stock picking lines
  2. +
  3. This ensures traceability of which product revision was ordered and received
  4. +
  5. The revision information is visible in both the purchase order and the stock picking
  6. +
+
+
+
+

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

+
    +
  • Axelspace
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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/axls-oca project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/stock_picking_product_revision/tests/__init__.py b/stock_picking_product_revision/tests/__init__.py new file mode 100644 index 00000000..62c3dbb5 --- /dev/null +++ b/stock_picking_product_revision/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_picking_product_revision diff --git a/stock_picking_product_revision/tests/test_stock_picking_product_revision.py b/stock_picking_product_revision/tests/test_stock_picking_product_revision.py new file mode 100644 index 00000000..5704b66e --- /dev/null +++ b/stock_picking_product_revision/tests/test_stock_picking_product_revision.py @@ -0,0 +1,243 @@ +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestStockPickingProductRevision(TransactionCase): + def setUp(self): + super(TestStockPickingProductRevision, self).setUp() + self.ProductTemplate = self.env["product.template"] + self.ProductRevision = self.env["product.revision"] + self.ProductProduct = self.env["product.product"] + self.StockMove = self.env["stock.move"] + self.StockMoveLine = self.env["stock.move.line"] + self.StockPicking = self.env["stock.picking"] + self.StockLocation = self.env["stock.location"] + self.StockLot = self.env["stock.lot"] + + # Create test user with necessary security groups + self.test_user = self.env["res.users"].create( + { + "name": "Test User", + "login": "test.user", + "email": "test.user@example.com", + } + ) + group_user = self.env.ref("base.group_user") + inventory_group = self.env.ref("stock.group_stock_user") + self.test_user.write( + {"groups_id": [(6, 0, [group_user.id, inventory_group.id])]} + ) + + # Test environment with test user + self.test_env = self.env(user=self.test_user.id) + + # Get stock locations + self.stock_location = self.StockLocation.search( + [("usage", "=", "internal")], limit=1 + ) + self.customer_location = self.StockLocation.search( + [("usage", "=", "customer")], limit=1 + ) + + # Create a product template for testing + self.template = self.ProductTemplate.create( + { + "name": "Test Product", + "type": "product", + "default_code": "TP-001", + "tracking": "lot", # Enable lot tracking + } + ) + + # Get the product variant + self.product = self.template.product_variant_ids[0] + + # Create a revision for the product template + self.template_rev1 = self.ProductRevision.create( + { + "name": "Template Rev 1", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a stock picking + self.picking = self.StockPicking.create( + { + "picking_type_id": self.env.ref("stock.picking_type_in").id, + "location_id": self.customer_location.id, + "location_dest_id": self.stock_location.id, + } + ) + + # Create a stock move + self.move = self.StockMove.create( + { + "name": "Test Move", + "product_id": self.product.id, + "product_uom_qty": 5.0, + "product_uom": self.product.uom_id.id, + "picking_id": self.picking.id, + "location_id": self.customer_location.id, + "location_dest_id": self.stock_location.id, + "product_revision_id": self.template_rev1.id, + } + ) + + # Create a lot for the product + self.lot = self.StockLot.create( + { + "name": "LOT-001", + "product_id": self.product.id, + "company_id": self.env.company.id, + } + ) + + def test_move_line_revision_fields(self): + """Test that stock move line revision fields are correctly set""" + # Create a move line + move_line = self.StockMoveLine.create( + { + "move_id": self.move.id, + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "qty_done": 5.0, + "location_id": self.customer_location.id, + "location_dest_id": self.stock_location.id, + } + ) + + # Check that the move line has the revision fields + self.assertEqual(move_line.product_revision_id, self.template_rev1) + self.assertEqual(move_line.product_revision_number, "1") + + # Create a new revision for the product template + template_rev2 = self.ProductRevision.create( + { + "name": "Template Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a new stock move with the new revision + new_move = self.StockMove.create( + { + "name": "Test Move 2", + "product_id": self.product.id, + "product_uom_qty": 5.0, + "product_uom": self.product.uom_id.id, + "picking_id": self.picking.id, + "location_id": self.customer_location.id, + "location_dest_id": self.stock_location.id, + "product_revision_id": template_rev2.id, + } + ) + + # Create a move line for the new move + new_move_line = self.StockMoveLine.create( + { + "move_id": new_move.id, + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "qty_done": 5.0, + "location_id": self.customer_location.id, + "location_dest_id": self.stock_location.id, + } + ) + + # Check that the new move line has the new revision + self.assertEqual(new_move_line.product_revision_id, template_rev2) + self.assertEqual(new_move_line.product_revision_number, "2") + + def test_copy_revision_to_lot_on_create(self): + """Test that revision is copied to lot when move line is created with lot""" + # Create a move line with lot + self.StockMoveLine.create( + { + "move_id": self.move.id, + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "qty_done": 5.0, + "location_id": self.customer_location.id, + "location_dest_id": self.stock_location.id, + "lot_id": self.lot.id, + } + ) + + # Check that the lot has the revision set + self.assertEqual(self.lot.revision_id, self.template_rev1) + + def test_copy_revision_to_lot_on_write(self): + """Test that revision is copied to lot when lot is assigned to move line""" + # Create a move line without lot + move_line = self.StockMoveLine.create( + { + "move_id": self.move.id, + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "qty_done": 5.0, + "location_id": self.customer_location.id, + "location_dest_id": self.stock_location.id, + } + ) + + # Create a new lot without revision + new_lot = self.StockLot.create( + { + "name": "LOT-002", + "product_id": self.product.id, + "company_id": self.env.company.id, + "revision_id": False, + } + ) + + # Assign the lot to the move line + move_line.lot_id = new_lot + + # Check that the lot has the revision set + self.assertEqual(new_lot.revision_id, self.template_rev1) + + def test_do_not_overwrite_lot_revision(self): + """Test that lot revision is not overwritten if it already has one""" + # Create a variant-specific revision + variant_rev = self.ProductRevision.create( + { + "name": "Variant Rev A", + "revision_number": "A", + "product_id": self.product.id, + "internal_product_id": "TP-001-V", + "active": True, + } + ) + + # Create a lot with the variant-specific revision + lot_with_rev = self.StockLot.create( + { + "name": "LOT-003", + "product_id": self.product.id, + "company_id": self.env.company.id, + "revision_id": variant_rev.id, + } + ) + + # Create a move line with the lot + self.StockMoveLine.create( + { + "move_id": self.move.id, + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "qty_done": 5.0, + "location_id": self.customer_location.id, + "location_dest_id": self.stock_location.id, + "lot_id": lot_with_rev.id, + } + ) + + # Check that the lot still has the variant-specific revision + self.assertEqual(lot_with_rev.revision_id, variant_rev) + self.assertNotEqual(lot_with_rev.revision_id, self.template_rev1) diff --git a/stock_picking_product_revision/views/stock_move_line_views.xml b/stock_picking_product_revision/views/stock_move_line_views.xml new file mode 100644 index 00000000..d1222fc3 --- /dev/null +++ b/stock_picking_product_revision/views/stock_move_line_views.xml @@ -0,0 +1,34 @@ + + + + + stock.picking.form.inherit.product.revision + stock.picking + + + + + + + + + + + + stock.move.line.tree.inherit.product.revision + stock.move.line + + + + + + + + + diff --git a/stock_quant_tree_lot_revision/README.rst b/stock_quant_tree_lot_revision/README.rst new file mode 100644 index 00000000..d4706573 --- /dev/null +++ b/stock_quant_tree_lot_revision/README.rst @@ -0,0 +1,86 @@ +============================= +Stock Quant Tree Lot Revision +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e2d762fd0d3154c0ad08ad16f6d3123fb9575dfc011fb9647db57c73ac423309 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Faxls--oca-lightgray.png?logo=github + :target: https://github.com/OCA/axls-oca/tree/wng-add-revision-functions/stock_quant_tree_lot_revision + :alt: OCA/axls-oca +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/axls-oca-wng-add-revision-functions/axls-oca-wng-add-revision-functions-stock_quant_tree_lot_revision + :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/axls-oca&target_branch=wng-add-revision-functions + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the stock quant tree view to display the revision number of lot/serial numbers when they have a revision. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to: + +#. Go to Inventory > Reporting > Inventory Report +#. The revision number will be displayed in a new column after the lot/serial number column when a lot/serial number has a revision + +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 +~~~~~~~ + +* Axelspace + +Contributors +~~~~~~~~~~~~ + +* `Axelspace Corporation `__: + + * WangTKurata + +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/axls-oca `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_quant_tree_lot_revision/__init__.py b/stock_quant_tree_lot_revision/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/stock_quant_tree_lot_revision/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_quant_tree_lot_revision/__manifest__.py b/stock_quant_tree_lot_revision/__manifest__.py new file mode 100644 index 00000000..ed10ccf8 --- /dev/null +++ b/stock_quant_tree_lot_revision/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2025 Axelspace +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Stock Quant Tree Lot Revision", + "summary": "Display revision number in stock quant tree view when lot/serial has revision", + "author": "Axelspace, Odoo Community Association (OCA)", + "website": "https://www.axelspace.com", + "license": "AGPL-3", + "category": "Inventory", + "version": "16.0.1.0.0", + "depends": ["stock", "product_lot_revision"], + "data": [ + "views/stock_quant_views.xml", + ], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/stock_quant_tree_lot_revision/i18n/ja_JP.po b/stock_quant_tree_lot_revision/i18n/ja_JP.po new file mode 100644 index 00000000..36c10c97 --- /dev/null +++ b/stock_quant_tree_lot_revision/i18n/ja_JP.po @@ -0,0 +1,36 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_quant_tree_lot_revision +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0-20230701\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-05-08 02:09+0000\n" +"PO-Revision-Date: 2025-05-08 02:09+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: stock_quant_tree_lot_revision +#: model:ir.model.fields,field_description:stock_quant_tree_lot_revision.field_stock_quant__lot_revision_number +msgid "Lot Revision Number" +msgstr "リビジョン番号" + +#. module: stock_quant_tree_lot_revision +#: model:ir.model,name:stock_quant_tree_lot_revision.model_stock_quant +msgid "Quants" +msgstr "保管ロット" + +#. module: stock_quant_tree_lot_revision +#: model_terms:ir.ui.view,arch_db:stock_quant_tree_lot_revision.view_stock_quant_tree_editable_lot_revision +msgid "Revision" +msgstr "リビジョン" + +#. module: stock_quant_tree_lot_revision +#: model:ir.model.fields,help:stock_quant_tree_lot_revision.field_stock_quant__lot_revision_number +msgid "The revision number of the lot/serial number" +msgstr "Lot/SN のリビジョン番号" diff --git a/stock_quant_tree_lot_revision/models/__init__.py b/stock_quant_tree_lot_revision/models/__init__.py new file mode 100644 index 00000000..70f8e6c8 --- /dev/null +++ b/stock_quant_tree_lot_revision/models/__init__.py @@ -0,0 +1 @@ +from . import stock_quant diff --git a/stock_quant_tree_lot_revision/models/stock_quant.py b/stock_quant_tree_lot_revision/models/stock_quant.py new file mode 100644 index 00000000..bfbbac0c --- /dev/null +++ b/stock_quant_tree_lot_revision/models/stock_quant.py @@ -0,0 +1,16 @@ +# Copyright 2025 Axelspace +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class StockQuant(models.Model): + _inherit = "stock.quant" + + lot_revision_number = fields.Char( + related="lot_id.revision_number", + string="Lot Revision Number", + readonly=True, + store=False, + help="The revision number of the lot/serial number", + ) diff --git a/stock_quant_tree_lot_revision/readme/CONTRIBUTORS.rst b/stock_quant_tree_lot_revision/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..bd765ad6 --- /dev/null +++ b/stock_quant_tree_lot_revision/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Axelspace Corporation `__: + + * WangTKurata diff --git a/stock_quant_tree_lot_revision/readme/DESCRIPTION.rst b/stock_quant_tree_lot_revision/readme/DESCRIPTION.rst new file mode 100644 index 00000000..68251262 --- /dev/null +++ b/stock_quant_tree_lot_revision/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module extends the stock quant tree view to display the revision number of lot/serial numbers when they have a revision. diff --git a/stock_quant_tree_lot_revision/readme/USAGE.rst b/stock_quant_tree_lot_revision/readme/USAGE.rst new file mode 100644 index 00000000..4217821e --- /dev/null +++ b/stock_quant_tree_lot_revision/readme/USAGE.rst @@ -0,0 +1,4 @@ +To use this module, you need to: + +#. Go to Inventory > Reporting > Inventory Report +#. The revision number will be displayed in a new column after the lot/serial number column when a lot/serial number has a revision diff --git a/stock_quant_tree_lot_revision/static/description/index.html b/stock_quant_tree_lot_revision/static/description/index.html new file mode 100644 index 00000000..b12d4d01 --- /dev/null +++ b/stock_quant_tree_lot_revision/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +Stock Quant Tree Lot Revision + + + +
+

Stock Quant Tree Lot Revision

+ + +

Beta License: AGPL-3 OCA/axls-oca Translate me on Weblate Try me on Runboat

+

This module extends the stock quant tree view to display the revision number of lot/serial numbers when they have a revision.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to Inventory > Reporting > Inventory Report
  2. +
  3. The revision number will be displayed in a new column after the lot/serial number column when a lot/serial number has a revision
  4. +
+
+
+

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

+
    +
  • Axelspace
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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/axls-oca project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/stock_quant_tree_lot_revision/tests/__init__.py b/stock_quant_tree_lot_revision/tests/__init__.py new file mode 100644 index 00000000..c190f65a --- /dev/null +++ b/stock_quant_tree_lot_revision/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_quant_tree_lot_revision diff --git a/stock_quant_tree_lot_revision/tests/test_stock_quant_tree_lot_revision.py b/stock_quant_tree_lot_revision/tests/test_stock_quant_tree_lot_revision.py new file mode 100644 index 00000000..37550807 --- /dev/null +++ b/stock_quant_tree_lot_revision/tests/test_stock_quant_tree_lot_revision.py @@ -0,0 +1,154 @@ +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestStockQuantTreeLotRevision(TransactionCase): + def setUp(self): + super(TestStockQuantTreeLotRevision, self).setUp() + self.ProductTemplate = self.env["product.template"] + self.ProductRevision = self.env["product.revision"] + self.ProductProduct = self.env["product.product"] + self.StockQuant = self.env["stock.quant"] + self.StockLocation = self.env["stock.location"] + self.StockLot = self.env["stock.lot"] + + # Create test user with necessary security groups + self.test_user = self.env["res.users"].create( + { + "name": "Test User", + "login": "test.user", + "email": "test.user@example.com", + } + ) + group_user = self.env.ref("base.group_user") + inventory_group = self.env.ref("stock.group_stock_user") + self.test_user.write( + {"groups_id": [(6, 0, [group_user.id, inventory_group.id])]} + ) + + # Test environment with test user + self.test_env = self.env(user=self.test_user.id) + + # Get stock locations + self.stock_location = self.StockLocation.search( + [("usage", "=", "internal")], limit=1 + ) + + # Create a product template for testing + self.template = self.ProductTemplate.create( + { + "name": "Test Product", + "type": "product", + "default_code": "TP-001", + "tracking": "lot", # Enable lot tracking + } + ) + + # Get the product variant + self.product = self.template.product_variant_ids[0] + + # Create a revision for the product template + self.template_rev1 = self.ProductRevision.create( + { + "name": "Template Rev 1", + "revision_number": "1", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a lot for the product with the revision + self.lot = self.StockLot.create( + { + "name": "LOT-001", + "product_id": self.product.id, + "company_id": self.env.company.id, + "revision_id": self.template_rev1.id, + } + ) + + # Create a quant for the product with the lot + self.quant = self.StockQuant.create( + { + "product_id": self.product.id, + "location_id": self.stock_location.id, + "quantity": 10.0, + "lot_id": self.lot.id, + } + ) + + def test_lot_revision_number_field(self): + """Test that the lot_revision_number field is correctly set""" + # Check that the quant has the lot_revision_number field + self.assertEqual(self.quant.lot_revision_number, "1") + + # Create a new revision for the product template + template_rev2 = self.ProductRevision.create( + { + "name": "Template Rev 2", + "revision_number": "2", + "product_tmpl_id": self.template.id, + "internal_product_id": "TP-001", + "active": True, + } + ) + + # Create a new lot with the new revision + new_lot = self.StockLot.create( + { + "name": "LOT-002", + "product_id": self.product.id, + "company_id": self.env.company.id, + "revision_id": template_rev2.id, + } + ) + + # Create a new quant with the new lot + new_quant = self.StockQuant.create( + { + "product_id": self.product.id, + "location_id": self.stock_location.id, + "quantity": 10.0, + "lot_id": new_lot.id, + } + ) + + # Check that the new quant has the new lot_revision_number + self.assertEqual(new_quant.lot_revision_number, "2") + + def test_variant_specific_revision(self): + """Test quant lot_revision_number with variant-specific revisions""" + # Create a variant-specific revision + variant_rev = self.ProductRevision.create( + { + "name": "Variant Rev A", + "revision_number": "A", + "product_id": self.product.id, + "internal_product_id": "TP-001-V", + "active": True, + } + ) + + # Create a lot with the variant-specific revision + variant_lot = self.StockLot.create( + { + "name": "LOT-003", + "product_id": self.product.id, + "company_id": self.env.company.id, + "revision_id": variant_rev.id, + } + ) + + # Create a quant with the variant-specific lot + variant_quant = self.StockQuant.create( + { + "product_id": self.product.id, + "location_id": self.stock_location.id, + "quantity": 10.0, + "lot_id": variant_lot.id, + } + ) + + # Check that the quant has the variant-specific lot_revision_number + self.assertEqual(variant_quant.lot_revision_number, "A") diff --git a/stock_quant_tree_lot_revision/views/stock_quant_views.xml b/stock_quant_tree_lot_revision/views/stock_quant_views.xml new file mode 100644 index 00000000..7e3e5b40 --- /dev/null +++ b/stock_quant_tree_lot_revision/views/stock_quant_views.xml @@ -0,0 +1,23 @@ + + + + + + stock.quant.tree.editable.lot.revision + stock.quant + + + + + + + + +