diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py index bf35aa22abb..73dbb148f75 100644 --- a/addons/mrp/tests/test_traceability.py +++ b/addons/mrp/tests/test_traceability.py @@ -434,3 +434,96 @@ class TestTraceability(TestMrpCommon): self.assertEqual(lot_B02.product_qty, 0) self.assertEqual(lot_B03.product_qty, 15) self.assertEqual(productA.qty_available, 15) + + def test_last_delivery_traceability(self): + """ + Suppose this structure (-> means 'produces') + 1 x Subcomponent A -> 1 x Component A -> 1 x EndProduct A + All three tracked by lots. Ensure that after validating Picking A (out) + for EndProduct A, all three lots' delivery_ids are set to + Picking A. + """ + + stock_location = self.env.ref('stock.stock_location_stock') + customer_location = self.env.ref('stock.stock_location_customers') + + # Create the three lot-tracked products. + subcomponentA = self._create_product('lot') + componentA = self._create_product('lot') + endproductA = self._create_product('lot') + + # Create production lots. + lot_subcomponentA, lot_componentA, lot_endProductA = self.env['stock.lot'].create([{ + 'name': 'lot %s' % product, + 'product_id': product.id, + 'company_id': self.env.company.id, + } for product in (subcomponentA, componentA, endproductA)]) + + # Create two boms, one for Component A and one for EndProduct A + self.env['mrp.bom'].create([{ + 'product_id': finished.id, + 'product_tmpl_id': finished.product_tmpl_id.id, + 'product_uom_id': self.uom_unit.id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})], + } for finished, component in [(endproductA, componentA), (componentA, subcomponentA)]]) + + self.env['stock.quant']._update_available_quantity(subcomponentA, stock_location, 1, lot_id=lot_subcomponentA) + + # Produce 1 component A + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = componentA + mo_form.product_qty = 1 + mo = mo_form.save() + mo.action_confirm() + mo_form = Form(mo) + mo_form.qty_producing = 1 + mo_form.lot_producing_id = lot_componentA + mo = mo_form.save() + mo.move_raw_ids[0].quantity_done = 1.0 + mo.button_mark_done() + + # Produce 1 endProduct A + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = endproductA + mo_form.product_qty = 1 + mo = mo_form.save() + mo.action_confirm() + mo_form = Form(mo) + mo_form.qty_producing = 1 + mo_form.lot_producing_id = lot_endProductA + mo = mo_form.save() + mo.move_raw_ids[0].quantity_done = 1.0 + mo.button_mark_done() + + # Create out picking for EndProduct A + pickingA_out = self.env['stock.picking'].create({ + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id}) + + moveA = self.env['stock.move'].create({ + 'name': 'Picking A move', + 'product_id': endproductA.id, + 'product_uom_qty': 1, + 'product_uom': endproductA.uom_id.id, + 'picking_id': pickingA_out.id, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id}) + + # Confirm and assign pickingA + pickingA_out.action_confirm() + pickingA_out.action_assign() + + # Set move_line lot_id to the mrp.production lot_producing_id + moveA.move_line_ids[0].write({ + 'qty_done': 1.0, + 'lot_id': lot_endProductA.id, + }) + # Transfer picking + pickingA_out._action_done() + + # Use concat so that delivery_ids is computed in batch. + for lot in lot_subcomponentA.concat(lot_componentA, lot_endProductA): + self.assertEqual(lot.delivery_ids.ids, pickingA_out.ids) diff --git a/addons/stock/models/stock_lot.py b/addons/stock/models/stock_lot.py index a3951ba411a..6e51923466b 100644 --- a/addons/stock/models/stock_lot.py +++ b/addons/stock/models/stock_lot.py @@ -192,27 +192,42 @@ class StockLot(models.Model): def _find_delivery_ids_by_lot(self, lot_path=None, delivery_by_lot=None): if lot_path is None: lot_path = set() - domain = [ ('lot_id', 'in', self.ids), ('state', '=', 'done'), '|', ('picking_code', '=', 'outgoing'), ('produce_line_ids', '!=', False) ] move_lines = self.env['stock.move.line'].search(domain) + moves_by_lot = { + lot_id: {'producing_lines': set(), 'barren_lines': set()} + for lot_id in move_lines.lot_id.ids + } + for line in move_lines: + if line.produce_line_ids: + moves_by_lot[line.lot_id.id]['producing_lines'].add(line.id) + else: + moves_by_lot[line.lot_id.id]['barren_lines'].add(line.id) if delivery_by_lot is None: delivery_by_lot = dict() for lot in self: delivery_ids = set() - if lot.id in lot_path: - continue - for line in move_lines.filtered(lambda ml: ml.lot_id.id == lot.id): - if line.produce_line_ids: - # Do the same process for lot_id contained in produce_line_ids, - # to fetch the end product deliveries + + if moves_by_lot.get(lot.id): + producing_move_lines = self.env['stock.move.line'].browse(moves_by_lot[lot.id]['producing_lines']) + barren_move_lines = self.env['stock.move.line'].browse(moves_by_lot[lot.id]['barren_lines']) + + if producing_move_lines: lot_path.add(lot.id) - for delivery_ids_set in line.produce_line_ids.lot_id._find_delivery_ids_by_lot(lot_path=lot_path, delivery_by_lot=delivery_by_lot).values(): - delivery_ids.update(delivery_ids_set) - else: - delivery_ids.add(line.picking_id.id) + next_lots = producing_move_lines.produce_line_ids.lot_id.filtered(lambda l: l.id not in lot_path) + next_lots_ids = set(next_lots.ids) + # If some producing lots are in lot_path, it means that they have been previously processed. + # Their results are therefore already in delivery_by_lot and we add them to delivery_ids directly. + delivery_ids.update(*[set(delivery_by_lot.get(lot_id)) for lot_id in (producing_move_lines.produce_line_ids.lot_id - next_lots).ids]) + + for lot_id, delivery_ids_set in next_lots._find_delivery_ids_by_lot(lot_path=lot_path, delivery_by_lot=delivery_by_lot).items(): + if lot_id in next_lots_ids: + delivery_ids.update(delivery_ids_set) + delivery_ids.update(barren_move_lines.picking_id.ids) + delivery_by_lot[lot.id] = list(delivery_ids) return delivery_by_lot