EH HR Policy Engine
HR business rules as safe, versioned data instead of hard-coded constants.
Why this module
EH HR Policy Engine
Rules that cannot run wild
The DSL supports only literals, context reads, comparisons, boolean logic, arithmetic and a conditional. No imports, no builtins, no ORM access from the evaluator. Unknown nodes are rejected with a clear error, so a policy body can be edited by an HR officer without opening a Python risk.
Constants become policy records
Late thresholds, overtime multipliers, leave caps, certificate-from-day and tenure minimums move out of model source into eh.hr.policy rows. Change a cap without a deployment. Each policy carries a code, a scope, a JSON body and a validity window.
Right rule for the right group
Resolution is most-specific-first: a matching department or tag policy wins, otherwise the engine falls back to the company-wide rule. Company is taken from the employee or the active company, and a global record rule keeps one company from reading another's rule set.
Day in the life
A late-arrival threshold changes mid-quarter
An HR officer opens the matching policy, edits the JSON body in the Ace editor and saves. The attendance feature calls the engine's evaluate method with the employee context on the relevant date. The engine resolves the active policy for that company and department, checks the valid-from and valid-to window, evaluates the restricted expression and returns a typed result. No code is deployed, no constant is touched, and another company's rules are never in scope.
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.
When a department-scoped and a company-wide policy both match, resolve returns the department or tag match first and only falls back to the unscoped record, so the most specific rule always wins.
When on_date is supplied, resolve filters on valid_from and valid_to, treating an empty bound as open-ended, so a policy that is not yet effective or already expired is skipped.
Company is derived from the employee or the active company, and a global record rule restricts every read to company_ids, so an officer cannot resolve or even list another company's policies.
A SQL unique constraint on code, company, department, tag and active blocks a second active policy for the same scope, so resolution never has to break a tie between two live rules.
Comparison operators return false when either side is missing and division guards against a zero or empty denominator, so a sparse context dict yields a defined result instead of an exception.
The config helper tolerates a body that was stored as a double-encoded JSON string from XML seeding by parsing it back to a dict before returning it.
What is inside
Built to do the job, end to end.
- Restricted DSL evaluator. A recursive evaluator over literals, dotted context reads, the comparison and arithmetic operators, and and or not, plus a conditional if node. Any other node raises a UserError. Exposed as a registered platform service named eh.hr.policy.engine with resolve, evaluate and config methods.
- eh.hr.policy model. A versioned, scoped record carrying code, name, company, optional department, optional employee tag, a JSON body, validity dates, an active flag and notes. Edited through a form with an Ace JSON editor and a list view under HR configuration, gated to the HR officer group.
- Scoping and security. Built on the EH HR Platform mixin, so every policy gets a correlation id for tracing. A global record rule enforces per-company reads, and access rules give HR admin full control, HR officer create and edit without delete, and self-service employees read-only.
Honest about the edges
What this does not do, so nothing surprises you.
- This is an engine for the EH HR Platform and depends on eh_hr_core. It ships no end-user feature on its own; attendance, leave, loan and payroll modules call it.
- The policy body is data only. The DSL is intentionally limited to reads, comparisons, boolean logic, arithmetic and a conditional. It cannot loop, call functions, import or mutate state.
- The version field records a revision number for manual tracking. This module does not auto-increment it on save or keep a separate history of prior bodies.
- Resolution is read-time and does not cache results in this module. Callers that need throughput should batch their lookups.
- No cron jobs, approval ladders or escalation logic live here. Those concerns belong to their own platform engines.
Odoo 18 HR policy engine, HR business rules DSL, no-code HR rules Odoo, leave cap configuration, overtime multiplier rules, employee eligibility rules, per company HR policy, versioned business rules Odoo, JSON policy editor Odoo, side-effect-free rule engine, HR platform service Odoo, grace period rules 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