diff --git a/web_easy_switch_operating_unit/README.rst b/web_easy_switch_operating_unit/README.rst
new file mode 100644
index 0000000000..3a998eb265
--- /dev/null
+++ b/web_easy_switch_operating_unit/README.rst
@@ -0,0 +1,10 @@
+.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png
+ :target: https://www.gnu.org/licenses/agpl
+ :alt: License: AGPL-3
+
+==========================================================
+Add menu to allow user to switch to another Operating Unit
+==========================================================
+
+This module adds a new menu in the top bar to switch to another Operating Unit.
+
diff --git a/web_easy_switch_operating_unit/__init__.py b/web_easy_switch_operating_unit/__init__.py
new file mode 100644
index 0000000000..0650744f6b
--- /dev/null
+++ b/web_easy_switch_operating_unit/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/web_easy_switch_operating_unit/__manifest__.py b/web_easy_switch_operating_unit/__manifest__.py
new file mode 100644
index 0000000000..d9b7e8dea9
--- /dev/null
+++ b/web_easy_switch_operating_unit/__manifest__.py
@@ -0,0 +1,19 @@
+# Copyright (C) 2026 CIT-Services
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Easy Switch Operating Unit",
+ "version": "18.0.1.0.0",
+ "category": "web",
+ "author": "CIT-Services, Odoo Community Association (OCA)",
+ "license": "AGPL-3",
+ "website": "https://github.com/OCA/operating-unit",
+ "depends": ["web", "operating_unit"],
+ "data": ["views/res_users_views.xml"],
+ "assets": {
+ "web.assets_backend": [
+ "web_easy_switch_operating_unit/static/src/js/*",
+ "web_easy_switch_operating_unit/static/src/xml/switch_operating_unit.xml",
+ ],
+ },
+}
diff --git a/web_easy_switch_operating_unit/models/__init__.py b/web_easy_switch_operating_unit/models/__init__.py
new file mode 100644
index 0000000000..9a5eb71871
--- /dev/null
+++ b/web_easy_switch_operating_unit/models/__init__.py
@@ -0,0 +1 @@
+from . import ir_http
diff --git a/web_easy_switch_operating_unit/models/ir_http.py b/web_easy_switch_operating_unit/models/ir_http.py
new file mode 100644
index 0000000000..b5300d5670
--- /dev/null
+++ b/web_easy_switch_operating_unit/models/ir_http.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2026 CIT-Services
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import models
+from odoo.http import request
+
+
+class IrHttp(models.AbstractModel):
+ _inherit = "ir.http"
+
+ def session_info(self):
+ info = super().session_info()
+
+ if self.env.user.has_group("base.group_user"):
+ user = request.env.user
+ info.update(
+ {
+ "user_operating_units": {
+ "current_operating_unit": (
+ user.default_operating_unit_id.id,
+ user.default_operating_unit_id.code,
+ ),
+ "allowed_operating_units": [
+ (ou.id, ou.code) for ou in user.operating_unit_ids
+ ],
+ }
+ }
+ )
+
+ return info
diff --git a/web_easy_switch_operating_unit/pyproject.toml b/web_easy_switch_operating_unit/pyproject.toml
new file mode 100644
index 0000000000..4231d0cccb
--- /dev/null
+++ b/web_easy_switch_operating_unit/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/web_easy_switch_operating_unit/static/description/icon.png b/web_easy_switch_operating_unit/static/description/icon.png
new file mode 100644
index 0000000000..ee270eb32c
Binary files /dev/null and b/web_easy_switch_operating_unit/static/description/icon.png differ
diff --git a/web_easy_switch_operating_unit/static/description/selection-off.png b/web_easy_switch_operating_unit/static/description/selection-off.png
new file mode 100644
index 0000000000..c8e922cc52
Binary files /dev/null and b/web_easy_switch_operating_unit/static/description/selection-off.png differ
diff --git a/web_easy_switch_operating_unit/static/description/selection-on.png b/web_easy_switch_operating_unit/static/description/selection-on.png
new file mode 100644
index 0000000000..8faf7d4def
Binary files /dev/null and b/web_easy_switch_operating_unit/static/description/selection-on.png differ
diff --git a/web_easy_switch_operating_unit/static/src/js/switch_operating_unit.esm.js b/web_easy_switch_operating_unit/static/src/js/switch_operating_unit.esm.js
new file mode 100644
index 0000000000..7c41cf8df5
--- /dev/null
+++ b/web_easy_switch_operating_unit/static/src/js/switch_operating_unit.esm.js
@@ -0,0 +1,56 @@
+/* eslint-disable jsdoc/check-tag-names */
+/** @odoo-module **/
+/* global window */
+
+import {Component} from "@odoo/owl";
+import {registry} from "@web/core/registry";
+import {session} from "@web/session";
+import {useService} from "@web/core/utils/hooks";
+import {user} from "@web/core/user";
+
+export class SwitchOperatingUnitMenu extends Component {
+ static template = "web_easy_switch_operating_unit.SwitchOperatingUnitMenu";
+ static props = {};
+
+ setup() {
+ this.orm = useService("orm");
+ if (!session.user_operating_units) {
+ this.user_operating_units = [];
+ this.allowed_operating_unit_ids = [];
+ this.current_operating_unit_id = false;
+ this.current_operating_unit_name = "No OU";
+ return;
+ }
+
+ this.user_operating_units =
+ session.user_operating_units.allowed_operating_units;
+ this.allowed_operating_unit_ids = this.user_operating_units.map((ou) =>
+ parseInt(ou[0], 10)
+ );
+ this.current_operating_unit_id =
+ session.user_operating_units.current_operating_unit[0];
+ this.current_operating_unit_name =
+ session.user_operating_units.current_operating_unit[1];
+ }
+
+ async onSwitchOperatingUnitClick(operatingUnitId) {
+ await this.orm.write("res.users", [user.userId], {
+ default_operating_unit_id: operatingUnitId || false,
+ });
+ window.location.reload();
+ }
+}
+
+const systrayItem = {
+ Component: SwitchOperatingUnitMenu,
+ isDisplayed() {
+ return (
+ session.user_operating_units &&
+ session.user_operating_units.allowed_operating_units.length > 0
+ );
+ },
+};
+
+registry
+ .category("systray")
+ .add("SwitchOperatingUnitMenu", systrayItem, {sequence: 10});
diff --git a/web_easy_switch_operating_unit/static/src/xml/switch_operating_unit.xml b/web_easy_switch_operating_unit/static/src/xml/switch_operating_unit.xml
new file mode 100644
index 0000000000..e3cb21f150
--- /dev/null
+++ b/web_easy_switch_operating_unit/static/src/xml/switch_operating_unit.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
diff --git a/web_easy_switch_operating_unit/tests/__init__.py b/web_easy_switch_operating_unit/tests/__init__.py
new file mode 100644
index 0000000000..e2983aa2a4
--- /dev/null
+++ b/web_easy_switch_operating_unit/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_ir_http
diff --git a/web_easy_switch_operating_unit/tests/test_ir_http.py b/web_easy_switch_operating_unit/tests/test_ir_http.py
new file mode 100644
index 0000000000..ba1792989d
--- /dev/null
+++ b/web_easy_switch_operating_unit/tests/test_ir_http.py
@@ -0,0 +1,94 @@
+# Copyright (C) 2026 CIT-Services
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from unittest.mock import MagicMock
+
+from odoo.tests import tagged
+from odoo.tests.common import TransactionCase
+
+
+@tagged("-at_install", "post_install")
+class TestIrHttp(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.operating_unit_1 = cls.env["operating.unit"].create(
+ {
+ "name": "Operating Unit 1",
+ "code": "TEST_OU1",
+ "partner_id": cls.env.company.partner_id.id,
+ }
+ )
+ cls.operating_unit_2 = cls.env["operating.unit"].create(
+ {
+ "name": "Operating Unit 2",
+ "code": "TEST_OU2",
+ "partner_id": cls.env.company.partner_id.id,
+ }
+ )
+
+ cls.user = cls.env["res.users"].create(
+ {
+ "name": "Test User",
+ "login": "test_ou_user",
+ "groups_id": [(6, 0, [cls.env.ref("base.group_user").id])],
+ "operating_unit_ids": [
+ (6, 0, [cls.operating_unit_1.id, cls.operating_unit_2.id])
+ ],
+ "default_operating_unit_id": cls.operating_unit_1.id,
+ }
+ )
+
+ cls.portal_user = cls.env["res.users"].create(
+ {
+ "name": "Portal User",
+ "login": "test_ou_portal",
+ "groups_id": [(6, 0, [cls.env.ref("base.group_portal").id])],
+ }
+ )
+
+ def test_session_info_internal_user(self):
+ """Test session_info for internal user includes operating unit data."""
+ mock_request = MagicMock()
+ mock_request.env.user = self.user
+ mock_request.session.uid = self.user.id
+ mock_request.cookies = {}
+
+ import odoo.http
+
+ odoo.http._request_stack.push(mock_request)
+ try:
+ info = self.env["ir.http"].with_user(self.user).session_info()
+ self.assertIn("user_operating_units", info)
+ ou_data = info["user_operating_units"]
+ self.assertEqual(
+ ou_data["current_operating_unit"],
+ (self.operating_unit_1.id, self.operating_unit_1.code),
+ )
+
+ allowed_ous = ou_data["allowed_operating_units"]
+ self.assertEqual(len(allowed_ous), 2)
+ self.assertIn(
+ (self.operating_unit_1.id, self.operating_unit_1.code), allowed_ous
+ )
+ self.assertIn(
+ (self.operating_unit_2.id, self.operating_unit_2.code), allowed_ous
+ )
+ finally:
+ odoo.http._request_stack.pop()
+
+ def test_session_info_portal_user(self):
+ """Test session_info for portal user does not include operating unit data."""
+ mock_request = MagicMock()
+ mock_request.env.user = self.portal_user
+ mock_request.session.uid = self.portal_user.id
+ mock_request.cookies = {}
+
+ import odoo.http
+
+ odoo.http._request_stack.push(mock_request)
+ try:
+ info = self.env["ir.http"].with_user(self.portal_user).session_info()
+ self.assertNotIn("user_operating_units", info)
+ finally:
+ odoo.http._request_stack.pop()
diff --git a/web_easy_switch_operating_unit/views/res_users_views.xml b/web_easy_switch_operating_unit/views/res_users_views.xml
new file mode 100644
index 0000000000..0ade040863
--- /dev/null
+++ b/web_easy_switch_operating_unit/views/res_users_views.xml
@@ -0,0 +1,17 @@
+
+
+
+ res.users.preferences.operating.unit
+ res.users
+
+
+
+
+
+
+
+