Skip to content

Commit 0db882c

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#58173 X-original-commit: 2e5c1ba Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
1 parent 47231e9 commit 0db882c

8 files changed

Lines changed: 369 additions & 6 deletions

File tree

addons/decimal_precision/i18n/decimal_precision.pot

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,17 @@ msgstr ""
110110
msgid "Usage"
111111
msgstr ""
112112

113+
#. module: decimal_precision
114+
#: code:addons/decimal_precision/models/decimal_precision.py:24
115+
#, python-format
116+
msgid "Warning!"
117+
msgstr ""
118+
119+
#. module: decimal_precision
120+
#: code:addons/decimal_precision/models/decimal_precision.py:26
121+
#, python-format
122+
msgid "You are setting a Decimal Accuracy less precise than the UOM:\n"
123+
" %s \n"
124+
"This may cause inconsistencies in reservations.\n"
125+
"Please increase the rounding of this unit of measure and the global decimal precision."
126+
msgstr ""

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'
@@ -14,6 +14,25 @@ class DecimalPrecision(models.Model):
1414
('name_uniq', 'unique (name)', """Only one value can be defined for each given usage!"""),
1515
]
1616

17+
@api.onchange('digits')
18+
def _onchange_digits(self):
19+
new_rounding = 1.0 / 10.0**self.digits
20+
dangerous_uom = self.env['uom.uom'].search([('rounding', '<', new_rounding)])
21+
if dangerous_uom:
22+
errors = ["'%s' (id=%s, precision=%s)." % (uom.name, str(uom.id), str(uom.rounding)) for uom in dangerous_uom]
23+
warning = {
24+
'title': _('Warning!'),
25+
'message':
26+
_(
27+
"You are setting a Decimal Accuracy less precise than"
28+
" the UOM:\n %s \n"
29+
"This may cause inconsistencies in reservations.\n"
30+
"Please increase the rounding of this unit of measure and the global decimal precision."
31+
) % ('\n'.join(errors))
32+
,
33+
}
34+
return {'warning': warning}
35+
1736
@api.model
1837
@tools.ormcache('application')
1938
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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,3 +894,107 @@ def test_product_produce_uom(self):
894894
move_line_finished = mo.move_finished_ids.mapped('move_line_ids').filtered(lambda m: m.qty_done)
895895
self.assertEqual(move_line_finished.qty_done, 1)
896896
self.assertEqual(move_line_finished.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.')
897+
898+
def test_product_produce_7(self):
899+
""" Check that for products tracked by lots,
900+
with component product UOM different from UOM used in the BOM,
901+
we do not create a new move line due to extra reserved quantity
902+
caused by decimal rounding conversions.
903+
"""
904+
905+
# the overall decimal accuracy is set to 3 digits
906+
precision = self.env.ref('product.decimal_product_uom')
907+
precision.write({
908+
'digits': 3
909+
})
910+
911+
# define L and ml, L has rounding .001 but ml has rounding .01
912+
# when producing e.g. 187.5ml, it will be rounded to .188L
913+
categ_test = self.env['uom.category'].create({'name': 'Volume Test'})
914+
915+
uom_L = self.env['uom.uom'].create({
916+
'name': 'Test Liters',
917+
'category_id': categ_test.id,
918+
'uom_type': 'reference',
919+
'rounding': 0.001
920+
})
921+
922+
uom_ml = self.env['uom.uom'].create({
923+
'name': 'Test ml',
924+
'category_id': categ_test.id,
925+
'uom_type': 'smaller',
926+
'rounding': 0.01,
927+
'factor_inv': 0.001,
928+
})
929+
930+
# create a product component and the final product using the component
931+
product_comp = self.env['product.product'].create({
932+
'name': 'Product Component',
933+
'type': 'product',
934+
'tracking': 'lot',
935+
'categ_id': self.env.ref('product.product_category_all').id,
936+
'uom_id': uom_L.id,
937+
'uom_po_id': uom_L.id,
938+
})
939+
940+
product_final = self.env['product.product'].create({
941+
'name': 'Product Final',
942+
'type': 'product',
943+
'tracking': 'lot',
944+
'categ_id': self.env.ref('product.product_category_all').id,
945+
'uom_id': uom_L.id,
946+
'uom_po_id': uom_L.id,
947+
})
948+
949+
# the products are tracked by lot, so we go through _generate_consumed_move_line
950+
lot_final = self.env['stock.production.lot'].create({
951+
'name': 'Lot Final',
952+
'product_id': product_final.id,
953+
})
954+
955+
lot_comp = self.env['stock.production.lot'].create({
956+
'name': 'Lot Component',
957+
'product_id': product_comp.id,
958+
})
959+
960+
# update the quantity on hand for Component, in a lot
961+
self.stock_location = self.env.ref('stock.stock_location_stock')
962+
self.env['stock.quant']._update_available_quantity(product_comp, self.stock_location, 1, lot_id=lot_comp)
963+
964+
# create a BOM for Final, using Component
965+
test_bom = self.env['mrp.bom'].create({
966+
'product_id': product_final.id,
967+
'product_tmpl_id': product_final.product_tmpl_id.id,
968+
'product_uom_id': uom_L.id,
969+
'product_qty': 1.0,
970+
'type': 'normal',
971+
'bom_line_ids': [(0, 0, {
972+
'product_id': product_comp.id,
973+
'product_qty': 375.00,
974+
'product_uom_id': uom_ml.id
975+
})],
976+
})
977+
978+
# create a MO for this BOM
979+
mo_product_final = self.env['mrp.production'].create({
980+
'product_id': product_final.id,
981+
'product_qty': 0.5,
982+
'product_uom_id': uom_L.id,
983+
'bom_id': test_bom.id,
984+
})
985+
mo_product_final.action_assign()
986+
self.assertEqual(mo_product_final.availability, 'assigned')
987+
988+
# produce
989+
context = {"active_ids": [mo_product_final.id], "active_id": mo_product_final.id}
990+
produce_form = Form(self.env['mrp.product.produce'].with_context(context))
991+
produce_form.product_qty = 0.5
992+
produce_form.lot_id = lot_final
993+
produce_wizard = produce_form.save()
994+
produce_wizard.do_produce()
995+
996+
# check that in _generate_consumed_move_line,
997+
# we do not create an extra move line because
998+
# of a conversion 187.5ml = 0.188L
999+
# thus creating an extra line with 'product_uom_qty': 0.5
1000+
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
@@ -2141,6 +2141,15 @@ msgstr ""
21412141

21422142
#. module: product
21432143
#: model:ir.model.fields,help:product.field_product_supplierinfo__product_code
2144+
#: code:addons/product/models/product_uom.py:63
2145+
#, python-format
2146+
msgid "This rounding precision is higher than the Decimal Accuracy (%s digits).\n"
2147+
"This may cause inconsistencies in reservations.\n"
2148+
"Please set a precision between %s and 1."
2149+
msgstr ""
2150+
2151+
#. module: product
2152+
#: model:ir.model.fields,help:product.field_product_supplierinfo_product_code
21442153
msgid "This vendor's product code will be used when printing a request for quotation. Keep empty to use the internal one."
21452154
msgstr ""
21462155

@@ -2450,6 +2459,15 @@ msgstr ""
24502459
#. module: product
24512460
#: model:ir.model.fields,field_description:product.field_product_product__weight
24522461
#: model:ir.model.fields,field_description:product.field_product_template__weight
2462+
#: code:addons/product/models/product_uom.py:63
2463+
#, python-format
2464+
msgid "Warning!"
2465+
msgstr ""
2466+
2467+
#. module: product
2468+
#: model:ir.model.fields,field_description:product.field_product_product_weight
2469+
#: model:ir.model.fields,field_description:product.field_product_template_weight
2470+
#: model:product.uom.categ,name:product.product_uom_categ_kgm
24532471
msgid "Weight"
24542472
msgstr ""
24552473

addons/stock/models/stock_move.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -824,9 +824,10 @@ def _prepare_move_line_vals(self, quantity=None, reserved_quant=None):
824824
'picking_id': self.picking_id.id,
825825
}
826826
if quantity:
827+
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
827828
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
829+
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
828830
uom_quantity_back_to_product_uom = self.product_uom._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
829-
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
830831
if float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
831832
vals = dict(vals, product_uom_qty=uom_quantity)
832833
else:
@@ -868,9 +869,9 @@ def _update_reserved_quantity(self, need, available_quantity, location_id, lot_i
868869
taken_quantity = self.product_uom._compute_quantity(taken_quantity_move_uom, self.product_id.uom_id, rounding_method='HALF-UP')
869870

870871
quants = []
872+
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
871873

872874
if self.product_id.tracking == 'serial':
873-
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
874875
if float_compare(taken_quantity, int(taken_quantity), precision_digits=rounding) != 0:
875876
taken_quantity = 0
876877

@@ -889,7 +890,11 @@ def _update_reserved_quantity(self, need, available_quantity, location_id, lot_i
889890
to_update = self.move_line_ids.filtered(lambda m: m.product_id.tracking != 'serial' and
890891
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)
891892
if to_update:
892-
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')
893+
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
894+
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
895+
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')
896+
if to_update and float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
897+
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += uom_quantity
893898
else:
894899
if self.product_id.tracking == 'serial':
895900
for i in range(0, int(quantity)):

0 commit comments

Comments
 (0)