From 76bb3e68328da0514077c7b850020a58504efb70 Mon Sep 17 00:00:00 2001 From: Kevin Graveman Date: Tue, 28 Sep 2021 13:49:57 +0200 Subject: [PATCH] [IMP] OD-1402: Force pickings to not have to wait for purchase orders --- .../models/stock_picking.py | 61 ++++++++++++++----- .../tests/test_stock_picking_force_assign.py | 55 +++++++++++++++++ 2 files changed, 102 insertions(+), 14 deletions(-) diff --git a/stock_picking_force_assign/models/stock_picking.py b/stock_picking_force_assign/models/stock_picking.py index b9451d2585dc..1f92a0e09093 100644 --- a/stock_picking_force_assign/models/stock_picking.py +++ b/stock_picking_force_assign/models/stock_picking.py @@ -10,18 +10,27 @@ class StockPicking(models.Model): def action_force_assign_pickings(self): """ Unreserve other pickings in order to reserve pickings in self """ link_template = '%s' + if self.state == 'waiting': + self._unchain_waiting(link_template) to_unreserve = self._force_assign_find_moves() - to_unreserve._do_unreserve() - self.message_post(body=_( - 'Unreserved picking(s) %s in order to assign this one' - ) % ', '.join(to_unreserve.mapped('picking_id').mapped( - lambda x: link_template % (x._name, x.id, x.name) - ))) - to_unreserve.mapped('picking_id').message_post(body=_( - 'Unreserved this picking in order to assign %s' - ) % ', '.join(self.mapped( - lambda x: link_template % (x._name, x.id, x.name) - ))) + if to_unreserve: + to_unreserve.mapped(lambda m: m._do_unreserve()) + self.message_post(body=_( + 'Unreserved picking(s) %s in order to assign this one' + ) % ', '.join(to_unreserve.mapped('picking_id').mapped( + lambda x: link_template % (x._name, x.id, x.name) + ))) + to_unreserve.mapped('picking_id').mapped(lambda p: p.message_post(body=_( + 'Unreserved this picking in order to assign %s' + ) % ', '.join(self.mapped( + lambda x: link_template % (x._name, x.id, x.name) + )))) + # Set procure_method of the affected moves to make_to_stock to not get stuck + to_unreserve.filtered( + lambda x: x.procure_method == 'make_to_order' + ).write({ + 'procure_method': 'make_to_stock' + }) return self.action_assign() def _force_assign_allow_partial(self): @@ -32,15 +41,15 @@ def _force_assign_allow_partial(self): def _force_assign_find_moves(self): """ Return moves to unreserve in order to reserve pickings in self """ - location = self.mapped('location_id') result = self.env['stock.move'] - assert len(location) == 1, 'Pickings need to be from the same location' float_compare = functools.partial( tools.float_compare, precision_digits=result._fields['product_qty'].digits, ) for move in self.mapped('move_lines'): + if move.state == 'cancel': + continue demand = ( move.product_qty - move.reserved_availability - move.availability @@ -50,7 +59,7 @@ def _force_assign_find_moves(self): candidates = self.env['stock.move'].search([ ('id', 'not in', result.ids), ('picking_id', 'not in', self.ids), - ('location_id', '=', location.id), + ('location_id', '=', move.location_id.id), ('product_id', '=', move.product_id.id), ('state', 'in', ('partially_available', 'assigned')), ], order='write_date asc') @@ -70,3 +79,27 @@ def _force_assign_find_moves(self): ) ) return result + + def _unchain_waiting(self, link_template): + """Set moves to make_to_stock and unlink move_orig_ids from moves. + Moves must be unlinked because _action_assign will otherwise only + consider availability in linked moves, instead of considering the + availability at the location. + Moves that have procure_method set to make_to_order will also + be skipped by _action_assign, so we set it to make_to_stock. + """ + res = False + to_unchain = self.move_lines.mapped('move_orig_ids') + if to_unchain: + self.message_post(body=_( + 'Decoupled operation(s) %s in order to assign this one' + ) % ', '.join(to_unchain.mapped( + lambda move: link_template % (move._name, move.id, move.name) + ))) + res = self.mapped('move_lines').filtered( + lambda move: move.procure_method == 'make_to_order' + or move.more_orig_ids + ).write( + {'procure_method': 'make_to_stock', 'move_orig_ids': [(5, 0, 0)]} + ) + return res diff --git a/stock_picking_force_assign/tests/test_stock_picking_force_assign.py b/stock_picking_force_assign/tests/test_stock_picking_force_assign.py index c2263dd26415..ce5357a4596e 100644 --- a/stock_picking_force_assign/tests/test_stock_picking_force_assign.py +++ b/stock_picking_force_assign/tests/test_stock_picking_force_assign.py @@ -8,10 +8,32 @@ class TestStockPickingForceAssign(TransactionCase): def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) + route_buy = self.env['stock.location.route'].create({ + 'name': 'Buy', + 'rule_ids': [(0, 0, { + 'name': 'Buy', + 'action': 'buy', + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + 'location_id': self.env.ref('stock.stock_location_stock').id, + })] + }) + route_mto = self.env.ref('stock.route_warehouse0_mto') self.product = self.env['product.product'].create({ 'name': 'stockable', 'type': 'product', }) + self.product2 = self.product.copy({ + 'name': 'stockable-mto', + 'purchase_ok': True, + 'sale_ok': True, + 'route_ids': [(6, 0, [route_mto.id, route_buy.id])], + 'seller_ids': [(0, 0, { + 'name': self.env.ref('base.main_partner').id, + 'min_qty': 1, + 'product_name': 'stockable', + 'product_code': 'stockable', + })], + }) self.assigned_picking = self.env['stock.picking'].create({ 'name': 'Picking to unassign', 'picking_type_id': self.env.ref('stock.picking_type_out').id, @@ -35,6 +57,28 @@ def setUp(self, *args, **kwargs): self.waiting_picking = self.assigned_picking.copy({ 'name': 'Picking to assign', }) + self.waiting_picking.action_assign() + self.assertEqual(self.waiting_picking.state, 'confirmed') + self.waitanother_picking = self.env['stock.picking'].create({ + 'name': 'Picking to unchain', + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + 'location_id': self.env.ref('stock.stock_location_stock').id, + 'location_dest_id': + self.env.ref('stock.stock_location_customers').id, + 'move_lines': [(0, 0, { + 'name': self.product2.name, + 'product_id': self.product2.id, + 'product_uom': self.product2.uom_id.id, + 'product_uom_qty': 12, + 'state': 'waiting', + 'procure_method': 'make_to_order', + })], + }) + self.env['stock.quant'].create({ + 'location_id': self.waitanother_picking.location_id.id, + 'product_id': self.product2.id, + 'quantity': 12, + }) def test_happy_flow(self): """ Test unreservation works """ @@ -70,3 +114,14 @@ def test_partial(self): self.waiting_picking.action_force_assign_pickings() self.assertEqual( self.waiting_picking.move_lines.reserved_availability, 42) + + def test_waiting_another(self): + """ Test unchaining moves waiting another operation """ + self.assertEqual(len(self.waitanother_picking), 1) + self.assertEqual(self.waitanother_picking.state, 'waiting') + self.assertEqual( + self.waitanother_picking.move_lines[0].procure_method, 'make_to_order') + self.waitanother_picking.action_force_assign_pickings() + self.assertNotEqual(self.waitanother_picking.state, 'waiting') + self.assertEqual( + self.waitanother_picking.move_lines[0].procure_method, 'make_to_stock')