# HIS Domain-Model Study — GNU Health → Your OB/GYN ERP

**Purpose:** Extract proven HIS domain-modelling patterns to guide the design of your new OB/GYN ERP.
**Companion to:** `obgy-erp-analysis.md` (your legacy system) — this doc maps that system onto an open-source HIS reference.
**Date:** 2026-06-15

---

## 0. Legal framing (read first)

This is a **conceptual / domain-model study**, not a code port.

| Activity | Status |
|---|---|
| Studying how GNU Health organises entities, relationships, workflows | ✅ Allowed — ideas & data models are not copyrightable |
| Re-implementing those concepts **from scratch** in your own stack (Laravel/PHP) | ✅ Clean — this is independent reimplementation |
| Copying GNU Health source (Python/XML) into your codebase | ❌ License violation (GPL/AGPL copyleft) |

Two practical facts make this safe: (1) GNU Health is **Tryton/Python**, your ERP is **PHP** — there is no code to copy, only concepts; (2) we are only reading a **GPL open-source** project, so even studying the source is unambiguous. Everything below is "what to build", expressed as target schema — **none of it is GNU Health code.**

> Note: **OCA `vertical-medical` is dead on Odoo 18** (empty repo, no modules). The only substantive open-source HIS worth studying is **GNU Health** (now on the Tryton platform). That is the sole reference used here.

---

## 1. The 8 patterns that fix your biggest problems

Your legacy analysis lists the structural debt. GNU Health solves each with a specific modelling pattern. This table is the core takeaway:

| Your pain point (from `obgy-erp-analysis.md`) | GNU Health pattern | What to build in your ERP |
|---|---|---|
| **Couple in one row** (`patients.wifename` + `husdandname` embedded) | **Party / Role separation** | One `parties` table = every person. Thin role tables: `patient`, `health_professional` each `→ party_id`. Spouse = a relationship row, not a column. |
| **No declared FKs** (300+ implicit relations) | Every relation is a declared **Many2One** | Real `FOREIGN KEY` constraints + `ON DELETE` rules. Referential integrity at the DB, not in PHP. |
| **3 parallel antenatal stacks** (`ancsheet` / `mainantenental` / `followup`) | **One pregnancy aggregate** (`patient.pregnancy`) owning prenatal/perinatal/puerperium children | A single `pregnancy` master + `prenatal_visit` child rows. Delete the other two stacks. |
| **GPA stored & inconsistent** (`pno/ab/ectopic/svd/cs` columns copied around) | **GPA computed** from the pregnancy list (single source of truth) | Don't store Gravida/Para. Derive from `COUNT(pregnancy)` + outcome counts. |
| **190 one-table-per-dropdown** (61% of tables) | **Selection enums** + a few shared catalogs | `ENUM`/lookup-by-type. One `lookups(type, code, label)` table replaces ~150 micro-tables. |
| **Visit/clinical decoupling** (clinical tables have no `visit_id`) | **Encounter (evaluation)** is the aggregate root every clinical record hangs off | Every clinical sheet `→ encounter_id`. Encounter links patient + professional + appointment + diagnosis. |
| **EAV "other investigations"** + parallel `semen`/`hormon` tables | **Generic lab structure**: catalog → request → result → analyte line | One `lab_test_type` catalog, one `lab_request`, one `lab_result`, one `lab_result_line` (analyte). Semen analysis & hormone panels become *test types*, not new tables. |
| **Inline payment on `visits`** (cash/visa/debt magic-ids) | **Service → invoice**; everything billable is a **product** | Separate `service` doc + `invoice`. Fees, tests, drugs all reference a `product`/`price_item`. |

---

## 2. Reference domain model (GNU Health, distilled)

### 2.1 Identity layer — the Party pattern
```
party (person OR org; role booleans: is_patient, is_healthprof, is_institution, is_pharmacy)
  ├─ ref (PUID — unique person id)
  ├─ dob, gender, marital_status            ← demographics live HERE, once
  ├─ person_name[] , alternative_ids[]       (name history, passports/NIDs)
  └─ du (domiciliary unit / household)
       ▲
   ┌───┴────────────┬─────────────────┐
patient        health_professional   institution
(clinical attrs)  (license, specialty)  (type, beds, wards)
```
**Why it matters for you:** your husband/wife problem disappears. Both are `party` rows; the patient is the wife (a `patient` wrapper); the husband is a `party` linked by a **relationship** row (`family_member` role = "husband"). A person who is both patient and staff is **one** record.

### 2.2 Encounter layer — appointment vs evaluation
GNU Health **splits the three things your `visits` table conflates**:

| Concern | GNU Health entity | Your `visits` column today |
|---|---|---|
| Scheduling / queue | `appointment` (state: free→confirmed→checked_in→done→no_show) | `visitdate`, `visitorder`, `visit_period` |
| Clinical encounter | `patient.evaluation` (the SOAP record; state: in_progress→done→**signed**) | *(missing — clinical data floats free)* |
| Money | `health_service` → `account.invoice` | `detectionvalue_cash/_visa/...` |

`patient.evaluation` (the **Encounter**) is the hub: `patient`, `appointment`, `healthprof`, `chief_complaint`, vitals (bp/hr/spo2…), `diagnosis → pathology`, child lines (symptoms, DDx, procedures), and on **sign** it locks (read-only) — an audit pattern you currently lack.

### 2.3 OB/GYN layer (the part that matters most to you)

This is the crown jewel. Your three antenatal stacks + obstetric history collapse into **one clean aggregate**:

```
patient (mother, biological_sex='f')
  │
  ├─ menstrual_history[]     (lmp, cycle length, regular?, dysmenorrhea, frequency/volume enums)
  ├─ pap_history[]           (Bethesda result enum: NILM/ASC-US/LSIL/HSIL/AIS…)
  ├─ mammography_history[]   (normal/abnormal + last date)
  ├─ colposcopy_history[]
  │
  └─ pregnancy[]  ◄── ONE master record per pregnancy (your ancsheet/mainantenental/followup unified)
        ├─ gravida (# sequence, unique per patient)
        ├─ current_pregnancy (bool — only ONE allowed, enforced)
        ├─ lmp → pdd (EDD = lmp + 280 days)
        ├─ fetuses, monozygotic        (multiple-gestation support)
        ├─ pregnancy_end_result (live_birth/abortion/stillbirth) + end_date
        │
        ├─ prenatal_evaluation[]   ← antenatal visits
        │     gestational_weeks = (visit_date − lmp)/7   [computed, never stored]
        │     fundal_height, fetus_heart_rate, efw, bpd/ac/hc/fl,
        │     hypertension, preeclampsia, diabetes, placenta_previa, iugr…
        │
        ├─ perinatal[]             ← delivery event
        │     admission_date, delivery_mode (SVD/vacuum/forceps/CS),
        │     presentation, episiotomy, laceration, placenta complications
        │     └─ monitoring[]      ← intrapartum partograph rows (bp, dilation, fetal HR, contractions)
        │
        ├─ puerperium_monitor[]    ← postpartum rows (lochia amount/color/odor, uterus involution)
        │
        └─ pregnancy_result[]      ← per-fetus outcome
              └─ newborn → party (the baby is a person)
                    └─ newborn record (APGAR 1/5, weight/length/HC, screening, reflexes)
```

**Obstetric modelling choices worth copying verbatim (as concepts):**
- **EDD** = `LMP + 280 days` (flat 40 weeks). Gestational age recomputed on demand everywhere — **never cached** → no stale data (fixes your duplicated EDD logic).
- **GPA is derived**: Gravida = `COUNT(pregnancy)`; Abortions/Stillbirths/Premature = count pregnancies by `end_result` (premature = live birth < 37 wk). One source of truth — kills the `pno/ab/ectopic` columns scattered across your sheets.
- **Forward vs "reverse" entry**: a pregnancy can be prospective (enter LMP) or recalled-past (enter end-date + weeks, back-calc LMP). Exactly models your "history vs active pregnancy" split — one entity, a `reverse` flag, not two table families.
- **Single active pregnancy** is a validated invariant (only one `current_pregnancy=true` per patient).
- **Per-fetus outcome row** → links to the **baby as a real person** (`party`), so the newborn enters the same patient registry. This is how `registeration`/delivery booking should work.

### 2.4 Investigations layer — kills your EAV + parallel test tables
```
lab_test_type (catalog)        e.g. "Semen Analysis", "Hormonal Profile", "CBC"
  ├─ code, category, specimen_type
  ├─ product_id  → makes it billable
  └─ analyte_template[]   (the expected result lines: e.g. count, motility, morphology / FSH, LH, E2…)

lab_request   (doctor orders a test_type for a patient; state draft→ordered→tested)
lab_result    (the result document; state draft→done→validated; validated_by + timestamp)
  └─ analyte_line[]   name, result(float) | result_text, units, lower/upper limit, WARNING if out-of-range
```
**Direct hit on your debt:** `semen`/`semen2`/`semeninfertility` and `hormon`/`hormonalprofile`/`hormonalprofile2` are not new tables — they are **two `lab_test_type` rows** with different analyte templates. Your `otherinvestigations`/`rows`/`values` EAV grid becomes structured `analyte_line`s with units and reference ranges (and automatic out-of-range flags).

### 2.5 Pharmacy & billing layer
```
medicament (catalog: wraps product; active_component, strength+unit, route, form, pregnancy_category A–X, pregnancy_warning)
prescription_order (header: patient, prescriber, pharmacy; allergy/pregnancy warning ACK gate; state)
  └─ prescription_line[]  medicament, dose+unit, frequency, indication→pathology, refills, dates
        └─ stock_move      (dispensing decrements pharmacy inventory)

health_service (header: patient, date)            ← everything billable converges here
  └─ service_line[]  product, qty, to_invoice  → account.invoice
```
Your `drugs`/`recepittmp`/`receiptdrugs` + `currentbalance` ledger maps cleanly: `medicament` catalog, `prescription_order/line`, and a **stock move** for dispensing (instead of mutating a `currentbalance` column). Your `detections` fee catalog and `visits` payments become `product` + `health_service` + `invoice`. The **pregnancy_category / pregnancy_warning** on the drug catalog is a safety feature you should adopt given your obstetric focus.

---

## 3. Migration mapping — your tables → target entities

| Legacy (your system) | Target entity (concept) | Notes |
|---|---|---|
| `patients` (couple) | `party` (×2: wife, husband) + `patient` (wife) + `family_member` (spouse link) | Split the embedded husband out. |
| `awusers` (as doctors) | `party` + `health_professional` | License/specialty on the role wrapper. |
| `visits` | `appointment` + `encounter` + `service`(billing) | Split the 3 concerns it conflates. |
| `detections` | `product` / `price_item` | Fee catalog → products. |
| `ancsheet` / `mainantenental` / `followup` | **`pregnancy`** (one) | Unify 3 stacks → 1 master. |
| `ancnewvisit` / `antenalvisit` / `followupvisit` | `prenatal_visit` | One child table. |
| `*us` / `ultrasoundobst` | `prenatal_visit` biometry fields (+ `imaging_study` for raw images) | Structured biometry, not free columns. |
| deliveries / `operativedetails` | `perinatal` (+ `perinatal_monitor`) | Delivery event + partograph. |
| postpartum data | `puerperium_monitor` | Lochia/involution. |
| `registeration` (delivery booking) / newborn | `pregnancy_result` → `party`(baby) → `newborn` | Baby becomes a real patient. |
| `phmenstrual` | `menstrual_history` | Keep, link to encounter. |
| `phobstetric` / `wifep` / `awifep` | **derived** from `pregnancy[]` | Compute GPA; don't store. |
| `phcontraception` / `phpastmedical/surgical/gyn/art` / `phfamily` | `patient.disease` (problem list) + `family_disease` history | Catalog-vs-instance split. |
| `infertility*` / `ivfsheet` / `mointoringsheet` | **custom ART module** (see §4 gap) | Model as an aggregate like `pregnancy`. |
| `semen*` / `hormon*` | `lab_test_type` + `lab_result` + `analyte_line` | Two test types, not 6 tables. |
| `invests` / `otherinvestigations` (EAV) | `lab_test_type` / `analyte_line` | Structured, with ranges. |
| `drugs` / `recepittmp` / `receiptdrugs` / `pharmacystore` | `medicament` / `prescription_order` / `prescription_line` / `stock_move` | Inventory via moves. |
| `totalbalance` / financial reports | `health_service` → `invoice` + payments | Proper AR. |
| `examination` / present-history Q&A | `encounter` child lines (symptoms/findings) | Hang off the encounter. |
| ~190 `id/title/del` lookups | `lookups(type,…)` or enums | Collapse. |

---

## 4. Where GNU Health does NOT help (your custom value)

GNU Health is **weak on assisted reproduction** — there is no deep IVF/ICSI/IUI cycle model, no folliculometry, no embryo grading, no stimulation protocol grid. Your `ivfsheet` / `mointoringsheet` / `folliculom` / embryo & stimulation children are **genuinely your domain IP**. Recommendation: keep them as a **first-class ART module**, but model it using the *same aggregate pattern* GNU Health uses for pregnancy:

```
art_cycle (master: patient, cycle_type ICSI/IVF/IUI, protocol, start_date, outcome)
  ├─ stimulation_day[]    (folliculometry: date, follicle sizes, E2, lining)
  ├─ procedure (OPU/ET)   (oocytes, embryos, grade, transfer date, #transferred)
  └─ outcome → links to pregnancy[]   (β-hCG → clinical pregnancy → the pregnancy aggregate above)
```
This bridges your strongest module into the clean obstetric model: a successful cycle **produces a `pregnancy`** record, giving you end-to-end continuity (infertility → cycle → pregnancy → delivery → newborn) that your current parallel-table design can't express.

---

## 5. Recommended build order

1. **Identity + encounter spine first**: `party` / `patient` / `health_professional` / `appointment` / `encounter`. Everything hangs off this.
2. **Pregnancy aggregate**: `pregnancy` + `prenatal_visit` + `perinatal` + `puerperium_monitor` + `pregnancy_result`/`newborn`. Migrate the 3 antenatal stacks into it.
3. **Lab + pharmacy**: generic `lab_*` and `medicament`/`prescription_*`. Fold semen/hormone/EAV in here.
4. **Billing**: `product` + `health_service` + `invoice`, replacing inline `visits` money.
5. **ART module** (your custom value), linked to pregnancy.
6. **Collapse lookups**, add **FK constraints**, enforce **UTF-8 everywhere**, and parameterise all queries (kills the SQL-injection class wholesale).

---

## 6. Source references (open-source, GPL — studied, not copied)

- GNU Health source: `https://codeberg.org/gnuhealth/his.git` (stable 5.0.5)
- Local clone analysed: `/root/his-analysis/gnuhealth/tryton/` (modules: `health`, `health_gyneco`, `health_pediatrics`, `health_lab`, `health_services`, `health_inpatient`, `health_stock`)
- Your system: `/home/amrtechogate/public_html/obgy-erp-analysis.md`
