EH HR Payroll
Salary structures, a sandboxed rule engine, and payslips that ride a configurable workflow.
Why this module
EH HR Payroll
Rules are data, not patched code
A salary structure is an ordered list of rules. Each rule is a fixed amount, a percentage of a base, or a short Python expression evaluated against a closed payslip namespace. Later rules read earlier results and running category totals, so gross-based tax and net subtotals fall out naturally.
Formulas that cannot reach the database
Rule expressions run through safe_eval with no imports, no builtins, and no ORM. The payslip and employee are exposed as read-only proxies that refuse underscore-prefixed names and name-mangle their backing stores, so a rule cannot read the cursor or the raw recordset. Negative tests prove the escape attempts fail the run.
Every slip change is on an immutable chain
Payslips are audited: create, write, and unlink emit before and after snapshots into an append-only log where each row carries the sha256 of the previous row. A serialized advisory lock orders appends, and verify_chain walks the whole log to flag the first tampered row.
Day in the life
A monthly pay run, start to finish
A payroll officer opens a pay run for the period, drops in the employees, and hits compute all. Each draft slip runs its structure rules in sequence: basic reads the period wage, allowances take a percentage of basic, a gross subtotal accumulates, tax computes off gross, and net falls out from the category kinds. Auto inputs (an approved loan instalment, recorded overtime) are pulled in fresh on every compute, so the slip always reflects the current balance. The officer reviews, an admin confirms then pays (the confirm and pay buttons are gated to admin), and the run exports a payment file listing each confirmed slip's net. Every transition and figure lands on the audit chain.
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.
Computing a slip twice does not double its lines. Compute unlinks the existing lines and rebuilds from scratch, so a re-run after editing a rule or changing the wage produces exactly one line per rule, proven by test.
A rule that writes payslip.env, payslip._record, or categories._d to reach the cursor or the internal store fails the run with a clear error instead of leaking the ORM. The proxies refuse every underscore-prefixed name and mangle their backing dicts so even single-underscore backdoors are closed.
A rule whose condition, amount, or quantity expression does not even compile is rejected at save time with a validation error, so a typo surfaces when you edit the rule, not halfway through a live pay run.
When a gated transition opens an approval chain, the real submitter is captured before the engine elevates to sudo, so the user who fired the transition cannot later approve their own request even if they hold an approver group.
Concurrent writes cannot fork the hash chain: a transaction-scoped Postgres advisory lock serializes appends so each row hashes against the true tail, and it releases automatically on commit or rollback. Adding the company column never rehashes existing rows, so the chain survives upgrades.
A paid slip is final. The workflow refuses any further transition out of it even if a misconfigured definition declares one, so a paid slip is reversed through an accounting credit note, never silently cancelled back.
Feeder inputs (loan, overtime, advance) are rebuilt on every compute while hand-entered inputs are left untouched, so the slip never deducts a stale instalment and never wipes a manual adjustment.
What is inside
Built to do the job, end to end.
- Salary structures and rules. eh.hr.salary.structure holds an ordered set of eh.hr.salary.rule records. Each rule has a category, a condition (always, numeric range, or Python expression), and an amount (fixed, percentage of a base, or expression). Categories carry a kind (basic, allowance, gross, deduction, net, employer contribution) that drives the derived totals.
- Payslip and the rule engine. eh.hr.payslip computes lines by running the structure's active rules in sequence against a closed namespace of contract wage, worked days, inputs, running category totals, and prior rule results. Worked-days and input lines feed formulas as worked_days.CODE and inputs.CODE. Gross, total deductions, and net are stored computes off the category kinds.
- Workflow, audit, and pay runs. The payslip rides the platform workflow engine (draft, computed, confirmed, paid, cancelled) with group-gated transitions and full audit emission. eh.hr.payslip.run batches slips for a period, computes them together, and exports a CSV payment file. A PDF payslip report is included.
- Built to be extended by a localization. The base ships no tax tables. _get_rule_helpers, _collect_auto_inputs, _contract_wage, and _render_bank_file are clean extension points a country or feeder module overrides to add PAYG-style helpers, loan and overtime feeders, real contract reads, and a country bank-file format.
Honest about the edges
What this does not do, so nothing surprises you.
- No country tax, social security, or superannuation tables ship in this module. The engine computes whatever rules you define; statutory tables and helper functions are added by a separate localization layer that overrides the documented extension points.
- The bank file export is a generic CSV of employee, reference, and net amount. A country-specific payment format (for example a fixed-width national file) is produced by overriding the render method in a localization module.
- Auto inputs from loans, overtime, advances, and gratuity are wired as extension points only. The base returns nothing; the actual feeders live in their own modules and append inputs on each compute.
- The period wage is entered per payslip as basic_wage and read by formulas as contract.wage. This module does not read a real employment contract; a localization can override the wage source.
- Salary rules are single expressions, not multi-statement scripts. Conditional logic uses inline expressions (a if cond else b). This keeps the sandbox portable across Odoo 16 to 19 and keeps formulas auditable.
- This module owns no workflow, approval, or audit code of its own. It composes the platform engines from eh_hr_core and eh_hr_engine_workflow, which are required dependencies.
Odoo 19 payroll, Odoo Community payroll, salary structure, salary rules, payslip computation, payroll engine, pay run batch, payslip batch, bank file export payroll, sandboxed salary formula, payroll audit trail, HR payroll Odoo, payslip workflow, gross net deduction payroll, configurable payroll rules
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