From 8d0b08752535e82a17af9166c9469b1bae8d5afa Mon Sep 17 00:00:00 2001 From: yhu-odoo Date: Fri, 19 Nov 2021 13:40:51 +0000 Subject: [PATCH] [IMP] mrp: enable manual consumption on components Currently, when changing qty_producing on a MO, consumed number of each component will always be updated. In this commit, we add a new boolean field manual_consumption to bom.line. If checked, that line won't be updated when changing qty_producing. The same but invisible field manual_consumption is also added to each line on MO. It will use the value on BOM as defualt value. When user manually set the consued qty, it will be checked automatically. Also, when user manually update the SML, the field on SM will also be set to true. Task-2687494 closes odoo/odoo#80118 Related: odoo/enterprise#25607 Signed-off-by: William Henrotin (whe) --- addons/mrp/models/mrp_bom.py | 10 ++ addons/mrp/models/stock_move.py | 27 +++- addons/mrp/static/src/scss/mrp_fields.scss | 4 + addons/mrp/tests/test_order.py | 142 +++++++++++++++++++ addons/mrp/views/mrp_bom_views.xml | 2 + addons/mrp/views/mrp_production_views.xml | 6 +- addons/mrp_account/tests/test_mrp_account.py | 1 + 7 files changed, 189 insertions(+), 3 deletions(-) diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 004283a1fc8..7288f7a54d7 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -70,6 +70,7 @@ class MrpBom(models.Model): help="Defines if you can consume more or less components than the quantity defined on the BoM:\n" " * Allowed: allowed for all manufacturing users.\n" " * Allowed with warning: allowed for all manufacturing users with summary of consumption differences when closing the manufacturing order.\n" + " Note that in the case of component Manual Consumption, where consumption is registered manually exclusively, consumption warnings will still be issued when appropriate also.\n" " * Blocked: only a manager can close a manufacturing order when the BoM consumption is not respected.", default='warning', string='Flexible Consumption', @@ -392,6 +393,11 @@ class MrpBomLine(models.Model): 'mrp.bom.line', string="BOM lines of the referred bom", compute='_compute_child_line_ids') attachments_count = fields.Integer('Attachments Count', compute='_compute_attachments_count') + tracking = fields.Selection(related='product_id.tracking') + manual_consumption = fields.Boolean( + 'Manual Consumption', default=False, compute='_compute_manual_consumption', store=True, copy=True, + help="When activated, then the registration of consumption for that component is recorded manually exclusively.\n" + "If not activated, and any of the components consumption is edited manually on the manufacturing order, Odoo assumes manual consumption also.") _sql_constraints = [ ('bom_qty_zero', 'CHECK (product_qty>=0)', 'All product quantities must be greater or equal to 0.\n' @@ -399,6 +405,10 @@ class MrpBomLine(models.Model): 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'), ] + @api.depends('product_id', 'tracking') + def _compute_manual_consumption(self): + self.filtered(lambda m: m.tracking != 'none').manual_consumption = True + @api.depends('product_id', 'bom_id') def _compute_child_bom_id(self): for line in self: diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 89dbad21c84..8d460f68a6d 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -140,6 +140,17 @@ class StockMove(models.Model): product_qty_available = fields.Float('Product On Hand Quantity', related='product_id.qty_available') product_virtual_available = fields.Float('Product Forecasted Quantity', related='product_id.virtual_available') description_bom_line = fields.Char('Kit', compute='_compute_description_bom_line') + manual_consumption = fields.Boolean( + 'Manual Consumption', compute='_compute_manual_consumption', store=True, + help="When activated, then the registration of consumption for that component is recorded manually exclusively.\n" + "If not activated, and any of the components consumption is edited manually on the manufacturing order, Odoo assumes manual consumption also.") + + @api.depends('state', 'product_id', 'operation_id') + def _compute_manual_consumption(self): + for move in self: + if move.state != 'draft': + continue + move.manual_consumption = move.bom_line_id.manual_consumption or move.product_id.tracking != 'none' @api.depends('bom_line_id') def _compute_description_bom_line(self): @@ -226,6 +237,12 @@ class StockMove(models.Model): mo = self.raw_material_production_id self._update_quantity_done(mo) + @api.onchange('quantity_done') + def _onchange_quantity_done(self): + if self.raw_material_production_id and not self.manual_consumption and \ + self.should_consume_qty != self.quantity_done: + self.manual_consumption = True + @api.model def default_get(self, fields_list): defaults = super(StockMove, self).default_get(fields_list) @@ -250,6 +267,9 @@ class StockMove(models.Model): - Moves from a copied MO - Backorders """ + if self.env.context.get('force_manual_consumption'): + for vals in vals_list: + vals['manual_consumption'] = True mo_id_to_mo = defaultdict(lambda: self.env['mrp.production']) product_id_to_product = defaultdict(lambda: self.env['product.product']) for values in vals_list: @@ -278,6 +298,8 @@ class StockMove(models.Model): return super().create(vals_list) def write(self, vals): + if self.env.context.get('force_manual_consumption'): + vals['manual_consumption'] = True if 'product_uom_qty' in vals and 'move_line_ids' in vals: # first update lines then product_uom_qty as the later will unreserve # so possibly unlink lines @@ -340,6 +362,7 @@ class StockMove(models.Model): if self.raw_material_production_id: action['views'] = [(self.env.ref('mrp.view_stock_move_operations_raw').id, 'form')] action['context']['show_destination_location'] = False + action['context']['force_manual_consumption'] = True elif self.production_id: action['views'] = [(self.env.ref('mrp.view_stock_move_operations_finished').id, 'form')] action['context']['show_source_location'] = False @@ -395,6 +418,7 @@ class StockMove(models.Model): return { 'state': 'confirmed', 'reservation_date': self.reservation_date, + 'manual_consumption': self.bom_line_id.manual_consumption or self.product_id.tracking != 'none', 'move_orig_ids': [Command.link(m.id) for m in self.mapped('move_orig_ids')], 'move_dest_ids': [Command.link(m.id) for m in self.mapped('move_dest_ids')] } @@ -424,7 +448,8 @@ class StockMove(models.Model): # Do not update extra product quantities if float_is_zero(self.product_uom_qty, precision_rounding=self.product_uom.rounding): return True - if self.has_tracking != 'none' or self.state == 'done': + if self.has_tracking != 'none' or self.state == 'done' or \ + self.manual_consumption or self._origin.manual_consumption: return True return False diff --git a/addons/mrp/static/src/scss/mrp_fields.scss b/addons/mrp/static/src/scss/mrp_fields.scss index e442db35271..e952f35588d 100644 --- a/addons/mrp/static/src/scss/mrp_fields.scss +++ b/addons/mrp/static/src/scss/mrp_fields.scss @@ -17,3 +17,7 @@ filter: brightness(0.9); } } + +.o_manual_consumption.font-weight-bold { + background-color: #D3D3D370; +} diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index f919181fd5f..c58488c8fb5 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -395,6 +395,148 @@ class TestMrpOrder(TestMrpCommon): production = production_form.save() production.button_mark_done() + def test_update_quantity_6(self): + """Test manual consumption mechanism. Test when manual consumption is + True, quantity_done won't be updated automatically. Bom line with tracked + products or operations should be set to manual consumption automatically. + Also test that when manually change quantity_done, manual consumption + will be set to True. Also test when create backorder, the namual consumption + should be set according to the bom. + """ + Product = self.env['product.product'] + product_nt = Product.create({ + 'name': 'No tracking', + 'type': 'product', + 'tracking': 'none',}) + product_sn = Product.create({ + 'name': 'Serial', + 'type': 'product', + 'tracking': 'serial',}) + product_lot = Product.create({ + 'name': 'Lot', + 'type': 'product', + 'tracking': 'lot',}) + bom = 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': product_nt.id, 'product_qty': 1}), + (0, 0, {'product_id': product_sn.id, 'product_qty': 1}), + (0, 0, {'product_id': product_lot.id, 'product_qty': 1}), + ], + }) + + # Bom linse with tracking products or operations should have manual_consumption set + self.assertEqual(bom.bom_line_ids.mapped('manual_consumption'), [False, True, True]) + + production_form = Form(self.env['mrp.production']) + production_form.product_id = self.product_6 + production_form.bom_id = bom + production_form.product_qty = 10 + production_form.product_uom_id = self.product_6.uom_id + production = production_form.save() + production.action_confirm() + production.action_assign() + production.is_locked = False + + # test no updating + production_form = Form(production) + production_form.qty_producing = 5 + production = production_form.save() + move_nt, move_sn, move_lot = production.move_raw_ids + self.assertEqual(move_nt.manual_consumption, False) + self.assertEqual(move_nt.quantity_done, 5) + self.assertEqual(move_sn.manual_consumption, True) + self.assertEqual(move_sn.quantity_done, 0) + self.assertEqual(move_lot.manual_consumption, True) + self.assertEqual(move_lot.quantity_done, 0) + + # test manual change + with production_form.move_raw_ids.edit(0) as move_product_2_line: + move_product_2_line.quantity_done = 6 + production = production_form.save() + self.assertEqual(move_nt.manual_consumption, True) + production_form.qty_producing = 8 + production = production_form.save() + self.assertEqual(move_nt.quantity_done, 6) + + # test manual consumption on backorder + action = production.button_mark_done() + warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context'])).save() + action = warning.action_confirm() + backorder_wizard = Form(self.env['mrp.production.backorder'].with_context(**action['context'])).save() + backorder_wizard.action_backorder() + backorder = production.procurement_group_id.mrp_production_ids[-1] + self.assertEqual(backorder.move_raw_ids.mapped('manual_consumption'), [False, True, True]) + + def test_update_quantity_7(self): + """Test manual consumption mechanism without bom. Test when manual consumption + is True, quantity_done won't be updated automatically. Component line with + tracked products should be set to manual consumption automatically. + Also test that when manually change quantity_done, manual consumption + will be set to True. Also test when create backorder, the namual consumption + should be set according to the product's tracking. + """ + Product = self.env['product.product'] + product_nt = Product.create({ + 'name': 'No tracking', + 'type': 'product', + 'tracking': 'none',}) + product_sn = Product.create({ + 'name': 'Serial', + 'type': 'product', + 'tracking': 'serial',}) + product_lot = Product.create({ + 'name': 'Lot', + 'type': 'product', + 'tracking': 'lot',}) + + production_form = Form(self.env['mrp.production']) + production_form.product_id = self.product_6 + production_form.bom_id = self.env['mrp.bom'] + production_form.product_qty = 10 + production_form.product_uom_id = self.product_6.uom_id + for product in [product_nt, product_sn, product_lot]: + with production_form.move_raw_ids.new() as line: + line.product_id = product + line.product_uom_qty = 10 + production = production_form.save() + + self.assertEqual(production.move_raw_ids.mapped('manual_consumption'), [False, True, True]) + + # test no updating + production_form = Form(production) + production_form.qty_producing = 5 + production = production_form.save() + move_nt, move_sn, move_lot = production.move_raw_ids + self.assertEqual(move_nt.manual_consumption, False) + self.assertEqual(move_nt.quantity_done, 5) + self.assertEqual(move_sn.manual_consumption, True) + self.assertEqual(move_sn.quantity_done, 0) + self.assertEqual(move_lot.manual_consumption, True) + self.assertEqual(move_lot.quantity_done, 0) + + # test manual change + with production_form.move_raw_ids.edit(0) as move_product_2_line: + move_product_2_line.quantity_done = 6 + production = production_form.save() + self.assertEqual(move_nt.manual_consumption, True) + production_form.qty_producing = 8 + production = production_form.save() + self.assertEqual(move_nt.quantity_done, 6) + + # test manual consumption on backorder + action = production.button_mark_done() + warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context'])).save() + action = warning.action_confirm() + backorder_wizard = Form(self.env['mrp.production.backorder'].with_context(**action['context'])).save() + backorder_wizard.action_backorder() + backorder = production.procurement_group_id.mrp_production_ids[-1] + self.assertEqual(backorder.move_raw_ids.mapped('manual_consumption'), [False, True, True]) + def test_update_plan_date(self): """Editing the scheduled date after planning the MO should unplan the MO, and adjust the date on the stock moves""" planned_date = datetime(2023, 5, 15, 9, 0) diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml index 67226f38c60..cf799305be3 100644 --- a/addons/mrp/views/mrp_bom_views.xml +++ b/addons/mrp/views/mrp_bom_views.xml @@ -96,6 +96,8 @@ + + diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index c0bad5d35d7..e29d9b80e0e 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -274,7 +274,7 @@ - + @@ -310,10 +310,12 @@