From 60e7803f3fbd552af40eb6154eaa804e70d0a54b Mon Sep 17 00:00:00 2001 From: Adrien Widart Date: Mon, 11 Oct 2021 12:32:13 +0000 Subject: [PATCH] [FIX] stock: define the order of products allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The allocation order is not based on the pickings order To reproduce the issue: (Need sale_management,mrp) 1. Create a storable product P 2. Create a BoM for another product dummy_P and add 1xP as component 3. Create and confirm a SO with 1x product P 4. Create and confirm a MO for 1x dummy_P 5. Process a receipt transfer with one P Error: On the SO, the reserved quantity is still 0. The product has been reserved for the MO, even if the picking of the SO has been created first The default order of several SM is based on the sequence and the identifier: https://github.com/odoo/odoo/blob/abc9fdaae2927214d98082f391a1dc0fc75e4c77/addons/stock/models/stock_move.py#L26 The default value of the field `sequence` is 10: https://github.com/odoo/odoo/blob/abc9fdaae2927214d98082f391a1dc0fc75e4c77/addons/stock/models/stock_move.py#L26 When creating a SO, the field isn't defined so its default value is used (10). However, when creating the MO, the field is defined thanks to the sequence of the related BOM line: https://github.com/odoo/odoo/blob/864d90a064f093bd6ba24d8464ee491a443a320e/addons/mrp/models/mrp_production.py#L940 Which, in our case, is equal to 1 As a result, when allocating the quantities in `_action_assign`, the recordset will contain first the SM of the MO and then the SM of the SO. This commit ensures the order will be based on the priority, the date and the identifier (the latter allows the search result to be deterministic) Side note: this commit also adds the identifier to the search in `_run_scheduler_tasks` to keep consistency between the different flows OPW-2524205 closes odoo/odoo#78497 X-original-commit: da2b336d467d95cda8ed21707535fb7119f5d777 Signed-off-by: Rémy Voet Signed-off-by: Adrien Widart --- addons/stock/models/stock_move.py | 2 +- addons/stock/models/stock_rule.py | 2 +- addons/stock/tests/test_stock_flow.py | 48 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 24e2c098eeb..8badb2b2fa4 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1929,7 +1929,7 @@ class StockMove(models.Model): ('procure_method', '=', 'make_to_stock'), ('reservation_date', '<=', fields.Date.today())] moves_to_reserve = self.env['stock.move'].search(expression.AND([static_domain, expression.OR(domains)]), - order='reservation_date, priority desc, date asc') + order='reservation_date, priority desc, date asc, id asc') moves_to_reserve._action_assign() def _rollup_move_dests(self, seen): diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index f79b8588a67..70790294fcc 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -537,7 +537,7 @@ class ProcurementGroup(models.Model): # Search all confirmed stock_moves and try to assign them domain = self._get_moves_to_assign_domain(company_id) moves_to_assign = self.env['stock.move'].search(domain, limit=None, - order='reservation_date, priority desc, date asc') + order='reservation_date, priority desc, date asc, id asc') for moves_chunk in split_every(1000, moves_to_assign.ids): self.env['stock.move'].browse(moves_chunk).sudo()._action_assign() if use_new_cursor: diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index b4af1fabb7a..57b95a7ac48 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -2020,3 +2020,51 @@ class TestStockFlow(TestStockCommon): # Validates the two receipts => It should raise an error as there is duplicate SN. with self.assertRaises(ValidationError): (receipt_1 | receipt_2).button_validate() + + def test_assign_qty_to_first_move(self): + """ Suppose two out picking waiting for an available quantity. When receiving such + a quantity, the latter should be assign to the picking with the highest priority + and the earliest scheduled date. """ + def create_picking(picking_type, from_loc, to_loc, sequence=10, delay=0): + picking = self.PickingObj.create({ + 'picking_type_id': picking_type, + 'location_id': from_loc, + 'location_dest_id': to_loc, + }) + self.MoveObj.create({ + 'name': self.productA.name, + 'sequence': sequence, + 'date': fields.Datetime.add(fields.Datetime.now(), second=delay), + 'reservation_date': fields.Date.today(), + 'product_id': self.productA.id, + 'product_uom_qty': 1, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking.id, + 'location_id': from_loc, + 'location_dest_id': to_loc, + }) + picking.action_confirm() + return picking + + def validate_picking(picking): + res_dict = picking.button_validate() + wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save() + wizard.process() + + out01 = create_picking(self.picking_type_out, self.stock_location, self.customer_location) + out02 = create_picking(self.picking_type_out, self.stock_location, self.customer_location, sequence=2, delay=1) + in01 = create_picking(self.picking_type_in, self.supplier_location, self.stock_location, delay=2) + + validate_picking(in01) + self.assertEqual(out01.state, 'assigned') + self.assertEqual(out02.state, 'confirmed') + + validate_picking(out01) + + out03 = create_picking(self.picking_type_out, self.stock_location, self.customer_location, delay=3) + out03.priority = "1" + in02 = create_picking(self.picking_type_in, self.supplier_location, self.stock_location, delay=4) + + validate_picking(in02) + self.assertEqual(out02.state, 'confirmed') + self.assertEqual(out03.state, 'assigned')