This module extends package types Odoo feature in order to better manage stock diff --git a/stock_storage_type/tests/__init__.py b/stock_storage_type/tests/__init__.py index ba7aa132776..bdd58279b1c 100644 --- a/stock_storage_type/tests/__init__.py +++ b/stock_storage_type/tests/__init__.py @@ -1,7 +1,6 @@ from . import ( test_auto_assign_storage_type, test_package_height_required, - test_package_type_message, test_stock_location, test_storage_type, test_storage_type_move, diff --git a/stock_storage_type/tests/test_package_type_message.py b/stock_storage_type/tests/test_package_type_message.py deleted file mode 100644 index 7a77d490d86..00000000000 --- a/stock_storage_type/tests/test_package_type_message.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2022 ACSONE SA -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo.tests import TransactionCase - - -class TestStorageType(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - - cls.stock_location = cls.env.ref("stock.stock_location_stock") - cls.pallets_location_storage_type = cls.env.ref( - "stock_storage_type.location_storage_type_pallets" - ) - cls.pallets_uk_location_storage_type = cls.env.ref( - "stock_storage_type.location_storage_type_pallets_uk" - ) - cls.cardboxes_location_storage_type = cls.env.ref( - "stock_storage_type.location_storage_type_cardboxes" - ) - cls.cardboxes_stock = cls.env.ref("stock_storage_type.stock_location_cardboxes") - cls.cardboxes_bin_1 = cls.env.ref( - "stock_storage_type.stock_location_cardboxes_bin_1" - ) - cls.cardboxes_bin_2 = cls.env.ref( - "stock_storage_type.stock_location_cardboxes_bin_2" - ) - cls.cardboxes_bin_3 = cls.env.ref( - "stock_storage_type.stock_location_cardboxes_bin_3" - ) - cls.cardboxes_bin_4 = cls.env.ref( - "stock_storage_type.stock_location_cardboxes_bin_4" - ) diff --git a/stock_storage_type/tests/test_stock_location.py b/stock_storage_type/tests/test_stock_location.py index 6381ee18195..7bdbd0df43e 100644 --- a/stock_storage_type/tests/test_stock_location.py +++ b/stock_storage_type/tests/test_stock_location.py @@ -267,28 +267,3 @@ def test_will_contain_product_lot_ids_quantity(self): self.assertTrue(quant) self.assertEqual(0.0, quant.quantity) self.assertFalse(location.location_will_contain_lot_ids) - - def test_location_is_empty_non_internal(self): - location = self.env.ref("stock.stock_location_customers") - # we always consider an non-internal location empty, the put-away - # rules do not apply and we can add as many quants as we want - self.assertTrue(location.location_is_empty) - self._update_qty_in_location(location, self.product, 10) - self.assertTrue(location.location_is_empty) - - def test_location_is_empty(self): - location = self.pallets_reserve_bin_1_location - self.assertTrue(location.only_empty) - self.assertTrue(location.location_is_empty) - self._update_qty_in_location(location, self.product, 10) - self.assertFalse(location.location_is_empty) - - # When the location has no "only_empty" rule, we don't - # care about if it is empty or not, we keep it as True so we - # can always put things inside. Not computing it prevents - # useless race conditions on concurrent writes. - category = location.computed_storage_category_id - category.allow_new_product_ids.filtered( - lambda rule: rule.allow_new_product == "empty" - ).allow_new_product = "mixed" - self.assertTrue(location.location_is_empty) diff --git a/stock_storage_type/tests/test_storage_type_move.py b/stock_storage_type/tests/test_storage_type_move.py index 1915122a3a6..28e20fadac1 100644 --- a/stock_storage_type/tests/test_storage_type_move.py +++ b/stock_storage_type/tests/test_storage_type_move.py @@ -1,6 +1,5 @@ # Copyright 2020 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo.exceptions import ValidationError from odoo.tools.safe_eval import const_eval from .common import TestStorageTypeCommon @@ -375,14 +374,6 @@ def _levels_for(packages): _get_possible_locations(pack_level), ) - # Set the quantities done in order to avoid immediate transfer wizard - for move_line in pack_level.move_line_ids: - move_line.qty_done = move_line.reserved_qty - - second_level.location_dest_id = third_level.location_dest_id - with self.assertRaises(ValidationError): - int_picking.button_validate() - def test_stock_move_no_package(self): """ Create a stock move for a product with lot restriction diff --git a/stock_storage_type/tests/test_storage_type_putaway_strategy.py b/stock_storage_type/tests/test_storage_type_putaway_strategy.py index a2e2f82b120..8a3da2bd688 100644 --- a/stock_storage_type/tests/test_storage_type_putaway_strategy.py +++ b/stock_storage_type/tests/test_storage_type_putaway_strategy.py @@ -143,6 +143,13 @@ def test_storage_strategy_only_empty_ordered_locations_pallets(self): self.pallets_bin_1_location | self.pallets_bin_3_location, ) + # Try to re-apply the putaways to check the same destinations are selected + int_picking.move_line_ids._apply_putaway_strategy() + self.assertEqual( + int_picking.move_line_ids.mapped("location_dest_id"), + self.pallets_bin_1_location | self.pallets_bin_3_location, + ) + def test_storage_strategy_max_weight_ordered_locations_pallets(self): """Test pallet max weight constraint on a location. @@ -829,3 +836,293 @@ def test_storage_strategy_with_view(self): "the move line's destination must stay in Stock as we have" " a 'none' strategy on it and it is in the sequence", ) + + def test_storage_strategy_same_ordered_locations_pallets_reapply(self): + """ + Check if location is well recomputed after filling it with another move + and after emptying other ones that are after in the ordering + + - The location is first computed on the last free one + - The location is filled in with another move + - The move's location destination is recomputed + """ + # Set pallets location type as only empty and remove specific pallet condition + self.pallets_location_storage_type.storage_category_id.write( + {"allow_new_product": "same"} + ) + self.pallets_location_storage_type.storage_category_id.allow_new_product_ids = ( + False + ) + # Set another product in bin 2 and bin 3 + self.env["stock.quant"]._update_available_quantity( + self.product2, self.pallets_bin_2_location, 1.0 + ) + self.env["stock.quant"]._update_available_quantity( + self.product3, self.pallets_bin_3_location, 1.0 + ) + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 96.0, + "product_uom": self.product.uom_id.id, + }, + ) + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack + in_picking.move_line_ids.qty_done = 48.0 + first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + first_package.product_packaging_id = self.product_pallet_product_packaging + # Put in pack again + ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id + ) + ml_without_package.qty_done = 48.0 + second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + second_pack.product_packaging_id = self.product_pallet_product_packaging + + # Validate picking + in_picking.button_validate() + # Assign internal picking + int_picking = in_picking.move_ids.move_dest_ids.picking_id + int_picking.action_assign() + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + # First move line goes into pallets bin 1 + # Second move line goes into pallets bin 3 as bin 1 is planned for + # first move line and bin 2 is already used + self.assertEqual( + int_picking.move_line_ids[0].mapped("location_dest_id"), + self.pallets_bin_1_location, + ) + + self.env["stock.quant"].with_context(inventory_mode=True).create( + { + "product_id": self.product3.id, + "location_id": self.pallets_bin_1_location.id, + "inventory_quantity": 10.0, + } + )._apply_inventory() + + # Void the bin 3 + quant = self.env["stock.quant"].search( + [ + ("product_id", "=", self.product3.id), + ("location_id", "=", self.pallets_bin_3_location.id), + ] + ) + quant.location_id = self.env.ref("stock.stock_location_customers") + + # Try to re-apply the putaways to check the good destination is selected + int_picking.move_line_ids._apply_putaway_strategy() + self.assertNotEqual( + int_picking.move_line_ids[0].mapped("location_dest_id"), + self.pallets_bin_1_location, + ) + self.assertEqual( + int_picking.move_line_ids[0].mapped("location_dest_id"), + self.pallets_bin_3_location, + ) + + self.env["stock.quant"].with_context(inventory_mode=True).create( + { + "product_id": self.product.id, + "location_id": self.pallets_bin_3_location.id, + "inventory_quantity": 10.0, + } + )._apply_inventory() + + int_picking.move_line_ids._apply_putaway_strategy() + self.assertEqual( + int_picking.move_line_ids[0].mapped("location_dest_id"), + self.pallets_bin_3_location, + ) + + def test_storage_strategy_same_lot_emptying(self): + """ + Ensure that if a location is being emptied, it becomes available + for a new movement. + + So: + - Fill in all carboxes location bins. + - Create an OUT move with quantity done (location will be 'being emptied') + - Do the reception + - Check the internal move will go to the BIN 1 + + """ + self.product_other = self.env.ref("product.product_product_10") + self.cardboxes_location_storage_type.storage_category_id.write( + {"allow_new_product": "same_lot"} + ) + # Fill in all locations + self.env["stock.quant"]._update_available_quantity( + self.product_other, + self.cardboxes_bin_1_location, + 1.0, + ) + self.env["stock.quant"]._update_available_quantity( + self.product_other, + self.cardboxes_bin_2_location, + 1.0, + ) + self.env["stock.quant"]._update_available_quantity( + self.product_other, + self.cardboxes_bin_3_location, + 1.0, + ) + self.env["stock.quant"]._update_available_quantity( + self.product_other, + self.cardboxes_bin_4_location, + 1.0, + ) + + # Create picking + in_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.receipts_picking_type.id, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product.id, + "product_uom_qty": 8.0, + "product_uom": self.product.uom_id.id, + "picking_type_id": self.receipts_picking_type.id, + }, + ), + ( + 0, + 0, + { + "name": self.product_lot.name, + "location_id": self.suppliers_location.id, + "location_dest_id": self.input_location.id, + "product_id": self.product_lot.id, + "product_uom_qty": 10.0, + "product_uom": self.product_lot.uom_id.id, + "picking_type_id": self.receipts_picking_type.id, + }, + ), + ], + } + ) + # Mark as todo + in_picking.action_confirm() + # Put in pack product + in_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product + ).qty_done = 4.0 + product_first_package = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + product_first_package.product_packaging_id = ( + self.product_cardbox_product_packaging + ) + # Put in pack product again + product_ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id and ml.product_id == self.product + ) + product_ml_without_package.qty_done = 4.0 + product_second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + product_second_pack.product_packaging_id = ( + self.product_cardbox_product_packaging + ) + + # Put in pack product lot + product_lot_ml = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id and ml.product_id == self.product_lot + ) + product_lot_ml.write({"qty_done": 5.0, "lot_name": "A0001"}) + product_lot_first_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + product_lot_first_pack.product_packaging_id = ( + self.product_lot_cardbox_product_packaging + ) + # Put in pack product lot again + product_lot_ml_without_package = in_picking.move_line_ids.filtered( + lambda ml: not ml.result_package_id and ml.product_id == self.product_lot + ) + product_lot_ml_without_package.write({"qty_done": 5.0, "lot_name": "A0002"}) + product_lot_second_pack = in_picking.action_put_in_pack() + # Ensure packaging is set properly on pack + product_lot_second_pack.product_packaging_id = ( + self.product_lot_cardbox_product_packaging + ) + + # Create a move to pick a bin location, so that location fill state + # will be 'being emptied'. + customers = self.env.ref("stock.stock_location_customers") + pick_picking = self.env["stock.picking"].create( + { + "picking_type_id": self.internal_picking_type.id, + "location_id": self.cardboxes_bin_1_location.id, + "location_dest_id": customers.id, + "move_ids": [ + ( + 0, + 0, + { + "name": self.product.name, + "location_id": self.cardboxes_bin_1_location.id, + "location_dest_id": customers.id, + "product_id": self.product_other.id, + "product_uom_qty": 8.0, + "product_uom": self.product_other.uom_id.id, + "picking_type_id": self.internal_picking_type.id, + }, + ), + ], + } + ) + + pick_picking.action_assign() + self.assertEqual( + pick_picking.move_line_ids.location_id, + self.cardboxes_bin_1_location, + ) + + pick_picking.move_line_ids.qty_done = 8.0 + + self.assertEqual("being_emptied", self.cardboxes_bin_1_location.fill_state) + + # Validate picking + in_picking.button_validate() + + # Assign internal picking + int_picking = in_picking.move_ids.mapped("move_dest_ids.picking_id") + int_picking.action_assign() + self.assertEqual(int_picking.location_dest_id, self.stock_location) + self.assertEqual( + int_picking.move_ids.mapped("location_dest_id"), self.stock_location + ) + product_mls = int_picking.move_line_ids.filtered( + lambda ml: ml.product_id == self.product + ) + self.assertEqual( + product_mls.mapped("location_dest_id"), self.cardboxes_bin_1_location + )