← 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.

  • stack Python · Odoo · PostgreSQL · analytic / OU
  • scope Labor batch · cost allocation · recovery · journals
  • role Design & implementation

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

  1. 01
    Labor batch

    Calendar-month payslip run for labor. Submit gated after period end (company timezone).

  2. 02
    Day payload

    Per-employee day map: work, leave types, holidays, off days, OT codes, and payable employment bounds.

  3. 03
    Cost allocation

    Payslip → project / cost-center percentages from timesheets → analytic lines → finance journals (OU-aware when mixed).

  4. 04
    Recovery / 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 runs

02 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 center

03 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.

← Petty-cash caseBack to work →