多公司

Odoo 多公司切换为什么前端总能跟上:switch company menu、user service 与上下文刷新边界讲透

很多人以为 Odoo 的多公司切换只是“点一下菜单,然后整页刷新”。但从 `switch_company_menu.js` 和 `user.js` 看,官方真正维护的是一套前端用户上下文:cookie 记录当前公司集合,`allowed_company_ids` 驱动 RPC 上下文,`userBus` 负责广播变化,路由层再决定是局部栈回退还是整页刷新。本文把这套链路讲透。

Odoo 开发 前端
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 10 阅读

Odoo 多公司有一个很容易让人低估的体验细节:

你切公司后,前端通常不是“傻刷新一下”就完了,而是会尽量保住当前工作上下文,同时又不让你留在一个已经没权限的页面里。

这件事如果只从 UI 看,很像一个普通下拉菜单;但从 switch_company_menu.jsuser.js 连起来看,它其实是一套前端上下文切换机制。

真正被切换的,不只是“当前公司名”,而是下面几层:

  • 当前激活公司集合;
  • 浏览器 cookie 里的 cids
  • 用户上下文中的 allowed_company_ids
  • 依赖上下文的权限判断与 RPC 缓存;
  • 当前路由和 action stack 是否还能合法停留。

一、菜单组件保存的不是最终状态,而是“待应用选择集”

SwitchCompanyMenu 里最关键的对象是 CompanySelector

它不是直接改全局用户状态,而是先维护一个本地的:

selectedCompaniesIds

这一步特别关键,因为它说明切公司菜单的交互不是“点即生效”,而是分成两层:

  1. 在菜单里勾选、取消、搜索、批量选;
  2. 确认后再真正写回用户上下文。

所以用户在菜单里勾来勾去时,改的是候选集;只有 apply() 才会真正触发全局切换。

二、为什么 logintotoggle 是两种不同动作

源码里 switchCompany(mode, companyId) 分了两种模式:

  • toggle
  • loginto

这不是命名细节,而是两种完全不同的用户意图。

toggle

表示:

  • 我想把这家公司加入或移出当前激活集合。

这是典型的多公司并行视角。

loginto

表示:

  • 我要把它放到主公司位置,甚至在单公司模式下替换当前根公司。

因此 loginto 会在必要时先清空,再 unshift 把目标公司放到第一位,然后直接 apply()

这对应的不是“勾选一个框”,而是“切换默认业务公司”。

三、真正的上下文更新发生在 user.activateCompanies()

CompanySelector.apply() 最终调的是:

user.activateCompanies(this.selectedCompaniesIds, { includeChildCompanies: false, reload: false })

这句里藏了两个重要边界。

1)更新的是用户上下文,不只是组件状态

user.js 里,updateActiveCompanies() 会做三件事:

  • 重建 activeCompanies
  • 把公司 id 集写进 cookie cids
  • allowed_company_ids 写回 context

也就是说,后续所有依赖 user.context 的 RPC,都将自动带上新的公司上下文。

这就是为什么 Odoo 切公司后,很多列表、按钮、搜索域会自然换口径。

2)这里特意选择了 reload: false

这说明前端并不想无脑整页刷新,而是优先尝试在当前 web client 里完成状态切换。

换句话说,官方默认策略是:

先尽量保住前端工作栈,再根据权限结果决定是否回退或替换页面。

四、为什么切公司后有时停在当前页,有时会退回上一步

apply() 里最值得看的,不是 activateCompanies(),而是后面的权限检查。

它会看当前 controller 是否有:

  • resModel
  • resId

如果有,就调用:

user.checkAccessRight(controller.props.resModel, "read", controller.props.resId)

注意这里检查的不是抽象模型权限,而是切到新公司上下文之后,当前这条记录还能不能读。

如果不能读,前端就不会硬留你在原页面,而是:

  • options.replace = true
  • actionStack 去掉最后一层

然后通过 router.pushState(state, options) 让界面回退到一个合法位置。

这正是为什么你会感觉 Odoo 的多公司切换“挺懂分寸”:

  • 有权限,就留在当前业务对象上;
  • 没权限,就自动退回,不把你卡在访问错误页面。

五、allowed_company_ids 为什么会影响缓存和性能感知

user.js 里有一段注释非常值得注意。

更新活跃公司后,它会把第一家公司保留在前,后面的公司 id 做稳定排序,目的是减少 allowed_company_ids 在上下文字符串里的熵。

这个设计听起来很工程味,但意义很现实:

很多 RPC 缓存键都会带上下文字符串。

如果你只是激活了同一组公司,但顺序不稳定:

  • 缓存命中率会变差;
  • 一些本来可复用的请求结果会被当成不同上下文;
  • 前端看起来就更容易“切一下就全重算”。

所以官方连公司 id 排序都管,不是强迫症,而是在保护缓存稳定性。

六、为什么 ACTIVE_COMPANIES_CHANGED 要走 userBus

切公司后,菜单本身也得同步。

官方没让每个组件各自去读 cookie,而是触发:

userBus.trigger("ACTIVE_COMPANIES_CHANGED")

然后菜单用 useBus(userBus, ...) 监听,收到事件后 reset() 自己的本地选择集。

这体现的是典型的前端基础设施思路:

  • user 负责状态源;
  • userBus 负责广播;
  • 各组件只是订阅者。

这样写的好处是,多公司相关 UI 不会和菜单强耦合。以后别的组件也能订阅同一个事件源。

七、树形公司选择不是“平铺多选”那么简单

CompanySelector 还处理了父子公司关系。

  • 选父公司时,会递归选子公司;
  • 取消父公司时,会递归取消分支;
  • _isSingleCompanyMode() 还会判断你当前选中的是否仍是同一根公司树下的完整集合。

这意味着 Odoo 的多公司切换不是简单的 checkbox list,而是一个有树语义的选择器。

这点在集团场景尤其重要。

因为用户想表达的往往不是“随便选四家公司”,而是:

  • 只看某个公司;
  • 看一个母公司及其全部分支;
  • 看几棵不同根公司的并行集合。

八、二开时最常见的误解

不够。还必须同步 user.context.allowed_company_ids,否则 RPC 语义不会真的跟上。

误区 2:切完直接 browser.location.reload()

虽然能工作,但会丢掉 Odoo 已经帮你做好的局部回退与权限保护体验。

误区 3:忽略当前记录读权限

结果就是切公司后落在一张已无权限的记录页,用户一脸懵。

误区 4:多公司菜单只考虑平铺列表

真实数据里常常有祖先公司、分支公司和树形选择逻辑。

九、结论

Odoo 多公司切换好用,不是因为那个下拉菜单好看,而是因为官方把它当成了一次完整的前端上下文切换:

  • 菜单先维护待应用选择集;
  • user.activateCompanies() 更新 cookie 与 allowed_company_ids
  • userBus 广播上下文变化;
  • 路由层再根据新权限决定留在当前页还是安全回退。

所以真正该理解的不是“菜单点哪儿”,而是:

多公司切换的核心不是 UI 选择,而是上下文、权限与路由三者一起重建。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。