| 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 dependency-light foundation every EH HR module builds on: a hash-chained audit log, strict multi-company mixins, typed per-company settings, and a clean service layer.
Why this module
EH HR Platform Core
An audit trail you can actually trust
Every create, write and unlink on an audited model writes an append-only row whose SHA-256 hash chains to the row before it. A single advisory lock serializes appends so concurrent transactions cannot fork the chain, and verify_chain walks the whole log in bounded batches to find the first tampered row. It is independent of mail.thread, so it stands on its own.
Multi-company that refuses to leak
The company-aware mixin makes company_id required, defaults it to the active company, and rejects cross-company writes even under sudo unless an explicit, audited override context is set. A global record rule isolates the audit log itself per company, so an officer in one company never reads another company's trail.
Build features, not plumbing
A typed and versioned settings registry, runtime feature flags with percentage rollout, a restart-durable rate limiter, DST-safe timezone math, and a small service registry mean feature modules inherit a consistent spine instead of reinventing it. Models stay thin; services do the work.
Day in the life
A change happens, and the record proves it
An HR officer updates a record on an audited model. The mixin captures the before and after of the whitelisted fields, and if nothing actually changed it writes no row. If it did change, write_event takes a transaction-scoped advisory lock, reads the chain tail, hashes the previous hash together with the model, record, action, actor and payload, and appends one immutable row stamped with the owning company. Later an auditor opens the Audit Log, filters to the last 24 hours, and groups by actor. If anyone has edited history directly in the database, verify_chain returns the id of the first row whose hash no longer matches. Meanwhile a daily cron quietly vacuums closed rate-limit windows so the counter table never grows unbounded.
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.
Hash-chain appends are serialized by a transaction-scoped Postgres advisory lock (pg_advisory_xact_lock), so two concurrent transactions cannot read the same tail and fork the chain. The lock releases automatically on commit or rollback.
The rate limiter increments via a single atomic INSERT ... ON CONFLICT DO UPDATE ... RETURNING upsert, so two workers racing the same bucket can never both read a stale count. No in-memory dict that would die on restart or fall out of sync across workers.
The audit log sets _log_access = False and refuses self-audit; it has no edit, create or delete in its list view, and the company_id column is deliberately excluded from the hash material so introducing it during an upgrade never invalidates existing rows.
A write whose audited fields are unchanged emits no audit row, so the trail records real state transitions rather than noise from touch-only saves.
Any write that moves records to another company is rejected unless the user is a member of that company and an explicit override is in context; the elevation itself is audited with every affected record id, not just the first.
Timezone day windows localize start and end of the civil date in the employee's zone before converting to UTC, so a 23-hour or 25-hour DST day is bounded correctly instead of silently dropping or double-counting an hour.
Writing a setting never destroys the old value: the previous active entry is archived and linked through a superseded_by pointer, and a unique constraint guarantees exactly one active entry per company and key.
Crons are declared without the legacy numbercall and doall fields that Odoo 18 carries forward from older releases, and the rate-limit garbage collector deletes only windows that closed past a cutoff, keeping the hot table small under load.
What is inside
Built to do the job, end to end.
- Hash-chained audit log. eh.hr.audit.log is append-only with _log_access disabled. write_event takes an advisory lock, hashes prev_hash plus the event fields with SHA-256, and stores the before and after payload as JSON. verify_chain re-walks the chain with keyset pagination and per-batch cache clearing to report the first broken row.
- Audited and company-aware mixins. eh.hr.audited.mixin emits create, write and unlink events with a configurable field whitelist. eh.hr.company.aware.mixin makes company_id required, defaults it to the active company, and refuses cross-company writes (even under sudo) unless an audited override context is present.
- Settings registry and feature flags. eh.hr.settings.entry is a typed, per-company, versioned key/value store with get and set helpers and a one-active-entry constraint. eh.hr.feature.flag gates by company, group, date window and deterministic percentage rollout via is_enabled.
- Rate limiter and timezone service. eh.hr.rate.limit is a fixed-window counter using an atomic SQL upsert, shared across workers and durable across restarts, vacuumed by a daily cron. The timezone service centralizes DST-safe local civil date and UTC day-window math in one place.
- Service layer and platform mixin. A tiny register_service and locate_service registry keeps business logic in plain-Python services rather than on models. eh.hr.platform.mixin stamps a stable correlation_id on create and offers emit_platform_event, which publishes on the bus and cross-writes an audit row for replay.
- Security model and OWL kit. Four implied access groups (Employee, Manager, Officer, Admin) plus Auditor and a UI-less Kiosk Device group, surfaced as a single access dropdown on the user form by the post-init hook. The frontend ships HrCard and HrStat OWL components, shared SCSS tokens, and a de-duplicating toast service.
Honest about the edges
What this does not do, so nothing surprises you.
- This is a platform and developer foundation module, not a feature app. On its own it adds an Audit Log and Platform Settings screen under an HR Platform menu but no attendance, leave, payroll or recruitment surface. Its value is realized through the feature modules that depend on it.
- The audit retention period and a redact-PII-in-exports flag are exposed as configuration values, but this module does not itself run an archival, export or redaction job; enforcing them is left to the modules and operational processes that consume those settings.
- emit_platform_event uses the in-process Odoo bus. Clustered or cross-node delivery would require swapping in a queue or message-broker adapter; the audit cross-write still records the event regardless.
- The OWL kit currently ships the HrCard and HrStat primitives plus a toast service and shared tokens, intended as building blocks for feature-module UIs rather than a complete component library.
- Targets Odoo 18 Community. It depends on base, mail, hr and the companion eh_hr_compat shim and is meant to be installed as part of the EH HR suite.
Odoo 18 HR platform, HR audit log, hash chain audit trail, tamper-evident audit log, multi-company HR Odoo, Odoo feature flags, rate limiting Odoo, per-company settings registry, Odoo HR framework, append-only audit log, DST-safe timezone Odoo, HR developer module, Odoo service layer, correlation id tracing, OWL component kit
Please log in to comment on this module