Skip to content

Commit db54f4d

Browse files
committed
[IMP] sale_stock_warehouse_multicompany: security
Allow to validate a picking linked to a SO of another company without requiring enabling access to that company
1 parent 8767505 commit db54f4d

File tree

3 files changed

+77
-30
lines changed

3 files changed

+77
-30
lines changed

Diff for: sale_stock_warehouse_multicompany/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from . import sale_order
33
from . import sale_order_line
44
from . import stock_route
5+
from . import stock_picking
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2025 Jacques-Etienne Baudoux (BCIM) <[email protected]>
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import models
5+
6+
7+
class StockPicking(models.Model):
8+
_inherit = "stock.picking"
9+
10+
def _get_allowed_companies(self):
11+
return (self.company_id | self.sudo().move_ids.sale_line_id.company_id) & self.env.user.company_ids
12+
13+
def button_validate(self):
14+
# Extend access to all related SO companies because many modules hook
15+
# _action_done on stock.picking and access the related sales order.
16+
# Without this, we would get an AccessError
17+
self = self.with_context(allowed_company_ids=self._get_allowed_companies().ids)
18+
return super().button_validate()
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright 2024 Camptocamp SA
2+
# Copyright 2025 Jacques-Etienne Baudoux (BCIM) <[email protected]>
23
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
34

5+
from odoo.exceptions import AccessError
46
from odoo.tests import common
57
from odoo.tests.common import Form
68

@@ -13,47 +15,73 @@ def setUpClass(cls):
1315
super().setUpClass()
1416
cls.env = cls.env["base"].with_context(**DISABLED_MAIL_CONTEXT).env
1517

16-
cls.company_1 = cls.env["res.company"].create({"name": "Test company 1"})
17-
cls.company_2 = cls.env["res.company"].create({"name": "Test company 2"})
18-
cls.warehouse_1 = cls.env["stock.warehouse"].create(
19-
{
20-
"name": "Base Warehouse",
21-
"reception_steps": "one_step",
22-
"delivery_steps": "ship_only",
23-
"code": "BWH",
24-
"company_ids": [(6, 0, (cls.company_2 + cls.company_1).ids)],
25-
}
26-
)
18+
group_user = cls.env.ref("base.group_user")
19+
group_sales_user = cls.env.ref("sales_team.group_sale_salesman")
20+
group_stock_user = cls.env.ref("stock.group_stock_user")
21+
group_multi_wh = cls.env.ref("stock.group_stock_multi_warehouses")
22+
23+
cls.company_sales = cls.env["res.company"].create({"name": "Sales company"})
24+
cls.company_stock = cls.env["res.company"].create({"name": "Stock company"})
25+
cls.warehouse_stock = cls.env["stock.warehouse"].search([("company_id", "=", cls.company_stock.id)], limit=1)
26+
27+
cls.user_sales = cls.env["res.users"].create({
28+
"name": "user company sales with access to company stock",
29+
"login": "user sales",
30+
"groups_id": [(6, 0, [
31+
group_user.id,
32+
group_sales_user.id,
33+
group_multi_wh.id,
34+
])],
35+
"company_id": cls.company_sales.id,
36+
"company_ids": [(6, 0, [cls.company_sales.id, cls.company_stock.id])]
37+
})
38+
cls.user_stock = cls.env["res.users"].create({
39+
"name": "user company stock without access to company sales",
40+
"login": "user stock",
41+
"groups_id": [(6, 0, [
42+
group_user.id,
43+
group_stock_user.id,
44+
])],
45+
"company_id": cls.company_stock.id,
46+
"company_ids": [(6, 0, [cls.company_sales.id, cls.company_stock.id])]
47+
})
48+
49+
cls.partner = cls.env.ref("base.res_partner_12")
2750
cls.product = cls.env["product.product"].create(
2851
{"name": "test_product", "type": "product", "invoice_policy": "delivery"}
2952
)
3053
cls.env["stock.quant"].with_context(inventory_mode=True).create(
3154
{
32-
"location_id": cls.warehouse_1.wh_output_stock_loc_id.id,
55+
"location_id": cls.warehouse_stock.wh_output_stock_loc_id.id,
3356
"product_id": cls.product.id,
3457
"quantity": 10.0,
3558
}
3659
)._apply_inventory()
3760

3861
def test_sale_stock_warehouse_multicompany(self):
39-
self.env.user.company_id = self.company_2
40-
41-
# Test company 2 create and confirm order
42-
with Form(self.env["sale.order"]) as order_form:
43-
order_form.partner_id = self.env.ref("base.res_partner_12")
44-
order_form.warehouse_id = self.warehouse_1
62+
# Sales user from sales company create and confirm order
63+
so_env_user_sales = self.env["sale.order"] \
64+
.with_user(self.user_sales) \
65+
.with_context(allowed_company_ids=self.user_sales.company_ids.ids)
66+
with Form(so_env_user_sales) as order_form:
67+
order_form.partner_id = self.partner
68+
order_form.warehouse_id = self.warehouse_stock
4569
with order_form.order_line.new() as line_form:
4670
line_form.product_id = self.product
4771
line_form.product_uom_qty = 1
48-
self.order = order_form.save()
49-
self.order.action_confirm()
50-
51-
# Test company 1 process logitic steps from warehouse_1
52-
self.env.user.company_id = self.company_1
53-
stock_location = self.warehouse_1.lot_stock_id
54-
self.assertEqual(self.order.picking_ids.location_id, stock_location)
55-
# Process picking
56-
self.order.picking_ids.move_ids_without_package.quantity_done = 1.0
57-
self.order.picking_ids.button_validate()
58-
59-
self.assertEqual(self.order.order_line.qty_delivered, 1.0)
72+
order = order_form.save()
73+
order.action_confirm()
74+
stock_location = self.warehouse_stock.lot_stock_id
75+
self.assertEqual(order.picking_ids.location_id, stock_location)
76+
77+
# Stock user from stock company processes the picking
78+
picking = order.picking_ids \
79+
.with_user(self.user_stock) \
80+
.with_context(allowed_company_ids=self.user_stock.company_id.ids)
81+
self.env.clear() # clear cache
82+
with self.assertRaises(AccessError):
83+
_ = picking.sale_id.name
84+
picking.move_ids_without_package.quantity_done = 1.0
85+
picking.button_validate()
86+
87+
self.assertEqual(order.order_line.qty_delivered, 1.0)

0 commit comments

Comments
 (0)