EH HR Approval Engine
Reusable N-step approval chains, defined as data, that any HR document can submit into.
Why this module
EH HR Approval Engine
A chain is a data record, not a deploy
Chains live on eh.hr.approval.chain with ordered steps on eh.hr.approval.step. Adding a step, choosing serial against any-of against all-of, or pointing a step at the line manager is editing data in the Approval Chains screen, not writing a new state machine in Python for every document type that needs sign-off.
Nobody approves their own request
The subject employee, the record's own user, and the real submitter captured before the engine's sudo are all blocked from deciding, even if they hold an approver group. The rule is enforced in decide() and re-enforced by an api.constrains on the decision, so it holds on direct ORM and sudo create paths too.
Inherit one mixin and submit
A model inherits eh.hr.approvable.mixin, sets an approval chain code, and gains action_submit_for_approval. The engine opens the request, resolves approvers per step, advances on each decision, and on approval or rejection fires the follow-up workflow transition back into eh_hr_engine_workflow.
Day in the life
A leave request that routes itself and refuses self-approval
An employee submits a document that inherits the approvable mixin. The engine captures who submitted it, opens a request against the configured chain, and resolves the first step to the employee's line manager. The manager opens the request and approves; the engine counts only authorized, non-subject decisions, advances to the HR officer step, and waits. If that step sits longer than its escalation window, the hourly cron notifies the step approvers and HR admins once and flags the request so it is not re-notified every run. When the final step approves, the engine fires the approve transition on the source document; if anyone rejects, it attempts the refuse transition. Every decision is written to the decision log, and the whole request is mail.thread tracked. The submitter, who also happens to be an officer, cannot approve their own request at any point.
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 subject employee's user, the record's user_id, and the real submitter (captured before sudo, since create_uid resolves to OdooBot on the engine path) are all forbidden from deciding, even with an approver group.
An api.constrains on eh.hr.approval.decision re-validates authorization and the self-approval block on every create, so a direct ORM or sudo'd write cannot bypass the interactive decide() check.
The cron escalates an overdue step once: it sets an escalated flag and timestamp so approvers are not re-notified each hourly run, and the flag resets only when the request advances to a new step. escalation_hours of 0 disables escalation entirely.
A conditional chain with no policy_code, or whose policy DSL raises, never auto-advances and stays pending. The safe default is to require explicit human action rather than silently approve.
company_id is stored on chains, steps, requests and decisions, sourced from the chain, and global record rules restrict every model to the user's allowed companies so one company's officer cannot read another's approvals.
A self-service employee sees only requests they raised or are the subject of; a team manager sees their own team's requests; an HR officer keeps the company-wide view, via OR-combined non-global record rules.
The all-of strategy advances a step only when the approving users together cover every approver group required by that step, not merely when one member of each has voted by name.
On rejection the engine attempts the refuse workflow transition; if no such transition exists from the record's current state it leaves the record untouched and the rejected request state stands as the audit record.
What is inside
Built to do the job, end to end.
- Five models. eh.hr.approval.chain (code, strategy, optional policy code, company, steps), eh.hr.approval.step (sequence, approver groups, dynamic approver, escalation hours), eh.hr.approval.request (state, current step, decisions, escalation bookkeeping, submitter), eh.hr.approval.decision (approve or reject, user, comment), and the eh.hr.approvable.mixin consumer models inherit.
- The engine surface. An eh.hr.approval.engine exposed both as a registered service for Python callers and as an AbstractModel for ORM and view callers. open_request creates a pending request and back-links the record; advance re-evaluates the current step after each decision and either moves on, closes as approved or rejected, or stays pending.
- Four strategies. Serial steps in order, parallel any-of where one approver suffices, parallel all-of where the approving users must cover every required group, and conditional where a named eh.hr.policy DSL decides when the step is satisfied using the approval count and the subject employee.
- Dynamic approver resolution. Per step you can route to the employee's line manager, the department manager, any HR officer, or none, in addition to static approver groups. Approvers are resolved from the subject employee at runtime, so the chain follows the org structure without naming people.
- Escalation cron. An hourly ir.cron, with the Odoo 16 numbercall and doall fields omitted, escalates steps past their escalation window: it posts to the request chatter, notifies through the notification engine when that module is present, and flags the request so it escalates once per step.
- Security and access. ir.model.access rows give HR admins full control of chains and steps, officers read and decision rights, and self-service employees read-only request visibility. Global per-company rules plus self, team-manager and officer read rules scope who sees which requests and decisions.
- Tests in the box. Negative-first authorization tests prove non-approvers, subjects, and delegated submitters are blocked on both the interactive and direct-create paths, that cross-company reads are denied, and that a genuine approver advances the chain. Separate tests cover the conditional strategy and overdue, fresh, and zero-hour escalation.
Honest about the edges
What this does not do, so nothing surprises you.
- Delegation is dynamic approver routing to the line manager, department manager or HR officer resolved from the org structure. There is no per-user out-of-office delegate field where one approver hands their queue to a named substitute.
- The shipped UI is the Approval Chains configuration form (HR admin) and a read-only Approval Requests list (HR officer). There is no end-user one-click inbox screen wired into a menu in this module; approvals are recorded by calling decide on the request or by the workflow engine driving the chain.
- Escalation notifies the current step's approvers and HR admins and flags the request. It does not automatically reassign or auto-approve the step; a human decision is still required to advance.
- The conditional strategy depends on the policy engine (eh_hr_engine_policy) being installed and a policy code being configured. Without a configured policy the conditional chain never auto-advances.
- This is a platform engine, not a standalone app. It depends on eh_hr_core and eh_hr_engine_workflow and is meant to be driven by approvable HR documents rather than used on its own.
- Notification-engine delivery on escalation is optional. If eh_hr_engine_notification is not installed the engine falls back to posting on the request chatter only.
odoo 16 hr approval, approval workflow odoo community, multi step approval chain, employee approval engine, hr sign-off workflow, dynamic approver line manager, approval escalation odoo, self approval prevention, segregation of duties hr, conditional approval policy, multi company hr approvals, approval audit trail odoo
Please log in to comment on this module