Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMP] stock_storage_type: Allow to re-apply putaway rules on computed location for a selected move line #938

Open
wants to merge 4 commits into
base: 16.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 59 additions & 12 deletions stock_storage_type/models/stock_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from odoo import api, fields, models
from odoo.fields import Command
from odoo.tools import float_compare, index_exists
from odoo.tools import float_compare, groupby, index_exists

_logger = logging.getLogger(__name__)
OUT_MOVE_LINE_DOMAIN = [
Expand Down Expand Up @@ -142,6 +142,23 @@
compute="_compute_only_empty", store=True, recursive=True
)

has_potential_product_mix_exception = fields.Boolean(
compute="_compute_has_potential_product_mix_exception",
store=True,
index=True,
help="This will represent a situation where several moves are pointing"
"to the location for different products and the location does"
"not allow mixed products.",
)
has_potential_lot_mix_exception = fields.Boolean(
compute="_compute_has_potential_lot_mix_exception",
store=True,
index=True,
help="This will represent a situation where several moves are pointing"
"to the location for different product lots and the location does"
"not allow mixed lots.",
)

def init(self): # pylint: disable=missing-return
super().init()
if not index_exists(self._cr, "stock_move_line_location_state_index"):
Expand Down Expand Up @@ -357,6 +374,36 @@
else:
rec.location_is_empty = True

@api.depends("do_not_mix_lots", "location_will_contain_lot_ids")
def _compute_has_potential_lot_mix_exception(self):
locations_with_exception = self.browse()
locations_without_exception = self.browse()
for location in self:
if (
location._should_compute_will_contain_lot_ids()
and len(location.location_will_contain_lot_ids) > 1
):
locations_with_exception |= location

Check warning on line 386 in stock_storage_type/models/stock_location.py

View check run for this annotation

Codecov / codecov/patch

stock_storage_type/models/stock_location.py#L386

Added line #L386 was not covered by tests
else:
locations_without_exception |= location
locations_with_exception.has_potential_lot_mix_exception = True
locations_without_exception.has_potential_lot_mix_exception = False

@api.depends("do_not_mix_lots", "location_will_contain_product_ids")
def _compute_has_potential_product_mix_exception(self):
locations_with_exception = self.browse()
locations_without_exception = self.browse()
for location in self:
if (
location._should_compute_will_contain_product_ids()
and len(location.location_will_contain_product_ids) > 1
):
locations_with_exception |= location
else:
locations_without_exception |= location
locations_with_exception.has_potential_product_mix_exception = True
locations_without_exception.has_potential_product_mix_exception = False

# method provided by "stock_putaway_hook"
def _putaway_strategy_finalizer(
self,
Expand Down Expand Up @@ -656,18 +703,18 @@
valid_no_mix = valid_locations.filtered("do_not_mix_products")
loc_ordered_by_qty = []
if valid_no_mix:
StockQuant = self.env["stock.quant"]
domain_quant = [("location_id", "in", valid_no_mix.ids)]
loc_ordered_by_qty = [
item["location_id"][0]
for item in StockQuant.read_group(
domain_quant,
["location_id", "quantity"],
["location_id"],
orderby="quantity",
for location, items in groupby(
valid_no_mix.quant_ids.sorted("quantity"),
lambda quant: quant.location_id,
):
loc_ordered_by_qty.extend(
[
location.id
for item in items
if (float_compare(item["quantity"], 0, precision_digits=2) > 0)
]
)
if (float_compare(item["quantity"], 0, precision_digits=2) > 0)
]

valid_location_ids = set(valid_locations.ids) - set(loc_ordered_by_qty)
ordered_valid_location_ids = loc_ordered_by_qty + [
id_ for id_ in self.ids if id_ in valid_location_ids
Expand Down
87 changes: 68 additions & 19 deletions stock_storage_type/models/stock_storage_category_capacity.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2022 ACSONE SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import _, api, fields, models
from odoo.osv.expression import AND, OR


class StorageCategoryProductCapacity(models.Model):
Expand Down Expand Up @@ -66,22 +67,40 @@ def _compute_has_restrictions(self):
]
)

def _get_product_location_domain(self, products):
def _get_product_lot_location_domain(self, lots):
"""
Helper to get products location domain
Helper to get product lots domain
"""
return [
"|",
# Ideally, we would like a domain which is a strict comparison:
# if we do not mix products, we should be able to filter on ==
# product.id. Here, if we can create a move for product B and
# set it's destination in a location already used by product A,
# then all the new moves for product B will be allowed in the
# location.
("location_will_contain_product_ids", "in", products.ids),
("location_will_contain_product_ids", "=", False),
# same comment as for the products
("location_will_contain_lot_ids", "in", lots.ids),
("location_will_contain_lot_ids", "=", False),
]

def _get_product_location_domain(self, products):
"""
Helper to get products location domain
"""
# Ideally, we would like a domain which is a strict comparison:
# if we do not mix products, we should be able to filter on ==
# product.id. Here, if we can create a move for product B and
# set it's destination in a location already used by product A,
# then all the new moves for product B will be allowed in the
# location.

# Take only locations that has no potential different products
# in it.
return OR(
[
[
("has_potential_product_mix_exception", "=", False),
("location_will_contain_product_ids", "in", products.ids),
],
[("location_will_contain_product_ids", "=", False)],
]
)

def _domain_location_storage_type(self, candidate_locations, quants, products):
"""
Compute a domain which applies the constraint of the
Expand All @@ -95,17 +114,47 @@ def _domain_location_storage_type(self, candidate_locations, quants, products):
]
# Build the domain using the 'allow_new_product' field
if self.allow_new_product == "empty":
location_domain.append(("location_is_empty", "=", True))
# We should include the destination location of the current
# stock move line to avoid excluding it if already selected
# Indeed, if the current move line point to the last void location,
# calling the putaway apply will recompute the destination location
# to the related stock.move destination as the rules consider
# there is no more room available (which is not true).
exclude_sml_ids = self.env.context.get("exclude_sml_ids")
if exclude_sml_ids:
lines_locations = (
self.env["stock.move.line"].browse(exclude_sml_ids).location_dest_id
)
if lines_locations:
location_domain = AND(
[
location_domain,
OR(
[
[
("location_is_empty", "=", False),
("id", "in", lines_locations.ids),
],
[("location_is_empty", "=", True)],
]
),
]
)
else:
location_domain = AND(
[location_domain, [("location_is_empty", "=", True)]]
)
elif self.allow_new_product == "same":
location_domain += self._get_product_location_domain(products)
location_domain = AND(
[location_domain, self._get_product_location_domain(products)]
)
elif self.allow_new_product == "same_lot":
lots = quants.mapped("lot_id")
# As same lot should filter also on same product
location_domain += self._get_product_location_domain(products)
location_domain += [
"|",
# same comment as for the products
("location_will_contain_lot_ids", "in", lots.ids),
("location_will_contain_lot_ids", "=", False),
]
location_domain = AND(
[location_domain, self._get_product_location_domain(products)]
)
location_domain = AND(
[location_domain, self._get_product_lot_location_domain(lots)]
)
return location_domain
123 changes: 123 additions & 0 deletions stock_storage_type/tests/test_storage_type_putaway_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,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):
# Add a category for max_weight 50
category_50 = self.env["stock.storage.category"].create(
Expand Down Expand Up @@ -812,3 +819,119 @@ 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
self.pallets_location_storage_type.write({"allow_new_product": "same"})
# 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,
)