Skip to content

Commit dfec985

Browse files
[IMP] stock_owner_restriction: increase method coverage
Extend following methods for the scenario where owner restriction should be forced: - _update_available_quantity() (stock.quant) - _update_reserved_quantity() (stock.quant) - Split lines to identify the owner_restriction setting into separate method for better extensibility. - Fix read_group() (stock.quant) which was incorrectly assigning the owner record in the domain instead of the id. - Add compute for restrict_partner_id to control owner assignment based on picking type restriction. - Unlink stock move lines depending on owner_restriction if owner_id is updated. - Update test cases
1 parent eaa3c48 commit dfec985

File tree

5 files changed

+132
-17
lines changed

5 files changed

+132
-17
lines changed

stock_owner_restriction/models/product.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ def _compute_quantities_dict(
1414
return super()._compute_quantities_dict(
1515
lot_id, owner_id, package_id, from_date=from_date, to_date=to_date
1616
)
17-
restricted_owner_id = self.env.context.get("force_restricted_owner_id", None)
18-
if owner_id is None and restricted_owner_id is None:
17+
restricted_owner = self.env.context.get("force_restricted_owner_id", None)
18+
if owner_id is None and restricted_owner is None:
1919
# Force owner to False if is None
2020
owner_id = False
21-
elif restricted_owner_id:
22-
owner_id = restricted_owner_id
21+
elif restricted_owner:
22+
owner_id = restricted_owner.id
2323
return super()._compute_quantities_dict(
2424
lot_id, owner_id, package_id, from_date=from_date, to_date=to_date
2525
)

stock_owner_restriction/models/stock_move.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
# Copyright 2020 Carlos Dauden - Tecnativa
22
# Copyright 2020 Sergio Teruel - Tecnativa
3+
# Copyright 2023-2024 Quartile
34
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
45
from collections import defaultdict
56

6-
from odoo import models
7+
from odoo import api, fields, models
78

89

910
class StockMove(models.Model):
1011
_inherit = "stock.move"
1112

13+
# Add compute method to the standard field
14+
restrict_partner_id = fields.Many2one(
15+
compute="_compute_restrict_partner_id",
16+
store=True,
17+
readonly=False,
18+
)
19+
20+
@api.depends("picking_type_id", "picking_id.owner_id", "move_dest_ids")
21+
def _compute_restrict_partner_id(self):
22+
for move in self:
23+
if move.picking_type_id.owner_restriction == "picking_partner":
24+
move.restrict_partner_id = move._get_owner_for_assign()
25+
else:
26+
move.restrict_partner_id = False
27+
1228
def _get_moves_to_assign_with_standard_behavior(self):
1329
"""This method is expected to be extended as necessary. e.g. you may not want to
1430
handle subcontracting receipts (whose picking type is normal incoming receipt
@@ -20,9 +36,16 @@ def _get_moves_to_assign_with_standard_behavior(self):
2036
or m.picking_type_id.owner_restriction == "standard_behavior"
2137
)
2238

39+
def _get_owner_restriction(self):
40+
"""This method is expected to be extended as necessary. e.g. different logic
41+
needs to be applied to moves in unbuild orders.
42+
"""
43+
self.ensure_one()
44+
return self.picking_type_id.owner_restriction
45+
2346
def _get_owner_for_assign(self):
2447
"""This method is expected to be extended as necessary. e.g. different logic
25-
needs to be applied for moves in manufacturing orders.
48+
needs to be applied to moves in manufacturing orders.
2649
"""
2750
self.ensure_one()
2851
partner = self.move_dest_ids.picking_id.owner_id
@@ -37,7 +60,8 @@ def _action_assign(self, force_qty=False):
3760
res = super(StockMove, moves)._action_assign(force_qty=force_qty)
3861
dict_key = defaultdict(lambda: self.env["stock.move"])
3962
for move in self - moves:
40-
if move.picking_type_id.owner_restriction == "unassigned_owner":
63+
owner_restriction = move._get_owner_restriction()
64+
if owner_restriction == "unassigned_owner":
4165
dict_key[False] |= move
4266
else:
4367
partner = move._get_owner_for_assign()

stock_owner_restriction/models/stock_picking.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@ class StockPicking(models.Model):
88
_inherit = "stock.picking"
99

1010
owner_restriction = fields.Selection(related="picking_type_id.owner_restriction")
11+
12+
def write(self, vals):
13+
if "owner_id" in vals:
14+
for pick in self:
15+
if pick.owner_restriction in ("unassigned_owner", "picking_partner"):
16+
pick.move_line_ids.unlink()
17+
return super().write(vals)

stock_owner_restriction/models/stock_quant.py

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright 2020 Carlos Dauden - Tecnativa
22
# Copyright 2020 Sergio Teruel - Tecnativa
3+
# Copyright 2023 Quartile Limited
34
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
45
from odoo import api, models
56
from odoo.osv import expression
@@ -33,20 +34,29 @@ def _gather(
3334
owner_id=self._get_restriction_owner_id(location_id, owner_id),
3435
strict=strict,
3536
)
36-
restricted_owner_id = self.env.context.get("force_restricted_owner_id", None)
37-
if owner_id is None or restricted_owner_id is None:
37+
restricted_owner = self.env.context.get("force_restricted_owner_id", None)
38+
if owner_id is None or restricted_owner is None:
3839
return records
3940
return records.filtered(
40-
lambda q: q.owner_id == (restricted_owner_id or self.env["res.partner"])
41+
lambda q: q.owner_id == (restricted_owner or self.env["res.partner"])
4142
)
4243

4344
@api.model
4445
def read_group(
4546
self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True
4647
):
47-
restricted_owner_id = self.env.context.get("force_restricted_owner_id", None)
48-
if restricted_owner_id is not None:
49-
domain = expression.AND([domain, [("owner_id", "=", restricted_owner_id)]])
48+
owner_in_domain = any(t[0] == "owner_id" for t in domain)
49+
if not owner_in_domain:
50+
restricted_owner = self.env.context.get("force_restricted_owner_id", None)
51+
if restricted_owner is not None:
52+
owner_domain = [
53+
(
54+
"owner_id",
55+
"=",
56+
restricted_owner and restricted_owner.id or False,
57+
),
58+
]
59+
domain = expression.AND([domain, owner_domain])
5060
return super(StockQuant, self).read_group(
5161
domain,
5262
fields,
@@ -68,9 +78,9 @@ def _get_available_quantity(
6878
strict=False,
6979
allow_negative=False,
7080
):
71-
restricted_owner_id = self.env.context.get("force_restricted_owner_id", None)
72-
if not owner_id and restricted_owner_id is not None:
73-
owner_id = restricted_owner_id
81+
restricted_owner = self.env.context.get("force_restricted_owner_id", None)
82+
if not owner_id and restricted_owner is not None:
83+
owner_id = restricted_owner
7484
return super()._get_available_quantity(
7585
product_id,
7686
location_id,
@@ -80,3 +90,51 @@ def _get_available_quantity(
8090
strict=strict,
8191
allow_negative=allow_negative,
8292
)
93+
94+
@api.model
95+
def _update_available_quantity(
96+
self,
97+
product_id,
98+
location_id,
99+
quantity,
100+
lot_id=None,
101+
package_id=None,
102+
owner_id=None,
103+
in_date=None,
104+
):
105+
restricted_owner = self.env.context.get("force_restricted_owner_id", None)
106+
if not owner_id and restricted_owner is not None:
107+
owner_id = restricted_owner
108+
return super()._update_available_quantity(
109+
product_id,
110+
location_id,
111+
quantity,
112+
lot_id=lot_id,
113+
package_id=package_id,
114+
owner_id=owner_id,
115+
in_date=in_date,
116+
)
117+
118+
@api.model
119+
def _update_reserved_quantity(
120+
self,
121+
product_id,
122+
location_id,
123+
quantity,
124+
lot_id=None,
125+
package_id=None,
126+
owner_id=None,
127+
strict=False,
128+
):
129+
restricted_owner = self.env.context.get("force_restricted_owner_id", None)
130+
if not owner_id and restricted_owner is not None:
131+
owner_id = restricted_owner
132+
return super()._update_reserved_quantity(
133+
product_id,
134+
location_id,
135+
quantity,
136+
lot_id=lot_id,
137+
package_id=package_id,
138+
owner_id=owner_id,
139+
strict=strict,
140+
)

stock_owner_restriction/tests/test_stock_owner_restriction.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def test_product_qty_available(self):
7070
# context depends of product qty_available
7171
self.assertEqual(
7272
self.product.with_context(
73-
force_restricted_owner_id=self.owner.id
73+
force_restricted_owner_id=self.owner
7474
).qty_available,
7575
500.00,
7676
)
@@ -166,3 +166,29 @@ def test_search_qty(self):
166166
[("id", "=", self.product.id), ("qty_available", ">", 499.00)]
167167
)
168168
self.assertTrue(products)
169+
170+
def test_update_available_quantity(self):
171+
self.env["stock.quant"].with_context(
172+
force_restricted_owner_id=self.owner
173+
)._update_available_quantity(
174+
self.product, self.picking_type_out.default_location_src_id, 100
175+
)
176+
self.assertEqual(self.product.qty_available, 500.00)
177+
# Invalidate the cache of the product record to ensure qty_available is recalculated
178+
self.product.invalidate_recordset()
179+
self.assertEqual(
180+
self.product.with_context(skip_restricted_owner=True).qty_available, 1100.00
181+
)
182+
183+
def test_update_reserved_quantity(self):
184+
self.env["stock.quant"].with_context(
185+
force_restricted_owner_id=self.owner
186+
)._update_reserved_quantity(
187+
self.product, self.picking_type_out.default_location_src_id, 100
188+
)
189+
self.assertEqual(self.product.qty_available, 500.00)
190+
# Invalidate the cache of the product record to ensure qty_available is recalculated
191+
self.product.invalidate_recordset()
192+
self.assertEqual(
193+
self.product.with_context(skip_restricted_owner=True).qty_available, 1000.00
194+
)

0 commit comments

Comments
 (0)