Skip to content
Merged
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
13 changes: 8 additions & 5 deletions stock_reserve_rule/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright 2019 Camptocamp SA
# Copyright 2019 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
# Copyright 2025 Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models
from odoo.tools.float_utils import float_compare
Expand Down Expand Up @@ -102,18 +103,20 @@ def _update_reserved_quantity(
next_quant = strategy.send(still_need)
if not next_quant:
continue
location, location_quantity, to_take = next_quant
to_take, location, lot, pack, owner, strict_reservation = (
next_quant
)
taken_in_loc = super(
StockMove, self.with_context(**exclusion_ctx)
)._update_reserved_quantity(
# in this strategy, we take as much as we can
# from this bin
to_take,
location_id=location,
lot_id=lot_id,
package_id=package_id,
owner_id=owner_id,
strict=strict,
lot_id=lot,
package_id=pack,
owner_id=owner,
strict=strict_reservation,
)
still_need -= taken_in_loc
# We should break between quants if original needs is fulfilled
Expand Down
24 changes: 1 addition & 23 deletions stock_reserve_rule/models/stock_quant.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,13 @@
# Copyright 2019 Camptocamp SA
# Copyright 2025 Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from collections import OrderedDict

from odoo import models
from odoo.osv import expression


class StockQuant(models.Model):
_inherit = "stock.quant"

def _group_by_location(self):
"""Return quants grouped by locations

Group by location, but keeping the order of the quants (if we have more
than one quant per location, the order is based on the first quant seen
in the location). Thus, it can be used on a recordset returned by
_gather.

The returned format is: [(location, quants)]

"""
seen = OrderedDict()
for quant in self:
location = quant.location_id
if location in seen:
seen[location] = seen[location] | quant
else:
seen[location] = quant
return [(loc, quants) for loc, quants in seen.items()]

def _get_gather_domain(
self,
product_id,
Expand Down
29 changes: 20 additions & 9 deletions stock_reserve_rule/models/stock_reserve_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,16 +210,19 @@ def _apply_strategy_default(self, quants):
# the _gather method, so based on fifo, fefo, ...
for quant in quants:
need = yield (
quant.location_id,
quant.quantity - quant.reserved_quantity,
need,
quant.location_id,
self.env["stock.lot"].browse(),
self.env["stock.quant.package"].browse(),
self.env["res.partner"].browse(),
False,
)

def _apply_strategy_empty_bin(self, quants):
need = yield
# Group by location (in this removal strategies, we want to consider
# the total quantity held in a location).
quants_per_bin = quants._group_by_location()
quants_per_bin = quants.grouped("location_id")
# We take goods only if we empty the bin.
# The original ordering (fefo, fifo, ...) must be kept.
product = fields.first(quants).product_id
Expand All @@ -236,7 +239,7 @@ def _apply_strategy_empty_bin(self, quants):
"location_id",
)
]
for location, location_quants in quants_per_bin:
for location, location_quants in quants_per_bin.items():
if location.id in locations_with_other_quants:
continue

Expand All @@ -251,14 +254,22 @@ def _apply_strategy_empty_bin(self, quants):
float_compare(need, location_quantity, precision_rounding=rounding)
!= -1
):
need = yield location, location_quantity, need
need = yield (
need,
location,
self.env["stock.lot"].browse(),
self.env["stock.quant.package"].browse(),
self.env["res.partner"].browse(),
False,
)

def _apply_strategy_packaging(self, quants):
need = yield
# Group by location (in this removal strategies, we want to consider
# the total quantity held in a location).
quants_per_bin = quants._group_by_location()

quants_per_key = quants.grouped(
key=lambda qu: (qu.location_id, qu.lot_id, qu.package_id, qu.owner_id)
)
product = fields.first(quants).product_id

packaging_type_filter = self.packaging_level_ids
Expand All @@ -285,7 +296,7 @@ def is_greater_eq(value, other):
return float_compare(value, other, precision_rounding=rounding) >= 0

for pack_quantity in packaging_quantities:
for location, location_quants in quants_per_bin:
for (location, lot, pack, owner), location_quants in quants_per_key.items():
location_quantity = sum(location_quants.mapped("quantity")) - sum(
location_quants.mapped("reserved_quantity")
)
Expand All @@ -302,4 +313,4 @@ def is_greater_eq(value, other):
if float_is_zero(take, precision_rounding=rounding):
continue
location_quantity -= take
need = yield location, location_quantity, take
need = yield take, location, lot, pack, owner, True
17 changes: 15 additions & 2 deletions stock_reserve_rule/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,23 @@ def _create_picking(self, wh, products=None, location_src_id=None):
return picking

def _update_qty_in_location(
self, location, product, quantity, in_date=None, package=None
self,
location,
product,
quantity,
in_date=None,
package=None,
lot=None,
owner=None,
):
self.env["stock.quant"]._update_available_quantity(
product, location, quantity, package_id=package, in_date=in_date
product,
location,
quantity,
lot_id=lot,
package_id=package,
owner_id=owner,
in_date=in_date,
)

def _create_rule(self, rule_values, removal_values):
Expand Down
119 changes: 118 additions & 1 deletion stock_reserve_rule/tests/test_reserve_rule.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
# Copyright 2019-2021 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
# Copyright 2019 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
# Copyright 2025 Akretion

from odoo import exceptions, fields

Expand Down Expand Up @@ -806,3 +807,119 @@ def test_rule_removal_location_parent_of_move_location(self):
[{"location_id": self.loc_zone1_bin1.id, "quantity": 100.0}],
)
self.assertEqual(move.state, "partially_available")

def test_rule_packaging_do_not_sum_smaller_pack(self):
"""Reserve only in case 1 quant matches existing packaging qty"""
self._setup_packagings(
self.product1,
[
("Transport Box", 100, self.transport_box),
],
)
# Put in stock 2 packs of 50pces
for cnt in range(2):
pack = self.env["stock.quant.package"].create({"name": f"PACK000{cnt}"})
self._update_qty_in_location(
self.loc_zone1_bin1,
self.product1,
50,
package=pack,
)

picking = self._create_picking(self.wh, [(self.product1, 200)])
move = picking.move_ids
self._create_rule(
{},
[
{
"location_id": self.loc_zone1.id,
"sequence": 1,
"removal_strategy": "packaging",
},
],
)
picking.action_assign()
self.assertEqual(move.state, "confirmed")

def test_rule_packaging_do_not_sum_different_owner(self):
"""
Reserve only in case 1 quant matches existing packaging qty (consider owner)
"""
self._setup_packagings(
self.product1,
[
("Transport Box", 100, self.transport_box),
],
)
# Put in stock 2 packs of 50pces
for cnt in range(2):
owner = self.env["res.partner"].create({"name": f"Parner {cnt}"})
self._update_qty_in_location(
self.loc_zone1_bin1,
self.product1,
50,
owner=owner,
)

picking = self._create_picking(self.wh, [(self.product1, 200)])
move = picking.move_ids
self._create_rule(
{},
[
{
"location_id": self.loc_zone1.id,
"sequence": 1,
"removal_strategy": "packaging",
},
],
)
picking.action_assign()
self.assertEqual(move.state, "confirmed")

def test_rule_packaging_do_not_sum_different_lots(self):
"""
Reserve only in case 1 quant matches existing packaging qty (consider lot)
"""
self.product1.tracking = "lot"
self._setup_packagings(
self.product1,
[
("Transport Box", 100, self.transport_box),
],
)
# Put in stock 2 packs of 50pces
lot = self.env["stock.lot"].create(
{"name": "Lot 1", "product_id": self.product1.id}
)
self._update_qty_in_location(
self.loc_zone1_bin1,
self.product1,
50,
lot=lot,
)
lot2 = self.env["stock.lot"].create(
{"name": "Lot 2", "product_id": self.product1.id}
)
self._update_qty_in_location(
self.loc_zone1_bin1,
self.product1,
100,
lot=lot2,
)

picking = self._create_picking(self.wh, [(self.product1, 200)])
move = picking.move_ids
self._create_rule(
{},
[
{
"location_id": self.loc_zone1.id,
"sequence": 1,
"removal_strategy": "packaging",
},
],
)
picking.action_assign()
self.assertEqual(move.state, "partially_available")
self.assertEqual(move.move_line_ids.lot_id, lot2)
self.assertEqual(move.quantity, 100.0)
Loading