EH HR Payroll
Salary structures, sequenced rules, and audited payslips on the EH HR Platform workflow engine, for Odoo 16 Community.
Why this module
EH HR Payroll
A real rule engine, not a flat table
Rules run in sequence and accumulate per category, so a later rule reads categories.GROSS or rules.BASIC.total like a proper payroll engine. Fixed, percentage-of-base, or a short Python expression. Gross, total deductions and net are derived from each category kind, not hand-keyed.
Sandboxed formulas that cannot escape
Rule expressions run in a closed namespace with no imports, no ORM, no builtins. Read-only proxies mediate every access, so a rule cannot reach payslip.env, the raw recordset, or the internal stores. Tests prove each of these escape attempts fails the run rather than leaking.
Every payslip is audited end to end
Each payslip rides the platform workflow with group-gated transitions and writes before/after snapshots to an append-only, hash-chained log you can verify on demand. Multi-company writes are scoped and refused across companies. The audit and workflow code is shared, not reinvented here.
Day in the life
A payroll officer runs a monthly pay batch.
A pay run is opened for the period and a payslip is created for each employee on a salary structure. Compute-all runs every draft slip in one pass: each structure's rules fire in sequence, basic reads the period wage as contract.wage, a housing allowance takes twenty percent of basic, tax takes ten percent of the running gross, and an employer contribution is computed but kept out of net. Recomputing is safe because the lines are rebuilt each time, never duplicated, and auto inputs fed in by loan or overtime modules are refreshed while manual inputs are left alone. An admin confirms and then pays the slips; each transition is group-gated and snapshotted to the hash-chained audit log. A payment file is exported from the confirmed slips, and each slip prints as a PDF payslip. Accounting posting, if the team runs it, is a separate optional bridge.
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.
A salary rule formula runs in a closed namespace with no imports, ORM or builtins. Read-only proxies refuse payslip.env, the wrapped recordset (payslip._record), and the name-mangled backing stores. Each escape attempt raises and fails the run instead of leaking, and is covered by a dedicated test.
A rule whose condition, amount, base or quantity expression does not even compile is rejected at save time with a clear error, so a broken formula is caught before a payroll run, not mid-compute.
Recomputing a payslip unlinks and rebuilds its lines rather than appending, so a double compute never duplicates lines. Auto inputs from feeder modules are rebuilt each compute while hand-entered inputs are preserved, so the slip always reflects the current loan balance or approved overtime.
Transitions are gated by group: officers compute and cancel from draft, admins confirm and pay. A final state (paid or cancelled) refuses any further transition, and a confirmed slip can be cancelled by an admin while a paid slip is terminal and must be reversed downstream, not cancelled.
Payslips and pay runs carry a required company and refuse cross-company writes unless an explicit audited override is set, so records never leak across companies through a null-company rule.
Every state change snapshots audited fields to an append-only, hash-chained log. A Postgres advisory lock serializes appends so concurrent runs cannot fork the chain, and verify_chain walks it to prove no row was edited after the fact.
A formula that references a category, rule, worked-days or input code that has not contributed reads a neutral zero rather than raising, so a partially configured structure still computes predictably.
What is inside
Built to do the job, end to end.
- Salary structures and sequenced rules. eh.hr.salary.structure groups eh.hr.salary.rule records applied in sequence. Each rule has a category, a condition (always, numeric range, or Python), and an amount that is a fixed value, a percentage of a base expression, or a Python expression. An appears_on_payslip flag lets a rule compute and feed later rules without showing a line.
- Categories that drive gross and net. eh.hr.salary.rule.category carries a kind (basic, allowance, gross, deduction, net, employer contribution, other). The payslip derives gross, total deductions and net from those kinds, so employer contributions are computed but excluded from take-home. Seeded categories BASIC, ALW, GROSS, DED, NET and EECON ship in the box.
- Payslips on the workflow engine. eh.hr.payslip computes its lines from the structure, accumulating per-category totals so later rules read earlier results. It rides the shared workflow mixin (draft, computed, confirmed, paid, cancelled), the audited mixin, and the strict company-aware mixin. Worked-days and input lines feed the formulas as worked_days.CODE and inputs.CODE.
- Pay-run batches and bank file export. eh.hr.payslip.run groups slips for a period, computes them all in one action, totals net, and exports a payment file from confirmed or paid slips. The default export is a generic CSV; a country localization overrides one method to emit its own bank format.
- Extension points for localizations and feeders. Documented hooks let other modules plug in without forking: _get_rule_helpers adds safe callables to the sandbox, _collect_auto_inputs feeds loan, overtime, advance or gratuity inputs, _on_payslip_paid marks sources processed on payment, and _contract_wage can read a real contract. Pay periods per year are inferred for annualisation.
- PDF payslip and security. A QWeb PDF payslip report prints the lines, categories and totals. Access rights are defined for HR admin, officer and employee self-service groups across every model, with the heavier write and delete rights reserved for admins.
Honest about the edges
What this does not do, so nothing surprises you.
- This is a salary-rule and payslip engine, not a country payroll pack. It ships no statutory tax tables, social-security brackets or local payslip layouts. Those are expected from a localization module that overrides the rule-helper and bank-file hooks.
- It does not post to accounting on its own. Journal posting and reversal are handled by a separate optional bridge module installed only if you run Odoo accounting.
- The bank file export is a generic CSV. A specific bank format (for example an ABA or SEPA file) requires a localization that overrides the render method.
- Rule formulas are single expressions, not multi-line statements, evaluated by safe_eval in eval mode for portability. Conditional logic uses inline a if cond else b.
- It computes from a per-period basic wage on the payslip by default. Reading a live employment contract requires a localization to override the contract-wage hook.
- It depends on the EH HR Platform engines (eh_hr_core, eh_hr_compat, eh_hr_engine_workflow) and standard Odoo hr; it is not a standalone payroll app.
Odoo 16 payroll, Odoo Community payroll, salary structure, salary rules, payslip, payslip computation engine, sandboxed salary rule, pay run batch, bank file export payroll, gross net deductions, employer contribution, multi-company payroll, hash-chained audit payroll, PDF payslip, ERP Heritage HR Platform
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