Skip to content

Q5 Plan — roots_hemms_ha_report (Final 1.0)

Plan version: Final 1.0 — Approved, ready to execute Approved: 2026-05-22 Module ID: roots_hemms_ha_report Layer: 3 (Hospital lens) — depends on Q1 (PM), Q3 (contracts, indirect via signature), Q4 (roots_hemms_signature) Implementation: Q5.1–Q5.12 ship sequentially, no external blockers (Q3 + Q4 already shipped)

0. Executive Summary

What we build: A new Layer 3 module roots_hemms_ha_report that produces the Annual Preventive Maintenance Report PDF required by HA Thailand and the MoPH มาตรฐานระบบบริการสุขภาพ ด้านที่ 6 standard. The report aggregates the PM data Q1 already captures, classifies every equipment item by the MoPH 3-tier risk model (สูง / กลาง / ต่ำ), shows planned vs actual PM per equipment for one Thai fiscal year (ปีงบประมาณ), locks via a JSON snapshot on submit, and routes through Q4's sign_oca bridge for the BME-head + Engineering-head + Director sign-off.

Why: Today BME engineers reconstruct this report manually in Excel each year — 2–4 weeks of work that's error-prone and audit-fragile. Surveyors ask for this document on day 1 of an HA Thailand visit; failing to produce it delays accreditation. Q5 turns Q1's PM data into a one-click compliance artifact, pairing with the existing ปจป. annual asset inspection (roots_hemms_asset_inspection) so HEMMS now covers both year-end compliance reports a Thai public hospital must produce.

Effort: 12 commits / ~2 dev sessions, 20 tests target, 5-page docs, 1 fiscal year demo (30 equipment × 3 depts, ~78% compliance with 3 documented exceptions).

Keystone wins (impact-ranked):

  1. One-click annual report (Q5.7 + Q5.8) — generate wizard builds lines from PM data, QWeb renders the full 7-section government-format PDF
  2. MoPH 83-device risk-tier seed (Q5.2) — every hospital gets the official classification table out of the box; editable for local additions
  3. WHO Fennigkoh-Smith EM score on the equipment form (Q5.3) — 4 sliders + live score lets BME engineers classify off-list equipment in seconds, with the score worksheet preserved in appendix B as auditor evidence
  4. Sign-off via Q4 bridge (Q5.9) — BME head + Engineering head
  5. Director sign in-app or via portal email; signed PDF auto-attached to chatter; no separate state, badge shows 🟡 รอลงนาม / 🟢 ลงนามครบ
  6. Lock-on-submit snapshot (Q5.6) — reprints are byte-identical even after the underlying equipment data changes; audit-defensible

License: LGPL-3 (wrapper) over AGPL-3 transitive dep (sign_oca via Q4). Same boundary pattern as Q2.1 / Q3 / Q4.

Status of open questions: All 6 resolved 2026-05-22 — OQ-A per-model bridge (no mixin); OQ-B noupdate="1" baseline; OQ-C 4 sliders on equipment form; OQ-D no re-sign cap; OQ-E Q4.6 fixtures reused; OQ-F Q5 = HA Report, contract follow-ons → Q6. See §8.


1. Purpose

Thai public hospitals are required by ประกาศกระทรวงสาธารณสุข เรื่อง การกำหนดลักษณะของสถานพยาบาลและมาตรฐานมาตรฐานระบบบริการสุขภาพ ด้านที่ 6 ข้อ 6.1.4 to maintain a current equipment register AND perform risk-based preventive maintenance with documented evidence. HA Thailand surveyors ask for the annual evidence on day 1.

The binding standard:

"แนวทางประเมินระดับความเสี่ยงของเครื่องมือแพทย์ที่ต้องการการบำรุงรักษา และกำหนดความถี่ในการตรวจสอบและบำรุงรักษาเชิงป้องกันตามรอบเวลา (พ.ศ. 2564)" — กองวิศวกรรมการแพทย์, กรมสนับสนุนบริการสุขภาพ, กระทรวงสาธารณสุข, co-developed with บริษัท อาร์เอฟเอส (รามาธิบดี/ม.มหิดล) and ECRI APAC.

No single official PDF form exists — hospitals build their own layout that proves these data points. Q5 implements that layout in QWeb, Thai-government style consistent with the existing ปจป. report.

Scope — in

  • Q5.1: Module skeleton, manifest, ir.sequence (HA/%(year)s/00001), paperformat (A4, TH Sarabun, Thai government margins)
  • Q5.2: hemms.ha.device.class vocabulary model + MoPH 83-device seed data (low/mid/high tier, freq months, duration hours), noupdate="1"
  • Q5.3: maintenance.equipment extension:
  • roots_risk_tier (Selection high/medium/low)
  • roots_pm_freq_months (Integer)
  • device_class_id (M2O hemms.ha.device.class)
  • roots_em_function / roots_em_application / roots_em_maintenance / roots_em_history (Integer sliders)
  • roots_em_score (Integer, computed/stored)
  • Waterfall compute: explicit roots_risk_tierdevice_class_id lookup → EM score
  • Collapsible "Risk Assessment (HA)" section on equipment form
  • Q5.4: hemms.ha.report header model:
  • name, fiscal_year, department_id, date_from/to, state
  • author_id, bme_head_id, engineering_head_id, director_id
  • Computed: total_equipment, pm_plan_count, pm_actual_count, compliance_pct, tier_high_count, tier_mid_count, tier_low_count
  • mail.thread + mail.activity.mixin, list/form views, menu
  • Q5.5: hemms.ha.report.line model:
  • equipment_id + related bilingual names + equipment_ref + serial_no + model + department_id
  • risk_tier, pm_freq_months (from equipment waterfall)
  • pm_planned_count (derived 12 / freq), pm_actual_count, pm_compliance_pct
  • result (pass/conditional/fail/not_done), exception_reason, responsible_id
  • Q5.6: hemms.ha.report.snapshot JSON model + action_submit() lock — snapshot stores line data + device-class versions + paperformat metadata; reprint always renders from snapshot
  • Q5.7: hemms.ha.report.generate wizard:
  • Fiscal year (Thai พ.ศ.) + department selector
  • Reuses roots_hemms_asset_inspection._get_fiscal_year_dates()
  • Filters active equipment by department + counts completed PM maintenance.request (kind = PM) in window
  • Q5.8: 7-section QWeb report:
  • Cover (ปก) — header fields
  • บทสรุปผู้บริหาร — totals + tier counts + compliance %
  • ตารางครุภัณฑ์ + ผลการบำรุงรักษา — per-equipment detail
  • รายการครุภัณฑ์ที่ไม่ได้ทำตามแผน — exceptions with reason
  • ภาคผนวก ก — MoPH 83-device table
  • ภาคผนวก ข — EM score worksheet (lines using EM path only)
  • หน้าลงนาม — 3 signature blocks (BME head / Eng head / Director)
  • Q5.9: sign_oca bridge (per-model pattern, no Q4 edit):
  • sign.oca.request._inherit adds hemms_ha_report_id (computed M2O from record_ref)
  • _check_signed super-extension writes roots_signed_pdf when record_ref._name == 'hemms.ha.report'
  • action_send_for_signature() on report with 3-signer Command.create() (BME head / Eng head / Director)
  • _check_ha_signers_resolvable() pre-flight guard
  • 2 new role seeds (role_bme_head, role_engineering_head); reuse Q4's roots_hemms_signature.role_hospital_signer for Director
  • Demo template XML (demo/sign_oca_template_ha_report.xml)
  • QWeb signature page: paper-style empty boxes when roots_signed_pdf blank; embed signed PDF image when set
  • Stat button + 🟡 รอลงนาม / 🟢 ลงนามครบ badge on header form
  • Q5.10: Tests — 8 unit + 12 integration = 20 tests target, green on hemms_demo AND hemms_private
  • Q5.11: 5-page docs under docs/modules/ha-report/ + this plan
  • Q5.12: Demo data — 1 FY (2569 พ.ศ.) hospital with 30 equipment × 3 depts, mixed tiers, ~78% compliance, 3 exceptions ready to print

Scope — out (deferred)

  • ❌ Multi-fiscal-year comparison reports — Q6+
  • ❌ Excel export (XLSX) alongside PDF — Q6+
  • ❌ Real-time compliance dashboard widget — Q6+
  • ❌ Auto-generate on fiscal year end (cron) — Q6+ (manual wizard for V1)
  • ❌ Per-department sub-reports inside one master report — Q6+
  • ❌ Equipment with multiple sub-locations / mobile equipment tracking — Q6+
  • ❌ Vendor performance scorecard inside the report — Q6 (was Q4 "Q5+")
  • ❌ Multi-stage renewal alerts / SLA breach / contract template wizard / workflow gating — Q6 (contract follow-ons; was Q4 "Q5+")
  • ❌ Auto-recalc compliance % when PM events backdated — by design; snapshot is immutable post-submit
  • ❌ Multi-language report (EN-primary version) — Q6+ (TH primary for V1)
  • ❌ OTP / 2FA at sign moment — Q6+ (sign_oca roadmap)
  • ❌ PKI / ETDA Level 2 certified signatures — Q6+ (sign_oca roadmap)

2. Design decisions (locked)

# Decision Choice
D1 Map roots_hemms_criticality (Value × Risk → R/Y/G) to MoPH (สูง/กลาง/ต่ำ)? No mapping — separate roots_risk_tier field. Criticality drives ops triage; risk_tier is the regulator's classification. Different semantics, different sources of truth. Conflating creates a leaky abstraction.
D2 Seed the 83-device MoPH table as data? Yes — hemms.ha.device.class with noupdate="1". Hospitals can add/edit local entries post-install without losing baseline on upgrade. Mirrors Q3 contract-type vocabulary pattern.
D3 Fiscal year handling Reuse roots_hemms_asset_inspection._get_fiscal_year_dates() (Thai ปีงบประมาณ Oct–Sep). Lift to a public helper if currently private.
D4 Snapshot mechanism JSON blob on hemms.ha.report.snapshot (mirrors Q2/asset-inspection). Not a cloned read-only model.
D5 Signature flow OCA sign_oca via Q4 (roots_hemms_signature dep).
D6 Where does sign integration live? Inside roots_hemms_ha_report — Q5 owns its own sign.oca.request extension (adds hemms_ha_report_id + further _check_signed). Does NOT modify roots_hemms_signature. Q5 reuses Q4 helpers (_roots_attach_signed_pdf_to_parent) via inheritance chain.
D7 Risk-tier source on equipment Three-step waterfall: (1) explicit roots_risk_tier → use it; (2) else device_class_id lookup → MoPH table; (3) else compute from EM score. Wizard surfaces which path was used per line. Auditable.
D8 Bilingual rendering TH primary, EN in parens for equipment names; section headings TH-only (consistent with ปจป. report).
D9 State advance on sign No state advanceroots_signed_pdf presence IS the signed status. Header stays in submitted. Matches Q4 OQ3. Simpler state machine, fewer transition tests, consistent house pattern.
D10 Lock-on-submit action_submit() builds JSON snapshot, freezes lines via state check on field readonly. Equipment data changes after submit do NOT alter the report. Reprint always from snapshot, byte-identical.
D11 Rejection path Reject via sign portal → clear roots_signed_pdf, archive old sign.oca.request, allow new spawn. Report stays in submitted throughout. Audit visible in chatter. No cycle cap (matches Q3 + Q4 patterns).
D12 Security scoping department_id on header + record rules: BME staff own-dept; BME head all; Engineering head / Director / Quality dept read all.
D13 EM score formula EM = Function + Application + Maintenance + History per WHO Fennigkoh-Smith. History range −2..+2 (can reduce score; not abs'd). Buckets: ≤12 drop / 12–14 → 1×/yr / 15–18 → 2×/yr / ≥19 → 3×/yr. Boundary interpretation documented in code comment (12 = drop; 14 = 1×/yr; 18 = 2×/yr; 19 = 3×/yr — inclusive upper bounds).
D14 EM editor UX 4 sliders on the equipment form inside a collapsible "Risk Assessment (HA)" section. Live roots_em_score next to inputs. Wizard-free.
D15 License / status LGPL-3, Alpha, maintainer jakapolr.

3. Data model

3.1 New models

hemms.ha.device.class                      (Q5.2 — MoPH 83-device vocab)
  ├─ name_en               Char         e.g. "Anesthesia Units"
  ├─ name_th               Char         e.g. "เครื่องดมยาสลบ"
  ├─ risk_tier             Selection    high/medium/low
  ├─ pm_freq_months        Integer      e.g. 6
  ├─ pm_duration_hours     Float        e.g. 2.4
  ├─ sequence              Integer      display order
  └─ active                Boolean

hemms.ha.report                            (Q5.4 — header)
  ├─ name                  Char         readonly, ir.sequence
  ├─ fiscal_year           Integer      Thai พ.ศ.
  ├─ department_id         M2O hr.department  (null = whole hospital)
  ├─ date_from / date_to   Date         auto from fiscal_year
  ├─ state                 Selection    draft / in_progress / submitted / archived
  ├─ author_id             M2O res.users
  ├─ bme_head_id           M2O res.users
  ├─ engineering_head_id   M2O res.users
  ├─ director_id           M2O res.users
  ├─ line_ids              O2M hemms.ha.report.line
  ├─ snapshot_id           M2O hemms.ha.report.snapshot
  ├─ sign_request_ids      O2M sign.oca.request  (via hemms_ha_report_id inverse)
  ├─ sign_request_count    Integer      computed, stored, sudo
  ├─ roots_signed_pdf      Binary       written by Q4 helper via _check_signed
  ├─ roots_signed_pdf_filename Char
  ├─ total_equipment       Integer      computed, stored
  ├─ pm_plan_count         Integer      computed, stored
  ├─ pm_actual_count       Integer      computed, stored
  ├─ compliance_pct        Float        computed, stored
  ├─ tier_high_count       Integer      computed, stored
  ├─ tier_mid_count        Integer      computed, stored
  ├─ tier_low_count        Integer      computed, stored
  ├─ signed_badge          Char         computed (🟡 รอลงนาม / 🟢 ลงนามครบ)
  └─ active                Boolean
  + mail.thread + mail.activity.mixin

hemms.ha.report.line                       (Q5.5 — per-equipment row)
  ├─ report_id             M2O parent, required, ondelete cascade
  ├─ equipment_id          M2O maintenance.equipment, required
  ├─ equipment_name_th     Char         related, store=True
  ├─ equipment_name_en     Char         related, store=True
  ├─ equipment_ref         Char         related (roots_ref_no), gov asset no.
  ├─ serial_no             Char         related
  ├─ model                 Char         related
  ├─ department_id         M2O          related
  ├─ risk_tier             Selection    high/medium/low (from equipment waterfall)
  ├─ risk_tier_source      Selection    explicit / device_class / em_score  (D7 audit)
  ├─ pm_freq_months        Integer      from equipment
  ├─ pm_planned_count      Integer      derived: 12 / pm_freq_months
  ├─ pm_actual_count       Integer      count of done PM events in window
  ├─ pm_compliance_pct     Float        computed
  ├─ result                Selection    pass / conditional / fail / not_done
  ├─ exception_reason      Text         required if not_done or fail
  └─ responsible_id        M2O res.users

hemms.ha.report.snapshot                   (Q5.6 — immutable JSON freeze)
  ├─ report_id             M2O hemms.ha.report (1:1)
  ├─ data                  Json          full line data + device_class versions
  ├─ frozen_at             Datetime
  ├─ frozen_by_id          M2O res.users
  └─ paperformat_hash      Char          guards against paperformat edits silently changing reprints

3.2 Inherited models

maintenance.equipment (INHERIT, Q5.3)
  ├─ device_class_id           M2O hemms.ha.device.class
  ├─ roots_risk_tier           Selection high/medium/low  (explicit override)
  ├─ roots_pm_freq_months      Integer                    (waterfall computed/stored)
  ├─ roots_em_function         Integer  (range 2..10, slider)
  ├─ roots_em_application      Integer  (range 1..5, slider)
  ├─ roots_em_maintenance      Integer  (range 1..5, slider)
  ├─ roots_em_history          Integer  (range -2..+2, slider)
  └─ roots_em_score            Integer  computed, stored

sign.oca.request (INHERIT, Q5.9)
  └─ hemms_ha_report_id        M2O hemms.ha.report  (compute from record_ref, store=True, index=True)
  + _check_signed() super-extension:
      super()  # ← Q4's chain handles inspection + contract
      for request in self:
          if request.state != "2_signed":
              continue
          record = request.record_ref
          if not record or record._name != "hemms.ha.report":
              continue
          try:
              self._roots_attach_signed_pdf_to_parent(request, record)   # Q4 helper, reused
          except Exception:
              _logger.exception(...)

4. Module layout

addons/roots/roots_hemms_ha_report/
  __init__.py
  __manifest__.py
  README.rst
  data/
    ir_sequence_data.xml                  # HA-YYYY-#####
    ha_device_class_data.xml              # MoPH 83-device seed (noupdate=1)
    sign_oca_role_data.xml                # role_bme_head + role_engineering_head (noupdate=1)
    paperformat_data.xml                  # A4 TH Sarabun (may reuse asset_inspection's)
  demo/
    sign_oca_template_ha_report.xml       # demo PDF template + field placements
    hemms_ha_report_demo.xml              # Q5.12 — 1 FY hospital scenario
  models/
    __init__.py
    hemms_ha_device_class.py              # Q5.2 — MoPH vocab
    maintenance_equipment.py              # Q5.3 — risk-tier + EM-score fields
    hemms_ha_report.py                    # Q5.4 — header
    hemms_ha_report_line.py               # Q5.5 — per-equipment
    hemms_ha_report_snapshot.py           # Q5.6 — JSON freeze
    sign_oca_request.py                   # Q5.9 — _check_signed super-extension
  wizard/
    __init__.py
    hemms_ha_report_generate.py           # Q5.7
    hemms_ha_report_generate_views.xml
  report/
    hemms_ha_report_templates.xml         # Q5.8 — 7-section QWeb
    hemms_ha_report_paperformat.xml       # may inherit from asset_inspection
  views/
    hemms_ha_device_class_views.xml
    maintenance_equipment_views.xml       # collapsible "Risk Assessment (HA)" section
    hemms_ha_report_views.xml             # list/form + stat button + badge
    menus.xml
  security/
    ir.model.access.csv
    ha_report_security.xml                # record rules (own-dept vs all)
  tests/
    __init__.py
    test_device_class.py                  # Q5.10 — U1
    test_em_score.py                      # Q5.10 — U2, U3
    test_risk_tier_waterfall.py           # Q5.10 — U4, U5, U6
    test_line_compute.py                  # Q5.10 — U7, U8
    test_generate_wizard.py               # Q5.10 — I1, I2, I3
    test_lock_on_submit.py                # Q5.10 — I4, I5, I6
    test_sign_bridge.py                   # Q5.10 — I7, I8, I9, I10
    test_security.py                      # Q5.10 — I11
    test_report_render.py                 # Q5.10 — I12
  i18n/
    th.po                                 # wrapper UI strings

5. UX touchpoints

5.1 maintenance.equipment form (Q5.3)

[ Existing tabs ]
  General / Inventory / Maintenance / …

[ NEW collapsible section: 🇹🇭 Risk Assessment (HA) ]
  Device Class (MoPH)   [ Anesthesia Units ▼ ]   ← lookup hemms.ha.device.class
  Risk Tier             [ High / Medium / Low ] ⓘ Auto-filled from class; override here
  PM Frequency (months) [ 6 ]                    ⓘ Auto-filled from class
  Source                ▶ device_class           ← read-only badge (D7 waterfall audit)

  ▼ EM Score Worksheet (if no Device Class)
    Function        [——●———————] 9
    Application     [—————●—————] 4
    Maintenance     [——●————————] 3
    History         [————●——————] 0
    Score           = 16 → Medium tier, 2× PM per year

5.2 hemms.ha.report list view

| Reference     | FY (พ.ศ.) | Dept       | State        | 🚦 | Compliance |
| HA-2569-00001 | 2569      | BME        | Submitted    | 🟢 | 78%        |
| HA-2569-00002 | 2569      | Radiology  | In Progress  | —  | —          |
| HA-2568-00001 | 2568      | BME        | Archived     | 🟢 | 82%        |

5.3 hemms.ha.report form

[ Header ]
  HA-2569-00001                            [ 🟡 รอลงนาม / 🟢 ลงนามครบ ]
  Fiscal Year (พ.ศ.)  2569
  Department          BME
  State               submitted

[ Header buttons ]
  [ Send for Signature ]  ← visible if state=='submitted' AND sign_request_count==0
  [ Print Report ]
  [ Archive ]             ← visible if state=='submitted'

[ Statbox ]
  [ 30 Equipment ] [ 78% Compliance ] [ 📝 1 Sign Request ]
                                       ↑ visible if sign_request_count > 0

[ Notebook tabs ]
  - Equipment Lines (o2m)
  - Exceptions (filtered domain on lines)
  - Signers (BME head / Eng head / Director m2o pickers)
  - Snapshot Info (read-only, post-submit)

[ Chatter ]
  Standard mail.thread — sign events post here

5.4 Generate wizard (Q5.7)

┌─ Generate HA Annual PM Report ──────────────────────────┐
│ Fiscal Year (พ.ศ.)   [ 2569 ▼ ]                         │
│ Department           [ BME ▼ ]   (blank = whole hospital) │
│                                                          │
│ Period:  1 ต.ค. 2568 – 30 ก.ย. 2569                      │
│ Equipment found: 30 (estimated)                          │
│                                                          │
│ [ Cancel ]                          [ Generate Report ]  │
└──────────────────────────────────────────────────────────┘

5.5 QWeb PDF (Q5.8) — 7 sections

┌─────────────────────────────────────────────────────────┐
│ ปก                                                       │
│  รายงานการบำรุงรักษาเชิงป้องกันประจำปี                    │
│  หน่วยงาน BME, รพ. ตัวอย่าง                             │
│  ปีงบประมาณ พ.ศ. 2569                                    │
│  เลขที่: HA-2569-00001                                   │
│  จัดทำโดย: [ผู้จัดทำ]                                    │
├─────────────────────────────────────────────────────────┤
│ บทสรุปผู้บริหาร                                          │
│  ครุภัณฑ์ทั้งหมด: 30                                     │
│  สูง: 5 | กลาง: 12 | ต่ำ: 13                             │
│  PM ตามแผน: 64 ครั้ง | ทำจริง: 50 | เป็นไปตามแผน: 78%   │
├─────────────────────────────────────────────────────────┤
│ ตารางครุภัณฑ์ + ผลการบำรุงรักษา                          │
│  | ลำดับ | ชื่อเครื่อง | รุ่น | S/N | เลขครุภัณฑ์ | ระดับ │
│  |       | (TH/EN)     |     |     |              | ความ │
│  |       |             |     |     |              | เสี่ยง| ความถี่ | แผน | จริง | % | ผล |
├─────────────────────────────────────────────────────────┤
│ รายการครุภัณฑ์ที่ไม่ได้ทำตามแผน                          │
│  3 รายการ พร้อมเหตุผล                                    │
├─────────────────────────────────────────────────────────┤
│ ภาคผนวก ก — ตาราง MoPH 83 ประเภท (อ้างอิง)              │
├─────────────────────────────────────────────────────────┤
│ ภาคผนวก ข — EM Score Worksheet (เฉพาะรายการที่ใช้)      │
├─────────────────────────────────────────────────────────┤
│ หน้าลงนาม                                                │
│  [ ☐ หัวหน้างานวิศวกรรมการแพทย์ ]                        │
│  [ ☐ หัวหน้าฝ่ายเทคนิค ]                                 │
│  [ ☐ ผู้อำนวยการ รพ. ]                                  │
│  ↑ paper-style boxes when roots_signed_pdf blank        │
│  ↑ embed signed PDF image when set                       │
└─────────────────────────────────────────────────────────┘

6. UX features captured

Feature From analysis In which Q5 step
ir.sequence HA-YYYY-##### Q3 D3 pattern Q5.1
Thai gov paperformat (A4, TH Sarabun) asset_inspection reuse Q5.1
MoPH 83-device vocab + seed Trusted source page 2–7 Q5.2
Risk-tier waterfall (D7) Trusted source + design Q5.3
EM score worksheet (Function + Application + Maintenance + History) WHO Fennigkoh-Smith + trusted source page 8–9 Q5.3
4 sliders + live score on equipment form OQ-C answer Q5.3
3-state machine (no signed) D9 + Q4 OQ3 alignment Q5.4
Computed totals + tier counts Trusted source executive summary Q5.4
Signed badge 🟡/🟢 D9 visualisation Q5.4
Per-equipment PM planned/actual/compliance Trusted source detail table Q5.5
Risk-tier source audit field D7 traceability Q5.5
JSON snapshot + lock-on-submit D4 + D10 (mirrors asset_inspection) Q5.6
Fiscal year wizard with Thai พ.ศ. D3 reuse Q5.7
PM event aggregation from Q1 Q1 maintenance.request data Q5.7
7-section QWeb PDF Trusted source layout Q5.8
Paper-style empty signature boxes fallback Q4 D11 pattern Q5.8
Stat button + bridge fields Q4 per-model bridge pattern Q5.9
_check_signed super-extension reusing Q4 helper Q4.5 helper inheritance Q5.9
3 signers (BME head / Eng head / Director) Trusted source signature block Q5.9
2 new role seeds + reuse Q4 hospital signer Q4 D12 extension Q5.9
Pre-flight _check_ha_signers_resolvable Q4 pattern Q5.9
Demo template for HA report Q4 D8 pattern Q5.9
20 tests on dual DB Q3 pattern Q5.10
Bilingual (TH) .po Standard Q5.11
5-page docs Q3 / Q4 standard Q5.11
1 FY demo hospital Standard Q5.12

7. Test Cases (20)

Unit tests (8)

# File Test Verifies
U1 test_device_class.py test_83_devices_seeded 83 records loaded; all 3 tiers represented; counts match trusted source (13 low / 17 mid / 53 high)
U2 test_em_score.py test_em_score_formula_boundaries F+A+M+H at each boundary (≤12, 12–14, 15–18, ≥19) → correct tier + freq per D13
U3 test_em_score.py test_em_history_negative_allowed History=−2 reduces score; not coerced to abs (D13)
U4 test_risk_tier_waterfall.py test_explicit_wins Explicit roots_risk_tier overrides device_class AND EM score (D7 step 1)
U5 test_risk_tier_waterfall.py test_device_class_fallback No explicit + has device_class → device_class wins over EM (D7 step 2)
U6 test_risk_tier_waterfall.py test_em_score_last_resort No explicit + no device_class → EM tier used (D7 step 3); risk_tier_source == 'em_score'
U7 test_line_compute.py test_pm_planned_count_derivation 12 / pm_freq_months math for 3-month / 6-month / 12-month freq (4 / 2 / 1)
U8 test_line_compute.py test_compliance_pct_handles_zero_plan planned=0 → compliance_pct=0.0 (no div-by-zero); planned=4 / actual=3 → 75%

Integration tests (12)

# File Test Verifies
I1 test_generate_wizard.py test_generate_builds_lines_for_dept Wizard dept=BME builds N lines for active BME equipment only; total_equipment matches
I2 test_generate_wizard.py test_fiscal_year_window_filters_pm_events Only PM maintenance.request closed in FY window contribute to pm_actual_count
I3 test_generate_wizard.py test_stale_stage_pm_excluded Non-Done PM requests excluded; only stage=Done counts
I4 test_lock_on_submit.py test_submit_freezes_lines After action_submit(), writes to line_ids raise UserError; state advances to submitted
I5 test_lock_on_submit.py test_snapshot_preserves_device_class_version Editing MoPH device_class after submit does NOT change submitted report's appendix A render
I6 test_lock_on_submit.py test_reprint_byte_identical PDF bytes from reprint hash-equal to first print
I7 test_sign_bridge.py test_action_send_for_signature_creates_request Creates sign.oca.request with record_ref pointing back, 3 signers with correct role assignments (BME head / Eng head / Director)
I8 test_sign_bridge.py test_pre_flight_guard_missing_signer Missing BME head / Eng head / Director → UserError, no sign.oca.request created
I9 test_sign_bridge.py test_check_signed_writes_pdf Stub request.state='2_signed' via Q4.6 helper → roots_signed_pdf populated, chatter posted, state UNCHANGED (stays submitted — D9)
I10 test_sign_bridge.py test_rejection_clears_and_respawns Reject → roots_signed_pdf cleared, old request archived, second action_send_for_signature() succeeds
I11 test_security.py test_bme_staff_own_dept_only BME staff in dept A cannot read/write reports for dept B (D12 record rule)
I12 test_report_render.py test_qweb_pdf_renders_signed_and_unsigned PDF renders for both empty-box (unsigned) and embedded-image (signed) cases; no QWeb errors; PDF byte length > 0

Test infrastructure

  • All TransactionCase, no HttpCase
  • Sign completion stubbed via roots_hemms_signature.tests.common._helper_sign_request_complete() from Q4.6
  • Fixtures: 1 hospital, 3 depts, 24 equipment (8 per tier), 1 FY window, demo BME head + Eng head + Director users (each with linked res.users and res.partner)
  • Green gate: run on hemms_demo AND on hemms_private (with Q2 + Q3 + Q4 installed + demo data) — same dual-DB gate as Q3/Q4

Coverage targets

Subsystem Coverage
MoPH seed integrity (Q5.2) U1
EM-score math (Q5.3) U2 + U3
Risk-tier waterfall (Q5.3) U4 + U5 + U6
Line math (Q5.5) U7 + U8
Generate wizard (Q5.7) I1 + I2 + I3
Lock-on-submit (Q5.6) I4 + I5 + I6
Sign bridge (Q5.9) I7 + I8 + I9 + I10
Security ACL (Q5.4) I11
QWeb render (Q5.8) I12

8. Open Questions — resolved

# Question Resolution
OQ-A Q4 mixin shape — does Q5 use a mixin or per-model bridge? Per-model bridge. Q4 did not ship a mixin (each bridged model has its own dedicated file with copy-paste shape). Q5 mirrors the pattern in its own module — does not edit roots_hemms_signature.
OQ-B MoPH 83-device seed — noupdate=0 or =1? noupdate="1" baseline. Hospitals can add/edit post-install without losing changes on module upgrade. Same as Q3 contract-type vocabulary.
OQ-C EM-score editor UX — sliders vs wizard? 4 sliders + live computed score on the equipment form, inside a collapsible "Risk Assessment (HA)" section. Discoverable, no extra click, score recomputes live.
OQ-D Re-sign cycle cap (D11)? No cap. Matches Q3 + Q4 patterns. Each rejection respawns a fresh sign.oca.request; old archived.
OQ-E Test fixtures from Q4 reusable? Yes. Q4.6 ships _helper_sign_request_complete() in roots_hemms_signature/tests/common.py. Q5 imports and reuses for I9/I10 stub completion.
OQ-F Scope priority — Q5 = HA Report or contract follow-ons? Q5 = HA Report. Contract follow-ons (multi-stage renewal alerts, SLA breach, vendor scorecard, spare-only RFQ, contract template wizard, workflow gating) → Q6. HA Report has higher demo value (new compliance artifact) than polish on already-shipped contract module.

9. Plan execution gotchas

  • Q5.2 MoPH seed multi-line names — the trusted source has equipment names like "Patient Warming Units, Air" and "Defibrillators and Noninvasive Pacemaker" split across lines in the PDF. Concatenate carefully when seeding; keep both EN and TH columns; preserve original spelling (don't auto-correct).
  • Q5.3 EM-score storageroots_em_score is computed=True, store=True. Depends on the 4 input fields. Ensure store invalidates when any input changes (verify via test).
  • Q5.3 boundary case ambiguity — the trusted source PDF shows bucket boundaries (≤12, 12–14, 15–18, ≥19) without explicit inclusivity. Q5 fixes them as: 12 = drop, 13/14 = 1×/yr, 15/16/17/18 = 2×/yr, 19+ = 3×/yr (per D13). Document in code comment + configuration.md.
  • Q5.5 pm_actual_count must filter by stage=Done — PM maintenance.request in In Progress or stale stages must NOT count toward actual. Otherwise compliance % is inflated.
  • Q5.5 PM kind filter — counts only maintenance.request where roots_kind_id.code == 'PM' (Q1 maintenance.kind code). Repair/Calibration kinds are out of scope for this report.
  • Q5.6 snapshot must include device_class_id.write_date per line — so later edits to MoPH table don't silently change reprints. Snapshot stores the resolved values, not just FK ids.
  • Q5.6 paperformat fingerprint — snapshot also stores a hash of the paperformat format, margin_*, header_* so paperformat edits flag a warning on reprint (do NOT silently change).
  • Q5.7 fiscal year helper — if roots_hemms_asset_inspection._get_fiscal_year_dates() is currently a private method (_method), lift to _get_thai_fiscal_year_dates() as a public utility before Q5.7 (in Q5.1 or as part of Q5.7).
  • Q5.8 QWeb appendix B — only render lines where risk_tier_source == 'em_score'. Hide section entirely if no such lines.
  • Q5.9 _check_signed super-chain ordersuper()._check_signed() MUST be called FIRST. Lets Q4 handle inspection/contract. Q5's HA-only branch must NOT raise if record_ref._name is not 'hemms.ha.report'.
  • Q5.9 demo template PDF — ship as base64 in XML data file, noupdate=1 (same as Q4 D8). Use a simple A4 PDF with 3 placeholder signature boxes.
  • Q5.9 role_hospital_signer reuse — Q4 already seeded this with partner_selection_policy='empty'. Q5 uses it as-is for the Director slot; partner_id passed explicitly via Command.create() (matches Q4 pattern).
  • Q5.10 sign_oca completion stub — verify Q4.6's _helper_sign_request_complete() exists in roots_hemms_signature/tests/common.py (not just in test files). If private/missing, lift in Q5.10 commit before relying.
  • Q5.10 dual-DB gate — must run on both hemms_demo AND hemms_private. Q3 had an I1 supplier-domain test that was skipped due to purchase dep; check no similar skip needed here.
  • Q5.11 docs sequencingdocs/modules/ha-report/index.md must be added to mkdocs.yml nav; otherwise it won't render.
  • Q5.12 demo data deps — depends on Q1 PM demo + maintenance.equipment demo. Generate after both module installs cleanly; verify compliance_pct lands on the intended ~78% to make the demo realistic.

10. Commit sequence (Q5.1 → Q5.12)

# Commit message Subject
Q5.1 chore(ha): skeleton + manifest + sequence + paperformat [Q5.1] Module installs cleanly. Sequence + paperformat ready.
Q5.2 feat(ha): hemms.ha.device.class + MoPH 83-device seed [Q5.2] Vocab model + 83-device seed; smoke test for count + tier distribution.
Q5.3 feat(ha): equipment risk-tier + EM-score sliders [Q5.3] maintenance.equipment extension; collapsible Risk Assessment section.
Q5.4 feat(ha): hemms.ha.report header + state machine [Q5.4] Header model, 3 states, totals, mail.thread, list/form, menu.
Q5.5 feat(ha): hemms.ha.report.line + per-equipment compute [Q5.5] Line model + derived counts + result enum.
Q5.6 feat(ha): snapshot + lock-on-submit [Q5.6] JSON snapshot model + action_submit freeze.
Q5.7 feat(ha): generate wizard [Q5.7] Fiscal year + dept wizard; builds lines from PM data.
Q5.8 feat(ha): QWeb report with appendices [Q5.8] 7-section government-format PDF + paper signature fallback.
Q5.9 feat(ha): sign_oca bridge + roles + demo template [Q5.9] sign.oca.request extension + 3-signer action + 2 role seeds + demo template + signed badge.
Q5.10 test(ha): 8 unit + 12 integration tests [Q5.10] 20 tests; dual-DB green.
Q5.11 docs(ha): 5-page module docs + plan [Q5.11] docs/modules/ha-report/ + q5-ha-report-plan.md + mkdocs.yml nav.
Q5.12 chore(ha): demo data — 1 FY hospital scenario [Q5.12] 30 equipment × 3 depts, 78% compliance, 3 exceptions.

11. Resume cue

When user says "เริ่ม Q5.1" / "start Q5" / "ship Q5" / continues this plan:

  1. Re-read this plan + project_q5_status.md for full context
  2. Confirm dependencies installed cleanly:
  3. Q1 (roots_hemms_pm) — PM event source
  4. Q3 (roots_hemms_service_contracts) — indirect via Q4
  5. Q4 (roots_hemms_signature) — sign bridge primitives
  6. Execute Q5.1 — module skeleton + manifest + sequence + paperformat
  7. After each commit: pre-commit run --all-files; run module tests (after Q5.10); smoke-install on both hemms_demo and hemms_private
  8. Final check at Q5.12: full end-to-end demo — generate wizard → review lines → submit → send for signature → all 3 sign → final PDF in chatter