Skip to content

Commit 0bdcbbd

Browse files
committed
[FIX] product_logistics_uom: Store value in system UOM
Before this change, the volume and weight values on product were no more stored into the expected kg and m3 uoms. To know the effective value you had to convert the value using the specified uom on the record. As side effect the arithmetic operations done on these fields in others addon as for exemple in 'delivery' were not correct. This change restore the default behavior by always storing the values into the default uoms whatever the uom specified on the product. 2 new fields are added on the product to allows the user to see and managed these values in the specified uom. When values are updated through an update of these new fields, the new values are converted into the default uoms and the result stored into the original fields. fixes #1312
1 parent f386b34 commit 0bdcbbd

File tree

15 files changed

+619
-33
lines changed

15 files changed

+619
-33
lines changed

product_logistics_uom/README.rst

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Product logistics UoM
2323
:target: https://runbot.odoo-community.org/runbot/135/16.0
2424
:alt: Try me on Runbot
2525

26-
|badge1| |badge2| |badge3| |badge4| |badge5|
26+
|badge1| |badge2| |badge3| |badge4| |badge5|
2727

2828
This module allows to choose an Unit Of Measure (UoM) for products weight and volume.
2929
It can be set product per product for users in group_uom.
@@ -33,6 +33,19 @@ Without this module, you only have the choice between Kg or Lb(s) and m³ for al
3333
For some business cases, you need to express in more precise UoM than default ones like Liters
3434
instead of M³.
3535

36+
Even if you choose another UoM for the weight or volume, the system will still
37+
store the value for these fields in the Odoo default UoM (Kg or Lb(s) and m³).
38+
This ensures that the arithmetic operations on these fields are correct and
39+
consistent with the rest of the addons.
40+
41+
Once this addon is installed values stored into the initial Volume and Weight fields
42+
on the product and product template models are no more rounded to the decimal
43+
precision defined for these fields. This could lead to some side effects into
44+
reportss where these fields are used. You can replace the fields by the new
45+
ones provided by this addon to avoid this problem (product_volume and product_weight).
46+
In any cases, since you use different UoM by product, you should most probably
47+
modify your reportss to display the right UoM.
48+
3649
**Table of contents**
3750

3851
.. contents::
@@ -57,6 +70,18 @@ To change on a specific product
5770

5871
#. Go the product form you can change the UoM directly.
5972

73+
Usage
74+
=====
75+
76+
Once installed and the 'Sell and purchase products in different units of measure'
77+
option is enabled, the 'Unit of Measure' field will become updatable on the
78+
'Product' form for users with the permission 'Manage Multiple Units of Measure'.
79+
80+
If the displayed value is 0.00 and a warning icon is displayed in front of the
81+
unit of measure, it means that the value is too small to be displayed in the
82+
current unit of measure. You should change the unit of measure to a larger one
83+
to see the value.
84+
6085
Bug Tracker
6186
===========
6287

@@ -74,12 +99,14 @@ Authors
7499
~~~~~~~
75100

76101
* Akretion
102+
* ACSONE SA/NV
77103

78104
Contributors
79105
~~~~~~~~~~~~
80106

81107
* Raphaël Reverdy <[email protected]>
82108
* Fernando La Chica <[email protected]>
109+
* Laurent Mignon <[email protected]>
83110

84111
Other credits
85112
~~~~~~~~~~~~~
@@ -108,7 +135,7 @@ promote its widespread use.
108135

109136
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
110137

111-
|maintainer-hparfr|
138+
|maintainer-hparfr|
112139

113140
This module is part of the `OCA/product-attribute <https://github.com/OCA/product-attribute/tree/16.0/product_logistics_uom>`_ project on GitHub.
114141

product_logistics_uom/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from . import models
2+
from .hooks import pre_init_hook

product_logistics_uom/__manifest__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
{
44
"name": "Product logistics UoM",
55
"summary": "Configure product weights and volume UoM",
6-
"version": "16.0.1.0.0",
6+
"version": "16.0.2.0.0",
77
"development_status": "Beta",
88
"category": "Product",
99
"website": "https://github.com/OCA/product-attribute",
10-
"author": " Akretion, Odoo Community Association (OCA)",
10+
"author": " Akretion, ACSONE SA/NV, Odoo Community Association (OCA)",
1111
"maintainers": ["hparfr"],
1212
"license": "AGPL-3",
1313
"installable": True,
@@ -18,4 +18,5 @@
1818
"views/res_config_settings.xml",
1919
"views/product.xml",
2020
],
21+
"pre_init_hook": "pre_init_hook",
2122
}

product_logistics_uom/hooks.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Copyright 2023 ACSONE SA
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
3+
4+
import logging
5+
6+
from odoo.tools import sql
7+
8+
_logger = logging.getLogger(__name__)
9+
10+
11+
def pre_init_hook(cr): # pragma: nocover
12+
"""Recompute the volume and weight column on product and template
13+
by converting the value from the uom defined on the product to the default uom
14+
"""
15+
if sql.column_exists(cr, "product_template", "volume_uom_id"):
16+
_logger.info("Recompute volume on product.product")
17+
# get default m3 uom
18+
cr.execute(
19+
"""
20+
SELECT res_id
21+
FROM ir_model_data
22+
WHERE module = 'uom' AND name = 'product_uom_cubic_meter'
23+
"""
24+
)
25+
m3_uom_id = cr.fetchone()[0]
26+
# get uom factor
27+
cr.execute(
28+
"""
29+
SELECT factor
30+
FROM uom_uom
31+
WHERE id = %s
32+
""",
33+
(m3_uom_id,),
34+
)
35+
m3_uom_factor = cr.fetchone()[0]
36+
# update volume where volume_uom_id is not null and not m3
37+
cr.execute(
38+
"""
39+
UPDATE product_product
40+
SET volume = product_product.volume / product_uom.factor * %s
41+
FROM uom_uom product_uom,
42+
product_template pt
43+
WHERE product_uom.id = volume_uom_id
44+
AND pt.id = product_product.product_tmpl_id
45+
AND volume_uom_id IS NOT NULL AND pt.volume_uom_id != %s
46+
""",
47+
(m3_uom_factor, m3_uom_id),
48+
)
49+
_logger.info(f"{cr.rowcount} product_product rows updated")
50+
# update product_template with 1 product_product
51+
cr.execute(
52+
"""
53+
UPDATE product_template
54+
SET Volume = unique_product.volume
55+
FROM (
56+
SELECT product_tmpl_id, volume
57+
FROM product_product
58+
WHERE volume is not null
59+
GROUP BY product_tmpl_id, volume
60+
HAVING COUNT(*) = 1
61+
) unique_product
62+
WHERE product_template.id = unique_product.product_tmpl_id
63+
AND product_template.volume_uom_id != %s
64+
""",
65+
(m3_uom_id,),
66+
)
67+
_logger.info(f"{cr.rowcount} product_template rows updated")
68+
if sql.column_exists(cr, "product_template", "weight_uom_id"):
69+
_logger.info("Recompute weight on product.product")
70+
# get default kg uom
71+
cr.execute(
72+
"""
73+
SELECT res_id
74+
FROM ir_model_data
75+
WHERE module = 'uom' AND name = 'product_uom_kgm'
76+
"""
77+
)
78+
kg_uom_id = cr.fetchone()[0]
79+
# get uom factor
80+
cr.execute(
81+
"""
82+
SELECT factor
83+
FROM uom_uom
84+
WHERE id = %s
85+
""",
86+
(kg_uom_id,),
87+
)
88+
kg_uom_factor = cr.fetchone()[0]
89+
# update weight where weight_uom_id is not null and not kg
90+
cr.execute(
91+
"""
92+
UPDATE product_product
93+
SET weight = product_product.weight / product_uom.factor * %s
94+
FROM uom_uom product_uom, product_template pt
95+
WHERE product_uom.id = weight_uom_id
96+
AND pt.id = product_product.product_tmpl_id
97+
AND weight_uom_id IS NOT NULL AND pt.weight_uom_id != %s
98+
""",
99+
(kg_uom_factor, kg_uom_id),
100+
)
101+
_logger.info(f"{cr.rowcount} product_product rows updated")
102+
# update product_template with 1 product_product
103+
cr.execute(
104+
"""
105+
UPDATE product_template
106+
SET weight = unique_product.weight
107+
FROM (
108+
SELECT product_tmpl_id, weight
109+
FROM product_product
110+
WHERE volume is not null
111+
GROUP BY product_tmpl_id, weight
112+
HAVING COUNT(*) = 1
113+
) unique_product
114+
WHERE product_template.id = unique_product.product_tmpl_id
115+
AND product_template.weight_uom_id != %s
116+
""",
117+
(kg_uom_id,),
118+
)
119+
_logger.info(f"{cr.rowcount} product_template rows updated")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright 2023 ACSONE SA
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
3+
4+
# pylint: disable=odoo-addons-relative-import
5+
from odoo.addons.product_logistics_uom.hooks import pre_init_hook
6+
7+
8+
def migrate(cr, version):
9+
"""Migrate data from product_logistics_uom module."""
10+
pre_init_hook(cr)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
from . import product_product
12
from . import product_template
23
from . import res_config_settings
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Copyright 2023 ACSONE SA
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
3+
4+
from odoo import api, fields, models
5+
from odoo.tools import float_is_zero
6+
7+
8+
class ProductProduct(models.Model):
9+
_inherit = "product.product"
10+
11+
# remove rounding from volume and weight
12+
# this is needed to avoid rounding errors when converting between units
13+
# and is safe since we display the volume and weight in the product's
14+
# volume and weight UOM. In the same time, we need to keep the volume
15+
# we ensure that no information is lost by storing the volume and weight
16+
# without rounding.
17+
volume = fields.Float(digits=False)
18+
weight = fields.Float(digits=False)
19+
20+
product_volume = fields.Float(
21+
"Volume in product UOM",
22+
digits="Volume",
23+
help="The volume in the product's volume UOM.",
24+
compute="_compute_product_volume",
25+
inverse="_inverse_product_volume",
26+
)
27+
product_weight = fields.Float(
28+
"Weight in product UOM",
29+
digits="Stock Weight",
30+
help="The weight in the product's weight UOM.",
31+
compute="_compute_product_weight",
32+
inverse="_inverse_product_weight",
33+
)
34+
35+
show_volume_uom_warning = fields.Boolean(
36+
help="Technical field used to warn the user to change the volume"
37+
"uom since the value for product_volume is too small and has been"
38+
"rounded.",
39+
compute="_compute_show_volume_uom_warning",
40+
)
41+
42+
show_weight_uom_warning = fields.Boolean(
43+
help="Technical field used to warn the user to change the weight"
44+
"uom since the value for product_weight is too small and has been"
45+
"rounded.",
46+
compute="_compute_show_weight_uom_warning",
47+
)
48+
49+
@api.depends("product_volume", "product_tmpl_id.volume_uom_id")
50+
def _compute_product_volume(self):
51+
odoo_volume_uom = (
52+
self.product_tmpl_id._get_volume_uom_id_from_ir_config_parameter()
53+
)
54+
for product in self:
55+
product.product_volume = odoo_volume_uom._compute_quantity(
56+
qty=product.volume,
57+
to_unit=product.volume_uom_id,
58+
round=False, # avoid losing information
59+
)
60+
61+
def _inverse_product_volume(self):
62+
odoo_volume_uom = (
63+
self.product_tmpl_id._get_volume_uom_id_from_ir_config_parameter()
64+
)
65+
for product in self:
66+
product.volume = product.volume_uom_id._compute_quantity(
67+
qty=product.product_volume,
68+
to_unit=odoo_volume_uom,
69+
round=False, # avoid losing information
70+
)
71+
72+
@api.depends("product_weight", "product_tmpl_id.weight_uom_id")
73+
def _compute_product_weight(self):
74+
odoo_weight_uom = (
75+
self.product_tmpl_id._get_weight_uom_id_from_ir_config_parameter()
76+
)
77+
for product in self:
78+
product.product_weight = odoo_weight_uom._compute_quantity(
79+
qty=product.weight,
80+
to_unit=product.weight_uom_id,
81+
round=False, # avoid losing information
82+
)
83+
84+
def _inverse_product_weight(self):
85+
odoo_weight_uom = (
86+
self.product_tmpl_id._get_weight_uom_id_from_ir_config_parameter()
87+
)
88+
for product in self:
89+
product.weight = product.weight_uom_id._compute_quantity(
90+
qty=product.product_weight, to_unit=odoo_weight_uom, round=False
91+
)
92+
93+
@api.depends("product_volume", "product_tmpl_id.volume_uom_id", "volume")
94+
def _compute_show_volume_uom_warning(self):
95+
odoo_volume_uom = (
96+
self.product_tmpl_id._get_volume_uom_id_from_ir_config_parameter()
97+
)
98+
for product in self:
99+
product.show_volume_uom_warning = (
100+
float_is_zero(
101+
product.product_volume, precision_rounding=odoo_volume_uom.rounding
102+
)
103+
and product.volume != 0.0
104+
)
105+
106+
@api.depends("product_weight", "product_tmpl_id.weight_uom_id", "weight")
107+
def _compute_show_weight_uom_warning(self):
108+
odoo_weight_uom = (
109+
self.product_tmpl_id._get_weight_uom_id_from_ir_config_parameter()
110+
)
111+
for product in self:
112+
product.show_weight_uom_warning = (
113+
float_is_zero(
114+
product.product_weight, precision_rounding=odoo_weight_uom.rounding
115+
)
116+
and product.weight != 0.0
117+
)

0 commit comments

Comments
 (0)