企业版薪资会计

Odoo 企业版工资单入账为什么不是“验证后生成一张分录”这么简单:按月合批、NET 规则、多银行卡拆付与支付边界讲透

很多人以为 Odoo 企业版 payroll 接会计,只是在工资单验证后顺手生成一张 journal entry。源码真正做的是一套按工资结构 journal 与月份分组、按 salary rule 决定合并/拆分、按 NET 规则处理净薪口径、按员工银行卡分摊付款,并且只有在分录已过账、NET 贷方科目可核销、银行账户可信时才允许进入 payment register。

企业 会计
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 7 阅读

很多团队第一次把 Odoo Enterprise Payroll 接到会计时,脑子里的模型通常很简单:

  • 工资单算完;
  • 点 Validate;
  • 系统自动生成一张会计分录;
  • 然后打款结束。

这个理解能描述表面结果,但解释不了很多企业现场常见现象:

  1. 为什么有些工资单是一批人合成一张分录,有些又是一人一张?
  2. 为什么同样叫 NET,真正进入会计分录的净薪金额还会被某些规则再扣掉?
  3. 为什么有的 salary rule 会被合并进同一行,有的却会按员工、按名称甚至按银行卡拆开?
  4. 为什么工资单已经 validated 了,系统还不让你直接 register payment?
  5. 为什么一个员工两张银行卡时,最后真的会拆成两笔付款,而不是只记一笔总额?

如果把 /home/ubuntu/odoo-temp/enterprise/hr_payroll_account 里的 models/hr_payslip.pymodels/hr_salary_rule.pymodels/account_payment.pymodels/res_partner_bank.py 和对应测试一起看,官方真正设计的不是“工资单顺手出分录”,而是:

先把 payroll 结果翻译成可审计的会计口径,再把可支付的净薪负债,谨慎地桥接到付款动作。

这也是它比“验证即记账”复杂得多的原因。


一、先说结论:工资单入账的核心对象不是“单张 slip”,而是“按 journal + 月份组织的 posting 批次”

action_payslip_done() 确实是入口,但真正关键的是后面的 _action_create_account_move()

源码没有简单地对每张工资单各建一张 account.move。它先把要入账的工资单收集起来,然后按两层维度分组:

  • 工资结构上的 journal
  • 会计日期 / 工资所属月份

如果公司启用了 batch_payroll_move_lines,那么同一 journal、同一月份的工资单会被并到一张分录里;如果没启用,就会拆成一人一张或一 slip 一张。

所以官方理解的不是:

一张工资单 = 一张会计分录

而是:

一批可以共享会计语义的工资单 = 一个 posting 容器

这点很重要,因为企业财务往往需要的是“月度工资负债”口径,而不是每个员工都独立占一张总账凭证。


二、为什么系统会把整批 payslip_run 也一起带进来

_action_create_account_move() 里有个很容易忽略的动作:

  • 如果某张工资单属于 payslip_run_id
  • 且这个 batch _are_payslips_ready()
  • 那么它会把整个 batch 的 slip 都并入 all_payslips

这说明 Odoo 不希望你在工资批次还没准备好时,零碎地记一半账、留一半工资单悬空。

也就是说,只要它判断这个 payroll batch 已经具备统一入账条件,就倾向于把整批一起推进会计。

这样做的好处很直接:

  • 月度工资分录更完整;
  • 批次级别的会计追踪更清晰;
  • 后续 pay run 上挂 move_id 也更自然。

hr_payslip_run.py 甚至专门给 run 加了 move_idaction_open_move(),说明官方默认就把“批次级工资入账”当成一等公民,而不是附属玩法。


三、NET 为什么不是“工资单最终金额原样进账”

最值得读的一段在 _prepare_slip_lines()

当它遍历工资单行时,如果碰到 line.code == 'NET',不会直接把 line.total 原样拿去记账。源码会再扫一遍工资单行,把那些 salary_rule_id.not_computed_in_net 为真的规则从 NET 里剔出去。

这背后传达的是一个很重要的会计边界:

工资单上的“净薪显示口径”不一定等于会计上应该挂到 NET 负债的口径。

为什么?因为某些规则虽然会影响工资单展示,但在会计上需要独立入账,而不是混进应付净薪里。

hr_salary_rule.py 里这个字段的帮助文本也写得很直白:

  • 如果勾选 Excluded from Net
  • 这个规则就不会计入 NET salary rule 的 journal entry
  • 应该单独配置借贷科目处理

所以真正进入会计的 NET,不是“页面看到多少就是多少”,而是经过规则口径再整理过的可支付净薪


四、为什么有些工资规则会合并,有些会拆开

这部分是 payroll-accounting 最容易被误解的地方。

_prepare_slip_lines()_prepare_line_values() 里,系统会根据公司设置与 salary rule 设置,决定会计行是合并还是拆分。

情况 1:公司启用批量合并

如果 company.batch_payroll_move_lines = True,系统倾向于把同类会计行合并。

这更适合财务想看月度汇总、而不想凭证被员工明细刷屏的场景。

情况 2:rule 没要求挂员工维度

如果 employee_move_line = False,即使不是批量模式,某些同类行也会被 merge。

这意味着这条规则在会计上更像总额口径,而不是员工应付款口径。

情况 3:rule 要挂员工维度

如果 employee_move_line = True,并且不是 batch 模式,系统会把 partner 设成员工的 work_contact_id,这样后续 payment register 才有明确的对手方可追踪。

情况 4:rule 还要求按名称拆分

split_move_lines 会进一步要求按工资单行名称拆,而不是只按 rule 名称合并。对报销、补发、扣回这类明细化项目很有用。

所以真正控制工资分录颗粒度的,不是一个总开关,而是三层叠加:

  • 公司是否批量合并
  • rule 是否挂员工
  • rule 是否按名称继续细拆

五、为什么多银行卡会在会计分录阶段就先拆开

_prepare_line_values() 里最企业版的一段,是对 多银行卡工资分配 的处理。

如果同时满足:

  • 当前不是 batch merge
  • 这条 rule 需要 employee_move_line
  • 员工 has_multiple_bank_accounts

那么系统不会只生成一条总额会计行,而是先调用 compute_salary_allocations(),再按每个银行账户生成多条 line,并把 employee_bank_account_id 写到行上。

也就是说,付款拆分不是到 payment wizard 才临时算的,而是在会计负债层就已经保留了“这部分净薪该去哪个银行账户”的痕迹。

这非常关键。

因为只有这样,后面的 payment register 才能顺着分录行,安全地产生多笔 outbound payment。

测试 test_hr_payroll_payment_multi_banks.py 也明确验证了这一点:

  • 一个员工两张银行卡
  • 分配比例 40% / 60%
  • 最终真的生成 2 笔付款
  • 每笔金额与对应银行卡匹配

所以多银行卡不是 UI 花活,而是从会计分录开始就被建模了。


六、为什么 system 允许“Adjustment Entry”兜底平账

工资规则和小数精度一多,借贷不平是很容易出现的边缘问题。

_prepare_adjust_line() 的做法很务实:

  • 先用 Payroll 精度比较借贷合计;
  • 如果不平,就往 journal 的 default_account_id 补一条 Adjustment Entry
  • 如果 journal 没默认科目,直接报错。

这说明 Odoo 的态度不是“有 0.01 差额也照样过”,而是:

工资凭证必须严格平衡;如果业务规则和取整让它不平,就显式打到调整科目,而不是悄悄吞掉误差。

这对财务审计是好事,因为差额被明牌展示,而不是隐藏在某条工资行里。


七、为什么 validated 还不能立刻 register payment

很多实施同学最容易踩的坑,就是看到工资单已经 validated 了,就默认应该可以付款。

源码在 action_register_payment() 里连设了好几道门槛:

  1. 不能是已 paid 的工资单
  2. NET salary rule 的 credit account 必须可 reconcile
  3. 员工银行账户必须允许 outbound payment(可信)
  4. 关联的 account.move 必须已经 posted

这几个限制合起来传达的其实是同一句话:

支付动作只能建立在“已确认、可核销、可追账、且收款账户可信”的净薪负债上。

特别是第二条很容易被忽略。

如果 NET 对应的贷方科目不可核销,后续付款就无法把“应付工资”和“实际付款”正确勾上,系统干脆不让你走下去。

而银行账户上的 allow_out_payment 检查,则明确表达了企业打款的合规边界:不是员工填了个账号就能直接放款。


八、为什么 hr_payroll_account 还要改 payment register 的合法科目类型

models/account_payment.py 只做了一件很短、但非常关键的事:

  • 如果上下文里有 hr_payroll_payment_register
  • 就把 liability_current 加进 payment account type 白名单

这段看起来小,却解决了 payroll payment 和普通 AP/AR payment 的本质差异。

普通付款向导更多围绕应收应付、银行现金这些常见科目类型转;但工资净薪常常挂在 流动负债 上。

如果不放开这类科目,工资支付根本没法沿着标准 payment register 走。

所以 hr_payroll_account 做的不是另起一个“工资付款系统”,而是:

在保留 account.payment 标准框架的前提下,为 payroll 的负债型付款打开一条合法通道。

这也是 Odoo 一贯的扩展风格:复用会计主框架,而不是在旁边再造一个影子流程。


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

误区 1:验证工资单就一定一人一张分录

不一定。可能按 journal + 月份合批。

误区 2:NET 行金额就是最终付款金额

不一定。not_computed_in_net 会把某些规则从可支付净薪里剔出去。

误区 3:多银行卡只是付款界面上的拆分

不是。会计分录阶段就已经写了银行账户级拆分语义。

误区 4:工资单 validated 就能付款

不是。还要看 move 是否 posted、NET 贷方是否可核销、账户是否可信。

误区 5:工资付款是 payroll 自己独立搞的一套支付逻辑

也不是。它仍然走 account.payment.register,只是对负债型工资支付做了特化放行。


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

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

1)先区分“工资结果”与“会计可支付结果”

显示给 HR 的净薪,不等于应付工资负债口径。

2)把分录颗粒度设计成可配置,而不是写死

合批、挂员工、按名称拆分,分别解决不同财务需求。

3)在会计层提前保留支付目标信息

多银行卡拆付之所以稳,就是因为分录行上已经知道钱要去哪。

4)付款入口必须卡住核销与信任边界

否则工资支付一旦放错账或打错户,代价远高于普通业务付款。


总结

hr_payroll_account 源码串起来以后,你会发现 Odoo 企业版真正实现的不是“工资单验证后自动记个账”。

它做的是一条完整的企业链路:

  • journal + 月份 组织工资入账批次;
  • salary rule 控制 NET 口径、借贷科目与分录颗粒度;
  • employee_move_line / split_move_lines / batch_payroll_move_lines 平衡汇总与明细;
  • salary allocations + employee_bank_account_id 支撑多银行卡拆付;
  • reconcile / posted / trusted bank account 守住付款边界;
  • 最终再复用 account.payment.register 完成净薪支付。

所以 Odoo 企业版工资会计的本质,不是“工资单顺手生成一张凭证”,而是“先把 payroll 结果翻译成可核销、可支付、可审计的负债,再谨慎推进付款”。

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


参考源码 - enterprise/hr_payroll_account/models/hr_payslip.py - enterprise/hr_payroll_account/models/hr_salary_rule.py - enterprise/hr_payroll_account/models/account_payment.py - enterprise/hr_payroll_account/models/res_partner_bank.py - enterprise/hr_payroll_account/models/hr_payslip_run.py - enterprise/hr_payroll_account/tests/test_hr_payroll_payment.py - enterprise/hr_payroll_account/tests/test_hr_payroll_payment_multi_banks.py - enterprise/hr_payroll_account/tests/test_res_partner_bank.py

DISCUSSION

评论区

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