EH HR Policy Engine
HR business rules as safe, scoped, versioned data instead of hard-coded constants.
Why this module
EH HR Policy Engine
A DSL that cannot do harm
The evaluator reads context values and emits a typed result. It cannot import, cannot call arbitrary Python, cannot touch the ORM and cannot mutate state. Unknown operators are rejected outright, so an HR officer editing a rule can never run code.
Configure, do not patch
Leave caps, overtime multipliers, late thresholds and eligibility tests live in policy records, not hard-coded constants in feature models. An administrator changes how the business runs by editing JSON, not by commissioning a developer on a billable day.
The right rule for the right people
Resolution prefers a department-specific policy, then falls back to the company-wide one, and can be filtered to a valid-from and valid-to window. Each company sees only its own rule set, enforced by a record rule on company_id.
Day in the life
One rule, resolved where it is needed
Payroll asks the engine for the overtime multiplier for a specific employee on a specific date. The engine resolves the policy for that employee's company, prefers a rule scoped to their department if one exists, checks the valid-from and valid-to window, then evaluates the JSON body against the supplied context and returns a typed number. No constant was hard-coded, no Python ran, and another company's rules were never in scope. When HR needs to change the multiplier next quarter, they edit a record, not the source.
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.
The evaluator whitelists a fixed set of nodes (literals, context reads, comparisons, boolean logic, arithmetic, conditional). Any unrecognised key raises a UserError rather than executing, so a malformed or malicious rule body fails loudly instead of running.
Comparisons return False when either side is missing rather than raising, and arithmetic coerces None to zero, so a rule referencing an absent context path degrades predictably instead of crashing payroll.
The div operator returns None when the divisor is zero or falsy, so a formula with a zero denominator yields a defined empty result instead of an exception.
When both a department-scoped and a company-wide policy match the same code, resolution deterministically returns the department-specific record first and only falls back to the unscoped company rule when no narrower match exists.
A global record rule forces every policy read through company_ids, so an officer in one company cannot see or resolve another company's thresholds, multipliers or formulas.
A JSON field seeded from XML text is stored as a JSON string. The config helper detects this and parses it back to a dict, so policies loaded from data files resolve identically to ones entered in the form.
A database unique constraint on code, company, department, tag and active blocks a second active policy for the same scope, so resolution never has to guess between two live records.
What is inside
Built to do the job, end to end.
- The policy model. Adds eh.hr.policy: a code, a human name, company and optional department and employee-tag scope, a JSON body, a valid-from and valid-to window, an active flag, a readonly version counter and free-text notes. Edited through a form with an ACE JSON editor.
- The evaluator service. A registered eh.hr.policy.engine service with resolve, evaluate and config methods. evaluate resolves the scoped policy and runs its DSL body against a context dict using dotted paths like employee.tenure_months. config returns a policy body as a plain dict for configuration-style rules.
- Security and scope. Three access tiers (admin full control, officer read, write and create, employee self read-only) plus a global company record rule. Built on eh_hr_core, which contributes the platform mixin and correlation id every record carries.
Honest about the edges
What this does not do, so nothing surprises you.
- This module ships the policy model and the evaluator service. It does not itself rewire leave, overtime, loan or payroll to consume policies; the feature modules that depend on it do that.
- The version field is a stored readonly counter and a notes field is provided, but this release does not automatically bump the version or snapshot prior bodies on each edit, so it is not a full revision-history store.
- Policy edits are not written to the hash-chained audit log automatically; this model carries the platform mixin and correlation id, not the audited mixin, so capturing who changed a rule is not built in here.
- Resolution runs a live database query on each call; there is no in-memory result cache in this release, so very hot paths should resolve once and reuse the result.
- The DSL is deliberately small: comparisons, boolean logic, four arithmetic operators and a conditional. It has no loops, no string functions and no user-defined functions by design.
- Ships with the policy form, list and menu. It does not seed example policies; you author the rule bodies your business needs.
- No external service, no network calls and no subscription. Everything runs inside your own Odoo 16 Community database.
HR policy engine Odoo, business rules engine Odoo 16, no-code HR rules, leave cap configuration, overtime multiplier rules, eligibility rules engine, JSON DSL Odoo, per-company HR policy, configurable HR constants, sandboxed rule evaluator, scoped policy resolution, Odoo Community HR platform, rules as data, department-scoped HR rules, ERP Heritage HR
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