结论先行
很多团队第一次看这条链,会把它想成“排了班,员工来打卡,工资单去读打卡”三步直连。企业版没有这么粗暴。源码把这条链拆成了 排班事实、考勤事实、工资口径 三层,因为三者的责任完全不同:排班负责“本来应该上多久”,考勤负责“实际上来了多久”,工资工时负责“这一段该不该进 payslip、按什么规则进”。
第一层:入口或表面动作
先看 hr_work_entry_planning_attendance。hr.employee._get_schedules_by_employee_by_work_type 会在合同 work_entry_source == 'planning' 时,把 published 的 planning.slot 拉进来,并把原本资源日历里的 work 区间扣掉。也就是说,工资工时生成时看到的“计划工时”并不是静态班次表,而是已经被排班结果重写后的 schedule。这一步把 planning 从“排班 app 自己看的东西”抬升成了 payroll 上游。
第二层:真正的业务护栏
第二层是加班与真实出勤的对账。hr.version._get_expected_hours_from_contract 在 planning-based 合同下,直接累计排班时长作为 expected hours;而 _get_attendance_intervals 又把 hr.attendance 的 check in/out 拉进来,与 planning 区间比较后产出 overtime/work entry 区间。这里的关键不是“读了考勤”这么简单,而是 planning 决定基线,attendance 决定偏差。没有这层拆分,你根本没法解释为什么某天排了 6 小时、打卡 8 小时,最后会多出 2 小时 overtime,而不是把 8 小时都算作正常班。
第三层:状态落点与边界
第三层才轮到工资。hr.payroll_attendance.hr_payslip._get_attendance_by_payslip 并不会对所有 payslip 都去扫考勤,它先判断 version_id.work_entry_source,只对 attendance 口径的工资单按时间窗去聚合 hr.attendance。这意味着工资读取考勤时,仍然尊重合同定义的来源;不是所有公司都愿意让工资直接跟着打卡走。企业版给的是“排班驱动 work entries,再由 payslip 按合同口径取值”的可切换架构。
为什么这套设计更稳
从业务上看,这样分层有三个直接收益。第一,排班经理可以只负责发布班次,不必碰工资规则。第二,考勤异常、补卡、加班审批可以在 attendance 层消化,而不是污染 planning。第三,工资核算拿到的是已经过合同策略筛过的一组 work entry / attendance 区间,争议更少,也更容易审计。
实战启示
如果你要自定义这条链,最容易犯的错有两个。一个是在排班确认时直接写工资字段,绕过 hr.version 对区间的重算;另一个是在 payslip 里直接 search hr.attendance,把 planning-based 合同也当成纯考勤制。前者会导致 overtime 与 work entry 不一致,后者会让排班合同丢失“计划工时”意义。
实战启示
调试时建议按这条顺序看:先确认 planning.slot 是否 published;再看合同版本 work_entry_source 和 overtime_from_attendance;再看 _get_attendance_intervals 产出的区间;最后才看 payslip 读到了哪些 attendance。把这四层串起来,排班、考勤、工资之间那些“明明都对却算不拢”的问题,基本都会落到一个明确边界上。
DISCUSSION
评论区