From 3ff01f271536b7578da8fbeaa281b73b2e6a3b40 Mon Sep 17 00:00:00 2001 From: Deniz Gallo Date: Wed, 22 Apr 2026 10:04:56 +0200 Subject: [PATCH 1/2] [IMP] lighting: add spare parts tab on product form --- lighting/i18n/es.po | 12 ++++++++++++ lighting/i18n/fr.po | 12 ++++++++++++ lighting/i18n/pt.po | 12 ++++++++++++ lighting/models/product.py | 31 +++++++++++++++++++++++++++++++ lighting/views/product_views.xml | 10 ++++++++++ 5 files changed, 77 insertions(+) diff --git a/lighting/i18n/es.po b/lighting/i18n/es.po index 19704c4a..f7aa9799 100644 --- a/lighting/i18n/es.po +++ b/lighting/i18n/es.po @@ -2568,6 +2568,11 @@ msgstr "Es max. potencia" msgid "Is recommended accessory" msgstr "" +#. module: lighting +#: model:ir.model.fields,field_description:lighting.field_lighting_product__is_spare_part +msgid "Is spare part" +msgstr "Es recambio" + #. module: lighting #: model:ir.model.constraint,message:lighting.constraint_lighting_product_voltage_voltage_uniq msgid "It already exists another voltage with the same parameters" @@ -4637,6 +4642,13 @@ msgstr "Fuentes" msgid "Spanish" msgstr "" +#. module: lighting +#: model:ir.model.fields,field_description:lighting.field_lighting_product__spare_part_ids +#: model_terms:ir.ui.view,arch_db:lighting.product_form_view +#: model_terms:ir.ui.view,arch_db:lighting.lighting_product_view +msgid "Spare parts" +msgstr "Recambios" + #. module: lighting #: model:ir.model,name:lighting.model_lighting_product_special_spectrum msgid "Special Spectrum" diff --git a/lighting/i18n/fr.po b/lighting/i18n/fr.po index 06e8c008..137c0ebf 100644 --- a/lighting/i18n/fr.po +++ b/lighting/i18n/fr.po @@ -2561,6 +2561,11 @@ msgstr "Est max. En watts" msgid "Is recommended accessory" msgstr "" +#. module: lighting +#: model:ir.model.fields,field_description:lighting.field_lighting_product__is_spare_part +msgid "Is spare part" +msgstr "Est pièce de rechange" + #. module: lighting #: model:ir.model.constraint,message:lighting.constraint_lighting_product_voltage_voltage_uniq msgid "It already exists another voltage with the same parameters" @@ -4635,6 +4640,13 @@ msgstr "Sources" msgid "Spanish" msgstr "" +#. module: lighting +#: model:ir.model.fields,field_description:lighting.field_lighting_product__spare_part_ids +#: model_terms:ir.ui.view,arch_db:lighting.product_form_view +#: model_terms:ir.ui.view,arch_db:lighting.lighting_product_view +msgid "Spare parts" +msgstr "Pièces de rechange" + #. module: lighting #: model:ir.model,name:lighting.model_lighting_product_special_spectrum msgid "Special Spectrum" diff --git a/lighting/i18n/pt.po b/lighting/i18n/pt.po index 93b832e0..0fd84b13 100644 --- a/lighting/i18n/pt.po +++ b/lighting/i18n/pt.po @@ -2444,6 +2444,18 @@ msgstr "Controle remoto" msgid "Recommended accessories" msgstr "Acessórios recomendados" +#. module: lighting +#: model:ir.model.fields,field_description:lighting.field_lighting_product_spare_part_ids +#: model:ir.ui.view,arch_db:lighting.product_form_view +#: model:ir.ui.view,arch_db:lighting.lighting_product_view +msgid "Spare parts" +msgstr "Peças sobressalentes" + +#. module: lighting +#: model:ir.model.fields,field_description:lighting.field_lighting_product_is_spare_part +msgid "Is spare part" +msgstr "É peça sobressalente" + #. module: lighting #: model:ir.model.fields,field_description:lighting.field_lighting_product_required_ids #: model:ir.ui.view,arch_db:lighting.product_form_view diff --git a/lighting/models/product.py b/lighting/models/product.py index 3741ea3d..9e40639a 100644 --- a/lighting/models/product.py +++ b/lighting/models/product.py @@ -1357,6 +1357,37 @@ def _search_is_required_accessory(self, operator, value): tracking=True, ) + # Spare parts tab + spare_part_ids = fields.Many2many( + comodel_name="lighting.product", + relation="lighting_product_spare_part_rel", + column1="product_id", + column2="spare_part_id", + string="Spare parts", + tracking=True, + ) + + is_spare_part = fields.Boolean( + string="Is spare part", + compute="_compute_is_spare_part", + search="_search_is_spare_part", + ) + + @api.depends("spare_part_ids") + def _compute_is_spare_part(self): + for rec in self: + rec.is_spare_part = bool( + self.env["lighting.product"].search([("spare_part_ids", "=", rec.id)]) + ) + + def _search_is_spare_part(self, operator, value): + ids = ( + self.env["lighting.product"] + .search([("spare_part_ids", "!=", False)]) + .mapped("spare_part_ids.id") + ) + return [("id", "in", ids)] + # logistics tab tariff_item = fields.Char( tracking=True, diff --git a/lighting/views/product_views.xml b/lighting/views/product_views.xml index 153b2966..9f971f1c 100644 --- a/lighting/views/product_views.xml +++ b/lighting/views/product_views.xml @@ -497,6 +497,11 @@ + + + + + + Date: Wed, 22 Apr 2026 10:35:44 +0200 Subject: [PATCH 2/2] [FIX] lighting: corrections on spare parts tab - product.py: rewrite _compute_is_spare_part with raw SQL to avoid N+1 queries on large recordsets (matches the pattern already used by _compute_is_optional_accessory). - product.py: add parent_spare_part_product_count computed field (raw-SQL compute) so the reverse lookup "which products list this record as a spare part" is available, matching the existing optional/required accessory pattern. - product.py: extend _check_product_dependency to forbid a product being its own spare part. - views/product_views.xml: add product_action_parent_spare_part_product action window and a stat button on the product form, matching the existing P. rec. acc. / P. mand. acc. buttons. - i18n/pt.po: fix translation reference syntax (double underscore on field refs, model_terms: on view arch refs) so the Portuguese translations actually load in Odoo 16. --- lighting/i18n/es.po | 5 ++++ lighting/i18n/fr.po | 5 ++++ lighting/i18n/pt.po | 13 ++++++---- lighting/models/product.py | 41 ++++++++++++++++++++++++++++---- lighting/views/product_views.xml | 36 ++++++++++++++++++++++++---- 5 files changed, 87 insertions(+), 13 deletions(-) diff --git a/lighting/i18n/es.po b/lighting/i18n/es.po index f7aa9799..a0ae7a9e 100644 --- a/lighting/i18n/es.po +++ b/lighting/i18n/es.po @@ -3447,6 +3447,11 @@ msgstr "" msgid "P. rec. acc." msgstr "" +#. module: lighting +#: model_terms:ir.ui.view,arch_db:lighting.product_form_view +msgid "P. sp. parts" +msgstr "P. recambios" + #. module: lighting #: model:ir.model.fields,field_description:lighting.field_lighting_product_category__parent_id #: model:ir.model.fields,field_description:lighting.field_lighting_product_group__parent_id diff --git a/lighting/i18n/fr.po b/lighting/i18n/fr.po index 137c0ebf..01903a28 100644 --- a/lighting/i18n/fr.po +++ b/lighting/i18n/fr.po @@ -3445,6 +3445,11 @@ msgstr "" msgid "P. rec. acc." msgstr "" +#. module: lighting +#: model_terms:ir.ui.view,arch_db:lighting.product_form_view +msgid "P. sp. parts" +msgstr "P. pièces" + #. module: lighting #: model:ir.model.fields,field_description:lighting.field_lighting_product_category__parent_id #: model:ir.model.fields,field_description:lighting.field_lighting_product_group__parent_id diff --git a/lighting/i18n/pt.po b/lighting/i18n/pt.po index 0fd84b13..d86f734d 100644 --- a/lighting/i18n/pt.po +++ b/lighting/i18n/pt.po @@ -2445,17 +2445,22 @@ msgid "Recommended accessories" msgstr "Acessórios recomendados" #. module: lighting -#: model:ir.model.fields,field_description:lighting.field_lighting_product_spare_part_ids -#: model:ir.ui.view,arch_db:lighting.product_form_view -#: model:ir.ui.view,arch_db:lighting.lighting_product_view +#: model:ir.model.fields,field_description:lighting.field_lighting_product__spare_part_ids +#: model_terms:ir.ui.view,arch_db:lighting.product_form_view +#: model_terms:ir.ui.view,arch_db:lighting.lighting_product_view msgid "Spare parts" msgstr "Peças sobressalentes" #. module: lighting -#: model:ir.model.fields,field_description:lighting.field_lighting_product_is_spare_part +#: model:ir.model.fields,field_description:lighting.field_lighting_product__is_spare_part msgid "Is spare part" msgstr "É peça sobressalente" +#. module: lighting +#: model_terms:ir.ui.view,arch_db:lighting.product_form_view +msgid "P. sp. parts" +msgstr "P. peças" + #. module: lighting #: model:ir.model.fields,field_description:lighting.field_lighting_product_required_ids #: model:ir.ui.view,arch_db:lighting.product_form_view diff --git a/lighting/models/product.py b/lighting/models/product.py index 9e40639a..ba271ae6 100644 --- a/lighting/models/product.py +++ b/lighting/models/product.py @@ -1366,6 +1366,26 @@ def _search_is_required_accessory(self, operator, value): string="Spare parts", tracking=True, ) + parent_spare_part_product_count = fields.Integer( + compute="_compute_parent_spare_part_product_count" + ) + + @api.depends("spare_part_ids") + def _compute_parent_spare_part_product_count(self): + if not self: + return + # Raw SQL for performance: the ORM approach triggers one query per + # record which is too slow on large recordsets + self.env.cr.execute( + "SELECT spare_part_id, COUNT(*)" + " FROM lighting_product_spare_part_rel" + " WHERE spare_part_id IN %s" + " GROUP BY spare_part_id", + (tuple(self.ids),), + ) + counts = dict(self.env.cr.fetchall()) + for rec in self: + rec.parent_spare_part_product_count = counts.get(rec.id, 0) is_spare_part = fields.Boolean( string="Is spare part", @@ -1375,10 +1395,19 @@ def _search_is_required_accessory(self, operator, value): @api.depends("spare_part_ids") def _compute_is_spare_part(self): + if not self: + return + # Raw SQL for performance: the ORM approach triggers one query per + # record which is too slow on large recordsets + self.env.cr.execute( + "SELECT DISTINCT spare_part_id" + " FROM lighting_product_spare_part_rel" + " WHERE spare_part_id IN %s", + (tuple(self.ids),), + ) + spare_ids = {row[0] for row in self.env.cr.fetchall()} for rec in self: - rec.is_spare_part = bool( - self.env["lighting.product"].search([("spare_part_ids", "=", rec.id)]) - ) + rec.is_spare_part = rec.id in spare_ids def _search_is_spare_part(self, operator, value): ids = ( @@ -1584,7 +1613,7 @@ def _check_composite_product(self): ) ) - @api.constrains("optional_ids", "required_ids") + @api.constrains("optional_ids", "required_ids", "spare_part_ids") def _check_product_dependency(self): for rec in self: if rec in rec.required_ids: @@ -1597,6 +1626,10 @@ def _check_product_dependency(self): "The current reference cannot be defined as a recomended accessory" ) ) + if rec in rec.spare_part_ids: + raise ValidationError( + _("The current reference cannot be defined as a spare part") + ) # TODO: REVIEW: Self ensure @api.constrains("product_group_id") diff --git a/lighting/views/product_views.xml b/lighting/views/product_views.xml index 9f971f1c..2c346f25 100644 --- a/lighting/views/product_views.xml +++ b/lighting/views/product_views.xml @@ -30,6 +30,18 @@

Create the first product

+ + Parent products as spare parts + lighting.product + tree,kanban,form + [('spare_part_ids', '=', active_id)] + {'default_spare_part_ids': [(4, active_id, False)]} + +

Create the first product

+
+
product.form lighting.product @@ -73,6 +85,20 @@ widget="statinfo" /> +