diff --git a/product_engineering_info/README.rst b/product_engineering_info/README.rst new file mode 100644 index 0000000..5424215 --- /dev/null +++ b/product_engineering_info/README.rst @@ -0,0 +1,86 @@ +======================== +Product Engineering Info +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2c100843c5c2e6ceeec2513fa0c459c4f393a598c68537d7c360ee254741052c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-qrtl%2Fthc--custom-lightgray.png?logo=github + :target: https://github.com/qrtl/thc-custom/tree/16.0/product_engineering_info + :alt: qrtl/thc-custom + +|badge1| |badge2| |badge3| + +This module extends products with engineering-oriented information. + +It adds an **Engineering** tab on the product form where engineers can +keep design data such as a product group code, compatible parts, E-BOM +references and technical notes. A few closely related fields are also +surfaced on the standard tabs (for example the successor part number on +*General Information* and manufacturer details on *Purchase*). + +The Engineering tab is also a convenient place for administrators to add +further engineering fields through Studio. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Product group codes are a simple design classification (e.g. sheet +metal, circuit board), managed under *Inventory > Configuration > +Products*. + +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 +------- + +* Quartile + +Contributors +------------ + +- `Quartile `__: + + - Toshikimi Shigenobu + +Maintainers +----------- + +.. |maintainer-nobuQuartile| image:: https://github.com/nobuQuartile.png?size=40px + :target: https://github.com/nobuQuartile + :alt: nobuQuartile + +Current maintainer: + +|maintainer-nobuQuartile| + +This module is part of the `qrtl/thc-custom `_ project on GitHub. + +You are welcome to contribute. diff --git a/product_engineering_info/__init__.py b/product_engineering_info/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/product_engineering_info/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_engineering_info/__manifest__.py b/product_engineering_info/__manifest__.py new file mode 100644 index 0000000..3d11b01 --- /dev/null +++ b/product_engineering_info/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Product Engineering Info", + "summary": "Add an Engineering tab with engineering fields and a " + "Product Group Code classification to products.", + "version": "16.0.1.0.0", + "category": "Inventory", + "license": "AGPL-3", + "author": "Quartile", + "website": "https://www.quartile.co", + "maintainers": ["nobuQuartile"], + "depends": ["purchase", "stock"], + "data": [ + "security/ir.model.access.csv", + "views/product_group_code_views.xml", + "views/product_template_views.xml", + ], + "installable": True, +} diff --git a/product_engineering_info/i18n/ja.po b/product_engineering_info/i18n/ja.po new file mode 100644 index 0000000..391e618 --- /dev/null +++ b/product_engineering_info/i18n/ja.po @@ -0,0 +1,314 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_engineering_info +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-24 01:35+0000\n" +"PO-Revision-Date: 2026-07-24 01:35+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_engineering_info +#. odoo-python +#: code:addons/product_engineering_info/models/product_template.py:0 +#, python-format +msgid "A product cannot be in its own compatible products (%s)." +msgstr "プロダクトを自身の互換プロダクトに含めることはできません(%s)。" + +#. module: product_engineering_info +#. odoo-python +#: code:addons/product_engineering_info/models/product_template.py:0 +#, python-format +msgid "A product cannot be its own successor (%s)." +msgstr "プロダクトを自身の後継に設定することはできません(%s)。" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_group_code__active +msgid "Active" +msgstr "有効" + +#. module: product_engineering_info +#: model:ir.model.fields,help:product_engineering_info.field_product_product__manufacturer_id +#: model:ir.model.fields,help:product_engineering_info.field_product_template__manufacturer_id +msgid "Actual manufacturer of the part (may differ from the vendor)." +msgstr "部品の実際の製造者(仕入先と異なる場合があります)。" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__allow_purchase +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__allow_purchase +msgid "Allow Purchase" +msgstr "購買許可" + +#. module: product_engineering_info +#: model_terms:ir.ui.view,arch_db:product_engineering_info.product_group_code_view_form +#: model_terms:ir.ui.view,arch_db:product_engineering_info.product_group_code_view_search +msgid "Archived" +msgstr "アーカイブ済" + +#. module: product_engineering_info +#: model:ir.model.fields.selection,name:product_engineering_info.selection__product_template__bom_status__approved +msgid "Approved" +msgstr "承認済み・未発効" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__bom_approval_date +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__bom_approval_date +msgid "BOM Approval Date" +msgstr "BOM承認日" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__bom_approver_id +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__bom_approver_id +msgid "BOM Approver" +msgstr "BOM承認者" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__bom_author_id +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__bom_author_id +msgid "BOM Author" +msgstr "BOM作成者" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__bom_link_released +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__bom_link_released +msgid "BOM Link (Released)" +msgstr "BOMリンク(リリース済み)" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__bom_link +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__bom_link +msgid "BOM Link (Working)" +msgstr "BOMリンク(作業中)" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__bom_revision +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__bom_revision +msgid "BOM Revision" +msgstr "BOMリビジョン" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__bom_status +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__bom_status +msgid "BOM Status" +msgstr "BOMステータス" + +#. module: product_engineering_info +#: model:ir.model.fields.selection,name:product_engineering_info.selection__product_template__bom_status__canceled +msgid "Canceled" +msgstr "取り下げ・キャンセル" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__compatible_product_ids +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__compatible_product_ids +msgid "Compatible P/N" +msgstr "互換 P/N" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_group_code__create_uid +msgid "Created by" +msgstr "作成者" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_group_code__create_date +msgid "Created on" +msgstr "作成日" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_group_code__display_name +msgid "Display Name" +msgstr "表示名" + +#. module: product_engineering_info +#: model:ir.model.fields.selection,name:product_engineering_info.selection__product_template__bom_status__draft +msgid "Draft" +msgstr "作成・改訂中" + +#. module: product_engineering_info +#: model_terms:ir.ui.view,arch_db:product_engineering_info.product_template_view_form +msgid "E-BOM" +msgstr "E-BOM" + +#. module: product_engineering_info +#: model_terms:ir.ui.view,arch_db:product_engineering_info.product_template_view_form +msgid "Engineering" +msgstr "エンジニアリング" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__engineering_folder_link +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__engineering_folder_link +msgid "Engineering Folder Link" +msgstr "エンジニアリングフォルダリンク" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_group_code__id +msgid "ID" +msgstr "ID" + +#. module: product_engineering_info +#: model:ir.model.fields.selection,name:product_engineering_info.selection__product_template__bom_status__in_review +msgid "In Review" +msgstr "レビュー中" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_group_code____last_update +msgid "Last Modified on" +msgstr "最終更新日" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_group_code__write_uid +msgid "Last Updated by" +msgstr "最終更新者" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_group_code__write_date +msgid "Last Updated on" +msgstr "最終更新日" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__legacy_description_cad +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__legacy_description_cad +msgid "Legacy Description (CAD)" +msgstr "旧説明(CAD)" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__legacy_number_cad +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__legacy_number_cad +msgid "Legacy Number (CAD)" +msgstr "旧番号(CAD)" + +#. module: product_engineering_info +#: model:ir.model.fields,help:product_engineering_info.field_product_product__bom_link_released +#: model:ir.model.fields,help:product_engineering_info.field_product_template__bom_link_released +msgid "Link to the BOM of the latest released revision." +msgstr "リリース済みの最新リビジョンのBOMへのリンク。" + +#. module: product_engineering_info +#: model:ir.model.fields,help:product_engineering_info.field_product_product__engineering_folder_link +#: model:ir.model.fields,help:product_engineering_info.field_product_template__engineering_folder_link +msgid "Link to the folder holding the engineering information." +msgstr "エンジニアリング情報が格納されているフォルダへのリンク。" + +#. module: product_engineering_info +#: model:ir.model.fields,help:product_engineering_info.field_product_product__bom_link +#: model:ir.model.fields,help:product_engineering_info.field_product_template__bom_link +msgid "Link to the working (editable) E-BOM." +msgstr "編集可能な作業中E-BOMへのリンク。" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__manufacturer_id +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__manufacturer_id +#: model_terms:ir.ui.view,arch_db:product_engineering_info.product_template_view_form +msgid "Manufacturer" +msgstr "製造者" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__manufacturer_pcode +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__manufacturer_pcode +msgid "Manufacturer Product Code" +msgstr "製造者製品コード" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__manufacturer_pname +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__manufacturer_pname +msgid "Manufacturer Product Name" +msgstr "製造者製品名" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_group_code__name +msgid "Name" +msgstr "名称" + +#. module: product_engineering_info +#: model:ir.model.fields.selection,name:product_engineering_info.selection__product_template__bom_status__obsolete +msgid "Obsolete" +msgstr "後継なしで廃止" + +#. module: product_engineering_info +#: model:ir.model.fields,help:product_engineering_info.field_product_product__successor_product_id +#: model:ir.model.fields,help:product_engineering_info.field_product_template__successor_product_id +msgid "Part that supersedes this one." +msgstr "この部品を置き換える後継部品。" + +#. module: product_engineering_info +#: model:ir.model.fields,help:product_engineering_info.field_product_product__compatible_product_ids +#: model:ir.model.fields,help:product_engineering_info.field_product_template__compatible_product_ids +msgid "Parts that may be used as substitutes when out of stock." +msgstr "在庫切れ時に代替として使用できる部品。" + +#. module: product_engineering_info +#: model:ir.model.fields.selection,name:product_engineering_info.selection__product_template__bom_status__pending_approval +msgid "Pending Approval" +msgstr "承認待ち" + +#. module: product_engineering_info +#: model:ir.model.fields,help:product_engineering_info.field_product_product__registered_by_id +#: model:ir.model.fields,help:product_engineering_info.field_product_template__registered_by_id +msgid "Person who first registered the part number." +msgstr "部品番号を最初に登録した担当者。" + +#. module: product_engineering_info +#: model:ir.model,name:product_engineering_info.model_product_template +msgid "Product" +msgstr "プロダクト" + +#. module: product_engineering_info +#: model:ir.model,name:product_engineering_info.model_product_group_code +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__group_code_id +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__group_code_id +msgid "Product Group Code" +msgstr "プロダクトグループコード" + +#. module: product_engineering_info +#: model:ir.actions.act_window,name:product_engineering_info.product_group_code_action +#: model:ir.ui.menu,name:product_engineering_info.product_group_code_menu +msgid "Product Group Codes" +msgstr "プロダクトグループコード" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__registered_by_id +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__registered_by_id +msgid "Registered By" +msgstr "登録者" + +#. module: product_engineering_info +#: model:ir.model.fields.selection,name:product_engineering_info.selection__product_template__bom_status__released +msgid "Released / Effective" +msgstr "発行済み・有効" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__successor_product_id +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__successor_product_id +msgid "Successor P/N" +msgstr "後継 P/N" + +#. module: product_engineering_info +#: model:ir.model.fields.selection,name:product_engineering_info.selection__product_template__bom_status__superseded +msgid "Superseded" +msgstr "新リビジョンに置き換えられた旧版" + +#. module: product_engineering_info +#: model:ir.model.fields,field_description:product_engineering_info.field_product_product__technical_notes +#: model:ir.model.fields,field_description:product_engineering_info.field_product_template__technical_notes +#: model_terms:ir.ui.view,arch_db:product_engineering_info.product_template_view_form +msgid "Technical Notes" +msgstr "技術メモ" + +#. module: product_engineering_info +#: model:ir.model.constraint,message:product_engineering_info.constraint_product_group_code_name_uniq +msgid "The Product Group Code must be unique." +msgstr "プロダクトグループコードは一意である必要があります。" + +#. module: product_engineering_info +#: model:ir.model.fields,help:product_engineering_info.field_product_product__allow_purchase +#: model:ir.model.fields,help:product_engineering_info.field_product_template__allow_purchase +msgid "" +"Whether the part may still be purchased. Unlike archiving, the product stays" +" active in Odoo (e.g. because stock still exists)." +msgstr "この部品を引き続き購買してよいか。アーカイブとは異なり、プロダクトはOdoo上で有効なまま(在庫が残っている場合など)。" diff --git a/product_engineering_info/models/__init__.py b/product_engineering_info/models/__init__.py new file mode 100644 index 0000000..53f2805 --- /dev/null +++ b/product_engineering_info/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_group_code +from . import product_template diff --git a/product_engineering_info/models/product_group_code.py b/product_engineering_info/models/product_group_code.py new file mode 100644 index 0000000..432a069 --- /dev/null +++ b/product_engineering_info/models/product_group_code.py @@ -0,0 +1,17 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductGroupCode(models.Model): + _name = "product.group.code" + _description = "Product Group Code" + _order = "name" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + + _sql_constraints = [ + ("name_uniq", "unique(name)", "The Product Group Code must be unique."), + ] diff --git a/product_engineering_info/models/product_template.py b/product_engineering_info/models/product_template.py new file mode 100644 index 0000000..45cb05e --- /dev/null +++ b/product_engineering_info/models/product_template.py @@ -0,0 +1,138 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command, _, api, fields, models +from odoo.exceptions import ValidationError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + allow_purchase = fields.Boolean( + default=True, + help="Whether the part may still be purchased. Unlike archiving, the " + "product stays active in Odoo (e.g. because stock still exists).", + ) + successor_product_id = fields.Many2one( + "product.template", + string="Successor P/N", + help="Part that supersedes this one.", + ) + manufacturer_id = fields.Many2one( + "res.partner", + help="Actual manufacturer of the part (may differ from the vendor).", + ) + manufacturer_pname = fields.Char(string="Manufacturer Product Name") + manufacturer_pcode = fields.Char(string="Manufacturer Product Code") + registered_by_id = fields.Many2one( + "res.partner", + string="Registered By", + help="Person who first registered the part number.", + ) + group_code_id = fields.Many2one( + "product.group.code", + string="Product Group Code", + ) + engineering_folder_link = fields.Char( + help="Link to the folder holding the engineering information.", + ) + bom_link = fields.Char( + string="BOM Link (Working)", + help="Link to the working (editable) E-BOM.", + ) + bom_revision = fields.Char(string="BOM Revision") + bom_author_id = fields.Many2one( + "res.partner", + string="BOM Author", + ) + bom_status = fields.Selection( + [ + ("draft", "Draft"), + ("in_review", "In Review"), + ("pending_approval", "Pending Approval"), + ("approved", "Approved"), + ("released", "Released / Effective"), + ("superseded", "Superseded"), + ("obsolete", "Obsolete"), + ("canceled", "Canceled"), + ], + string="BOM Status", + ) + bom_approver_id = fields.Many2one( + "res.partner", + string="BOM Approver", + ) + bom_approval_date = fields.Date(string="BOM Approval Date") + bom_link_released = fields.Char( + string="BOM Link (Released)", + help="Link to the BOM of the latest released revision.", + ) + technical_notes = fields.Text() + compatible_product_ids = fields.Many2many( + "product.template", + relation="product_template_compatible_rel", + column1="product_tmpl_id", + column2="compatible_product_tmpl_id", + string="Compatible P/N", + help="Parts that may be used as substitutes when out of stock.", + ) + legacy_number_cad = fields.Char(string="Legacy Number (CAD)") + legacy_description_cad = fields.Char(string="Legacy Description (CAD)") + + @api.constrains("successor_product_id") + def _check_successor_product_id(self): + for product in self: + if product.successor_product_id == product: + raise ValidationError( + _( + "A product cannot be its own successor (%s).", + product.display_name, + ) + ) + + @api.constrains("compatible_product_ids") + def _check_compatible_product_ids(self): + for product in self: + if product in product.compatible_product_ids: + raise ValidationError( + _( + "A product cannot be in its own compatible products (%s).", + product.display_name, + ) + ) + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + # Keep the compatibility relation symmetric: if the new product lists a + # compatible part, link it back on that part too. + for record in records: + targets = record.compatible_product_ids + if targets: + targets.with_context(skip_compatible_sync=True).write( + {"compatible_product_ids": [Command.link(record.id)]} + ) + return records + + def write(self, vals): + if self.env.context.get("skip_compatible_sync") or ( + "compatible_product_ids" not in vals + ): + return super().write(vals) + # Snapshot the relation before the write so we can mirror the exact + # additions/removals onto the other side, whatever command form is used. + old = {rec.id: set(rec.compatible_product_ids.ids) for rec in self} + res = super().write(vals) + for rec in self: + new = set(rec.compatible_product_ids.ids) + added = new - old[rec.id] - {rec.id} + removed = old[rec.id] - new - {rec.id} + if added: + self.browse(added).with_context(skip_compatible_sync=True).write( + {"compatible_product_ids": [Command.link(rec.id)]} + ) + if removed: + self.browse(removed).with_context(skip_compatible_sync=True).write( + {"compatible_product_ids": [Command.unlink(rec.id)]} + ) + return res diff --git a/product_engineering_info/readme/CONFIGURE.md b/product_engineering_info/readme/CONFIGURE.md new file mode 100644 index 0000000..a8577dd --- /dev/null +++ b/product_engineering_info/readme/CONFIGURE.md @@ -0,0 +1,3 @@ +Product group codes are a simple design classification (e.g. sheet +metal, circuit board), managed under *Inventory > Configuration > +Products*. diff --git a/product_engineering_info/readme/CONTRIBUTORS.md b/product_engineering_info/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..04192ea --- /dev/null +++ b/product_engineering_info/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Quartile](https://www.quartile.co): + - Toshikimi Shigenobu diff --git a/product_engineering_info/readme/DESCRIPTION.md b/product_engineering_info/readme/DESCRIPTION.md new file mode 100644 index 0000000..dcf25e3 --- /dev/null +++ b/product_engineering_info/readme/DESCRIPTION.md @@ -0,0 +1,10 @@ +This module extends products with engineering-oriented information. + +It adds an **Engineering** tab on the product form where engineers can +keep design data such as a product group code, compatible parts, E-BOM +references and technical notes. A few closely related fields are also +surfaced on the standard tabs (for example the successor part number on +*General Information* and manufacturer details on *Purchase*). + +The Engineering tab is also a convenient place for administrators to add +further engineering fields through Studio. diff --git a/product_engineering_info/security/ir.model.access.csv b/product_engineering_info/security/ir.model.access.csv new file mode 100644 index 0000000..a34b2d8 --- /dev/null +++ b/product_engineering_info/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_group_code_user,product.group.code user,model_product_group_code,base.group_user,1,0,0,0 +access_product_group_code_manager,product.group.code manager,model_product_group_code,stock.group_stock_manager,1,1,1,1 diff --git a/product_engineering_info/static/description/index.html b/product_engineering_info/static/description/index.html new file mode 100644 index 0000000..bbe1305 --- /dev/null +++ b/product_engineering_info/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +Product Engineering Info + + + +
+

Product Engineering Info

+ + +

Beta License: AGPL-3 qrtl/thc-custom

+

This module extends products with engineering-oriented information.

+

It adds an Engineering tab on the product form where engineers can +keep design data such as a product group code, compatible parts, E-BOM +references and technical notes. A few closely related fields are also +surfaced on the standard tabs (for example the successor part number on +General Information and manufacturer details on Purchase).

+

The Engineering tab is also a convenient place for administrators to add +further engineering fields through Studio.

+

Table of contents

+ +
+

Configuration

+

Product group codes are a simple design classification (e.g. sheet +metal, circuit board), managed under Inventory > Configuration > +Products.

+
+
+

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

+
    +
  • Quartile
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

Current maintainer:

+

nobuQuartile

+

This module is part of the qrtl/thc-custom project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/product_engineering_info/tests/__init__.py b/product_engineering_info/tests/__init__.py new file mode 100644 index 0000000..15f445b --- /dev/null +++ b/product_engineering_info/tests/__init__.py @@ -0,0 +1 @@ +from . import test_product_engineering_info diff --git a/product_engineering_info/tests/test_product_engineering_info.py b/product_engineering_info/tests/test_product_engineering_info.py new file mode 100644 index 0000000..04b4fd8 --- /dev/null +++ b/product_engineering_info/tests/test_product_engineering_info.py @@ -0,0 +1,53 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestProductEngineeringInfo(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + Product = cls.env["product.template"] + cls.product_a = Product.create({"name": "Part A"}) + cls.product_b = Product.create({"name": "Part B"}) + cls.product_c = Product.create({"name": "Part C"}) + + def test_compatible_sync_on_create(self): + product = self.env["product.template"].create( + { + "name": "Part D", + "compatible_product_ids": [Command.link(self.product_a.id)], + } + ) + self.assertIn(product, self.product_a.compatible_product_ids) + self.assertIn(self.product_a, product.compatible_product_ids) + + def test_compatible_sync_add_on_write_and_unlink(self): + self.product_a.compatible_product_ids = [Command.link(self.product_b.id)] + self.assertIn(self.product_b, self.product_a.compatible_product_ids) + self.assertIn(self.product_a, self.product_b.compatible_product_ids) + self.product_a.compatible_product_ids = [Command.unlink(self.product_b.id)] + self.assertNotIn(self.product_b, self.product_a.compatible_product_ids) + self.assertNotIn(self.product_a, self.product_b.compatible_product_ids) + + def test_compatible_sync_set_replaces_both_sides(self): + self.product_a.compatible_product_ids = [Command.link(self.product_b.id)] + self.product_a.compatible_product_ids = [Command.set([self.product_c.id])] + self.assertNotIn(self.product_a, self.product_b.compatible_product_ids) + self.assertIn(self.product_a, self.product_c.compatible_product_ids) + + def test_compatible_sync_on_unlink(self): + self.product_a.compatible_product_ids = [Command.link(self.product_b.id)] + self.product_a.unlink() + self.assertFalse(self.product_b.compatible_product_ids) + + def test_compatible_self_reference_blocked(self): + with self.assertRaises(ValidationError): + self.product_a.compatible_product_ids = [Command.link(self.product_a.id)] + + def test_successor_self_reference_blocked(self): + with self.assertRaises(ValidationError): + self.product_a.successor_product_id = self.product_a diff --git a/product_engineering_info/views/product_group_code_views.xml b/product_engineering_info/views/product_group_code_views.xml new file mode 100644 index 0000000..37c3f3d --- /dev/null +++ b/product_engineering_info/views/product_group_code_views.xml @@ -0,0 +1,38 @@ + + + + product.group.code.tree + product.group.code + + + + + + + + + product.group.code.search + product.group.code + + + + + + + + + Product Group Codes + product.group.code + tree + + + diff --git a/product_engineering_info/views/product_template_views.xml b/product_engineering_info/views/product_template_views.xml new file mode 100644 index 0000000..894e4f0 --- /dev/null +++ b/product_engineering_info/views/product_template_views.xml @@ -0,0 +1,62 @@ + + + + product.template.form.engineering + product.template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup/product_engineering_info/odoo/addons/product_engineering_info b/setup/product_engineering_info/odoo/addons/product_engineering_info new file mode 120000 index 0000000..a757a19 --- /dev/null +++ b/setup/product_engineering_info/odoo/addons/product_engineering_info @@ -0,0 +1 @@ +../../../../product_engineering_info \ No newline at end of file diff --git a/setup/product_engineering_info/setup.py b/setup/product_engineering_info/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/product_engineering_info/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)