From 347f140fe63612ee05eafa773151cdccfecdc209 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Wed, 7 Dec 2016 18:38:47 +0100 Subject: [PATCH] [FIX] mrp: rounding issues and efficiency replacement Before, when you would put a quantity in a BoM line that was not rounded to its UoM (e.g. use 0.3 piece with rounding 1.0) it would not round this quantity in the MO also (when exploding the BoM), and the user could also enter a not rounded quantity himself for the quantity produced or consumed. The moves were split with the exact quantities, but the moves were validated taking into account rounded quantities. This resulted in either moving 2 pieces instead of 1 (entering 0.7 or 1 piece) or raising an error telling you can not process moves with 0 quantity (0.3 piece). As product_efficiency and product_rounding was removed from the BoM/BoM line in v10, if the rounding is correctly handled, the feature could work like in v9, by e.g. putting 1.03 in the bom line (97% efficiency) and rounding up when exploding the BoM (e.g. producing 10 pieces would put it to 11). When "producing", we round the produced qty to the product uom (it does not make sense to produce 1.32 if your uom rounding is 1.0). The user might now change the produced quantity, that's why we round-up when validating the move: indeed, if the user slightly increase the produced value (below the uom unit), it actually means he produced more. This way, instead of not rounding anything and doing rounding behind the scenes, leading to errors, we round everything and the user sees the result directly. --- addons/mrp/models/mrp_bom.py | 5 ++++ addons/mrp/models/mrp_workorder.py | 5 ++-- addons/mrp/models/stock_move.py | 10 ++++++-- addons/mrp/tests/test_order.py | 32 ++++++++++++++++++++++++ addons/mrp/wizard/mrp_product_produce.py | 11 +++++--- 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 3feb7b498be..6ca6f8bbf4c 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -4,6 +4,7 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_round class MrpBom(models.Model): @@ -138,6 +139,10 @@ class MrpBom(models.Model): templates_done |= current_line.product_id.product_tmpl_id boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line})) else: + # We round up here because the user expects that if he has to consume a little more, the whole UOM unit + # should be consumed. + rounding = current_line.product_uom_id.rounding + line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP') lines_done.append((current_line, {'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line})) return boms_done, lines_done diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index fc0da1ecdab..23cd851b780 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -6,7 +6,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round from odoo.addons import decimal_precision as dp @@ -275,7 +275,8 @@ class MrpWorkorder(models.Model): raw_moves = self.move_raw_ids.filtered(lambda x: (x.has_tracking == 'none') and (x.state not in ('done', 'cancel')) and x.bom_line_id) for move in raw_moves: if move.unit_factor: - move.quantity_done += self.qty_producing * move.unit_factor + rounding = move.product_uom.rounding + move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) # Transfer quantities from temporary to final move lots or make them final for move_lot in self.active_move_lot_ids: diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 39f190ece58..1d67c226bca 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -3,7 +3,7 @@ from odoo import api, exceptions, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round from odoo.addons import decimal_precision as dp @@ -225,8 +225,14 @@ class StockMove(models.Model): moves_to_unreserve = self.env['stock.move'] # Create extra moves where necessary for move in moves: + # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However, + # it is possible that the user changed the value before posting the inventory by a value that should be + # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it + # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be + # consumed/produced and the moves are split correctly). rounding = move.product_uom.rounding - if float_compare(move.quantity_done, 0.0, precision_rounding=rounding) <= 0: + move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP') + if move.quantity_done <= 0: continue moves_todo |= move moves_todo |= move._create_extra_move() diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 64a42fbb136..0623a892e52 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -427,3 +427,35 @@ class TestMrpOrder(TestMrpCommon): # check the consumed quants of the newly produced quant self.assertEquals(sum(second_move.quant_ids.mapped('consumed_quant_ids').mapped('qty')), 2) + + def test_rounding(self): + """ In previous versions we had rounding and efficiency fields. We check if we can still do the same, but with only the rounding on the UoM""" + self.product_6.uom_id.rounding = 1.0 + bom_eff = self.env['mrp.bom'].create({'product_id': self.product_6.id, + 'product_tmpl_id': self.product_6.product_tmpl_id.id, + 'product_qty': 1, + 'product_uom_id': self.product_6.uom_id.id, + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': self.product_2.id, 'product_qty': 2.03}), + (0, 0, {'product_id': self.product_8.id, 'product_qty': 4.16}) + ]}) + production = self.env['mrp.production'].create({'name': 'MO efficiency test', + 'product_id': self.product_6.id, + 'product_qty': 20, + 'bom_id': bom_eff.id, + 'product_uom_id': self.product_6.uom_id.id,}) + #Check the production order has the right quantities + self.assertEqual(production.move_raw_ids[0].product_qty, 41, 'The quantity should be rounded up') + self.assertEqual(production.move_raw_ids[1].product_qty, 84, 'The quantity should be rounded up') + + # produce product + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': production.id, + 'active_ids': [production.id], + }).create({ + 'product_qty': 8, + }) + produce_wizard.do_produce() + self.assertEqual(production.move_raw_ids[0].quantity_done, 16, 'Should use half-up rounding when producing') + self.assertEqual(production.move_raw_ids[1].quantity_done, 34, 'Should use half-up rounding when producing') diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index 3ac2b1bf7c3..b636fc15ec7 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -6,7 +6,7 @@ from datetime import datetime from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round class MrpProductProduce(models.TransientModel): _name = "mrp.product.produce" @@ -80,13 +80,16 @@ class MrpProductProduce(models.TransientModel): raise UserError(_('You should at least produce some quantity')) for move in moves.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')): if move.unit_factor: - move.quantity_done_store += quantity * move.unit_factor + rounding = move.product_uom.rounding + move.quantity_done_store += float_round(quantity * move.unit_factor, precision_rounding=rounding) moves = self.production_id.move_finished_ids.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')) for move in moves: + rounding = move.product_uom.rounding if move.product_id.id == self.production_id.product_id.id: - move.quantity_done_store += quantity + move.quantity_done_store += float_round(quantity, precision_rounding=rounding) elif move.unit_factor: - move.quantity_done_store += quantity * move.unit_factor + # byproducts handling + move.quantity_done_store += float_round(quantity * move.unit_factor, precision_rounding=rounding) self.check_finished_move_lots() if self.production_id.state == 'confirmed': self.production_id.write({