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 @@