启动环境
首先新建一个odoo12环境,并在应用列表中搜索sales
并安装。
开启多公司功能
打开settings页面,然后点击点击General Settings
, 接着再勾选Multi-companies
之后点击保存。页面会重新加载,再次回到设定那栏,选择如图中蓝色方框中的companies
进入公司列表,再创建一个名为company sub
的公司,并把下面选项中的parent
字段的值设置成系统默认自带的YourCompany
,这代表company sub
是YourCompany
的子公司,在多公司规则中,子公司记录之前是独立的,同时父公司可以看到所有子公司的记录。
再次刷新页面,此时页面的右上角便会出现一个可点击的下拉列表允许你切换当前使用的公司。
多公司的基本操作
多公司就如字面意思一样简单直了,所以展示起来也很直接。
单据操作
打开sales
模块,默认进来的时候可以发现模块自带了一些demo数据,我们随便点击一个单据进去,由于我们开启了多公司功能并且是用Mitchell Admin
登录,系统会为我们当前用户分配多公司权利
(只有开启了多公司权利的用户才能使用多公司功能,稍后会讲解这个。),在Other Information
中会出现Company
字段,这允许我们通过设置这个字段来让单据数据归属到对应公司。
现在我们设置Company
字段的值为我们刚创建的公司company sub
,并回到列表视图,在右上角切换当前的公司为company sub
,页面刷新好后我们便会发现刚刚编辑的单据出现在了company sub
公司的列表视图中,并且我们再也看不到其他单据内容。
此时我们再新建一个单据,并随机填入数据,再通过右上角切换回YourCompany
公司,我们会发现刚刚在company sub
公司新建的单据也在列表中,这就是刚刚提到的,父公司可以看到子公司全部数据。
用户相关操作
在Settings
中打开用户列表,并点击我们当前的用户,可以看到视图中多了Multi Companies
的操作项,其中Allowed Companies
就是允许该用户能切换到哪些公司,Current Company
则代表用户当前是归属到哪个公司。
再往下拉点,在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已经为我们完成了大量基础代码,我们只要通过两个步骤,即可实现基本的多公司功能。
- 在自定的相关模型中新增
company_id
字段
在所有需要多公司功能相关的字段中都需要加上以下示例代码
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id)
- 当然如果单据之前是存在明显关联的外键关系,比如订单和订单项,那么订单项定义该字段时可以通过
related
属性自动从单据身上获取。 - 增加
ir.rule
过滤规则 在security
中新建xml,并定义相关过滤规则并引入,以下是相关的示例代码
<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_force
和global
不需要改懂,只要略微修改其余选项对应你的模型即可。
对模块下模型依次执行以上两个步骤,就基本实现多公司功能了。但是这并不意味着万事大吉了。
在上面的定义代码中我们显然可以知道创建记录的默认公司是用户当前的公司,那么在以下情景中就可能产生问题:
- 比如我们在A公司创建订单,那么在父公司B是可以查看并操作A公司的订单,如果此时在B公司中对A公司执行的订单生成送货单的操作,如果没相关处理,那么这个送货单的就
Company_id
字段的值就是B公司的了,这显然不是我们想要的。 - 模块中有相关
cron
时,基本执行cron
的对应的用户是基本是超级管理员,那么cron
处理的单据,也可能归属错误的公司。 - 有些程序员可能会写
sql
语句来提高查询性能,如果没注意处理多公司,也会导致只想要单个公司相关数据时,查询了多个公司的数据。
所以实行了多公司功能的模块,在相关的方法处理中需要更小心,一个比较好的解决方案就是生产相关单据时,主动带上当前单据的Company_id
,在写sql相关操作时,也主动获取当前用户公司(可能还有子公司),去查询。
odoo与多公司相关的实现
之前的段落已经介绍了Odoo多公司的基本逻辑与操作,接下来的我再介绍的一部分多公司内部代码实现,以便更进一步了解多公司和方便定制扩展。
权限组相关
在odoo/addons/base/security/base_groups.xml
中有个一个group_multi_company
的权限组,也就是多公司权利。
<record model="res.groups" id="group_multi_company">
<field name="name">Multi Companies</field>
</record>
它的定义十分简单,但是作用很大,多公司相关功能都是围绕着这个权限组做的。简单的就是属于这个权限组,就可以可以使用多公司的全部功能。
比如许多视图文件中定义Company_id
字段都是用如下方式:
<field name="company_id" groups="base.group_multi_company"/>
也就是只有拥有多公司权利的用户才可以查看并编辑这个字段,不属于这个权限组的用户看不到这个字段。
odoo也在代码中为这个权限组做了一些自动加入取消的处理:
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)]})
这也是上文中提到的settings
和Allowed Companies
之间关系的逻辑,这里就不多做介绍了。
session相关
odoo也在web/models/ir_http.py
文件中重写了session_info
方法,这个方法中提供了用户的公司列表,以及是否显示切换公司的下拉框参数:
我们可以在后台以request.session
或者前台编写js模块时以var session = require('web.session');
方式来接触session以便获取相关变量。
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
文件包含了多公司下拉框的处理逻辑,主要逻辑也很简单:
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.py
的Users
模型中的write
方法,有这么一小段代码:
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()
方法。