From 78f3c3fd9f4e07e2e5f7505f534c2760f4ac0b39 Mon Sep 17 00:00:00 2001 From: kanda999 Date: Wed, 3 Jun 2026 03:16:07 +0000 Subject: [PATCH] [IMP] product_chemical --- product_chemical/__manifest__.py | 13 +- product_chemical/i18n/ja.po | 284 +++++++++++++++++- .../migrations/18.0.1.1.0/pre-migration.py | 45 +++ product_chemical/models/__init__.py | 8 + .../models/product_chemical_law.py | 50 +++ .../models/product_chemical_law_line.py | 46 +++ .../product_chemical_location_amount.py | 68 +++++ .../models/product_chemical_major_category.py | 30 ++ .../models/product_chemical_minor_category.py | 31 ++ .../models/product_chemical_substance.py | 36 +++ .../models/product_chemical_substance_line.py | 43 +++ product_chemical/models/product_template.py | 37 ++- product_chemical/models/stock_location.py | 15 + product_chemical/readme/CONTRIBUTORS.md | 2 +- product_chemical/readme/DESCRIPTION.md | 20 +- product_chemical/security/ir.model.access.csv | 8 + .../views/product_chemical_category_views.xml | 150 +++++++++ .../views/product_chemical_law_views.xml | 95 ++++++ ...product_chemical_location_amount_views.xml | 55 ++++ .../views/product_chemical_menu.xml | 44 +++ .../product_chemical_substance_views.xml | 65 ++++ .../views/product_template_views.xml | 69 +++++ .../views/stock_location_views.xml | 26 ++ 23 files changed, 1225 insertions(+), 15 deletions(-) create mode 100644 product_chemical/migrations/18.0.1.1.0/pre-migration.py create mode 100644 product_chemical/models/product_chemical_law.py create mode 100644 product_chemical/models/product_chemical_law_line.py create mode 100644 product_chemical/models/product_chemical_location_amount.py create mode 100644 product_chemical/models/product_chemical_major_category.py create mode 100644 product_chemical/models/product_chemical_minor_category.py create mode 100644 product_chemical/models/product_chemical_substance.py create mode 100644 product_chemical/models/product_chemical_substance_line.py create mode 100644 product_chemical/models/stock_location.py create mode 100644 product_chemical/security/ir.model.access.csv create mode 100644 product_chemical/views/product_chemical_category_views.xml create mode 100644 product_chemical/views/product_chemical_law_views.xml create mode 100644 product_chemical/views/product_chemical_location_amount_views.xml create mode 100644 product_chemical/views/product_chemical_menu.xml create mode 100644 product_chemical/views/product_chemical_substance_views.xml create mode 100644 product_chemical/views/stock_location_views.xml diff --git a/product_chemical/__manifest__.py b/product_chemical/__manifest__.py index 0e2dc83..a4ab328 100644 --- a/product_chemical/__manifest__.py +++ b/product_chemical/__manifest__.py @@ -2,15 +2,22 @@ # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). { "name": "Product Chemical", - "version": "18.0.1.0.0", + "version": "18.0.1.1.0", "category": "Inventory/Inventory", - "summary": "Add chemical flag to products", + "summary": "Manage chemical substance information on products", "author": "Quartile", "website": "https://www.quartile.co", "license": "LGPL-3", - "depends": ["product"], + "depends": ["product", "stock"], "data": [ + "security/ir.model.access.csv", + "views/product_chemical_law_views.xml", + "views/product_chemical_category_views.xml", + "views/product_chemical_substance_views.xml", + "views/product_chemical_location_amount_views.xml", "views/product_template_views.xml", + "views/stock_location_views.xml", + "views/product_chemical_menu.xml", ], "installable": True, } diff --git a/product_chemical/i18n/ja.po b/product_chemical/i18n/ja.po index 2a1ba7f..f74ae97 100644 --- a/product_chemical/i18n/ja.po +++ b/product_chemical/i18n/ja.po @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 18.0+e\n" +"Project-Id-Version: Odoo Server 18.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-18 03:15+0000\n" -"PO-Revision-Date: 2026-02-18 03:15+0000\n" +"POT-Creation-Date: 2026-05-26 00:00+0000\n" +"PO-Revision-Date: 2026-05-26 00:00+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -18,16 +18,284 @@ msgstr "" #. module: product_chemical #: model:ir.model.fields,field_description:product_chemical.field_product_product__is_chemical #: model:ir.model.fields,field_description:product_chemical.field_product_template__is_chemical +#: model:ir.ui.menu,name:product_chemical.menu_product_chemical_root +#: model_terms:ir.ui.view,arch_db:product_chemical.product_template_form_view #: model_terms:ir.ui.view,arch_db:product_chemical.product_template_search_view msgid "Chemical" msgstr "化学物質" #. module: product_chemical -#: model:ir.model,name:product_chemical.model_product_template -msgid "Product" +#: model:ir.actions.act_window,name:product_chemical.product_chemical_law_action +#: model:ir.ui.menu,name:product_chemical.menu_product_chemical_law +msgid "Chemical Laws" +msgstr "化学物質法律" + +#. module: product_chemical +#: model:ir.model,name:product_chemical.model_product_chemical_law +msgid "Chemical Law" +msgstr "化学物質法律" + +#. module: product_chemical +#: model:ir.model,name:product_chemical.model_product_chemical_major_category +msgid "Chemical Major Category" +msgstr "化学物質大分類" + +#. module: product_chemical +#: model:ir.model,name:product_chemical.model_product_chemical_minor_category +msgid "Chemical Minor Category" +msgstr "化学物質小分類" + +#. module: product_chemical +#: model:ir.model,name:product_chemical.model_product_chemical_substance +msgid "Chemical Substance" +msgstr "化学物質" + +#. module: product_chemical +#: model:ir.actions.act_window,name:product_chemical.product_chemical_substance_action +#: model:ir.model.fields,field_description:product_chemical.field_product_template__chemical_substance_line_ids +#: model:ir.ui.menu,name:product_chemical.menu_product_chemical_substance +#: model_terms:ir.ui.view,arch_db:product_chemical.product_template_form_view +msgid "Chemical Substances" +msgstr "化学物質マスタ" + +#. module: product_chemical +#: model:ir.model,name:product_chemical.model_product_chemical_law_line +msgid "Product Chemical Law" +msgstr "プロダクト化学物質法律" + +#. module: product_chemical +#: model:ir.model,name:product_chemical.model_product_chemical_substance_line +msgid "Product Chemical Substance Line" +msgstr "プロダクト化学物質明細" + +#. module: product_chemical +#: model:ir.model,name:product_chemical.model_product_chemical_location_amount +msgid "Product Chemical Amount by Location" +msgstr "ロケーション別プロダクト化学物質成分量" + +#. module: product_chemical +#: model:ir.actions.act_window,name:product_chemical.product_chemical_major_category_action +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law__major_category_ids +#: model:ir.ui.menu,name:product_chemical.menu_product_chemical_major_category +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_law_view_form +msgid "Major Categories" +msgstr "大分類" + +#. module: product_chemical +#: model:ir.actions.act_window,name:product_chemical.product_chemical_minor_category_action +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_major_category__minor_category_ids +#: model:ir.ui.menu,name:product_chemical.menu_product_chemical_minor_category +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_major_category_view_form +msgid "Minor Categories" +msgstr "小分類" + +#. module: product_chemical +#: model:ir.actions.act_window,name:product_chemical.product_chemical_location_amount_action +#: model:ir.ui.menu,name:product_chemical.menu_product_chemical_location_amount +msgid "Chemicals by Location" +msgstr "ロケーション別化学物質" + +#. module: product_chemical +#: model_terms:ir.ui.view,arch_db:product_chemical.stock_location_view_form +msgid "Chemicals" +msgstr "化学物質" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_substance__cas_no +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_substance_line__cas_no +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_location_amount__cas_no +#: model_terms:ir.ui.view,arch_db:product_chemical.product_template_search_view +msgid "CAS No." +msgstr "CAS番号" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_substance__content_rate +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_substance_line__content_rate +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_location_amount__content_rate +msgid "Content Rate (%)" +msgstr "含有率 (%)" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law__code +msgid "Code" +msgstr "コード" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law__name +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_major_category__name +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_minor_category__name +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_substance__name +msgid "Name" +msgstr "名称" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_substance__active +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law__active +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_major_category__active +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_minor_category__active +msgid "Active" +msgstr "有効" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law__sequence +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_major_category__sequence +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_minor_category__sequence +msgid "Sequence" +msgstr "シーケンス" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law__note +msgid "Note" +msgstr "メモ" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_substance_line__substance_id +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_location_amount__substance_id +msgid "Substance" +msgstr "化学物質" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_template__chemical_substance_ids +msgid "Substances" +msgstr "化学物質" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law_line__major_category_id +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_major_category_view_form +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_minor_category_view_search +msgid "Major Category" +msgstr "大分類" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law_line__minor_category_id +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_minor_category_view_form +msgid "Minor Category" +msgstr "小分類" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law_line__law_id +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_major_category__law_id +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_minor_category__law_id +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_major_category_view_search +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_minor_category_view_search +msgid "Law" +msgstr "法律" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_law__product_count +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_law_view_form +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_law_view_list +msgid "Products" msgstr "プロダクト" #. module: product_chemical -#: model:ir.model.fields,field_description:product_chemical.field_product_template__smart_search -msgid "Smart Search" -msgstr "" +#: model:ir.model.fields,field_description:product_chemical.field_product_template__chemical_law_line_ids +msgid "Laws" +msgstr "法律" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_template__risk_assessment_pdf +msgid "Risk Assessment Sheet" +msgstr "リスクアセスメント表" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_template__risk_assessment_pdf_filename +msgid "Risk Assessment Sheet Filename" +msgstr "リスクアセスメント表ファイル名" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_template__chemical_location_amount_ids +#: model_terms:ir.ui.view,arch_db:product_chemical.product_template_form_view +msgid "Component Amount by Location" +msgstr "ロケーション別成分量" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_location_amount__quantity +msgid "On Hand Qty" +msgstr "手持在庫数" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_location_amount__location_id +msgid "Location" +msgstr "ロケーション" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_product_chemical_location_amount__component_amount +msgid "Component Amount" +msgstr "成分量" + +#. module: product_chemical +#: model:ir.model.fields,field_description:product_chemical.field_stock_location__chemical_amount_ids +msgid "Chemical Components" +msgstr "化学物質成分" + +#. module: product_chemical +#: model_terms:ir.ui.view,arch_db:product_chemical.product_template_form_view +msgid "Applicable Laws" +msgstr "適用法律" + +#. module: product_chemical +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_substance_view_form +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_substance_view_search +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_law_view_form +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_law_view_search +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_major_category_view_form +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_minor_category_view_form +msgid "Archived" +msgstr "アーカイブ済" + +#. module: product_chemical +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_law_view_form +msgid "Categories" +msgstr "カテゴリ" + +#. module: product_chemical +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_law_view_form +msgid "Notes" +msgstr "メモ" + +#. module: product_chemical +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_law_view_form +msgid "Law Name" +msgstr "法律名" + +#. module: product_chemical +#: model_terms:ir.ui.view,arch_db:product_chemical.product_chemical_substance_view_form +msgid "Substance Name" +msgstr "化学物質名" + +#. module: product_chemical +#: model:ir.model.constraint,message:product_chemical.constraint_product_chemical_law_name_uniq +msgid "Law name must be unique." +msgstr "法律名は一意である必要があります。" + +#. module: product_chemical +#: model:ir.model.constraint,message:product_chemical.constraint_product_chemical_substance_cas_no_uniq +msgid "CAS No. must be unique." +msgstr "CAS番号は一意である必要があります。" + +#. module: product_chemical +#: model:ir.model.constraint,message:product_chemical.constraint_product_chemical_major_category_name_law_uniq +msgid "Major category name must be unique per law." +msgstr "大分類名は法律ごとに一意である必要があります。" + +#. module: product_chemical +#: model:ir.model.constraint,message:product_chemical.constraint_product_chemical_minor_category_name_major_uniq +msgid "Minor category name must be unique per major category." +msgstr "小分類名は大分類ごとに一意である必要があります。" + +#. module: product_chemical +#: model:ir.model.constraint,message:product_chemical.constraint_product_chemical_law_line_product_law_uniq +msgid "Each law can be added only once per product." +msgstr "1プロダクトに対し各法律は1度のみ追加できます。" + +#. module: product_chemical +#: model:ir.model.constraint,message:product_chemical.constraint_product_chemical_substance_line_product_substance_uniq +msgid "Each substance can be added only once per product." +msgstr "1プロダクトに対し各化学物質は1度のみ追加できます。" + +#. module: product_chemical +#: model:ir.model.constraint,message:product_chemical.constraint_product_chemical_substance_line_content_rate_range +#: model:ir.model.constraint,message:product_chemical.constraint_product_chemical_substance_content_rate_range +msgid "Content rate must be between 0 and 100." +msgstr "含有率は0以上100以下である必要があります。" diff --git a/product_chemical/migrations/18.0.1.1.0/pre-migration.py b/product_chemical/migrations/18.0.1.1.0/pre-migration.py new file mode 100644 index 0000000..b6675ba --- /dev/null +++ b/product_chemical/migrations/18.0.1.1.0/pre-migration.py @@ -0,0 +1,45 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +# Replace the legacy product.chemical.cas master with the new +# product.chemical.substance master. The new master has a separate name field +# in addition to CAS No. and a default content rate, so we drop the legacy +# tables and let Odoo recreate them. Any existing line data is discarded; the +# previous schema was only used for early prototyping. + + +def migrate(cr, version): + cr.execute("DROP TABLE IF EXISTS product_chemical_cas_line CASCADE") + cr.execute("DROP TABLE IF EXISTS product_chemical_cas CASCADE") + cr.execute( + """ + DELETE FROM ir_model_data + WHERE module = 'product_chemical' + AND model IN ( + 'product.chemical.cas', + 'product.chemical.cas.line' + ) + """ + ) + cr.execute( + """ + DELETE FROM ir_model_fields + WHERE model IN ( + 'product.chemical.cas', + 'product.chemical.cas.line' + ) + OR relation IN ( + 'product.chemical.cas', + 'product.chemical.cas.line' + ) + """ + ) + cr.execute( + """ + DELETE FROM ir_model + WHERE model IN ( + 'product.chemical.cas', + 'product.chemical.cas.line' + ) + """ + ) diff --git a/product_chemical/models/__init__.py b/product_chemical/models/__init__.py index e8fa8f6..58fcf65 100644 --- a/product_chemical/models/__init__.py +++ b/product_chemical/models/__init__.py @@ -1 +1,9 @@ +from . import product_chemical_law +from . import product_chemical_major_category +from . import product_chemical_minor_category +from . import product_chemical_substance +from . import product_chemical_law_line +from . import product_chemical_substance_line +from . import product_chemical_location_amount from . import product_template +from . import stock_location diff --git a/product_chemical/models/product_chemical_law.py b/product_chemical/models/product_chemical_law.py new file mode 100644 index 0000000..43cf3ca --- /dev/null +++ b/product_chemical/models/product_chemical_law.py @@ -0,0 +1,50 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ProductChemicalLaw(models.Model): + _name = "product.chemical.law" + _description = "Chemical Law" + _order = "sequence, name" + + name = fields.Char(required=True, translate=True) + code = fields.Char() + sequence = fields.Integer(default=10) + active = fields.Boolean(default=True) + note = fields.Text() + major_category_ids = fields.One2many( + "product.chemical.major.category", + "law_id", + string="Major Categories", + ) + product_count = fields.Integer(compute="_compute_product_count") + + _sql_constraints = [ + ("name_uniq", "unique(name)", "Law name must be unique."), + ] + + def _compute_product_count(self): + line_data = self.env["product.chemical.law.line"]._read_group( + [("law_id", "in", self.ids)], + groupby=["law_id"], + aggregates=["product_tmpl_id:count_distinct"], + ) + counts = {law.id: count for law, count in line_data} + for rec in self: + rec.product_count = counts.get(rec.id, 0) + + def action_view_products(self): + self.ensure_one() + product_ids = ( + self.env["product.chemical.law.line"] + .search([("law_id", "=", self.id)]) + .product_tmpl_id.ids + ) + action = self.env["ir.actions.act_window"]._for_xml_id( + "product.product_template_action" + ) + action["domain"] = [("id", "in", product_ids)] + action["context"] = {"default_is_chemical": True} + return action diff --git a/product_chemical/models/product_chemical_law_line.py b/product_chemical/models/product_chemical_law_line.py new file mode 100644 index 0000000..b88afd1 --- /dev/null +++ b/product_chemical/models/product_chemical_law_line.py @@ -0,0 +1,46 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class ProductChemicalLawLine(models.Model): + _name = "product.chemical.law.line" + _description = "Product Chemical Law" + + product_tmpl_id = fields.Many2one( + "product.template", + required=True, + ondelete="cascade", + index=True, + ) + law_id = fields.Many2one( + "product.chemical.law", required=True, ondelete="restrict" + ) + major_category_id = fields.Many2one( + "product.chemical.major.category", + domain="[('law_id', '=', law_id)]", + ondelete="restrict", + ) + minor_category_id = fields.Many2one( + "product.chemical.minor.category", + domain="[('major_category_id', '=', major_category_id)]", + ondelete="restrict", + ) + + _sql_constraints = [ + ( + "product_law_uniq", + "unique(product_tmpl_id, law_id)", + "Each law can be added only once per product.", + ), + ] + + @api.onchange("law_id") + def _onchange_law_id(self): + self.major_category_id = False + self.minor_category_id = False + + @api.onchange("major_category_id") + def _onchange_major_category_id(self): + self.minor_category_id = False diff --git a/product_chemical/models/product_chemical_location_amount.py b/product_chemical/models/product_chemical_location_amount.py new file mode 100644 index 0000000..32afb30 --- /dev/null +++ b/product_chemical/models/product_chemical_location_amount.py @@ -0,0 +1,68 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ProductChemicalLocationAmount(models.Model): + _name = "product.chemical.location.amount" + _description = "Product Chemical Amount by Location" + _auto = False + _order = "product_tmpl_id, location_id, substance_id" + _depends = { + "product.template": ["is_chemical"], + "product.product": ["product_tmpl_id"], + "product.chemical.substance.line": [ + "product_tmpl_id", + "substance_id", + "content_rate", + ], + "product.chemical.substance": ["cas_no"], + "stock.quant": ["product_id", "location_id", "quantity"], + "stock.location": ["usage"], + } + + product_tmpl_id = fields.Many2one( + "product.template", string="Product", readonly=True + ) + location_id = fields.Many2one( + "stock.location", string="Location", readonly=True + ) + substance_id = fields.Many2one( + "product.chemical.substance", string="Substance", readonly=True + ) + cas_no = fields.Char(string="CAS No.", readonly=True) + quantity = fields.Float(string="On Hand Qty", readonly=True) + content_rate = fields.Float(string="Content Rate (%)", readonly=True) + component_amount = fields.Float(string="Component Amount", readonly=True) + + @property + def _table_query(self): + return """ + SELECT + ROW_NUMBER() OVER () AS id, + pt.id AS product_tmpl_id, + sq.location_id AS location_id, + sub_line.substance_id AS substance_id, + sub.cas_no AS cas_no, + SUM(sq.quantity) AS quantity, + sub_line.content_rate AS content_rate, + SUM(sq.quantity) * sub_line.content_rate / 100.0 + AS component_amount + FROM product_template pt + JOIN product_product pp ON pp.product_tmpl_id = pt.id + JOIN product_chemical_substance_line sub_line + ON sub_line.product_tmpl_id = pt.id + JOIN product_chemical_substance sub + ON sub.id = sub_line.substance_id + JOIN stock_quant sq ON sq.product_id = pp.id + JOIN stock_location sl + ON sl.id = sq.location_id AND sl.usage = 'internal' + WHERE pt.is_chemical = TRUE + GROUP BY + pt.id, + sq.location_id, + sub_line.substance_id, + sub.cas_no, + sub_line.content_rate + """ diff --git a/product_chemical/models/product_chemical_major_category.py b/product_chemical/models/product_chemical_major_category.py new file mode 100644 index 0000000..8d5b87b --- /dev/null +++ b/product_chemical/models/product_chemical_major_category.py @@ -0,0 +1,30 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ProductChemicalMajorCategory(models.Model): + _name = "product.chemical.major.category" + _description = "Chemical Major Category" + _order = "law_id, sequence, name" + + name = fields.Char(required=True, translate=True) + law_id = fields.Many2one( + "product.chemical.law", required=True, ondelete="cascade", index=True + ) + sequence = fields.Integer(default=10) + active = fields.Boolean(default=True) + minor_category_ids = fields.One2many( + "product.chemical.minor.category", + "major_category_id", + string="Minor Categories", + ) + + _sql_constraints = [ + ( + "name_law_uniq", + "unique(law_id, name)", + "Major category name must be unique per law.", + ), + ] diff --git a/product_chemical/models/product_chemical_minor_category.py b/product_chemical/models/product_chemical_minor_category.py new file mode 100644 index 0000000..d8eb614 --- /dev/null +++ b/product_chemical/models/product_chemical_minor_category.py @@ -0,0 +1,31 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ProductChemicalMinorCategory(models.Model): + _name = "product.chemical.minor.category" + _description = "Chemical Minor Category" + _order = "major_category_id, sequence, name" + + name = fields.Char(required=True, translate=True) + major_category_id = fields.Many2one( + "product.chemical.major.category", + required=True, + ondelete="cascade", + index=True, + ) + law_id = fields.Many2one( + related="major_category_id.law_id", store=True, index=True + ) + sequence = fields.Integer(default=10) + active = fields.Boolean(default=True) + + _sql_constraints = [ + ( + "name_major_uniq", + "unique(major_category_id, name)", + "Minor category name must be unique per major category.", + ), + ] diff --git a/product_chemical/models/product_chemical_substance.py b/product_chemical/models/product_chemical_substance.py new file mode 100644 index 0000000..dd010d6 --- /dev/null +++ b/product_chemical/models/product_chemical_substance.py @@ -0,0 +1,36 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class ProductChemicalSubstance(models.Model): + _name = "product.chemical.substance" + _description = "Chemical Substance" + _order = "name" + + name = fields.Char(required=True, translate=True) + cas_no = fields.Char(string="CAS No.", required=True) + content_rate = fields.Float( + string="Content Rate (%)", + default=0.0, + help="Default content rate (%) used when adding this substance to a product.", + ) + active = fields.Boolean(default=True) + + _sql_constraints = [ + ("cas_no_uniq", "unique(cas_no)", "CAS No. must be unique."), + ( + "content_rate_range", + "CHECK(content_rate >= 0 AND content_rate <= 100)", + "Content rate must be between 0 and 100.", + ), + ] + + @api.depends("name", "cas_no") + def _compute_display_name(self): + for rec in self: + if rec.name and rec.cas_no: + rec.display_name = f"[{rec.cas_no}] {rec.name}" + else: + rec.display_name = rec.name or rec.cas_no or "" diff --git a/product_chemical/models/product_chemical_substance_line.py b/product_chemical/models/product_chemical_substance_line.py new file mode 100644 index 0000000..f318782 --- /dev/null +++ b/product_chemical/models/product_chemical_substance_line.py @@ -0,0 +1,43 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class ProductChemicalSubstanceLine(models.Model): + _name = "product.chemical.substance.line" + _description = "Product Chemical Substance Line" + + product_tmpl_id = fields.Many2one( + "product.template", + required=True, + ondelete="cascade", + index=True, + ) + substance_id = fields.Many2one( + "product.chemical.substance", required=True, ondelete="restrict" + ) + cas_no = fields.Char(related="substance_id.cas_no", store=True, string="CAS No.") + content_rate = fields.Float( + string="Content Rate (%)", + required=True, + default=0.0, + ) + + _sql_constraints = [ + ( + "product_substance_uniq", + "unique(product_tmpl_id, substance_id)", + "Each substance can be added only once per product.", + ), + ( + "content_rate_range", + "CHECK(content_rate >= 0 AND content_rate <= 100)", + "Content rate must be between 0 and 100.", + ), + ] + + @api.onchange("substance_id") + def _onchange_substance_id(self): + if self.substance_id: + self.content_rate = self.substance_id.content_rate diff --git a/product_chemical/models/product_template.py b/product_chemical/models/product_template.py index c87047c..e6c0065 100644 --- a/product_chemical/models/product_template.py +++ b/product_chemical/models/product_template.py @@ -1,10 +1,45 @@ # Copyright 2026 Quartile (https://www.quartile.co) # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -from odoo import fields, models +from odoo import api, fields, models class ProductTemplate(models.Model): _inherit = "product.template" is_chemical = fields.Boolean(string="Chemical") + chemical_law_line_ids = fields.One2many( + "product.chemical.law.line", "product_tmpl_id", string="Laws" + ) + chemical_substance_line_ids = fields.One2many( + "product.chemical.substance.line", + "product_tmpl_id", + string="Chemical Substances", + ) + chemical_substance_ids = fields.Many2many( + "product.chemical.substance", + compute="_compute_chemical_substance_ids", + search="_search_chemical_substance_ids", + string="Substances", + ) + risk_assessment_pdf = fields.Binary(string="Risk Assessment Sheet") + risk_assessment_pdf_filename = fields.Char( + string="Risk Assessment Sheet Filename" + ) + chemical_location_amount_ids = fields.One2many( + "product.chemical.location.amount", + "product_tmpl_id", + string="Component Amount by Location", + readonly=True, + ) + + @api.depends("chemical_substance_line_ids.substance_id") + def _compute_chemical_substance_ids(self): + for rec in self: + rec.chemical_substance_ids = rec.chemical_substance_line_ids.substance_id + + def _search_chemical_substance_ids(self, operator, value): + lines = self.env["product.chemical.substance.line"].search( + [("substance_id", operator, value)] + ) + return [("id", "in", lines.product_tmpl_id.ids)] diff --git a/product_chemical/models/stock_location.py b/product_chemical/models/stock_location.py new file mode 100644 index 0000000..4eaa039 --- /dev/null +++ b/product_chemical/models/stock_location.py @@ -0,0 +1,15 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class StockLocation(models.Model): + _inherit = "stock.location" + + chemical_amount_ids = fields.One2many( + "product.chemical.location.amount", + "location_id", + string="Chemical Components", + readonly=True, + ) diff --git a/product_chemical/readme/CONTRIBUTORS.md b/product_chemical/readme/CONTRIBUTORS.md index c4020f4..097598f 100644 --- a/product_chemical/readme/CONTRIBUTORS.md +++ b/product_chemical/readme/CONTRIBUTORS.md @@ -1,2 +1,2 @@ - [Quartile](https://www.quartile.co): - - Yoshi Tashiro + - Tatsuki Kanda diff --git a/product_chemical/readme/DESCRIPTION.md b/product_chemical/readme/DESCRIPTION.md index b7e2bd3..868380f 100644 --- a/product_chemical/readme/DESCRIPTION.md +++ b/product_chemical/readme/DESCRIPTION.md @@ -1,2 +1,18 @@ -This module adds a "Chemical" boolean field to product templates, allowing users to -flag products as chemicals. +This module adds chemical substance management to products. When the "Chemical" +flag on a product is enabled, a "Chemical" tab is shown that allows users to: + +* Attach one or more applicable laws to the product (with optional major / + minor category per law). +* Upload a risk assessment sheet (PDF). +* Register CAS Numbers contained in the product together with their content + rate (%). +* Review the component amount per stock location, calculated from on-hand + quantity multiplied by each CAS content rate. + +Master data managed by this module: + +* **Laws** — referenced by products; each law lists the products that use it. +* **Major / Minor categories** — organised under their parent law. +* **CAS Numbers** — reusable across products. + +Products can be searched by CAS Number from the product search view. diff --git a/product_chemical/security/ir.model.access.csv b/product_chemical/security/ir.model.access.csv new file mode 100644 index 0000000..86313e5 --- /dev/null +++ b/product_chemical/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_chemical_law_user,product.chemical.law user,model_product_chemical_law,base.group_user,1,1,1,1 +access_product_chemical_major_category_user,product.chemical.major.category user,model_product_chemical_major_category,base.group_user,1,1,1,1 +access_product_chemical_minor_category_user,product.chemical.minor.category user,model_product_chemical_minor_category,base.group_user,1,1,1,1 +access_product_chemical_substance_user,product.chemical.substance user,model_product_chemical_substance,base.group_user,1,1,1,1 +access_product_chemical_law_line_user,product.chemical.law.line user,model_product_chemical_law_line,base.group_user,1,1,1,1 +access_product_chemical_substance_line_user,product.chemical.substance.line user,model_product_chemical_substance_line,base.group_user,1,1,1,1 +access_product_chemical_location_amount_user,product.chemical.location.amount user,model_product_chemical_location_amount,base.group_user,1,0,0,0 diff --git a/product_chemical/views/product_chemical_category_views.xml b/product_chemical/views/product_chemical_category_views.xml new file mode 100644 index 0000000..616bc2a --- /dev/null +++ b/product_chemical/views/product_chemical_category_views.xml @@ -0,0 +1,150 @@ + + + + product.chemical.major.category.list + product.chemical.major.category + + + + + + + + + + product.chemical.major.category.form + product.chemical.major.category + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+ + product.chemical.major.category.search + product.chemical.major.category + + + + + + + + + + + + Major Categories + product.chemical.major.category + list,form + + + + product.chemical.minor.category.list + product.chemical.minor.category + + + + + + + + + + + product.chemical.minor.category.form + product.chemical.minor.category + +
+ + +
+
+ + + + + + + + + + +
+
+
+
+ + product.chemical.minor.category.search + product.chemical.minor.category + + + + + + + + + + + + + + Minor Categories + product.chemical.minor.category + list,form + +
diff --git a/product_chemical/views/product_chemical_law_views.xml b/product_chemical/views/product_chemical_law_views.xml new file mode 100644 index 0000000..8d857ed --- /dev/null +++ b/product_chemical/views/product_chemical_law_views.xml @@ -0,0 +1,95 @@ + + + + product.chemical.law.list + product.chemical.law + + + + + + + + + + + product.chemical.law.form + product.chemical.law + +
+
+ +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + product.chemical.law.search + product.chemical.law + + + + + + + + + + Chemical Laws + product.chemical.law + list,form + + diff --git a/product_chemical/views/product_chemical_location_amount_views.xml b/product_chemical/views/product_chemical_location_amount_views.xml new file mode 100644 index 0000000..a1bd8b4 --- /dev/null +++ b/product_chemical/views/product_chemical_location_amount_views.xml @@ -0,0 +1,55 @@ + + + + product.chemical.location.amount.list + product.chemical.location.amount + + + + + + + + + + + + + + product.chemical.location.amount.search + product.chemical.location.amount + + + + + + + + + + + + + + + + Chemicals by Location + product.chemical.location.amount + list + {'search_default_group_location': 1, 'search_default_group_substance': 1} + + diff --git a/product_chemical/views/product_chemical_menu.xml b/product_chemical/views/product_chemical_menu.xml new file mode 100644 index 0000000..4f5d73c --- /dev/null +++ b/product_chemical/views/product_chemical_menu.xml @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/product_chemical/views/product_chemical_substance_views.xml b/product_chemical/views/product_chemical_substance_views.xml new file mode 100644 index 0000000..2e457ee --- /dev/null +++ b/product_chemical/views/product_chemical_substance_views.xml @@ -0,0 +1,65 @@ + + + + product.chemical.substance.list + product.chemical.substance + + + + + + + + + + product.chemical.substance.form + product.chemical.substance + +
+ + +
+
+ + + + + + + + + +
+
+
+
+ + product.chemical.substance.search + product.chemical.substance + + + + + + + + + + Chemical Substances + product.chemical.substance + list,form + +
diff --git a/product_chemical/views/product_template_views.xml b/product_chemical/views/product_template_views.xml index 34beced..4611011 100644 --- a/product_chemical/views/product_template_views.xml +++ b/product_chemical/views/product_template_views.xml @@ -8,6 +8,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -16,6 +80,11 @@ + + + + stock.location.form.chemical + stock.location + + + + + + + + + + + + + + + + + + + + +