From 6c0744d67215b7d52f7597c87abbfbe6367502e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20=28avd=29?= Date: Thu, 16 Jun 2022 12:05:31 +0000 Subject: [PATCH] [FIX] stock: speed up _find_delivery_ids_by_lot Rewrite _find_delivery_ids_by_lot to reduce the number of recursive calls by grouping the producing move lines together before each recursive calls. This reduces the number of search calls, speeding up the whole method. Add a unittest to check if the delivery_ids field of a lot recordset is properly computed. opw-2824464 X-original-commit: e7b9f14d4a9ce4ed61451b24b567f751fb4bcbe2 Part-of: odoo/odoo#95398 --- addons/mrp/tests/test_traceability.py | 93 +++++++++++++++++++++++++++ addons/stock/models/stock_lot.py | 37 +++++++---- 2 files changed, 119 insertions(+), 11 deletions(-) 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