From 4d653af6e92408dd5e191dbfe54f3908a18f21ee Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Sun, 13 Oct 2019 18:02:27 +0000 Subject: [PATCH] [REF] stock: planned transfer: additional moves Before this patch, move added in a planned transfer once it is ready are directly marked as assigned and the reservation is disabled on them. People found it hard to understand why the check availability button did not appear, plus the push rules were not applied. Now we chose to use the autoconfirm mechanism on the added move and we don't reserve them, so the check availability button reappear. task-2081844 --- addons/stock/models/stock_move.py | 12 ++- addons/stock/models/stock_picking.py | 27 +++-- addons/stock/tests/test_move2.py | 152 +++++++++++++++++++-------- 3 files changed, 128 insertions(+), 63 deletions(-) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index b100d4939d2..ee407987709 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -399,8 +399,9 @@ class StockMove(models.Model): @api.model def default_get(self, fields_list): # We override the default_get to make stock moves created after the picking was confirmed - # directly as available. This allows to create extra move lines in - # the fp view. + # directly as available in immediate transfer mode. This allows to create extra move lines + # in the fp view. In planned transfer, the stock move are marked as `additional` and will be + # auto-confirmed. defaults = super(StockMove, self).default_get(fields_list) if self.env.context.get('default_picking_id'): picking_id = self.env['stock.picking'].browse(self.env.context['default_picking_id']) @@ -408,10 +409,11 @@ class StockMove(models.Model): defaults['state'] = 'done' defaults['product_uom_qty'] = 0.0 defaults['additional'] = True - elif picking_id.state not in ['draft', 'confirmed']: - defaults['state'] = 'assigned' + elif picking_id.state not in ['cancel', 'draft', 'done']: + if picking_id.immediate_transfer: + defaults['state'] = 'assigned' defaults['product_uom_qty'] = 0.0 - defaults['additional'] = True + defaults['additional'] = True # to trigger `_autoconfirm_picking` return defaults def name_get(self): diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 28c68bfe01b..f8d35c3396f 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -473,6 +473,7 @@ class Picking(models.Model): for picking in self: picking.has_packages = picking.move_line_ids.filtered(lambda ml: ml.result_package_id) + @api.depends('immediate_transfer', 'is_locked', 'state') def _compute_show_check_availability(self): """ According to `picking.show_check_availability`, the "check availability" button will be displayed in the form view of a picking. @@ -589,18 +590,7 @@ class Picking(models.Model): if after_vals: self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals) if vals.get('move_lines'): - # Do not run autoconfirm if any of the moves has an initial demand. If an initial demand - # is present in any of the moves, it means the picking was created through the "planned - # transfer" mechanism. - pickings_to_not_autoconfirm = self.env['stock.picking'] - for picking in self: - if picking.state != 'draft': - continue - for move in picking.move_lines: - if not float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding): - pickings_to_not_autoconfirm |= picking - break - (self - pickings_to_not_autoconfirm)._autoconfirm_picking() + self._autoconfirm_picking() return res def unlink(self): @@ -906,8 +896,17 @@ class Picking(models.Model): return immediate_pickings def _autoconfirm_picking(self): - for picking in self.filtered(lambda picking: picking.immediate_transfer and picking.state not in ('done', 'cancel') and (picking.move_lines or picking.package_level_ids)): - picking.action_confirm() + """ Automatically run `action_confirm` on `self` if the picking is an immediate transfer or + if the picking is a planned transfer and one of its move was added after the initial + call to `action_confirm`. Note that `action_confirm` will only work on draft moves. + """ + for picking in self: + if picking.state in ('done', 'cancel'): + continue + if not picking.move_lines and not picking.package_level_ids: + continue + if picking.immediate_transfer or any(move.additional for move in picking.move_lines): + picking.action_confirm() def _create_backorder(self): """ This method is called when the user chose to create a backorder. It will create a new diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py index 65e158aef03..f4932803b1e 100644 --- a/addons/stock/tests/test_move2.py +++ b/addons/stock/tests/test_move2.py @@ -1348,50 +1348,6 @@ class TestSinglePicking(TestStockCommon): self.assertEqual(move_lines[0].lot_id.id, serial1.id) self.assertEqual(move_lines[1].lot_id.id, serial2.id) - def test_add_move_when_picking_is_available_1(self): - """ Check that any move added in a picking once it's assigned is directly considered as - assigned and bypass the reservation. - """ - delivery_order = self.env['stock.picking'].create({ - 'location_id': self.pack_location, - 'location_dest_id': self.customer_location, - 'picking_type_id': self.picking_type_out, - }) - self.MoveObj.create({ - 'name': self.productA.name, - 'product_id': self.productA.id, - 'product_uom_qty': 2, - 'product_uom': self.productA.uom_id.id, - 'picking_id': delivery_order.id, - 'location_id': self.pack_location, - 'location_dest_id': self.customer_location, - }) - - # make some stock - pack_location = self.env['stock.location'].browse(self.pack_location) - self.env['stock.quant']._update_available_quantity(self.productA, pack_location, 2) - - # assign - delivery_order.action_confirm() - delivery_order.action_assign() - self.assertEqual(delivery_order.state, 'assigned') - - # add a move - move2 = self.MoveObj\ - .with_context(default_picking_id=delivery_order.id)\ - .create({ - 'name': self.productA.name, - 'product_id': self.productB.id, - 'product_uom_qty': 1, - 'product_uom': self.productA.uom_id.id, - 'picking_id': delivery_order.id, - 'location_id': self.pack_location, - 'location_dest_id': self.customer_location, - }) - - self.assertEqual(move2.state, 'assigned') - self.assertEqual(delivery_order.state, 'assigned') - def test_use_create_lot_use_existing_lot_1(self): """ Check the behavior of a picking when `use_create_lot` and `use_existing_lot` are set to False and there's a move for a tracked product. @@ -1824,12 +1780,120 @@ class TestSinglePicking(TestStockCommon): move.product_id = self.productA move.product_uom_qty = 10 picking = picking.save() + self.assertEqual(picking.immediate_transfer, False) + self.assertEqual(picking.state, 'draft') picking = Form(picking) picking.move_ids_without_package.remove(0) picking = picking.save() self.assertEqual(len(picking.move_ids_without_package), 0) + def test_additional_move_1(self): + """ On a planned trasfer, add a stock move when the picking is already ready. Check that + the check availability button appears and work. + """ + # Make some stock for productA and productB. + receipt = self.env['stock.picking'].create({ + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + 'picking_type_id': self.picking_type_in, + }) + move_1 = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 10, + 'product_uom': self.productA.uom_id.id, + 'picking_id': receipt.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + }) + move_2 = self.MoveObj.create({ + 'name': self.productB.name, + 'product_id': self.productB.id, + 'product_uom_qty': 10, + 'product_uom': self.productB.uom_id.id, + 'picking_id': receipt.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location, + }) + receipt.action_confirm() + move_1.quantity_done = 10 + move_2.quantity_done = 10 + receipt.button_validate() + self.assertEqual(self.productA.qty_available, 10) + self.assertEqual(self.productB.qty_available, 10) + + # Create a delivery for 1 productA, reserve, check the picking is ready + delivery_order = self.env['stock.picking'].create({ + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + 'picking_type_id': self.picking_type_out, + 'move_type': 'one', + }) + move_3 = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 10, + 'product_uom': self.productA.uom_id.id, + 'picking_id': delivery_order.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + }) + delivery_order.action_confirm() + delivery_order.action_assign() + self.assertEqual(delivery_order.state, 'assigned') + + # Add a unit of productB, the check_availability button should appear. + delivery_order = Form(delivery_order) + with delivery_order.move_ids_without_package.new() as move: + move.product_id = self.productB + move.product_uom_qty = 10 + delivery_order = delivery_order.save() + + # The autocoform ran, the picking shoud be confirmed and reservable. + self.assertEqual(delivery_order.state, 'confirmed') + self.assertEqual(delivery_order.show_mark_as_todo, False) + self.assertEqual(delivery_order.show_check_availability, True) + + delivery_order.action_assign() + self.assertEqual(delivery_order.state, 'assigned') + self.assertEqual(delivery_order.show_check_availability, False) + self.assertEqual(delivery_order.show_mark_as_todo, False) + + stock_location = self.env['stock.location'].browse(self.stock_location) + self.assertEqual(self.env['stock.quant']._gather(self.productA, stock_location).reserved_quantity, 10.0) + self.assertEqual(self.env['stock.quant']._gather(self.productB, stock_location).reserved_quantity, 10.0) + + def test_additional_move_2(self): + """ On an immediate trasfer, add a stock move when the picking is already ready. Check that + the check availability button doest not appear. + """ + # Create a delivery for 1 productA, check the picking is ready + delivery_order = self.env['stock.picking'].create({ + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + 'picking_type_id': self.picking_type_out, + 'immediate_transfer': True, + 'move_ids_without_package': [(0, 0, { + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom': self.productA.uom_id.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + })], + }) + self.assertEqual(delivery_order.state, 'assigned') + + # Add a unit of productB, the check_availability button should not appear. + delivery_order = Form(delivery_order) + with delivery_order.move_ids_without_package.new() as move: + move.product_id = self.productB + delivery_order = delivery_order.save() + + self.assertEqual(delivery_order.state, 'assigned') + self.assertEqual(delivery_order.show_check_availability, False) + self.assertEqual(delivery_order.show_mark_as_todo, False) + def test_owner_1(self): """Make a receipt, set an owner and validate""" owner1 = self.env['res.partner'].create({'name': 'owner'})