HR Payroll Engine
Sandboxed salary rules, sequenced payslips, hash-chained audit on Odoo 18.
Why this module
HR Payroll Engine
Rules that read each other
Structures run rules in sequence, accumulating per-category totals. A later rule reads categories.GROSS or rules.BASIC.total, so gross, deductions, employer contributions and net all derive from the same chain. Amounts come from a fixed value, a percentage of any base expression, or a Python expression.
A rule cannot escape
Rule formulas evaluate with safe_eval in eval mode. The payslip and employee are wrapped in read-only proxies that refuse every underscore-prefixed name, and the backing stores are name-mangled, so a rule cannot reach env, the cursor, or the raw recordset. Negative tests prove payslip.env and payslip._record both fail the run.
Every change recorded
Payslips carry a workflow state and an audit mixin. Each transition and each tracked field change writes a row to an append-only, sha256 hash-chained audit log, so an edit that does not recompute the whole downstream chain is detectable. Cross-company writes are refused unless explicitly overridden and audited.
Day in the life
A monthly pay run, start to file
A payroll officer opens a pay run for the period and adds a payslip per employee against a salary structure. Compute all runs every draft slip through its rules: basic from the contract wage, a housing allowance as a percentage of basic, a gross subtotal, tax as a percentage of gross, an employer contribution excluded from net. Each slip shows its lines and a derived gross, total deductions, and net. An admin confirms the run, marks slips paid, and exports a CSV payment file built from the confirmed and paid slips. Throughout, every transition and field change is written to the hash-chained audit log.
Edge cases
The cases most modules quietly ignore.
In the shipped code today, each one a place where a cheaper module silently does the wrong thing.
Compute unlinks existing lines and rebuilds them, and rebuilds feeder-generated auto inputs while leaving manual inputs untouched, so recomputing a slip never duplicates lines and always reflects the current state.
safe_eval permits single-underscore names, so the proxies refuse any underscore-prefixed access and name-mangle their stores. A rule reading payslip.env, payslip._record, categories._d or inputs._d fails the run with a UserError rather than reaching the ORM.
Rule expressions are compile-checked on save. A formula that does not even parse raises a ValidationError at save time, not mid pay run, so a typo never breaks a live payroll.
Paid and cancelled are final states. The workflow mixin refuses any further transition out of a final state even if a misconfigured definition declares one, so a paid slip cannot be silently re-driven.
Compute and cancel from draft are officer-level; confirm, pay, and cancel from confirmed are admin-only. A user outside a transition's allowed groups is rejected, so confirming and paying stay separated from data entry.
Payslips and pay runs default to the active company and refuse a cross-company write, even under sudo, unless an explicit override context is set and the elevation is written to the audit log.
The engine infers pay periods per year (daily, weekly, fortnightly, monthly, annual) from the period span and exposes it to rules as periods, so localization rules can annualise without hardcoding a frequency.
What is inside
Built to do the job, end to end.
- Salary structures and rules. eh.hr.salary.structure holds sequenced eh.hr.salary.rule records with unique codes. Each rule has a category, a condition (always, numeric range, or Python), an amount mode (fixed, percentage of a base, or code), a quantity expression, and a flag for whether it appears on the slip while still feeding later rules.
- Categories that drive totals. eh.hr.salary.rule.category carries a semantic kind (basic, allowance, gross, deduction, net, employer contribution, other). The payslip derives gross from basic and allowance lines, total deductions from deduction lines, and net as gross minus deductions, all from the kinds rather than hardcoded codes.
- Payslips and their lines. eh.hr.payslip computes one line per rule that appears on the slip, plus worked-days lines read as worked_days.CODE and input lines read as inputs.CODE. Manual inputs sit alongside feeder auto inputs. Slips are sequenced PAY/year/ and ride the configurable workflow.
- Pay runs and bank file. eh.hr.payslip.run batches payslips for a period, computes every draft slip at once, tracks slip count and net total, and exports a CSV payment file from confirmed and paid slips. _render_bank_file is an extension point a localization overrides to emit a country-specific format.
- Extension points. Feeder hooks (_collect_auto_inputs, _sync_auto_inputs, _on_payslip_paid) let loan, overtime, advance, and gratuity modules push inputs and mark sources processed once paid. _get_rule_helpers lets a localization add safe callables such as a PAYG helper to the rule sandbox.
- Workflow and audit, from the platform. The module owns no workflow or audit code. State, transitions, group gating, and the final-state guard come from eh_hr_engine_workflow; the append-only hash-chained audit log and strict multi-company scoping come from eh_hr_core mixins shared across the suite.
Honest about the edges
What this does not do, so nothing surprises you.
- Base ships a generic worked-example structure and categories, not country tax tables. PAYG, superannuation, social-insurance brackets and similar are provided by separate localization rules and the _get_rule_helpers extension point, not by this module.
- The bank-file export is a generic CSV of employee, reference, and net amount. A real bank format such as an ABA file is produced by a localization that overrides _render_bank_file.
- This module computes and records payslips but does not post them to accounting. Journal posting and reversal are handled by a separate accounting bridge referenced in the workflow notes.
- Employee self-service is read-only access to structures, rules, and slips. There is no employee portal beyond standard record access.
- The wage fed to rules as contract.wage defaults to the payslip's own basic_wage for the period. Reading a real dated contract is an override point (_contract_wage), not base behaviour.
- Requires the EH HR Platform base, compat, and workflow-engine modules plus Odoo hr. It is a calculation engine on the platform, not a standalone payroll app.
odoo 18 payroll, salary structure odoo, salary rules engine, payslip computation, sandboxed payroll rules, pay run batch, hash-chained audit payroll, multi-company payroll odoo, payslip workflow, employer contribution payroll, gross net deduction calculation, community payroll odoo 18
Need this fitted to the way you work?
ERP Heritage delivers end to end Odoo work: Odoo Implementation, Customization and Development, Integration, Migration, Consultation, Support and Training. We help teams put this module into production, shape it to their process, and keep it running.
We work with businesses across Australia (Melbourne, Sydney, Brisbane, Perth, Adelaide, Canberra) and the Middle East (Dubai, Abu Dhabi, Riyadh, Jeddah, Doha, Kuwait City, Muscat). Start a conversation at erpheritage.com.au or email info@erpheritage.com.au.
Languages
Available in 19 languages
The interface ships translated out of the box. Switch language in Odoo and the fields, menus, and messages follow.
Please log in to comment on this module