Use Many2One field for product and variant on stock reporting

Closes #14456
This commit is contained in:
Cédric Krier
2025-12-30 17:42:00 +01:00
parent e63358d39c
commit 06c2a56e69
11 changed files with 125 additions and 28 deletions
+105 -16
View File
@@ -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'),
]
+2 -2
View File
@@ -96,7 +96,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="context_model">stock.reporting.inventory.range.context</field>
<field
name="domain"
eval="[('product', '=', (Eval('active_model', 'product.template'), Eval('active_id', -1)))]"
eval="[('product_reference', '=', (Eval('active_model', 'product.template'), Eval('active_id', -1)))]"
pyson="1"/>
<field
name="context"
@@ -177,7 +177,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="context_model">stock.reporting.inventory.range.context</field>
<field
name="domain"
eval="[('product', '=', (Eval('active_model', 'product.template'), Eval('active_id', -1)))]"
eval="[('product_reference', '=', (Eval('active_model', 'product.template'), Eval('active_id', -1)))]"
pyson="1"/>
<field
name="context"
@@ -148,7 +148,7 @@ Check inventory::
... inventory, = Inventory.find([])
>>> 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)
@@ -2,8 +2,8 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="product"/>
<field name="product" colspan="3"/>
<label name="product_reference"/>
<field name="product_reference" colspan="3"/>
<label name="from_date"/>
<field name="from_date"/>
@@ -2,6 +2,8 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="product_reference" tree_invisible="1"/>
<field name="product_template" expand="2"/>
<field name="product" expand="2"/>
<field name="from_date" string="Date"/>
<field name="input_quantity" symbol="unit" optional="1"/>
@@ -2,8 +2,8 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="product"/>
<field name="product" colspan="3"/>
<label name="product_reference"/>
<field name="product_reference" colspan="3"/>
<label name="quantity"/>
<field name="quantity" symbol="unit"/>
@@ -2,6 +2,8 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="product_reference" tree_invisible="1"/>
<field name="product_template" expand="2"/>
<field name="product" expand="2"/>
<field name="quantity" symbol="unit"/>
</tree>
@@ -2,8 +2,8 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="product"/>
<field name="product" colspan="3"/>
<label name="product_reference"/>
<field name="product_reference" colspan="3"/>
<label name="date"/>
<field name="date"/>
@@ -2,6 +2,8 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="product_reference" tree_invisible="1"/>
<field name="product_template" expand="2"/>
<field name="product" expand="2"/>
<field name="date"/>
<field name="origin" expand="1" optional="1"/>
@@ -2,8 +2,8 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
<label name="product"/>
<field name="product" colspan="3"/>
<label name="product_reference"/>
<field name="product_reference" colspan="3"/>
<label name="output_quantity"/>
<field name="output_quantity" symbol="unit"/>
@@ -2,6 +2,8 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="product_reference" tree_invisible="1"/>
<field name="product_template" expand="2"/>
<field name="product" expand="2"/>
<field name="output_quantity" symbol="unit" optional="0"/>
<field name="average_quantity" symbol="unit" optional="0"/>