From 06c2a56e695d97c81572cf09bf7785800d6a64f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 30 Dec 2025 17:42:00 +0100 Subject: [PATCH] Use Many2One field for product and variant on stock reporting Closes #14456 --- modules/stock/stock_reporting.py | 121 +++++++++++++++--- modules/stock/stock_reporting.xml | 4 +- .../scenario_stock_reporting_inventory.rst | 4 +- .../view/reporting_inventory_daily_form.xml | 4 +- .../view/reporting_inventory_daily_list.xml | 2 + .../stock/view/reporting_inventory_form.xml | 4 +- .../stock/view/reporting_inventory_list.xml | 2 + .../view/reporting_inventory_move_form.xml | 4 +- .../view/reporting_inventory_move_list.xml | 2 + .../reporting_inventory_turnover_form.xml | 4 +- .../reporting_inventory_turnover_list.xml | 2 + 11 files changed, 125 insertions(+), 28 deletions(-) diff --git a/modules/stock/stock_reporting.py b/modules/stock/stock_reporting.py index 7410141913..2d42083f80 100644 --- a/modules/stock/stock_reporting.py +++ b/modules/stock/stock_reporting.py @@ -47,10 +47,36 @@ class _InventoryMixin: __slots__ = () company = fields.Many2One('company.company', "Company") - product = fields.Reference("Product", [ + product_reference = fields.Reference("Product Reference", [ ('product.product', "Variant"), ('product.template', "Product"), - ]) + ], + context={ + 'company': Eval('company', -1), + }, + depends=['company']) + product_template = fields.Many2One( + 'product.template', "Product", + states={ + 'invisible': ( + Eval('context', {}).get('product_type', 'product.template') + != 'product.template'), + }, + context={ + 'company': Eval('company', -1), + }, + depends=['company']) + product = fields.Many2One( + 'product.product', "Variant", + states={ + 'invisible': ( + Eval('context', {}).get('product_type', 'product.template') + != 'product.product'), + }, + context={ + 'company': Eval('company', -1), + }, + depends=['company']) unit = fields.Function( fields.Many2One('product.uom', "Unit"), 'on_change_with_unit') @@ -108,9 +134,14 @@ class _InventoryMixin: ) if context.get('product_type') == 'product.product': - product = Concat('product.product,', move.product) + product_reference = Concat('product.product,', move.product) + product = move.product + product_template = Literal(None) else: - product = Concat('product.template,', product_table.template) + product_reference = Concat( + 'product.template,', product_table.template) + product = Literal(None) + product_template = product_table.template quantities = ( quantities .join(product_table, @@ -149,6 +180,8 @@ class _InventoryMixin: quantities = quantities.select( Min(move.id).as_('id'), + product_reference.as_('product_reference'), + product_template.as_('product_template'), product.as_('product'), date_column.as_('date'), next_date_column.as_('next_date'), @@ -173,7 +206,7 @@ class _InventoryMixin: + Coalesce(quantities.input_quantity, 0) - Coalesce(quantities.output_quantity, 0), window=Window( - [quantities.product], + [quantities.product_reference], order_by=[*cls._quantities_order_by(quantities)])) if period: @@ -208,7 +241,8 @@ class _InventoryMixin: query = quantities.join(cache, 'LEFT', condition=( cache.product - == cls.product.sql_id(quantities.product, cls))) + == cls.product_reference.sql_id( + quantities.product_reference, cls))) quantity += Coalesce(cache.quantity, 0) else: query = quantities @@ -217,6 +251,8 @@ class _InventoryMixin: .select( quantities.id.as_('id'), Literal(company).as_('company'), + quantities.product_reference.as_('product_reference'), + quantities.product_template.as_('product_template'), quantities.product.as_('product'), quantities.input_quantity.as_('input_quantity'), quantities.output_quantity.as_('output_quantity'), @@ -261,10 +297,20 @@ class _InventoryMixin: & (move.state.in_(['done', 'assigned', 'draft']))) return state_clause - @fields.depends('product') + @fields.depends('product_reference') def on_change_with_unit(self, name=None): - if self.product: - return self.product.default_uom + if self.product_reference: + return self.product_reference.default_uom + + @classmethod + def view_attributes(cls): + return super().view_attributes() + [ + ('/tree/field[@name="product_template"]', 'tree_invisible', + Eval('product_type', 'product.template') + != 'product.template'), + ('/tree/field[@name="product"]', 'tree_invisible', + Eval('product_type', 'product.template') != 'product.product'), + ] class InventoryContext(_InventoryContextMixin): @@ -459,10 +505,36 @@ class InventoryTurnover(ModelSQL, ModelView): __name__ = 'stock.reporting.inventory.turnover' company = fields.Many2One('company.company', "Company") - product = fields.Reference("Product", [ + product_reference = fields.Reference("Product", [ ('product.product', "Variant"), ('product.template', "Product"), - ]) + ], + context={ + 'company': Eval('company', -1), + }, + depends=['company']) + product_template = fields.Many2One( + 'product.template', "Product", + states={ + 'invisible': ( + Eval('context', {}).get('product_type', 'product.template') + != 'product.template'), + }, + context={ + 'company': Eval('company', -1), + }, + depends=['company']) + product = fields.Many2One( + 'product.product', "Variant", + states={ + 'invisible': ( + Eval('context', {}).get('product_type', 'product.template') + != 'product.product'), + }, + context={ + 'company': Eval('company', -1), + }, + depends=['company']) unit = fields.Function( fields.Many2One('product.uom', "Unit"), 'on_change_with_unit') @@ -506,8 +578,11 @@ class InventoryTurnover(ModelSQL, ModelView): return (inventory .select( - cls.product.sql_id(inventory.product, cls).as_('id'), + cls.product_reference.sql_id( + inventory.product_reference, cls).as_('id'), Literal(company).as_('company'), + inventory.product_reference.as_('product_reference'), + inventory.product_template.as_('product_template'), inventory.product.as_('product'), round_sql( output_quantity, @@ -518,9 +593,23 @@ class InventoryTurnover(ModelSQL, ModelView): round_sql( output_quantity / NullIf(average_quantity, 0), cls.turnover.digits[1]).as_('turnover'), - group_by=[inventory.product])) + group_by=[ + inventory.product_reference, + inventory.product_template, + inventory.product, + ])) - @fields.depends('product') + @fields.depends('product_reference') def on_change_with_unit(self, name=None): - if self.product: - return self.product.default_uom + if self.product_reference: + return self.product_reference.default_uom + + @classmethod + def view_attributes(cls): + return super().view_attributes() + [ + ('/tree/field[@name="product_template"]', 'tree_invisible', + Eval('product_type', 'product.template') + != 'product.template'), + ('/tree/field[@name="product"]', 'tree_invisible', + Eval('product_type', 'product.template') != 'product.product'), + ] diff --git a/modules/stock/stock_reporting.xml b/modules/stock/stock_reporting.xml index a59ff566e9..3827e8c50b 100644 --- a/modules/stock/stock_reporting.xml +++ b/modules/stock/stock_reporting.xml @@ -96,7 +96,7 @@ this repository contains the full copyright notices and license terms. --> stock.reporting.inventory.range.context stock.reporting.inventory.range.context >> inventory.quantity 9.0 - >>> assertEqual(inventory.product.__class__.__name__, product_type) + >>> assertEqual(inventory.product_reference.__class__.__name__, product_type) >>> with config.set_context( ... location=warehouse_loc.id, @@ -236,4 +236,4 @@ Check Inventory turnover:: 5.5 >>> turnover.turnover 0.045 - >>> assertEqual(turnover.product.__class__.__name__, product_type) + >>> assertEqual(turnover.product_reference.__class__.__name__, product_type) diff --git a/modules/stock/view/reporting_inventory_daily_form.xml b/modules/stock/view/reporting_inventory_daily_form.xml index 8a51ae27a5..5366bd6e07 100644 --- a/modules/stock/view/reporting_inventory_daily_form.xml +++ b/modules/stock/view/reporting_inventory_daily_form.xml @@ -2,8 +2,8 @@
-