| Availability |
Odoo Online
Odoo.sh
On Premise
|
| Odoo Apps Dependencies |
•
Discuss (mail)
• Employees (hr) |
| Community Apps Dependencies | Show |
| Lines of code | 875 |
| Technical Name |
eh_hr_core |
| License | LGPL-3 |
| Website | https://erpheritage.com.au |
| Versions | 16.0 17.0 18.0 19.0 |
EH HR Platform Core
The audited, multi-company foundation every EH HR module is built on.
Why this module
EH HR Platform Core
An audit trail you can prove
Every create, write, and unlink on an audited model writes an append-only row whose sha256 hash chains to the row before it. A transaction-scoped Postgres advisory lock serializes appends so two concurrent writes can never fork the chain. verify_chain() walks the whole log in bounded batches and names the first row that does not match. The access rules grant read only, never write, create, or delete.
Multi-company that does not leak
The company-aware mixin makes company_id required, defaults it to the active company, and refuses any write that moves a record into a company the user does not belong to. Even a sudo cross-company write is rejected unless an explicit context key is set, and every such elevation is written to the audit log with all affected record ids. A global record rule isolates the audit log itself per company.
Thin core, sharp edges
One dependency-light layer that the workflow, approval, policy, notification, and feature modules all build on. Typed per-company settings that version on write instead of destroying the old value, runtime feature flags with deterministic percentage rollout, a restart-durable rate limiter, and a four-tier access ladder. The hard cross-cutting concerns live here, once, correctly.
Day in the life
An HR officer changes a record, and the trail holds.
An officer updates an employee record. Behind the form, the audited mixin captures a before snapshot of only the whitelisted fields, lets the write commit, then appends a single audit row with the before and after payload, the actor, a correlation id, and the owning company. The row's hash folds in the previous row's hash, so the entry is locked into an ordered chain the moment it lands. A second officer in another company saves at the same instant. A Postgres advisory lock makes the two appends strictly serial, so both rows chain cleanly with no fork. Later an auditor with read-only access runs verify_chain over the whole log and gets back a clean result, proof that nothing was edited after the fact. Neither officer can see the other company's trail, because a global record rule scopes the audit log per company. No mail.thread, no plugin, just the platform doing its job.
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 hash chain is inherently serial. Two concurrent transactions could read the same tail and fork it, so appends take a transaction-scoped Postgres advisory lock (pg_advisory_xact_lock) that releases automatically on commit or rollback. Appends are strictly ordered under any worker count.
The owning company_id column is deliberately excluded from the hash material. Adding it never alters an existing row's hash, so a chain written before the column existed still verifies after the upgrade that introduces per-company isolation.
A missing actor is normalized to 0 on both the append and the verify path. An earlier version hashed it as 0 on append but as the string False on verify, which silently broke the chain for every actorless system row. Both paths now share one canonical material function.
Cross-company writes are refused even under sudo unless the explicit allow_cross_company context key is set. When the override is used, every affected record id is written to the audit log, not just the first, so a multi-record elevation is fully reconstructable.
Writing a setting does not overwrite the old value. The previous active entry is archived and pointed at its successor, and a unique constraint allows exactly one active entry per company and key, so the current value is unambiguous and the full history survives.
The public-endpoint throttle is a single atomic SQL upsert with RETURNING, so two gunicorn or gevent workers racing the same bucket can never both read a stale count. The counter lives in a table, not an in-memory dict, so it is shared across workers and survives a restart. A header-less caller collapses to one shared bounded bucket.
The rate-limit table is vacuumed by a daily cron that deletes only closed windows past a cutoff, keeping the high-churn counter table small no matter how busy the endpoints get. The cron is defined without the ir.cron numbercall and doall fields that Odoo 19 dropped.
Event timestamps are stored in UTC. Computing the civil date an event belongs to, and the UTC window that bounds an employee's local day, is centralized in one timezone service that correctly handles the 23-hour and 25-hour days at DST transitions and falls back from employee to company timezone to UTC.
What is inside
Built to do the job, end to end.
- Hash-chained audit log. eh.hr.audit.log records who did what to which record, with before and after JSON payloads, a correlation id, and the owning company. Each row stores the sha256 of the previous row plus its own fields. Appends are serialized by a Postgres advisory lock, and verify_chain() walks the log with keyset pagination and a per-batch cache clear so the working set stays bounded at any size. The model has no create, write, or unlink access for anyone.
- Audited record mixin. eh.hr.audited.mixin emits audit rows automatically on create, write, and unlink. A class-level whitelist controls which fields are captured (default: all stored fields except mail and bookkeeping fields), writes capture a before snapshot of only the changed audited fields, and a write that changes nothing audited emits no row.
- Strict company-aware mixin. eh.hr.company.aware.mixin makes company_id required and defaulted, rejects writes into a company the user is not a member of, blocks cross-company moves even under sudo unless explicitly overridden, and audits every elevation with all affected record ids. It deliberately refuses the leaky null-company global-rule pattern.
- Typed, versioned settings. eh.hr.settings.entry is a per-company key/value registry with typed values (string, integer, float, boolean, JSON) used instead of ir.config_parameter. Writes version the old value rather than destroying it, a unique constraint guarantees one active entry per company and key, and a res.config.settings page surfaces the platform-wide knobs.
- Feature flags and rate limiter. eh.hr.feature.flag gates rollouts by company, user group, date window, and deterministic percentage on user id, used to roll engine versions out safely. eh.hr.rate.limit is an atomic fixed-window counter for public kiosk, mobile, and visitor endpoints that is worker-shared, restart-durable, and vacuumed daily.
- Service layer, timezone, and OWL kit. A plain-Python service registry (register_service, locate_service) keeps business logic off the ORM and reusable across crons, RPC, wizards, and triggers, with a DST-safe timezone service and a platform mixin that mints correlation ids and emits bus events cross-written to the audit log. The frontend ships HrCard and HrStat OWL components with a skeleton loading state and a debounced toast service. A four-tier access ladder (Employee, Manager, Officer, Admin) plus Auditor and Kiosk Device groups is wired on install.
Honest about the edges
What this does not do, so nothing surprises you.
- This is a platform layer, not a feature app. It ships mixins, services, the audit log, settings, flags, and a component kit. The actual HR surfaces (attendance, leave, and so on) live in separate eh_hr modules that depend on this one.
- Workflow state machines, approval chains, the policy DSL, and notification dispatch are explicitly not in this module. They belong to the engine modules (workflow, approval, policy, notification).
- The audit log exposes a configurable retention setting and a verify_chain integrity check, but this module does not ship an automatic archival or pruning cron. Old rows are not exported or removed unless a consuming module or operator does so.
- The platform event bus is in-process (bus.bus). Clustered or cross-node delivery requires swapping in a queue or message-broker adapter; the hook is there but no external broker is included.
- A redact-PII-in-exports flag is exposed as a setting, but the redaction itself is applied by the feature modules that produce exports, not by this core layer.
- Cross-version compatibility (Odoo 16 to 19) is provided through the eh_hr_compat dependency for groups, constraints, and access helpers. This listing targets the Odoo 19 Community release line.
Odoo 19 HR audit log, Odoo HR multi-company, append-only audit trail Odoo, hash chained audit Odoo, Odoo HR feature flags, Odoo rate limiting, Odoo HR settings registry, Odoo OWL component kit, Odoo HR platform module, Odoo 19 Community HR, tamper evident audit Odoo, per-company settings Odoo, Odoo correlation id tracing, DST safe timezone Odoo HR, Odoo HR access groups
Please log in to comment on this module