fix: Recalculate raw move quantities directly from BOM lines and refine decimal cleaning for production order moves and consumed quantities.

This commit is contained in:
Suherdy Yacob 2026-03-16 09:57:41 +07:00
parent 4b9efd08a8
commit 696d1a3b90

View File

@ -38,24 +38,25 @@ class MrpProduction(models.Model):
return moves return moves
def _update_raw_moves(self, factor): def _update_raw_moves(self, factor):
# Odoo core _update_raw_moves multiplies `old_qty * factor` and rounds UP. # We override this to ensure "To Consume" (product_uom_qty) is calculated from BOM line "truth"
# If factor is 0.9799118079... and old_qty is 71435, the math results in 70000.00000000004 # rather than multiplying an old (possibly noisy) quantity by a factor.
# Odoo's UP rounding elevates this dust to 70000.001!
# We must let Odoo do its thing, but then surgically clean the dust.
res = super()._update_raw_moves(factor) res = super()._update_raw_moves(factor)
clean_res = [] clean_res = []
for move, old_qty, new_qty in res: for move, old_qty, new_qty in res:
exact_qty = old_qty * factor if move.bom_line_id and self.bom_id:
# Clean floating point microscopic dust (e.g. 70000.0000000004 -> 70000.0) # Recalculate the clean truth directly from BOM
clean_exact_qty = round(exact_qty, 8) clean_factor = round(self.product_uom_id._compute_quantity(self.product_qty, self.bom_id.product_uom_id, round=False) / self.bom_id.product_qty, 8)
line_quantity = round(clean_factor * move.bom_line_id.product_qty, 8)
from odoo.tools import float_round
ideal_qty = float_round(clean_exact_qty, precision_rounding=move.product_uom.rounding, rounding_method='UP') from odoo.tools import float_round
ideal_qty = float_round(line_quantity, precision_rounding=move.product_uom.rounding, rounding_method='UP')
if move.product_uom_qty != ideal_qty:
move.write({'product_uom_qty': ideal_qty}) if move.product_uom_qty != ideal_qty:
clean_res.append((move, old_qty, ideal_qty)) move.write({'product_uom_qty': ideal_qty})
clean_res.append((move, old_qty, ideal_qty))
else:
clean_res.append((move, old_qty, new_qty))
return clean_res return clean_res
@ -132,18 +133,38 @@ class MrpProduction(models.Model):
def _clean_lingering_decimals(self): def _clean_lingering_decimals(self):
for production in self: for production in self:
if not production.bom_id:
continue
# Pre-calculate clean factor for this production
clean_factor = round(production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id, round=False) / production.bom_id.product_qty, 8)
for move in production.move_raw_ids: for move in production.move_raw_ids:
rounding = move.product_uom.rounding if not move.bom_line_id:
if move.product_uom_qty: continue
# Clean quantities that moved away from clean decimals by exactly the rounding amount
# E.g. 60.030 instead of 60.000 where component increment is 0.010 # Calculate what the quantity SHOULD be according to BOM and MO Qty
clean_qty = round(move.product_uom_qty, 2) line_quantity = round(clean_factor * move.bom_line_id.product_qty, 8)
if 0.000001 < abs(move.product_uom_qty - clean_qty) < (rounding * 1.5): from odoo.tools import float_round
move.product_uom_qty = clean_qty ideal_qty = float_round(line_quantity, precision_rounding=move.product_uom.rounding, rounding_method='UP')
for ml in move.move_line_ids:
if ml.quantity: # 1. Fix "To Consume" (product_uom_qty)
if abs(move.product_uom_qty - ideal_qty) > 0.000001:
move.product_uom_qty = ideal_qty
# 2. Fix "Consumed" (move_line_ids.quantity)
# If the line is FULLY consumed (e.g. qty_producing == product_qty),
# then move lines should match the ideal quantity.
if production.qty_producing == production.product_qty:
for ml in move.move_line_ids:
if abs(ml.quantity - ideal_qty) > 0.000001:
ml.quantity = ideal_qty
else:
# Otherwise just round to 2 decimals if it's "close enough" to a clean decimal
# but drifted by microscopic dust.
for ml in move.move_line_ids:
clean_done = round(ml.quantity, 2) clean_done = round(ml.quantity, 2)
if 0.000001 < abs(ml.quantity - clean_done) < (rounding * 1.5): if 0.000001 < abs(ml.quantity - clean_done) < (move.product_uom.rounding * 1.5):
ml.quantity = clean_done ml.quantity = clean_done
def _merge_finished_move_lines(self): def _merge_finished_move_lines(self):