人力资源

Odoo 考勤里的 Extra Hours 为什么不等于 Payroll 可直接发薪:Attendance、Overtime Line 和 Work Entry 的边界到底在哪

很多人以为员工多打了几小时卡,Odoo 就该直接产生可发薪的加班记录。源码并不是这么设计的。本文把 Attendance、overtime line、work entry 以及 payroll 边界讲清楚。

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

先说结论

在 Odoo 里,员工今天多上了几小时班,不等于系统马上就得到了一条可以直接进工资单的加班工资

官方源码至少拆了三层语义:

  1. Attendance:员工实际打卡了多久
  2. Overtime Line / Extra Hours:这些时长里,哪些部分被规则认定为额外工时
  3. Work Entry / Payroll:这些额外工时最终要不要变成可发薪、按什么类型发薪、按什么倍率发薪

这三层故意没有揉成一层。

所以你在界面里看到 worked_hoursovertime_hoursvalidated_overtime_hours,不要以为最后一个就已经等于“工资系统认定的加班工资”。它更像是:

考勤模块先把“额外工时候选结果”算出来,至于是否进入薪资口径,还要看后续 work entry / payroll 设计。


为什么官方不把“打卡时长”和“可发薪加班”直接绑死

因为这两件事在业务上根本不是同一层。

一个员工晚上多待了 2 小时,可能有几种完全不同的解释:

  • 真的是应该发钱的加班
  • 只是晚走,但公司不认定为加班
  • 需要主管审批后才算
  • 可以换调休,不一定发工资
  • 属于弹性工时,不应直接算加班
  • 只是考勤异常,之后还要人工修正

所以 Odoo 在 hr_attendance 里没有偷懒地做成“多打卡 = 多发钱”。

它先解决的是:

  • 员工实际工作了多久
  • 相对排班来说,多出来或少掉了多少
  • 这些时长是否要审批
  • 规则命中了哪一种 overtime rule

薪资怎么吃这些结果,被留在更后面的层。

这就是边界感。


hr.attendance 里那 3 个小时字段,分别在说什么

源码里最容易让人误会的是这几个字段:

  • worked_hours
  • overtime_hours
  • validated_overtime_hours

1)worked_hours:你实际上班了多久

这个最朴素。

hr.attendance 里,worked_hours 来自签到签退区间,并且在非弹性日历下会把午休区间扣掉。

它回答的问题是:

这条考勤记录实际覆盖了多少工作时长?

它不关心这是不是加班。

2)overtime_hours:规则识别出来的额外工时

overtime_hours 不是简单的 worked_hours - 标准工时

源码里它是把 linked_overtime_idsmanual_duration 累加出来。也就是说,真正起作用的是 overtime line,不是考勤本身。

换句话说:

考勤只是原材料,加班时长是规则计算后的结果。

3)validated_overtime_hours:已批准的额外工时

这个字段只统计状态为 approved 的 overtime line。

所以它回答的问题也不是“应该发多少钱”,而是:

在考勤模块内部,已经被批准通过的额外工时有多少。

请注意这个措辞:在考勤模块内部

它仍然是 HR/Attendance 语义,不是最终 payroll 语义。


Extra Hours 真正落地的核心对象,其实不是 Attendance,而是 hr.attendance.overtime.line

如果只盯着 hr.attendance,你会误以为系统直接在考勤表上算加班。

但源码真正单独建了一个模型:hr.attendance.overtime.line

这个模型里有几个特别关键的字段:

  • employee_id
  • date
  • status
  • duration
  • manual_duration
  • time_start
  • time_stop
  • amount_rate
  • rule_ids

这说明官方认定“额外工时”不是一列附属数字,而是一种可被审批、可被规则命中、可携带倍率信息的独立结果对象

这里有两个很重要的信号。

信号一:它有 status

状态可能是:

  • to_approve
  • approved
  • refused

这就说明 extra hours 不是天然有效,而是可能经过管理动作。

信号二:它有 amount_rate

amount_rate 不是纯展示字段,它表达的是“这段额外工时可能对应什么倍率”。

但更关键的是,这个模型里还有一条非常耐人寻味的注释:

  • # in payroll: rate, work_entry_type
  • # in time_off: convertible_to_time_off

这几乎已经把官方设计意图直接写在脸上了:

当前这个 overtime line 只是中间结果,后面可以往 payroll 方向长,也可以往 time off 方向长。

也就是说,extra hours 在这里还没有被锁死成“工资条上的钱”。


加班行是怎么生成的:不是增量补丁,而是“删掉重算”

hr.attendance 里的 _update_overtime() 很值得注意。

它的做法不是“发现一条新打卡,就只补一点差值”。

相反,它会:

  1. 先根据员工和日期范围找到相关 overtime lines
  2. 把这些 overtime lines 删掉
  3. 重新收集这一段时间内的考勤
  4. 按版本、规则集、排班区间重新计算
  5. 再统一创建新的 overtime lines

也就是说,官方把 extra hours 当成一种派生结果,而不是主数据。

这在实施上很关键。

很多人会想:

  • 我能不能手改某条考勤上的 overtime_hours?
  • 我能不能直接把某条加班数字写死?

源码告诉你:别把派生结果当主入口。

因为只要考勤、员工、签到签退或规则变了,这些 overtime line 很可能会被整段重算。

所以更稳定的入口通常是:

  • 修考勤原始记录
  • 修 overtime rule
  • 修 ruleset / schedule / version
  • 再让系统重算

而不是硬改最终汇总数字。


Odoo 不只会算“正向加班”,它还会算“负向差额”

这是这套设计里最容易被忽略、但很体现深度的一点。

在 quantity rule 的计算里,如果公司开启了 absence_management,并且结果小于员工容差,系统会返回 undertime,也就是负向额外工时。

这说明官方不是在算一个简单的“超过 8 小时就是加班”。

它在表达的是:

相对预期工时,这个人今天究竟是超了、刚好、还是少了。

所以 extra hours 在源码里的本质,更像“相对预期工时的偏差管理”,而不只是“奖励性加班”。

这也解释了为什么它不能直接等于 payroll。

因为 payroll 往往只接收某种被认可的、可计薪的结果;但 attendance 这里先处理的是更广义的工时偏差。


Rule 为什么分 quantitytiming

hr.attendance.overtime.rule 里,官方把规则分成两类:

  • quantity
  • timing

这也是很多实施会忽略的设计点。

quantity:多出来多少

这种规则关注的是:

  • 一天/一周工作总量有没有超
  • 预期工时来自合同排班还是固定值
  • 员工容差、公司容差是多少

也就是“量”的问题。

timing:多出来的时间落在什么时段

这种规则关注的是:

  • 是否发生在工作日 / 非工作日
  • 是否发生在请假期间
  • 是否发生在特定排班之外
  • 是否落在指定时段内

也就是“时间位置”的问题。

这两类规则叠加后,系统不只是知道“多了几小时”,还知道:

  • 这些小时为什么算 extra
  • 属于哪种规则命中
  • 应该套什么倍率

所以 extra hours 其实已经是一种规则解释过的中间层结果


为什么 overtime line 的 time_start/time_stop 还会等于整条考勤区间

这里也很容易让开发者困惑。

在新版生成逻辑 _generate_overtime_vals_v2() 里,最终写入 overtime line 时,time_starttime_stop 用的是整条 attendance 的 check_in/check_out,真正拆分后的额外工时长度则放在 duration 里。

这说明什么?

说明这条 overtime line 更像:

“这条考勤记录,在某一天、按某组规则,贡献了多少额外工时”

而不是“数据库里必须保存最精细的额外工时切片”。

所以你不能简单把 overtime line 看成一个精确的物理时间段,它更像是考勤结果上的规则归属记录

这也再次说明:它距离最终 payroll line,还有一层业务翻译。


为什么 validated_overtime_hours 也不等于 Work Entry

因为 work entry 解决的是另一件事。

hr_work_entry 里,核心模型是 hr.work.entry,字段包括:

  • work_entry_type_id
  • duration
  • amount_rate
  • state
  • version_id

而在 hr.version 里,work entry 的默认生成来源是 work_entry_source = 'calendar'

也就是说,基础工作录入的生成逻辑首先在处理:

  • 员工在这个期间理论上应该上什么班
  • 哪些区间是正常出勤
  • 哪些区间是 leave
  • 对应应该落成什么 work_entry_type

官方数据里确实预置了:

  • Attendance
  • Overtime Hours
  • Out of Contract
  • 各种 leave type

但在当前你能看到的基础源码里,Attendance 的 overtime line 并没有直接把自己自动变成 payroll-ready work entry

它只是已经准备好了两个很关键的钩子:

  • amount_rate
  • 注释里提到的 work_entry_type

这说明官方是在给后续薪资或本地化模块留接口,而不是在 hr_attendance 里直接做完所有发薪逻辑。

所以边界可以总结成一句话:

validated extra hours = 已批准的考勤额外工时;work entry = 薪资与工时制度真正消费的标准化输入。

两者相关,但不是同一个层。


对实施和开发来说,最容易踩的 4 个坑

坑一:把 worked_hours - 标准工时 当成最终加班

这几乎一定过于粗糙。

因为还有:

  • 午休扣减
  • 时段规则
  • 周规则
  • 容差
  • 请假区间
  • 审批状态

坑二:看到 validated_overtime_hours 就直接发工资

这通常只在非常简单的制度里才勉强可行。

只要企业有下面任一情况,就应该再加一层映射:

  • 不同类型加班有不同工资项
  • 加班可转调休
  • 周末与节假日倍率不同
  • 部分加班需要落成特定 work entry type

坑三:手改汇总字段,不改规则来源

因为 _update_overtime() 会重算,很多“修过但又被冲掉”的问题就是这么来的。

坑四:搞不清 attendance 视角和 payroll 视角

Attendance 在回答:

  • 实际发生了什么
  • 相对预期偏差多少
  • 哪些偏差被批准

Payroll 在回答:

  • 哪些时长可计薪
  • 用什么工资项或 work entry type 计薪
  • 按什么倍率结算

两个问题相关,但并不相同。


如果你要做二开,正确切入点通常是什么

如果你的目标是“让已批准的 extra hours 进入薪资”,更稳的思路通常不是改 worked_hours 或硬写工资单,而是明确补齐这层转换:

  1. hr.attendance.overtime.line 出发
  2. 依据 statusrule_idsamount_rate 判断哪些可计薪
  3. 映射到明确的 work_entry_type
  4. 再进入 payroll 或本地化薪资逻辑

如果你的目标是“加班可换调休”,也不应该直接从 attendance 总数硬扣,而应围绕 overtime line 的审批结果去做转换。

因为官方已经把这里设计成分叉点了。


调试这类问题时,我建议按这个顺序看

  1. 先看 attendance 原始记录 - check_in - check_out - worked_hours

  2. 再看 ruleset 和 rule - quantity 还是 timing - 容差是多少 - 是否按合同排班取 expected hours - 是否启用 absence management

  3. 再看 overtime line - 生成了没有 - status 是什么 - duration / manual_duration 是多少 - amount_rate 从哪条规则来

  4. 最后才看 work entry / payroll - 有没有做映射 - work_entry_type 是什么 - 是否真的进入薪资消费链路

这样你会比一上来盯工资单,排错快很多。


一句话记忆法

Attendance 记录事实,Overtime Line 记录规则解释后的额外工时,Work Entry / Payroll 才决定这些工时如何被制度化消费。

把这三层分开,你看 Odoo 人力源码会清楚很多。

DISCUSSION

评论区

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