From 291eb73ce109bc1df3100c0a24b0adad7cf8078d Mon Sep 17 00:00:00 2001 From: William Henrotin Date: Tue, 26 Mar 2019 08:58:11 +0000 Subject: [PATCH] [IMP] mrp: flexible component consumption Purpose: The quantities to consume on Bill of Material lines should be either strictly used or be taken as a reference more or less adjustable. This commit adds a setting on BoM to specify if the consumption is 'strict' or 'flexible'. This new option has the following impacts: On produce wizard: if consumption is set to 'strict', the done quantities are prefilled and checked when saving the wizard. If set to 'flexible', the production flow remains the same as present one. On workorders: if consumption is set to 'strict', the Validate button will save the consumed data, and propose to fill the remaining ones until the total is registered. If set to 'flexible', two button are displayed. 'Validate' to register the current component and pass to next step either the quantity to consume is complete or not, and 'Continue Consumption' to registered the current component quantity but leaving the user the possibility to add more quantity (and possibly another lot number) for the current component This commit also revert partially d3617fd852dbe471ca667f8cf3573a9327d6b064 as the warning become some sort of an error Task: 1889393 --- addons/mrp/data/mrp_demo.xml | 1 + addons/mrp/models/mrp_abstract_workorder.py | 17 ++++++ addons/mrp/models/mrp_bom.py | 7 +++ addons/mrp/models/mrp_production.py | 18 +----- addons/mrp/tests/test_order.py | 61 ++++++++++++++++++++ addons/mrp/tests/test_workorder_operation.py | 2 + addons/mrp/views/mrp_bom_views.xml | 1 + addons/mrp/views/mrp_production_views.xml | 4 +- addons/mrp/wizard/mrp_product_produce.py | 2 + 9 files changed, 93 insertions(+), 20 deletions(-) diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index c168e4902ea..b8c2014d6e1 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.xml @@ -304,6 +304,7 @@ 3 + flexible diff --git a/addons/mrp/models/mrp_abstract_workorder.py b/addons/mrp/models/mrp_abstract_workorder.py index 54b31101140..1a34ac13c68 100644 --- a/addons/mrp/models/mrp_abstract_workorder.py +++ b/addons/mrp/models/mrp_abstract_workorder.py @@ -19,6 +19,11 @@ class MrpAbstractWorkorder(models.AbstractModel): product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, readonly=True) final_lot_id = fields.Many2one('stock.production.lot', string='Lot/Serial Number', domain="[('product_id', '=', product_id)]") product_tracking = fields.Selection(related="product_id.tracking") + consumption = fields.Selection([ + ('strict', 'Strict'), + ('flexible', 'Flexible')], + required=True, + ) @api.onchange('qty_producing') def _onchange_qty_producing(self): @@ -217,6 +222,8 @@ class MrpAbstractWorkorder(models.AbstractModel): def _update_raw_moves(self): """ Once the production is done, the lots written on workorder lines are saved on stock move lines""" + # Before writting produce quantities, we ensure they respect the bom strictness + self._strict_consumption_check() vals_list = [] workorder_lines_to_process = self.workorder_line_ids.filtered(lambda line: line.qty_done > 0) for line in workorder_lines_to_process: @@ -227,6 +234,16 @@ class MrpAbstractWorkorder(models.AbstractModel): self.workorder_line_ids.unlink() self.env['stock.move.line'].create(vals_list) + def _strict_consumption_check(self): + if self.consumption == 'strict': + for move in self.move_raw_ids: + lines = self.workorder_line_ids.filtered(lambda l: l.move_id == move) + qty_done = sum(lines.mapped('qty_done')) + qty_to_consume = sum(lines.mapped('qty_to_consume')) + rounding = self.product_uom_id.rounding + if float_compare(qty_done, qty_to_consume, precision_rounding=rounding) != 0: + raise UserError(_('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.') % lines[0].product_id.name) + class MrpAbstractWorkorderLine(models.AbstractModel): _name = "mrp.abstract.workorder.line" diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 02418f2c588..284421cb283 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -61,6 +61,13 @@ class MrpBom(models.Model): 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('mrp.bom'), required=True) + consumption = fields.Selection([ + ('strict', 'Strict'), + ('flexible', 'Flexible')], + help="Defines if you can consume more or less components than the quantity defined on the BoM.", + default='strict', + string='Consumption' + ) @api.onchange('product_id') def onchange_product_id(self): diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 11c96450c2b..5c5dd19a6d2 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -177,10 +177,6 @@ class MrpProduction(models.Model): post_visible = fields.Boolean( 'Allowed to Post Inventory', compute='_compute_post_visible', help='Technical field to check when we can post') - consumed_less_than_planned = fields.Boolean( - compute='_compute_consumed_less_than_planned', - help='Technical field used to see if we have to display a warning or not when confirming an order.') - user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self._uid) company_id = fields.Many2one( 'res.company', 'Company', @@ -340,19 +336,6 @@ class MrpProduction(models.Model): else: order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids) - @api.multi - @api.depends('move_raw_ids.quantity_done', 'move_raw_ids.product_uom_qty') - def _compute_consumed_less_than_planned(self): - """ Display a warning to the user if a component of the BoM has less - quantity than planned. - """ - for order in self: - order.consumed_less_than_planned = any(order.move_raw_ids.filtered( - lambda move: float_compare(move.quantity_done, - move.product_uom_qty, - precision_rounding=move.product_uom.rounding) == -1) - ) - @api.multi @api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done', 'is_locked') def _get_produced_qty(self): @@ -740,6 +723,7 @@ class MrpProduction(models.Model): 'state': len(workorders) == 0 and 'ready' or 'pending', 'qty_producing': quantity, 'capacity': operation.workcenter_id.capacity, + 'consumption': self.bom_id.consumption, }) if workorders: workorders[-1].next_work_order_id = workorder.id diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 99827f021fe..6821c41b5ce 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -5,6 +5,7 @@ from odoo.tests import Form from datetime import datetime, timedelta from odoo.fields import Datetime as Dt +from odoo.exceptions import UserError from odoo.addons.mrp.tests.common import TestMrpCommon @@ -504,6 +505,7 @@ class TestMrpOrder(TestMrpCommon): mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='serial', qty_base_1=1, qty_final=2) self.assertEqual(len(mo), 1, 'MO should have been created') + bom.consumption = 'flexible' lot_p1_1 = self.env['stock.production.lot'].create({ 'name': 'lot1', 'product_id': p1.id, @@ -549,6 +551,7 @@ class TestMrpOrder(TestMrpCommon): mo, _, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot', qty_base_1=10, qty_final=1) self.assertEqual(len(mo), 1, 'MO should have been created') + mo.bom_id.consumption = 'flexible' first_lot_for_p1 = self.env['stock.production.lot'].create({ 'name': 'lot1', 'product_id': p1.id, @@ -701,6 +704,7 @@ class TestMrpOrder(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 4) self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1) + mo.bom_id.consumption = 'flexible' mo.action_assign() produce_form = Form(self.env['mrp.product.produce'].with_context({ @@ -723,6 +727,7 @@ class TestMrpOrder(TestMrpCommon): # try adding another product that doesn't belong to the BoM with produce_form.workorder_line_ids.new() as line: line.product_id = self.product_4 + line.qty_done = 1 produce_wizard = produce_form.save() produce_wizard.do_produce() @@ -841,6 +846,62 @@ class TestMrpOrder(TestMrpCommon): self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_location, strict=True).quantity, -1) self.assertEqual(self.env['stock.quant']._gather(p_final, self.stock_location, strict=True).quantity, 3) + def test_product_produce_9(self): + """ Check that no produce line are created when the consumed products are not tracked """ + self.stock_location = self.env.ref('stock.stock_location_stock') + mo, bom, p_final, p1, p2 = self.generate_mo() + self.assertEqual(len(mo), 1, 'MO should have been created') + + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) + + mo.action_assign() + + produce_form = Form(self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + })) + + with self.assertRaises(UserError): + # try adding another line for a bom product to increase the quantity + with produce_form.workorder_line_ids.new() as line: + line.product_id = p1 + line.qty_done = 1 + product_produce = produce_form.save() + product_produce.do_produce() + + with self.assertRaises(UserError): + # Try updating qty_done + product_produce = produce_form.save() + product_produce.workorder_line_ids[0].qty_done += 1 + product_produce.do_produce() + + with self.assertRaises(UserError): + # try adding another product + produce_form = Form(self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + })) + with produce_form.workorder_line_ids.new() as line: + line.product_id = self.product_4 + line.qty_done = 1 + product_produce = produce_form.save() + product_produce.do_produce() + + # try adding another line for a bom product but the total quantity is good + produce_form = Form(self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + })) + produce_form.qty_producing = 1 + + with produce_form.workorder_line_ids.new() as line: + line.product_id = p1 + line.qty_done = 1 + product_produce = produce_form.save() + product_produce.workorder_line_ids[1].qty_done -= 1 + product_produce.do_produce() + def test_product_produce_uom(self): plastic_laminate = self.env.ref('mrp.product_product_plastic_laminate') bom = self.env.ref('mrp.mrp_bom_plastic_laminate') diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index b182ff72c03..00bb48f31a0 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -25,6 +25,7 @@ class TestWorkOrderProcess(common.TransactionCase): self.env['stock.move'].search([('product_id', 'in', [product_bolt.id, product_screw.id])])._do_unreserve() (product_bolt + product_screw).write({'type': 'product'}) + self.env.ref("mrp.mrp_bom_desk").consumption = 'flexible' production_table_form = Form(self.env['mrp.production']) production_table_form.product_id = dining_table production_table_form.bom_id = self.env.ref("mrp.mrp_bom_desk") @@ -128,6 +129,7 @@ class TestWorkOrderProcess(common.TransactionCase): bom = self.env['mrp.bom'].browse(self.ref("mrp.mrp_bom_desk")) bom.routing_id = self.ref('mrp.mrp_routing_1') + bom.consumption = 'flexible' bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_sheet).operation_id = bom.routing_id.operation_ids[0] bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_leg).operation_id = bom.routing_id.operation_ids[1] diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml index 44f59931539..cbbd8e4c61a 100644 --- a/addons/mrp/views/mrp_bom_views.xml +++ b/addons/mrp/views/mrp_bom_views.xml @@ -73,6 +73,7 @@ + diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 80175d8c3f9..be2ead0ec44 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -35,9 +35,7 @@
- -