From d8ae935e7482abbc6476096108ca79dc7933867c Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Mon, 25 Sep 2017 18:26:25 +0200 Subject: [PATCH] [IMP] mrp: traceability of quantity of non-tracked product When producing a finished product with a lot, also the raw materials without lot tracking should split the move lines to at least know the quantities consumed for every lot produced. This way, the traceability report will not confuse the user. --- addons/mrp/data/mrp_demo.xml | 2 +- addons/mrp/models/mrp_production.py | 4 +- addons/mrp/models/mrp_workorder.py | 12 +++-- addons/mrp/models/stock_move.py | 41 ++++++++++++++++- addons/mrp/tests/test_unbuild.py | 58 +++++++++++++++++++++++- addons/mrp/views/stock_move_views.xml | 7 +-- addons/mrp/wizard/mrp_product_produce.py | 53 +++++++--------------- 7 files changed, 128 insertions(+), 49 deletions(-) diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index 33cc801b9fc..b15716264b4 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.xml @@ -232,7 +232,7 @@ E-COM92 - + diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 1f574c1d24e..59bafc27487 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -549,8 +549,10 @@ class MrpProduction(models.Model): consume_move_lines = moves_to_do.mapped('active_move_line_ids') for moveline in moves_to_finish.mapped('active_move_line_ids'): if moveline.move_id.has_tracking != 'none': + if any([not ml.lot_produced_id for ml in consume_move_lines]): + raise UserError(_('You can not consume without telling for which lot you consumed it')) # Link all movelines in the consumed with same lot_produced_id false or the correct lot_produced_id - filtered_lines = consume_move_lines.filtered(lambda x: x.lot_produced_id == moveline.lot_id or not x.lot_produced_id) + filtered_lines = consume_move_lines.filtered(lambda x: x.lot_produced_id == moveline.lot_id) moveline.write({'consume_line_ids': [(6, 0, [x for x in filtered_lines.ids])]}) else: # Link with everything diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 18329432edd..61e7865d5cc 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -294,11 +294,15 @@ class MrpWorkorder(models.Model): # For each untracked component without any 'temporary' move lines, # (the new workorder tablet view allows registering consumed quantities for untracked components) # we assume that only the theoretical quantity was used - 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 and not move.move_line_ids.filtered(lambda m: not m.done_wo): + for move in self.move_raw_ids: + if move.has_tracking == 'none' and (move.state not in ('done', 'cancel')) and move.bom_line_id\ + and move.unit_factor and not move.move_line_ids.filtered(lambda ml: not ml.done_wo): rounding = move.product_uom.rounding - move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) + if self.product_id.tracking != 'none': + qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) + move._generate_consumed_move_line(qty_to_add, self.final_lot_id) + else: + 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_line in self.active_move_line_ids: diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 966e3365b6c..4eb94b2379a 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -68,6 +68,7 @@ class StockMove(models.Model): help='Technical Field to order moves') needs_lots = fields.Boolean('Tracking', compute='_compute_needs_lots') order_finished_lot_ids = fields.Many2many('stock.production.lot', compute='_compute_order_finished_lot_ids') + finished_lots_exist = fields.Boolean('Finished Lots Exist', compute='_compute_order_finished_lot_ids') @api.depends('active_move_line_ids.qty_done', 'active_move_line_ids.product_uom_id') def _compute_done_quantity(self): @@ -76,8 +77,13 @@ class StockMove(models.Model): @api.depends('raw_material_production_id.move_finished_ids.move_line_ids.lot_id') def _compute_order_finished_lot_ids(self): for move in self: - if move.product_id.tracking != 'none' and move.raw_material_production_id: - move.order_finished_lot_ids = move.raw_material_production_id.move_finished_ids.mapped('move_line_ids.lot_id').ids + if move.raw_material_production_id.move_finished_ids: + finished_lots_ids = move.raw_material_production_id.move_finished_ids.mapped('move_line_ids.lot_id').ids + if finished_lots_ids: + move.order_finished_lot_ids = finished_lots_ids + move.finished_lots_exist = True + else: + move.finished_lots_exist = False @api.depends('product_id.tracking') def _compute_needs_lots(self): @@ -178,6 +184,37 @@ class StockMove(models.Model): }) return self.env['stock.move'] + def _generate_consumed_move_line(self, qty_to_add, final_lot, lot=False): + if lot: + ml = self.move_line_ids.filtered(lambda ml: ml.lot_id == lot and not ml.lot_produced_id) + else: + ml = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_id) + if ml: + new_quantity_done = (ml.qty_done + qty_to_add) + if new_quantity_done >= ml.product_uom_qty: + ml.write({'qty_done': new_quantity_done, 'lot_produced_id': final_lot.id}) + else: + new_qty_reserved = ml.product_uom_qty - new_quantity_done + default = {'product_uom_qty': new_quantity_done, + 'qty_done': new_quantity_done, + 'lot_produced_id': final_lot.id} + ml.copy(default=default) + ml.with_context(bypass_reservation_update=True).write({'product_uom_qty': new_qty_reserved, 'qty_done': 0}) + else: + vals = { + 'move_id': self.id, + 'product_id': self.product_id.id, + 'location_id': self.location_id.id, + 'location_dest_id': self.location_dest_id.id, + 'product_uom_qty': 0, + 'product_uom_id': self.product_uom.id, + 'qty_done': qty_to_add, + 'lot_produced_id': final_lot.id, + } + if lot: + vals.update({'lot_id': lot.id}) + self.env['stock.move.line'].create(vals) + class PushedFlow(models.Model): _inherit = "stock.location.path" diff --git a/addons/mrp/tests/test_unbuild.py b/addons/mrp/tests/test_unbuild.py index 8c53593ab0f..0c98b64b386 100644 --- a/addons/mrp/tests/test_unbuild.py +++ b/addons/mrp/tests/test_unbuild.py @@ -269,8 +269,6 @@ class TestUnbuild(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot_1) self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5, lot_id=lot_2) mo.action_assign() - for ml in mo.move_raw_ids.mapped('move_line_ids'): - ml.qty_done = ml.product_qty produce_wizard = self.env['mrp.product.produce'].with_context({ 'active_id': mo.id, @@ -279,6 +277,8 @@ class TestUnbuild(TestMrpCommon): 'product_qty': 5.0, 'lot_id': lot_final.id, }) + for pl in produce_wizard.produce_line_ids: + pl.qty_done = pl.qty_to_consume produce_wizard.do_produce() mo.button_mark_done() @@ -399,3 +399,57 @@ class TestUnbuild(TestMrpCommon): self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 1, 'You should have get your product with lot 1 in stock') self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have the 3 basic product for lot 2 in stock') self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 2, 'You should have get one product back for lot 3') + + + def test_production_links_with_non_tracked_lots(self): + """ This test produces an MO in two times and checks that the move lines are linked in a correct way + """ + mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_1='none', tracking_base_2='lot') + lot_1 = self.env['stock.production.lot'].create({ + 'name': 'lot_1', + 'product_id': p2.id, + }) + + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_1) + lot_finished_1 = self.env['stock.production.lot'].create({ + 'name': 'lot_finished_1', + 'product_id': p_final.id, + }) + + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + }).create({ + 'product_qty': 3.0, + 'lot_id': lot_finished_1.id, + }) + + produce_wizard.produce_line_ids[0].lot_id = lot_1.id + produce_wizard.do_produce() + + lot_2 = self.env['stock.production.lot'].create({ + 'name': 'lot_2', + 'product_id': p2.id, + }) + + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 4, lot_id=lot_2) + lot_finished_2 = self.env['stock.production.lot'].create({ + 'name': 'lot_finished_2', + 'product_id': p_final.id, + }) + + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + }).create({ + 'product_qty': 2.0, + 'lot_id': lot_finished_2.id, + }) + + produce_wizard.produce_line_ids[0].lot_id = lot_2.id + produce_wizard.do_produce() + mo.button_mark_done() + ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and m.lot_produced_id == lot_finished_1) + self.assertEqual(ml.qty_done, 12.0, 'Should have consumed 12 for the first lot') + ml = mo.finished_move_line_ids[1].consume_line_ids.filtered(lambda m: m.product_id == p1 and m.lot_produced_id == lot_finished_2) + self.assertEqual(ml.qty_done, 8.0, 'Should have consumed 8 for the second lot') \ No newline at end of file diff --git a/addons/mrp/views/stock_move_views.xml b/addons/mrp/views/stock_move_views.xml index 9a1e0970836..879f576f3f9 100644 --- a/addons/mrp/views/stock_move_views.xml +++ b/addons/mrp/views/stock_move_views.xml @@ -26,7 +26,7 @@