Skip to content

Commit 2e5c1ba

Browse files
author
Fanny He
committed
[FIX] product, decimal_precision, stock, mrp: Prevent quant reservation issue
One can set a Unit of Measure precision rounding more precise than the overall Decimal Accuracy. This can trigger the error 'It is not possible to unreserve more products', as there will be inconsistencies on reserved quantities due to decimal roundings. We add 2 warnings on Units of Measure and Decimal Accuracy. Therefore, users are warned when their UOM/decimal changes can cause inconsistencies in quants reservation. Also, we want to prevent the issue at the root by ensuring that both the move and the quant have the same reserved quantity. The contrary can happen e.g. when a product is defined in Liters (rounding .001) but used in an operation in ml (rounding .01, so e.g. 187.5ml). The conversions between the 2 UOM can cause the differences of reservations. In _prepare_move_line_vals we make sure uom_quantity_back_to_product_uom is not more precise than the overall Decimal Accuracy. That way, if the reserved quantity changes between UOM conversions, the move line created is in the quant/product UOM, and we do not have move line's reserved quantity > quantity in stock. We do the same for _update_reserved_quantity (stock.move), when updating a move line. In _generate_consumed_move_line, because of UOM conversions, new_quantity_done can differ from ml.product_uom_qty, which triggers the creation of an extra move line, leading to reserved quantity > quantity in stock. To prevent that, we compare the reserved quantities in the product UOM. opw 2206902 (Example 1) opw 2171541 opw 2221227 opw 2233937 opw 2198775 and many more closes odoo#58126 X-original-commit: 39c0c7e Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
1 parent 59c748d commit 2e5c1ba

8 files changed

Lines changed: 372 additions & 6 deletions

File tree

addons/decimal_precision/i18n/decimal_precision.pot

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ msgstr ""
9999
msgid "Usage"
100100
msgstr ""
101101

102+
#. module: decimal_precision
103+
#: code:addons/decimal_precision/models/decimal_precision.py:23
104+
#, python-format
105+
msgid "Warning!"
106+
msgstr ""
107+
108+
#. module: decimal_precision
109+
#: code:addons/decimal_precision/models/decimal_precision.py:25
110+
#, python-format
111+
msgid "You are setting a Decimal Accuracy less precise than the UOM:\n"
112+
" %s \n"
113+
"This may cause inconsistencies in reservations.\n"
114+
"Please increase the rounding of this unit of measure and the global decimal precision."
115+
msgstr ""
116+
102117
#. module: decimal_precision
103118
#: model:ir.model,name:decimal_precision.model_decimal_precision
104119
msgid "decimal.precision"

addons/decimal_precision/models/decimal_precision.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- encoding: utf-8 -*-
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

4-
from odoo import api, fields, models, tools
4+
from odoo import api, fields, models, tools, _
55

66
class DecimalPrecision(models.Model):
77
_name = 'decimal.precision'
@@ -13,6 +13,25 @@ class DecimalPrecision(models.Model):
1313
('name_uniq', 'unique (name)', """Only one value can be defined for each given usage!"""),
1414
]
1515

16+
@api.onchange('digits')
17+
def _onchange_digits(self):
18+
new_rounding = 1.0 / 10.0**self.digits
19+
dangerous_uom = self.env['uom.uom'].search([('rounding', '<', new_rounding)])
20+
if dangerous_uom:
21+
errors = ["'%s' (id=%s, precision=%s)." % (uom.name, str(uom.id), str(uom.rounding)) for uom in dangerous_uom]
22+
warning = {
23+
'title': _('Warning!'),
24+
'message':
25+
_(
26+
"You are setting a Decimal Accuracy less precise than"
27+
" the UOM:\n %s \n"
28+
"This may cause inconsistencies in reservations.\n"
29+
"Please increase the rounding of this unit of measure and the global decimal precision."
30+
) % ('\n'.join(errors))
31+
,
32+
}
33+
return {'warning': warning}
34+
1635
@api.model
1736
@tools.ormcache('application')
1837
def precision_get(self, application):

addons/mrp/models/stock_move.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def _generate_consumed_move_line(self, qty_to_add, final_lot, lot=False):
239239
qty_to_add -= quantity_to_process
240240

241241
new_quantity_done = (ml.qty_done + quantity_to_process)
242-
if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0:
242+
if float_compare(ml.product_uom_id._compute_quantity(new_quantity_done, ml.product_id.uom_id), ml.product_qty, precision_rounding=rounding) >= 0:
243243
ml.write({'qty_done': new_quantity_done, 'lot_produced_id': final_lot.id})
244244
else:
245245
new_qty_reserved = ml.product_uom_qty - new_quantity_done

addons/mrp/tests/test_order.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,3 +736,109 @@ def test_product_produce_6(self):
736736
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).mapped('quantity_done')), 200)
737737
self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id == p2).mapped('quantity_done')), 200)
738738
self.assertEqual(sum(mo.move_finished_ids.mapped('quantity_done')), 100)
739+
740+
def test_product_produce_7(self):
741+
""" Check that for products tracked by lots,
742+
with component product UOM different from UOM used in the BOM,
743+
we do not create a new move line due to extra reserved quantity
744+
caused by decimal rounding conversions.
745+
"""
746+
747+
# the overall decimal accuracy is set to 3 digits
748+
precision = self.env.ref('product.decimal_product_uom')
749+
precision.write({
750+
'digits': 3
751+
})
752+
753+
# define L and ml, L has rounding .001 but ml has rounding .01
754+
# when producing e.g. 187.5ml, it will be rounded to .188L
755+
categ_test = self.env['uom.category'].create({'name': 'Volume Test'})
756+
757+
uom_L = self.env['uom.uom'].create({
758+
'name': 'Test Liters',
759+
'category_id': categ_test.id,
760+
'uom_type': 'reference',
761+
'rounding': 0.001
762+
})
763+
764+
uom_ml = self.env['uom.uom'].create({
765+
'name': 'Test ml',
766+
'category_id': categ_test.id,
767+
'uom_type': 'smaller',
768+
'rounding': 0.01,
769+
'factor_inv': 0.001,
770+
})
771+
772+
# create a product component and the final product using the component
773+
product_comp = self.env['product.product'].create({
774+
'name': 'Product Component',
775+
'type': 'product',
776+
'tracking': 'lot',
777+
'categ_id': self.env.ref('product.product_category_all').id,
778+
'uom_id': uom_L.id,
779+
'uom_po_id': uom_L.id,
780+
})
781+
782+
product_final = self.env['product.product'].create({
783+
'name': 'Product Final',
784+
'type': 'product',
785+
'tracking': 'lot',
786+
'categ_id': self.env.ref('product.product_category_all').id,
787+
'uom_id': uom_L.id,
788+
'uom_po_id': uom_L.id,
789+
})
790+
791+
# the products are tracked by lot, so we go through _generate_consumed_move_line
792+
lot_final = self.env['stock.production.lot'].create({
793+
'name': 'Lot Final',
794+
'product_id': product_final.id,
795+
})
796+
797+
lot_comp = self.env['stock.production.lot'].create({
798+
'name': 'Lot Component',
799+
'product_id': product_comp.id,
800+
})
801+
802+
# update the quantity on hand for Component, in a lot
803+
self.stock_location = self.env.ref('stock.stock_location_stock')
804+
self.env['stock.quant']._update_available_quantity(product_comp, self.stock_location, 1, lot_id=lot_comp)
805+
806+
# create a BOM for Final, using Component
807+
test_bom = self.env['mrp.bom'].create({
808+
'product_id': product_final.id,
809+
'product_tmpl_id': product_final.product_tmpl_id.id,
810+
'product_uom_id': uom_L.id,
811+
'product_qty': 1.0,
812+
'type': 'normal',
813+
'bom_line_ids': [(0, 0, {
814+
'product_id': product_comp.id,
815+
'product_qty': 375.00,
816+
'product_uom_id': uom_ml.id
817+
})],
818+
})
819+
820+
# create a MO for this BOM
821+
mo_product_final = self.env['mrp.production'].create({
822+
'product_id': product_final.id,
823+
'product_qty': 0.5,
824+
'product_uom_id': uom_L.id,
825+
'bom_id': test_bom.id,
826+
})
827+
mo_product_final.action_assign()
828+
self.assertEqual(mo_product_final.availability, 'assigned')
829+
830+
# produce
831+
context = {"active_ids": [mo_product_final.id], "active_id": mo_product_final.id}
832+
product_final_produce = self.env['mrp.product.produce'].with_context(context).create({
833+
'product_qty': 0.5,
834+
'lot_id': lot_final.id,
835+
})
836+
for produce_line in product_final_produce.produce_line_ids:
837+
produce_line.qty_done = produce_line.qty_to_consume
838+
product_final_produce.do_produce()
839+
840+
# check that in _generate_consumed_move_line,
841+
# we do not create an extra move line because
842+
# of a conversion 187.5ml = 0.188L
843+
# thus creating an extra line with 'product_uom_qty': 0.5
844+
self.assertEqual(len(mo_product_final.move_raw_ids.move_line_ids), 1, 'One move line should exist for the MO.')

addons/product/i18n/product.pot

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,15 @@ msgstr ""
16871687

16881688
#. module: product
16891689
#: model:ir.model.fields,help:product.field_product_supplierinfo__product_code
1690+
#: code:addons/product/models/product_uom.py:63
1691+
#, python-format
1692+
msgid "This rounding precision is higher than the Decimal Accuracy (%s digits).\n"
1693+
"This may cause inconsistencies in reservations.\n"
1694+
"Please set a precision between %s and 1."
1695+
msgstr ""
1696+
1697+
#. module: product
1698+
#: model:ir.model.fields,help:product.field_product_supplierinfo_product_code
16901699
msgid "This vendor's product code will be used when printing a request for quotation. Keep empty to use the internal one."
16911700
msgstr ""
16921701

@@ -1867,6 +1876,15 @@ msgstr ""
18671876
#. module: product
18681877
#: model:ir.model.fields,field_description:product.field_product_product__weight
18691878
#: model:ir.model.fields,field_description:product.field_product_template__weight
1879+
#: code:addons/product/models/product_uom.py:63
1880+
#, python-format
1881+
msgid "Warning!"
1882+
msgstr ""
1883+
1884+
#. module: product
1885+
#: model:ir.model.fields,field_description:product.field_product_product_weight
1886+
#: model:ir.model.fields,field_description:product.field_product_template_weight
1887+
#: model:product.uom.categ,name:product.product_uom_categ_kgm
18701888
msgid "Weight"
18711889
msgstr ""
18721890

addons/stock/models/stock_move.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -835,9 +835,10 @@ def _prepare_move_line_vals(self, quantity=None, reserved_quant=None):
835835
'picking_id': self.picking_id.id,
836836
}
837837
if quantity:
838+
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
838839
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
840+
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
839841
uom_quantity_back_to_product_uom = self.product_uom._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
840-
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
841842
if float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
842843
vals = dict(vals, product_uom_qty=uom_quantity)
843844
else:
@@ -879,9 +880,9 @@ def _update_reserved_quantity(self, need, available_quantity, location_id, lot_i
879880
taken_quantity = self.product_uom._compute_quantity(taken_quantity_move_uom, self.product_id.uom_id, rounding_method='HALF-UP')
880881

881882
quants = []
883+
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
882884

883885
if self.product_id.tracking == 'serial':
884-
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
885886
if float_compare(taken_quantity, int(taken_quantity), precision_digits=rounding) != 0:
886887
taken_quantity = 0
887888

@@ -899,7 +900,11 @@ def _update_reserved_quantity(self, need, available_quantity, location_id, lot_i
899900
to_update = self.move_line_ids.filtered(lambda m: m.product_id.tracking != 'serial' and
900901
m.location_id.id == reserved_quant.location_id.id and m.lot_id.id == reserved_quant.lot_id.id and m.package_id.id == reserved_quant.package_id.id and m.owner_id.id == reserved_quant.owner_id.id)
901902
if to_update:
902-
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
903+
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
904+
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
905+
uom_quantity_back_to_product_uom = to_update[0].product_uom_id._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
906+
if to_update and float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
907+
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += uom_quantity
903908
else:
904909
if self.product_id.tracking == 'serial':
905910
for i in range(0, int(quantity)):

0 commit comments

Comments
 (0)