← Work · private · enterprise
Case study · Odoo 14
Labor payroll — timesheet compute, project cost split, safe recovery
Construction / enterprise Odoo: default payslip compute was too thin for field labor — leaves, OT, mid-period hire/exit, and project costing. Built a monthly labor path with allocation and cutover recovery. Client details omitted.
Problem
Field labor pay depended on timesheets, leaves, absences, overtime tiers, and mid-period join/exit — too heavy and brittle for default payslip compute. Costs had to land on the right projects and operating units, not a single flat salary expense. A calendar-cutoff change also left a paid period partially under-computed, so later months needed a controlled recovery without double-paying or skipping real posting.
Domain
- 01Labor batch
Calendar-month payslip run for labor. Submit gated after period end (company timezone).
- 02Day payload
Per-employee day map: work, leave types, holidays, off days, OT codes, and payable employment bounds.
- 03Cost allocation
Payslip → project / cost-center percentages from timesheets → analytic lines → finance journals (OU-aware when mixed).
- 04Recovery / comparison
Source paid period vs target draft; positive-only deltas; attested audit. Managed report-only batches can recompute without posting costs.
Design decisions
- Batch SQL payload over per-slip thrash — build day maps and salary snapshots once, then compute slips from that payload.
- Separate salary vs variable windows — after a cutoff transition, calendar salary and variable (OT / incentive) windows can differ intentionally.
- Trusted in-process context — report-only and precomputed flags cannot be forged via ordinary RPC; only managed comparison batches skip real costing.
- Serialize concurrent compute — row locks and advisory locks on employee×period so recovery apply and recompute cannot race.
Engineering patterns
01 Trusted report-only context
RPC may set flags, but only managed comparison batches receive report-only behavior. Real labor batches always run cost allocation.
# Anonymized pattern — not client source
report_only = all(
slip.batch.is_managed_comparison() for slip in slips
)
# context flags alone cannot skip allocation on real runs02 Project days → allocation to 100%
Count timesheet days per project, floor percentages, push remainder to the largest project. Leftover share goes to an employee fallback cost center when lines do not cover the full slip.
# Anonymized pattern — not client source
days_by_project = count_timesheet_days(exclude_internal=True)
pct = floor(days * 100 / total)
# remainder → largest project
# leftover % → fallback cost center03 Serialize recovery vs recompute
Lock the relevant payslip runs, then take an advisory lock on company + employee + period keys so recovery apply and fast compute cannot race the same attendance / OT deltas.
# Anonymized pattern — not client source
SELECT id FROM hr_payslip_run WHERE id = ANY(...) FOR UPDATE
# + advisory lock (company, employee, date_from, date_to)Constraints & edges
- Labor batch = one complete calendar month; no submit on/before period end
- Leave precedence over conflicting attendance; unpaid sick reportable but not paid
- Mid-month join/exit clamps payable days; rehire must not inherit old exit cut
- Recovery is positive-only, attested, period-scoped — no auto-deduction, no manual recovery lines
- Journal create only after finance state; block if batch journals already exist
Batch / compute surface
Ops work inside Odoo payroll runs: build the day payload, compute slips, allocate cost, post journals, and when needed open a recovery / comparison path. This case is about the payroll close path — not the mobile API hub.
Outcome
Labor payroll runs from a timesheet-built day payload with clear leave / OT / absence rules and employment bounds. Project costing and multi-OU journals stay on the same path as compute. Cutover underpayment is recovered through an attested, positive-only workflow that cannot be forged via context. Comparison batches can recompute without posting costs. Ops get a clear monthly labor process instead of ad-hoc sheet edits and spreadsheet checks.
Source code stays private. Related notes: petty-cash case study, architecture, ORM example.