企业版薪资文档

Odoo 企业版工资单发文档为什么不是“生成 PDF 后发邮件”这么简单:链接权限、Worker Payroll Folder、声明队列与 Documents 归档边界讲透

很多人以为 Odoo 企业版 payroll 接 Documents,只是工资单出 PDF、发封邮件这么简单。源码真正实现的是一套把工资单和申报声明安全落到 Documents 的机制:已验证/已支付工资单才允许发送、文档默认走“Anyone with the link”但内部不可见、员工有账号时进个人空间、没有账号时落 Worker Payroll Folder,声明文档还会经过 pdf_to_post 队列与 cron 异步投递,避免把 payroll 文档误做成普通内部附件。

人力资源 企业
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

很多团队第一次看 Odoo Enterprise Payroll + Documents 时,会把它理解成一个很轻的流程:

  • 工资单生成 PDF;
  • 系统发员工一封邮件;
  • 员工点开附件或链接查看;
  • 事情结束。

这个总结只描述了表层体验,解释不了下面这些企业现场很常见的问题:

  1. 为什么工资单文档默认不是普通内部附件,而是“Anyone with the link”可查看?
  2. 为什么员工有用户账号时文档进个人空间,没有账号时却会掉进 Worker Payroll Folder
  3. 为什么 payroll manager 自己也不自动拥有这些文档的可见权限?
  4. 为什么某些 payroll declaration 不是立即发 Documents,而是先进入 pdf_to_post 队列?
  5. 为什么系统宁可异步 cron 投递,也不把所有工资单和声明都在当前事务里一次性发完?

如果把 /home/ubuntu/odoo-temp/enterprise/documents_hr_payroll 里的 models/hr_payslip.pymodels/hr_payroll_employee_declaration.pymodels/hr_payroll_declaration_mixin.py 和测试 tests/test_documents.py 串起来看,官方真正设计的不是“工资单 PDF 邮件发送器”,而是:

把 payroll 文档转成可长期访问、可安全隔离、且能适配离职后访问边界的 Documents 资产。

这也是它比“生成附件后发邮件”复杂得多的原因。


一、先说结论:工资单文档的核心不是附件,而是“可控访问的 Documents 对象”

documents_hr_payroll/models/hr_payslip.py 里,hr.payslip 继承了 documents.mixin

这意味着工资单文档不是停留在 ir.attachment 这一层就结束,而是会被桥接成 documents.document,并围绕 Documents 的:

  • owner
  • partner
  • folder
  • access ids
  • access via link
  • internal access

这些语义来管理。

所以工资单 PDF 在企业版里的真正目标,不是“把文件生出来”,而是:

把工资单变成一个有明确归属、访问边界和长期链接策略的文档对象。

这就是为什么它值得单独读源码,而不是把它当成 mail attachment 功能。


二、为什么发送工资单前必须先过状态和邮箱检查

action_resend_payslips() 做的第一件事不是发邮件,而是先卡边界:

  • 当前用户必须有 hr_payroll.group_hr_payroll_user
  • 工资单状态必须在 validatedpaid
  • 员工必须至少有 private_emailwork_email

这组限制很有意味。

官方不是把工资单发送理解成“HR 想发就发”,而是把它理解成:

只有正式完成的工资结果,且员工存在可用接收地址时,才允许对外发送文档入口。

同时,如果工资单还没有对应文档,系统不是假装发成功,而是明确给出 warning。

这说明“发邮件”在这里其实是第二层动作,第一层前提永远是:Documents 里的可访问文档已经存在。


_get_document_vals_access_rights() 是这套设计里最值得反复读的一小段:

  • access_via_link = 'view'
  • access_internal = 'none'
  • is_access_via_link_hidden = True

注释更直接:

  • 所有工资单都应通过 “Anyone with the link” 可访问
  • 这样即使员工和其用户被归档,文档也仍然可访问

这一步非常关键。

很多人一看到“Anyone with the link”就条件反射地觉得不安全,但 payroll 这里的逻辑恰恰不是“放宽”,而是换一种更稳定的访问载体

因为工资单是非常特殊的 HR 文档:

  • 员工可能离职;
  • 用户账号可能被归档或停用;
  • 但历史工资单在某些国家和公司制度下,仍需要让员工长期取回。

如果只依赖内部用户权限,员工一旦离职,历史工资单的访问链也会一起断掉。

所以 Odoo 选择的是:

内部默认不给看,但给一个受控的永久链接入口。

这和普通内部知识库文档完全不是同一个思路。


四、为什么 payroll manager 也不天然拥有文档访问权

测试 test_payslip_document_creation()test_hr_payroll_employee_declaration_document_creation_simple() 很能说明问题。

文档创建出来以后,测试明确校验:

  • 文档 owner 是员工本人(如果员工有 user)
  • partner 也是员工用户对应的 partner
  • payroll manager、documents manager、其他用户默认都没有访问权限

这说明官方要隔离的不是“外人 vs 内部人”,而是:

连内部管理角色也不能因为自己职位高,就顺手通过 Documents 权限看到所有工资单。

换句话说,工资单的可见范围不是由 Documents 的常规内部共享思路主导,而是被 payroll 的隐私边界重新收紧了。

对很多企业来说,这一点比“能不能自动发邮件”重要得多。


五、为什么员工有账号时进个人空间,没有账号时进 Worker Payroll Folder

_get_document_folder() 的逻辑非常值得注意:

  • 如果员工有 user_id,则走 super(),本质上会进入员工自己的文档空间 / My Drive 语义
  • 如果员工没有用户账号,则落到公司级的 Worker Payroll Folder

同时:

  • _get_document_partner() 优先拿员工用户对应的 partner
  • 否则退回 work_contact_id
  • _get_document_owner() 也优先设成员工用户

这套逻辑背后的目标很清楚:

有账号的员工

让工资单作为“属于本人”的个人文档来管理。

没账号的员工

仍然要有一套公司代持的安全存放区,并通过特定 access 让当事人可看。

所以 Worker Payroll Folder 的本质不是共享资料夹,而是:

给无系统账号员工准备的 payroll 文档托管层。

这比“没有用户就不能发文档”高级得多,也更贴近真实企业人事场景。


六、为什么没有 partner 时,系统宁可不建 document

测试 test_payslip_document_creation_with_no_partner() 说明了另一条边界:

  • 如果员工没有 user_id.partner_id
  • 也没有 work_contact_id
  • 那么工资单可以排队生成 PDF
  • 但最终不会创建 documents.document

这说明 Odoo 明确拒绝生成“无归属对象、无访问主体”的 payroll 文档。

也就是说,文档存在的前提,不只是文件本身生成成功,而是系统知道这个文档最终属于谁、给谁看。

这对 payroll 场景特别重要,因为工资单不是公共材料,不能先随手落地,再慢慢补权限。


七、为什么 declaration 走 pdf_to_post 队列,而不是立即塞进 Documents

hr_payroll_employee_declaration.py 里给 declaration 新增了两个状态:

  • pdf_to_post
  • pdf_posted

并配了字段:

  • pdf_to_post
  • document_id

这里最值得注意的是 action_post_in_documents()_cron_generate_pdf() 的配合。

动作不是立刻 _post_pdf(),而是:

  1. 先把记录标成 pdf_to_post=True
  2. 触发 hr_payroll.ir_cron_generate_payslip_pdfs
  3. 由 cron 批量抓取声明记录
  4. 再调用 _post_pdf() 投递到 Documents

这表达的是一个很明确的工程取向:

Payroll 声明文档的生成与投递,允许异步化、批处理化,而不是强绑在当前交互事务里。

这么做的好处有三类:

  • 大批量声明时不阻塞前端操作
  • PDF 生成和文档创建失败时更容易重试
  • 可以统一控制一次投递多少份,避免瞬时写爆 Documents

这也是企业系统处理“敏感批量文件”时非常常见的设计。


八、为什么系统要先检查重名文档,避免重复发声明

_get_posted_documents() 会按 pdf_filename 统计已有 active 文档;_post_pdf() 则会跳过那些已经投递过的文件名。

这说明 declaration 的 Documents 投递,不是简单地“每点一次就新建一个 document”,而是有明确的幂等意识

对 payroll 声明类文件来说,这很重要:

  • 避免 HR 多点几次按钮,Documents 里出现一堆重复件
  • 避免员工多次收到看起来内容一样、链接不同的文档
  • 避免 declaration 的状态和文档数量失去对应关系

当然,这里的去重口径主要基于文件名,不是全量内容哈希;但对声明投递这条业务链来说,已经足够实用。


_post_pdf() 创建 documents.document 时,同样设置了:

  • access_via_link = 'view'
  • access_internal = 'none'
  • is_access_via_link_hidden = True

并且:

  • 如果员工有 user,就把 owner 设给该 user
  • 如果没有 user,owner 可以为空
  • 但 partner/folder/access_ids 会补出可访问路径

这说明 payroll declaration 与 payslip 在访问哲学上是一致的:

优先保证“当事人长期可取回”,而不是“内部账号还活着时凑合能看”。

这也解释了为什么 mixin 层提供了 _get_posted_document_owner()_get_posted_mail_template() 这些可覆写点——因为不同 payroll declaration 可能有不同 owner 语义,但访问边界模型是一致的。


十、为什么 mixin 层要单独做 documents_countaction_see_documents

hr_payroll_declaration_mixin.py 不是只图方便加个按钮。

它把 declaration 类对象和 Documents 的关系提升成了一个正式 UI 能力:

  • documents_count 统计当前 sheet / declaration 下已经发布的文档数
  • action_see_documents() 直接给出 Documents 视图入口
  • action_post_in_documents() 则把下属 line 的发布动作统一代理出去

这说明官方不是把 Documents 当“附件仓库”,而是当 payroll declaration 的正式后置存储层。

用户在 payroll 声明页里看到的,不再只是一个 PDF 二进制字段,而是一个可浏览、可跳转、可计数的文档结果集。


十一、实战里最容易误解的 5 个点

误区 1:工资单发文档就是发邮件附件

不是。核心是 documents.document,邮件只是文档入口通知。

不完全是。这里是为了应对离职、归档后的长期访问需求,同时内部权限反而被收紧到 none

误区 3:内部 HR 或 Documents Manager 自然能看到这些文档

不是。测试明确验证很多内部角色默认无权访问。

误区 4:没用户账号的员工就没法拿到 payroll 文档

也不是。系统会用 Worker Payroll Folder + partner access 托管。

误区 5:声明文档点击发布就立刻同步完成

不一定。很多时候只是进入 pdf_to_post 队列,真正投递由 cron 异步完成。


十二、对二开和实施最有价值的启发

如果你要扩展这套能力,最值得学的是下面几条:

1)先定义 payroll 文档的长期访问策略,再谈邮件模板

工资单类文档的核心不是“怎么发”,而是“离职后谁还能安全取回”。

2)文档 owner、partner、folder 要分情况建模

有账号员工与无账号员工,不能用同一套偷懒逻辑处理。

3)敏感批量文档优先走异步投递

这样更稳,也更适合失败重试与批量控制。

4)把去重和状态机当成一等公民

pdf_to_post / pdf_posted 看似简单,实际上是 payroll 文档可靠性的关键。


总结

documents_hr_payroll 源码串起来以后,你会发现 Odoo 企业版真正实现的不是“工资单 PDF 发邮件”。

它做的是一条更完整的 payroll 文档链路:

  • 把工资单和声明转换成 Documents 资产
  • link access + internal none 处理长期访问与内部隔离;
  • owner / partner / folder 区分有账号员工和无账号员工;
  • Worker Payroll Folder 托管无系统账号场景;
  • pdf_to_post + cron 异步投递声明文档;
  • documents_count / action_see_documents 把文档结果正式嵌回 payroll UI。

所以 Odoo 企业版 payroll 发文档的本质,不是“生成 PDF 后发封邮件”,而是“把敏感薪资文档转成可长期访问、可严格隔离、且适配不同员工身份状态的文档资产”。

这才是这套企业版源码最值得学的地方。


参考源码 - enterprise/documents_hr_payroll/models/hr_payslip.py - enterprise/documents_hr_payroll/models/hr_payroll_employee_declaration.py - enterprise/documents_hr_payroll/models/hr_payroll_declaration_mixin.py - enterprise/documents_hr_payroll/tests/test_documents.py

DISCUSSION

评论区

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