EHRPlus · Production incident audit
emr.practitioner — 232 rows
OpenMRS — 251 users / 371 providers
pgdog in front of ehrplus-odoodb
Per-provider remediation worksheet → · all 232 practitioners with a specific fix each (CSV)
Two independent paths write and read provider identity, and they disagree. The write path runs once, at record creation. The read path runs on every printed report.
There is no write() override, so editing a practitioner in Odoo never reaches OpenMRS.
Failures in create() are caught and logged, then swallowed — the record saves anyway
with a null uuid.
The whole read path hinges on one line in bahmni_pos/controllers/clinical.py:1166:
Practitioner.search([("provider_uuid", "in", provider_uuids)])
No active_test=False — so archived practitioners silently vanish from reports they
signed. No uniqueness guarantee — so one provider uuid can return several different doctors.
Pressing Synchronize on any of the rows below renames a live OpenMRS account
belonging to a different person and overwrites their username — including writing the literal
username KAHS onto three of them, which locks those staff out. Any remediation
script must skip this set until a human resolves each case.
OpenMRS system_id embeds the Odoo record that created the account
(clinical-provider-<odoo_id>-<hash>), which gives an independent provenance
trail. Comparing it against the current Odoo row exposes two distinct failure modes.
| Odoo id | Odoo says | OpenMRS account actually is | OpenMRS username | Cause |
|---|---|---|---|---|
| 7 | Dr. bipin Agrawal | Dipendra Jung Shahi | dipena.jung.shahi | row recycled |
| 14 | Dr. Dirishya Bishwakarma | Opendra Kumar Sah | opena.kumar.sah | row recycled |
| 19 | Dr.Suraj P. rana | Dr.Kabiraj Poudel | kabiraj.poudel | row recycled |
| 39 | Nabin Kumar Mahat | Sanjiv Sharma | sanjiv.sharma | row recycled |
| 52 | Dr.pujan shrestha | Appu Subedi | appu.subedi | row recycled |
| 161 | Sushma Khadka | Dipendra Gharti Mahar | dipendra.magar | cloned from id 149 |
| 162 | Bhagawati Rawal | Dipendra Gharti Mahar | dipendra.magar | cloned from id 149 |
The row was created and synced for person A, then later renamed to person B in Odoo. Because
there is no write() sync, OpenMRS kept person A. Two humans now share one row, and
the system_id still names the original.
uuid, provider_uuid, lab_uid and last_sync_date
are declared readonly but not copy=False
(bahmni_pos/models/hospital/practitioner.py:81-99). Duplicating a practitioner in the
Odoo UI clones the foreign keys, so three Odoo rows — and three SENAITE lab contacts — now resolve
to one OpenMRS provider. The two rows have been flip-flopping the account's name since June.
Live evidence. One provider uuid, two different doctors with two different NMC numbers:
$ curl -s -X POST http://localhost:8069/api/providers/ \
-d '{"providers":["afc895e4-7454-4f13-8feb-775426bc240d"]}'
[{"name":"Dipendra Gharti Mahar","profession":"medical_records_technician",
"license":"B-14759 Med GM","title":"HA"},
{"name":"Bhagawati Rawal", "profession":"healthcare_administrator",
"license":"B-6322", "title":"HA"}]
This provider signs live encounters, so any report rendering that visit lists both people as signatories.
POST /api/providers/encounter_provider row or as an
orders.orderer, posted to the live endpoint on kahs.
superman, 501,544 records, and LABSYSTEM, 4) are excluded from the
scale — they should simply be filtered out of report provider lists.
The 16 break into three groups:
| Group | Count | Why it fails | Fix |
|---|---|---|---|
| Archived in Odoo Dr Nali Hada — 113 encounters, 245 orders |
1 | Odoo search() defaults to active=True; the controller never passes active_test=False. Every report she signed loses her name and NMC on reprint. |
one kwarg |
| Bahmni-native providers 44-8, 33-1, 34-9, 29-9, … |
13 | No Odoo row and person_name is NULL in OpenMRS — there is no name to fall back to anywhere. 44-8 alone carries 436 encounters and 607 orders. |
needs hospital |
| System accounts superman, LABSYSTEM |
2 | Legacy Bahmni service accounts. superman holds 143,422 encounters and 358,122 orders of historical attribution. |
exclude |
fetchProviders in bridge-api/src/domains/reports/processors/processor-fetchers.ts:289
wraps the call in try { … } catch { return [] }. If Odoo 500s, or the uuid set is
empty (the endpoint answers HTTP 400), the report renders with zero providers and
nothing above debug level is logged. A missing doctor and a dead backend look identical.
| Bucket | Count | Detail |
|---|---|---|
| Never synced, active | 23 | No uuid, no provider, no lab contact. Cannot log in to clinical, cannot be picked as a provider. Splits into 11 duplicates of an already-synced record and 12 genuinely new staff. |
| Never synced, archived | 17 | Correct end state. No action. |
| Duplicate usernames | 13 | No unique constraint in Odoo. KAHS/kahs is on 12 rows; arun.upreti on 3. OpenMRS does enforce uniqueness — this is the direct cause of most sync failures. |
| Username drift | 8 | Odoo and OpenMRS disagree on the login name. OpenMRS is the source of truth here — staff log in with those. e.g. Odoo susmita.pandey vs OpenMRS susmita.pandey10. |
| Duplicate NMC licences | 25 | Shared by clearly different people, both synced: 20305 = Rajendara Giri and Harihar Adhikari; 19847 = Kushal Joshi and Kamal Raj Joshi; 18466 = Samjhana Shrestha and Samjhana Khatri. |
| Archived in Odoo, live in OpenMRS | 6 | ids 6, 37, 39, 84, 139, 161. Archiving does not retire anything, and unlink() calls super() before its retire loop — so deletion has never retired an OpenMRS user either. |
The licence duplicates are not cosmetic:
who_insurance/models/claim/claim.py:3145 puts license_number straight onto
the insurance claim as the practitioner's NMC number.
This is a prerequisite, not a nice-to-have. Without copy=False and the unique
constraint, the Phase 1 data fixes re-corrupt within weeks.
copy=False on uuid, provider_uuid, lab_uid, last_sync_date — stops UI duplication from cloning identities._sql_constraints: unique on provider_uuid and uuid (partial, WHERE … IS NOT NULL) plus unique username. The username constraint forces the KAHS×12 problem to surface instead of failing silently.active_test=False in get_provider_information — archived doctors reappear in reports.unlink(): iterate before super(), not after.self → record bug in the SENAITE block (practitioner.py:277-330) — a multi-record sync currently writes the first record's details onto every lab contact.sync_state + sync_error fields, set in the create() except handler. A filterable list of what failed — the lazy alternative to a retry queue.fetchProviders.Ships as a normal just deploy + module upgrade. Touches no data.
| What | Count | Action |
|---|---|---|
| Cloned rows 161, 162 | 2 | Clear uuid/provider_uuid/lab_uid, then sync 162 fresh. 161 is archived and superseded by row 232 — leave it. Row 149 keeps the original account untouched. |
| Active unsynced duplicating a synced record | 11 | Archive ids 13, 17, 22, 23, 40, 48, 71, 104, 111, 138, 143. Confirm each pairing on the worksheet first — several matched on licence only, and the licence field is itself dirty. |
| Archived in Odoo, live in OpenMRS | 6 | Retire the OpenMRS user and provider. Hold id 39 back — it is in the wrong-person set. |
| Never synced, archived | 17 | Nothing. Already correct. |
Blocked on two dirty fields, both of which are why they never synced in the first place:
KAHS) and 54 (kahs) collide. Assign firstname.lastname.35727. One is wrong.Once filled in, the existing action_synchronize() already does the right thing.
Two sets no script can decide:
Deliverable: one CSV worksheet per set for the records officer, then a scripted apply.
Either the hospital identifies them and we create Odoo rows that adopt the existing
provider_uuid, or we accept them and add a name fallback in
ProviderProcessor so reports show something.
superman and LABSYSTEM should be excluded from report provider lists
either way.
The same read-only audit used to produce this report doubles as the verification suite:
| Check | Now | Target |
|---|---|---|
Wrong-person links (system_id vs Odoo id, and name comparison) | 7 | 0 |
| Provider uuids returning more than one practitioner | 1 | 0 |
Active practitioners with NULL uuid | 23 | 0 |
In-use providers resolved through POST /api/providers/ | 103 / 119 | 117 / 119 |
Duplicate usernames in emr_practitioner | 13 | 0 |
Target for provider resolution is 117, not 119 — superman and LABSYSTEM
are deliberately excluded rather than resolved.
ehrplus-odoodb is not actually unhealthy
Odoo connects via HOST=odoodb-dog → pgdog (ehrplus-odoodb-dog) →
ehrplus-odoodb, whose Postgres listens on 5433. pgdog is healthy and
passing traffic. The container's healthcheck is
pg_isready -h localhost -U odoo with no -p 5433, so it probes the wrong
port and reports unhealthy forever. Cosmetic, but it is what lights up the fleet
health check across the sites running pgdog.