Skip to content
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
10 changes: 10 additions & 0 deletions web_easy_switch_operating_unit/README.rst
Original file line number Diff line number Diff line change
@@ -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.

1 change: 1 addition & 0 deletions web_easy_switch_operating_unit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
19 changes: 19 additions & 0 deletions web_easy_switch_operating_unit/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (C) 2026 CIT-Services <https://cit-services.eu/>
# 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",
],
},
}
1 change: 1 addition & 0 deletions web_easy_switch_operating_unit/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import ir_http
30 changes: 30 additions & 0 deletions web_easy_switch_operating_unit/models/ir_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright (C) 2026 CIT-Services <https://cit-services.eu/>
# 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
3 changes: 3 additions & 0 deletions web_easy_switch_operating_unit/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="web_easy_switch_operating_unit.SwitchOperatingUnitMenu">
<div class="o_switch_company_menu dropdown">
<a
role="button"
class="dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
href="#"
>
<span class="oe_topbar_name">
<i class="fa fa-building-o d-md-none" />
<span class="d-none d-md-inline-block">
<t
t-if="current_operating_unit_name"
t-esc="current_operating_unit_name"
/>
<t t-else="">No OU</t>
</span>
</span>
</a>
<div class="dropdown-menu dropdown-menu-end" role="menu">
<div
class="dropdown-item d-flex py-0 px-0"
role="button"
t-on-click="() => this.onSwitchOperatingUnitClick(0)"
>
<div
t-attf-class="d-flex flex-grow-1 align-items-center py-2 px-3 {{ current_operating_unit_id === 0 ? 'bg-100' : '' }}"
>
<span class="company_label">Clear Operating Unit</span>
</div>
</div>

<t
t-foreach="user_operating_units"
t-as="operating_unit"
t-key="operating_unit[0]"
>
<t
t-set="is_allowed"
t-value="allowed_operating_unit_ids.includes(operating_unit[0])"
/>
<t
t-set="is_current"
t-value="operating_unit[0] === current_operating_unit_id"
/>

<div
class="dropdown-item d-flex py-0 px-0"
role="button"
t-on-click="() => this.onSwitchOperatingUnitClick(operating_unit[0])"
>
<div
t-attf-class="d-flex flex-grow-1 align-items-center py-2 px-3 {{ is_current ? 'bg-100' : '' }}"
>
<span
t-attf-class="company_label {{ is_allowed ? '' : 'text-muted' }}"
>
<t t-esc="operating_unit[1]" />
</span>
</div>
</div>
</t>
</div>
</div>
</t>

</templates>
1 change: 1 addition & 0 deletions web_easy_switch_operating_unit/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_ir_http
94 changes: 94 additions & 0 deletions web_easy_switch_operating_unit/tests/test_ir_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright (C) 2026 CIT-Services <https://cit-services.eu/>
# 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()
17 changes: 17 additions & 0 deletions web_easy_switch_operating_unit/views/res_users_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_users_form_simple_modif" model="ir.ui.view">
<field name="name">res.users.preferences.operating.unit</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form_simple_modif" />
<field name="arch" type="xml">
<field name="company_id" position="after">
<field
name="default_operating_unit_id"
options="{'no_create': True}"
readonly="0"
/>
</field>
</field>
</record>
</odoo>
Loading