0%

Odoo多公司指南

最近因为项目上需要实现多公司功能,所以也针对odoo的多公司这块研究了一番。这篇文章我会基于sale模块简单讲解下如何开启多公司功能,如何让项目支持多公司,以及odoo与多公司相关的实现(如果有扩展需求,可以更快速的找到切入点)。

这篇指南也是基于odoo12的,在其他版本中,多公司功能的具体细节可能会有略微不同。

启动环境

首先新建一个odoo12环境,并在应用列表中搜索sales并安装。

开启多公司功能

打开settings页面,然后点击点击General Settings, 接着再勾选Multi-companies之后点击保存。页面会重新加载,再次回到设定那栏,选择如图中蓝色方框中的companies进入公司列表,再创建一个名为company sub的公司,并把下面选项中的parent字段的值设置成系统默认自带的YourCompany,这代表company subYourCompany的子公司,在多公司规则中,子公司记录之前是独立的,同时父公司可以看到所有子公司的记录。
alt

再次刷新页面,此时页面的右上角便会出现一个可点击的下拉列表允许你切换当前使用的公司。

多公司的基本操作

多公司就如字面意思一样简单直了,所以展示起来也很直接。

单据操作

打开sales模块,默认进来的时候可以发现模块自带了一些demo数据,我们随便点击一个单据进去,由于我们开启了多公司功能并且是用Mitchell Admin登录,系统会为我们当前用户分配多公司权利(只有开启了多公司权利的用户才能使用多公司功能,稍后会讲解这个。),在Other Information中会出现Company字段,这允许我们通过设置这个字段来让单据数据归属到对应公司。

现在我们设置Company字段的值为我们刚创建的公司company sub,并回到列表视图,在右上角切换当前的公司为company sub,页面刷新好后我们便会发现刚刚编辑的单据出现在了company sub公司的列表视图中,并且我们再也看不到其他单据内容。

此时我们再新建一个单据,并随机填入数据,再通过右上角切换回YourCompany公司,我们会发现刚刚在company sub公司新建的单据也在列表中,这就是刚刚提到的,父公司可以看到子公司全部数据。

用户相关操作

Settings中打开用户列表,并点击我们当前的用户,可以看到视图中多了Multi Companies的操作项,其中Allowed Companies就是允许该用户能切换到哪些公司,Current Company则代表用户当前是归属到哪个公司。
alt

再往下拉点,在Extra Rights中还有一项与多公司有关的选项Multi Companies,也就是多公司权利,这是odoo中实际控制用户是否可以切换公司,是否在页面显示Company字段的重要选项。如果你勾选了Multi Companies,并且用户的Allowed Companies数量 > 1,那么即使不在settings中开启多公司功能,该用户也可以使用多公司功能。

那么读者可能就会好奇了,如果是这样,那么在settings中开启多公司功能有什么作用呢?它们之前的区别在于,如果设定中开启了多公司功能,那么全用户都拥有改多公司权利,可以查看到页面中Company相关字段,同时,除非Allowed Companies数量 = 1,右上角的多公司切换选项才会消失。如果不通过settings中开启多公司的话,那么可以在用户页面主动分配多公司权利和Allowed Companies,这样就可以只允许特定用户使用多公司的功能。需要注意的是单独Allowed Companies是没有效果的,需要同时勾选中Multi Companies

为自定义模块实现多公司功能

让项目支持多公司不算复杂,因为odoo已经为我们完成了大量基础代码,我们只要通过两个步骤,即可实现基本的多公司功能。

  1. 在自定的相关模型中新增company_id字段
    在所有需要多公司功能相关的字段中都需要加上以下示例代码
    1
    company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id)
    当然如果单据之前是存在明显关联的外键关系,比如订单和订单项,那么订单项定义该字段时可以通过related属性自动从单据身上获取。
  2. 增加ir.rule过滤规则
    security中新建xml,并定义相关过滤规则并引入,以下是相关的示例代码
    1
    2
    3
    4
    5
    6
    <record id="account_comp_rule" model="ir.rule">
    <field name="name">Account multi-company</field>
    <field name="model_id" ref="model_account_account"/>
    <field name="global" eval="True"/>
    <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
    </record>
    其中domain_forceglobal不需要改懂,只要略微修改其余选项对应你的模型即可。

对模块下模型依次执行以上两个步骤,就基本实现多公司功能了。但是这并不意味着万事大吉了。
在上面的定义代码中我们显然可以知道创建记录的默认公司是用户当前的公司,那么在以下情景中就可能产生问题:

  1. 比如我们在A公司创建订单,那么在父公司B是可以查看并操作A公司的订单,如果此时在B公司中对A公司执行的订单生成送货单的操作,如果没相关处理,那么这个送货单的就Company_id字段的值就是B公司的了,这显然不是我们想要的。
  2. 模块中有相关cron时,基本执行cron的对应的用户是基本是超级管理员,那么cron处理的单据,也可能归属错误的公司。
  3. 有些程序员可能会写sql语句来提高查询性能,如果没注意处理多公司,也会导致只想要单个公司相关数据时,查询了多个公司的数据。

所以实行了多公司功能的模块,在相关的方法处理中需要更小心,一个比较好的解决方案就是生产相关单据时,主动带上当前单据的Company_id,在写sql相关操作时,也主动获取当前用户公司(可能还有子公司),去查询。

odoo与多公司相关的实现

之前的段落已经介绍了Odoo多公司的基本逻辑与操作,接下来的我再介绍的一部分多公司内部代码实现,以便更进一步了解多公司和方便定制扩展。

权限组相关

odoo/addons/base/security/base_groups.xml中有个一个group_multi_company的权限组,也就是多公司权利。

1
2
3
<record model="res.groups" id="group_multi_company">
<field name="name">Multi Companies</field>
</record>

它的定义十分简单,但是作用很大,多公司相关功能都是围绕着这个权限组做的。简单的就是属于这个权限组,就可以可以使用多公司的全部功能。
比如许多视图文件中定义Company_id字段都是用如下方式:

1
<field name="company_id" groups="base.group_multi_company"/>

也就是只有拥有多公司权利的用户才可以查看并编辑这个字段,不属于这个权限组的用户看不到这个字段。

odoo也在代码中为这个权限组做了一些自动加入取消的处理:

1
2
3
4
5
6
group_multi_company = self.env.ref('base.group_multi_company', False)
if group_multi_company and 'company_ids' in values:
if len(user.company_ids) <= 1 and user.id in group_multi_company.users.ids:
user.write({'groups_id': [(3, group_multi_company.id)]})
elif len(user.company_ids) > 1 and user.id not in group_multi_company.users.ids:
user.write({'groups_id': [(4, group_multi_company.id)]})

这也是上文中提到的settingsAllowed Companies之间关系的逻辑,这里就不多做介绍了。

session相关

odoo也在web/models/ir_http.py文件中重写了session_info方法,这个方法中提供了用户的公司列表,以及是否显示切换公司的下拉框参数:
我们可以在后台以request.session或者前台编写js模块时以var session = require('web.session');方式来接触session以便获取相关变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def session_info(self):
user = request.env.user
display_switch_company_menu = user.has_group('base.group_multi_company') and len(user.company_ids) > 1
version_info = odoo.service.common.exp_version()
return {
"session_id": request.session.sid,
"uid": request.session.uid,
"is_system": user._is_system() if request.session.uid else False,
"is_admin": user._is_admin() if request.session.uid else False,
"user_context": request.session.get_context() if request.session.uid else {},
....
"company_id": user.company_id.id if request.session.uid else None,
"partner_id": user.partner_id.id if request.session.uid and user.partner_id else None,
"user_companies": {'current_company': (user.company_id.id, user.company_id.name), 'allowed_companies': [(comp.id, comp.name) for comp in user.company_ids]} if display_switch_company_menu else False,
....
}

前台显示相关

web/static/src/js/widgets/switch_company_menu.js文件包含了多公司下拉框的处理逻辑,主要逻辑也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
tart: function () {
var companiesList = '';
if (this.isMobile) {
companiesList = '<li class="bg-info">' +
_t('Tap on the list to change company') + '</li>';
}
else {
this.$('.oe_topbar_name').text(session.user_companies.current_company[1]);
}
_.each(session.user_companies.allowed_companies, function(company) {
var a = '';
if (company[0] === session.user_companies.current_company[0]) {
a = '<i class="fa fa-check mr8"></i>';
} else {
a = '<span style="margin-right: 24px;"/>';
}
companiesList += '<a role="menuitem" href="#" class="dropdown-item" data-menu="company" data-company-id="' +
company[0] + '">' + a + company[1] + '</a>';
});
this.$('.dropdown-menu').html(companiesList);
return this._super();
},

//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------

/**
* @private
* @param {MouseEvent} ev
*/
_onClick: function (ev) {
ev.preventDefault();
var companyID = $(ev.currentTarget).data('company-id');
this._rpc({
model: 'res.users',
method: 'write',
args: [[session.uid], {'company_id': companyID}],
})
.then(function() {
location.reload();
});
},

start方法中根据刚刚提到的session获取公司列表并渲染,_onClick则表视用户选了某个公司就把对应公司id写入到用户的company_id字段中,由此也可以用户当前使用的公司是通过company_id来判断的。

这里渲染出的下拉项是列表的,有的用户可能是希望可以呈现出公司之间上下级关系,那么这时可以重写后台session_info的方法,再配合第三方组件库如ztree重写前台中的js逻辑实现。

ir.rule相关

ir.rule规则,除了我在前文提到在xml中定义的相关数据,odoo还特意写了一个特殊的处理。
base/models/res_users.pyUsers模型中的write方法,有这么一小段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
if 'company_id' in values:
for user in self:
# if partner is global we keep it that way
if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']:
user.partner_id.write({'company_id': user.company_id.id})
# clear default ir values when company changes
self.env['ir.default'].clear_caches()

# clear caches linked to the users
if 'groups_id' in values:
self.env['ir.model.access'].call_cache_clearing_methods()
self.env['ir.rule'].clear_caches()
self.has_group.clear_cache(self)

这里可能是因为ir.rule相关权限的domain_forece只会计算一次,然后会缓存下来,所以当相关值变更的时候需要执行self.env['ir.default'].clear_caches()方法。

总结

多公司总体而言并不难理解,但是使用多公司功能时,那么相关数据逻辑的方法上就要注意处理company_id相关的数据,以免功能发生与预期不一样的行为。所以模块设计初期时也要考虑是否需要多公司功能,这样可以避免后期再决定加入时过高的检查成本。