先说结论
很多人第一次看 Odoo HR,会以为员工目录就是把 hr.employee 直接展示给所有内部用户。
源码告诉你:并不是。
Odoo 实际上拆了三层:
- 公开可见层:
hr.employee.public - 真实主数据层:
hr.employee - 本人自助编辑入口:
res.users上一组和员工关联的字段
所以“我能看到同事”“我能改自己的电话”“HR 能不能看到私人信息”,并不是同一个问题。
为什么 Odoo 要单独做 hr.employee.public
hr.employee.public 不是一个普通模型,而是一个 SQL view。
源码里它通过 hr_employee e 加上当前有效 hr.version v 拼出一张“公开员工档案视图”。这件事很关键,因为它说明 Odoo 的思路不是:
给所有人一点点
hr.employee权限。
而是:
干脆单独准备一张公开视图,只暴露应该公开的字段。
这张公开视图里能看到的,主要是组织协作需要的内容,比如:
- 姓名
- 部门
- 岗位
- 工作邮箱、工作电话
- 工作地点
- 直属经理、下属
- 头像
- 在线/出勤状态
你会注意到,这些字段大多都服务于“认识同事、找到人、协作沟通”,而不是服务于“查看完整人事档案”。
为什么它不是把所有字段都复制过去
源码里 hr.employee 有一段很值得实施顾问反复看:
- 如果当前用户有 HR 读权限,就直接读
hr.employee - 如果没有,就走
search_fetch()/fetch()的 Hack 路径 - 这条路径会去读
hr.employee.public - 然后只把公开字段复制到
hr.employee的缓存里
这个设计非常妙。
它的效果是:
- 普通用户依然像是在“搜索员工”
- 但底层其实只拿到了公开字段
- 一旦你去读私密字段,就直接触发访问错误
也就是说,Odoo 不是靠“前端别显示某些字段”来做安全,而是连 ORM 读取口径 都分开了。
私密字段为什么不是“隐藏一下就行”
源码里的 _check_private_fields() 很直接:
如果你请求的字段不在 hr.employee.public 里,就抛 AccessError。
这背后的含义很现实:
- 住址
- 紧急联系人
- 银行账户
- 证件信息
- 薪酬相关数据
这些不应该只是“默认折叠”,而应该是 默认不可读。
很多系统做权限时喜欢靠界面层遮挡,但只要 API 或导出没控住,最后还是会漏。Odoo 在员工档案这里显然吸取了教训:公开资料和私密资料必须是模型级边界。
那员工怎么改自己的私人资料
这里就进入第三层了:res.users。
在 /home/ubuntu/odoo-temp/addons/hr/models/res_users.py 里,Odoo 定义了两组字段:
HR_READABLE_FIELDSHR_WRITABLE_FIELDS
并把它们并入:
SELF_READABLE_FIELDSSELF_WRITEABLE_FIELDS
这意味着什么?
意思是:
普通员工虽然没有 HR 管理权限,但可以通过“我的偏好 / 我的资料”安全地读写属于自己的某些员工字段。
这些字段包括:
private_streetprivate_phoneprivate_emailemergency_contactemergency_phonemobile_phonework_emailwork_location_idbarcodepin
也就是说,Odoo 没有让员工直接去编辑完整 hr.employee 表单,而是给了一个 受控的自助入口。
为什么“我的资料”放在 res.users,数据却落到 hr.employee
看起来有点绕,但业务上很合理。
res.users 是“账号主体”,而很多自助编辑动作都是从用户自己的偏好页发起的;可真正的人力主数据又应该沉淀在 hr.employee。
所以 Odoo 采用了 related 字段:
res.users.work_phone -> employee_id.work_phoneres.users.private_email -> employee_id.private_emailres.users.work_location_id -> employee_id.work_location_id
这等于给员工一个“从用户侧改员工字段”的窗口。
它不是双表重复存储,而是:
- 入口在
res.users - 主档在
hr.employee
这就兼顾了“用户自助”与“HR 主数据归口”。
为什么还要有 _sync_user() 这种同步逻辑
在 hr.employee 里,_sync_user() 会把这些信息和用户联动:
work_contact_iduser_id- 头像
- 时区
tz
创建员工时,如果指定了 user_id,源码会:
- 用
_sync_user()补齐员工上的联动值 - 必要时移除旧的
work_contact_id - 保证同一公司里一个用户只绑定一个员工
这里最值得记住的一点是:
账号和员工是关联关系,不是完全相同的对象。
所以需要同步,但不能偷懒地把两者混成一张表。
为什么普通用户还能打开员工详情页
源码里连 get_formview_id() 和 get_formview_action() 都做了分流:
- HR 用户打开的是
hr.employee表单 - 普通内部用户打开的是
hr.employee.public表单
这背后其实是一个很成熟的产品判断:
用户想看“员工信息”,不代表他应该进入“员工主数据维护界面”。
于是同样是点进一个同事:
- HR 看到的是管理界面
- 普通员工看到的是公开档案
体验上很顺,权限上也很稳。
这套设计解决了哪些现实问题
1. 员工目录可以开放,但不会把人事档案全暴露
组织通讯录需要可见性,人事隐私需要边界。
2. 自助维护可以下放,但不会绕过主数据归口
员工能改自己的私人电话和紧急联系人,不代表他拿到了 HR 全权限。
3. 账号对象和员工对象既联动又不混淆
这样以后做:
- 多公司
- 一个用户对应一个公司员工
- 公开档案
- 离职归档
都比较容易保持一致性。
实施时最容易踩的坑
坑一:把员工目录权限直接开到 hr.employee
短期省事,长期高风险。
坑二:把“我的偏好”当成只改 res.users
其实很多字段最终落的是 hr.employee,如果定制时没理解 related 链路,很容易改坏。
坑三:以为公开档案只是一个简化视图
不是。它本质上是 Odoo 专门做出来的 安全边界模型。
一句话记忆
Odoo 不是把 hr.employee 随便裁掉几列给大家看,而是用 hr.employee.public 管公开可见,用 res.users 管本人自助,用 hr.employee 管真实主档。
DISCUSSION
评论区