[FIX] mrp: reservation rounding for under-consumption backorders

Steps to reproduce:
1. Create a BoM with a component of 1 quantity
2. Create a manufacturing order with this BoM
3. Set the quantity to produce to 5 and confirm
4. Set the producing quantity to 3.1
5. Set the quantity done for the raw material to 3.09
6. Mark the MO as done and create a backorder
7. Click 'Unreserve' on the backorder
8. 'It is not possible to unreserve more products of ...'

Before this commit:
`_compute_quantity` is called with the default rounding method,
rounding quantity UP, which introduces reservation inconsistencies.

After this commit:
`_compute_quantity` rounds with the HALF-UP method, preventing
rounding errors.
Also, use the correct UoM in float comparisons.

opw-3601737

closes odoo/odoo#144620

X-original-commit: f6c15e131e253c3780ce6daa53b0cf27f60fa464
Signed-off-by: Quentin Wolfs (quwo) <quwo@odoo.com>
Signed-off-by: Walravens Mathieu (wama) <wama@odoo.com>
This commit is contained in:
Mathieu Walravens
2023-12-05 14:57:16 +00:00
parent c4f42b813e
commit 769e1ce829
2 changed files with 34 additions and 5 deletions
+5 -5
View File
@@ -1836,19 +1836,19 @@ class MrpProduction(models.Model):
product_uom = initial_move.product_id.uom_id
if not initial_move.picked:
for move_line in initial_move.move_line_ids:
available_qty = move_line.product_uom_id._compute_quantity(move_line.quantity, product_uom)
if float_compare(available_qty, 0, precision_rounding=move_line.product_uom_id.rounding) <= 0:
available_qty = move_line.product_uom_id._compute_quantity(move_line.quantity, product_uom, rounding_method="HALF-UP")
if float_compare(available_qty, 0, precision_rounding=product_uom.rounding) <= 0:
continue
ml_by_move.append((available_qty, move_line, move_line.copy_data()[0]))
moves = list(initial_move | backorder_moves)
move = moves and moves.pop(0)
move_qty_to_reserve = move.product_qty
move_qty_to_reserve = move.product_qty # Product UoM
for index, (quantity, move_line, ml_vals) in enumerate(ml_by_move):
taken_qty = min(quantity, move_qty_to_reserve)
taken_qty_uom = product_uom._compute_quantity(taken_qty, move_line.product_uom_id)
taken_qty_uom = product_uom._compute_quantity(taken_qty, move_line.product_uom_id, rounding_method="HALF-UP")
if float_is_zero(taken_qty_uom, precision_rounding=move_line.product_uom_id.rounding):
continue
move_line.write({
@@ -1867,7 +1867,7 @@ class MrpProduction(models.Model):
while float_compare(quantity, 0, precision_rounding=product_uom.rounding) > 0 and move:
# Do not create `stock.move.line` if there is no initial demand on `stock.move`
taken_qty = min(move_qty_to_reserve, quantity)
taken_qty_uom = product_uom._compute_quantity(taken_qty, move_line.product_uom_id)
taken_qty_uom = product_uom._compute_quantity(taken_qty, move_line.product_uom_id, rounding_method="HALF-UP")
if move == initial_move:
move_line.quantity += taken_qty_uom
elif not float_is_zero(taken_qty_uom, precision_rounding=move_line.product_uom_id.rounding):
+29
View File
@@ -322,6 +322,35 @@ class TestMrpProductionBackorder(TestMrpCommon):
# 300 Grams consumed and 700 reserved
self.assertAlmostEqual(self.env['stock.quant']._gather(product_component, self.stock_location).reserved_quantity, 0.7)
def test_rounding_backorder(self):
"""test backorder component rounding doesn't introduce reservation issues"""
production, _, _, p1, p2 = self.generate_mo(qty_final=5, qty_base_1=1, qty_base_2=1)
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 100)
production.action_assign()
production_form = Form(production)
production_form.qty_producing = 3.1
production = production_form.save()
details_operation_form = Form(production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations'))
with details_operation_form.move_line_ids.edit(0) as ml:
ml.quantity = 3.09
details_operation_form.save()
action = production.button_mark_done()
backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
backorder_form.save().action_backorder()
backorder = production.procurement_group_id.mrp_production_ids[-1]
# 3.09 consumed and 1.9 reserved
self.assertAlmostEqual(self.env['stock.quant']._gather(p1, self.stock_location).reserved_quantity, 1.9)
self.assertAlmostEqual(backorder.move_raw_ids.filtered(lambda m: m.product_id == p1).move_line_ids.quantity, 1.9)
# Make sure we don't have an unreserve errors
backorder.do_unreserve()
def test_tracking_backorder_series_serial_1(self):
""" Create a MO of 4 tracked products (serial) with pbm_sam.