Skip to content

Q4 Plan — roots_hemms_signature (Final 1.0)

Plan version: Final 1.0 — Approved, ready to execute Approved: 2026-05-22 Module ID: roots_hemms_signature Layer: 3 (Hospital lens) — wraps OCA Layer 2 module sign_oca (to be vendored in Q4.1) Implementation: Q4.1–Q4.3 ship together; Q4.4 blocked by Q3 done; Q4.5–Q4.6 follow Q4.4

0. Executive Summary

What we build: Vendor OCA sign_oca and add a thin roots_hemms_signature wrapper that bridges two HEMMS records to the signing engine:

  1. hemms.asset.inspection — Committee chair + 2 members sign the annual ปจป. report inside Odoo instead of on paper
  2. hemms.service.contract — Vendor signs via portal email link; hospital authorized signer co-signs in-app

The wrapper exposes action_send_for_signature() on both models, shows sign_request_count smart buttons, attaches the final signed PDF to the parent record's chatter when all signers complete, and ships pre-configured demo templates so the demo is plug-and-play.

Why: sign_oca is Beta-but-functional OCA work and gives us ETDA-Level-1-equivalent e-signature (image + chained hash + audit log) without the closed-source Odoo Enterprise sign module. The two chosen bridges are the touch-points where offline paper is most painful — the ปจป. committee needs a clean audit trail for HA Thailand, and chasing vendors for paper signatures on service contracts is a known time sink.

Effort: 6 commits / ~1.5 dev sessions, 14 tests target, 4-page docs.

Keystone wins (impact-ranked):

  1. Vendor signs contracts via email link (Q4.4) — kills the paper chase; vendor receives email, signs in browser, hospital sees signed PDF in chatter
  2. Committee signs ปจป. in-app with audit trail (Q4.3) — replaces the printed-and-signed paper cycle with a hashed digital trail suitable for HA Thailand audit
  3. Pre-built demo templates (Q4.3 + Q4.4) — fresh install yields working e-signature on day 1; no template setup required for demo

License: LGPL-3 (wrapper) over AGPL-3 dependency (sign_oca) — compatible at distribution. Same pattern as maintenance_stock (Q2.1) and maintenance_partner (Q3).

Status of open questions: All 5 resolved 2026-05-22 — OQ1 manual offline for internal sign-offs (T1/T2/T6/T7 deferred); OQ2 no workflow integration; OQ3 no state gating on Q3 contract; OQ4 _check_signed extension; OQ5 simplified scope (contract-related Q4 items move to Q5+). See §8 for the locked answers.


1. Purpose

The hospital BME team and external vendors today sign critical records on paper:

  • Annual ปจป. inspection report — chair + 2 committee members sign 3 printed copies (HA Thailand evidence file)
  • Service / MA contracts with vendors — paper round-trip can take 2–3 weeks per contract

Q4 brings e-signature to these two touch-points only. Other HEMMS sign-offs (BME Manager stage-4 sign-off, ward staff receipt, monthly PM report) stay on paper for V1 (see OQ1, §8).

Scope — in

  • Q4.1: Vendor OCA sign_oca under addons/oca/sign_oca
  • Q4.2: roots_hemms_signature module skeleton (manifest, security, data, demo scaffolding) — LGPL-3, depends on sign_oca + roots_hemms_asset_inspection
  • Q4.3: Bridge on hemms.asset.inspection:
  • sign_request_ids (o2m via record_ref domain)
  • sign_request_count (computed, stored, sudo)
  • roots_signed_pdf (Binary) + roots_signed_pdf_filename (Char)
  • action_send_for_signature() method
  • Smart button "📝 N Sign Requests" on form
  • Rewire qweb signature block: if roots_signed_pdf present, embed signed image; else keep paper-style empty boxes (fallback)
  • Q4.4: Bridge on hemms.service.contract (depends on Q3 done):
  • Same 4 fields + button + method
  • Vendor signs via portal email link; hospital signer in-app
  • Q4.5: _check_signed extension on sign.oca.request:
  • When state transitions to 2_signed AND record_ref is one of our two bridged models → write signed PDF to parent's roots_signed_pdf
    • roots_signed_pdf_filename, post a chatter message
  • No state advance on parent (OQ3 + OQ2)
  • Demo data:
  • 3 sign.oca.role records: Committee Chair, Committee Member, Hospital Signer, Vendor Signer
  • 1 demo template for ปจป. inspection (PDF + field placements for 3 signatures + chair signed_on + member signed_on × 2)
  • 1 demo template for service contract (PDF + 2 signature fields)
  • Bilingual: TH .po file covering wrapper strings only (sign_oca core TH translation is OCA's responsibility)

Scope — out (deferred)

  • ❌ Internal sign-offs T1 / T2 / T6 / T7 (BME Manager / Ward / PM) — paper for V1 (OQ1) — Q5+
  • ❌ Sequential signing order enforcement (e.g. chair must sign last) — OQ2 — Q5+
  • ❌ State gating (Q3 contract requires signed before active) — OQ3 — Q5+
  • ❌ Multi-stage renewal alerts (90/60/30) — OQ5 — Q5
  • ❌ SLA breach detection — OQ5 — Q5
  • ❌ Vendor performance scorecard — OQ5 — Q5
  • ❌ Spare-only contract auto-RFQ routing — OQ5 — Q5
  • ❌ Contract template + copy wizard — OQ5 — Q5
  • ❌ OTP / 2FA at sign moment — sign_oca roadmap, not built — Q6+
  • ❌ PKI / ETDA Level 2 certified signatures (pyHanko / endevise) — sign_oca roadmap, not built — Q6+
  • ❌ Bridge on maintenance.request — OQ1 — Q5+
  • ❌ Workflow gating on contract coverage — OQ5 — Q5

2. Design decisions (locked)

# Decision Choice
D1 Build pattern Thin wrapper module roots_hemms_signature modelled exactly on agreement_sign_oca source. ~150 LoC + 1 wizard view per bridged model. Do NOT install OCA agreement_sign_oca or project_task_sign_oca — they target other models
D2 License LGPL-3 (wrapper) over AGPL-3 (sign_oca) — same boundary pattern as Q2.1 / Q3
D3 Internal sign-offs (T1/T2/T6/T7) Paper for V1. No e-sign UI on maintenance.request (OQ1). Static signature blocks in PM workflow stay; ward staff signs on paper printout
D4 Sequencing within Q4 Q4.1 → Q4.2 → Q4.3 → (wait for Q3) → Q4.4 → Q4.5 → Q4.6. Q4.3 can ship and demo before Q3 lands
D5 Completion hook _check_signed extension on sign.oca.request (OQ4). When request transitions to 2_signed, lookup record_ref; if it's hemms.asset.inspection or hemms.service.contract, write final PDF to parent's roots_signed_pdf, post chatter. No state advance (OQ3)
D6 Workflow ordering None (OQ2). All signers in a request can sign in any order. sign_oca's "all signed → final PDF" stays default
D7 Q3 contract state gating None (OQ3). Contract can become state=active without a signed request. Signing is optional / parallel to lifecycle
D8 Demo templates Ship pre-built. Each template = a base PDF (placeholder ปจป. form / generic MA contract) + field placements seeded via XML data with noupdate=1. Admin can edit afterwards
D9 Bridge field shape Mirrors agreement_sign_oca field set: sign_request_ids (o2m via record_ref) + sign_request_count (Integer, stored, sudo computed via read_group) + roots_signed_pdf (Binary) + roots_signed_pdf_filename (Char). Smart-button visible only if count > 0
D10 Field naming roots_* prefix on inherited model fields (roots_signed_pdf); bare names on technical fields that mirror OCA shape (sign_request_ids, sign_request_count — same as agreement_sign_oca). Matches Q3 D9
D11 qweb fallback ปจป. report keeps paper-style empty signature boxes as fallback when roots_signed_pdf is unset. When set, embed the signed PDF via an <img> (rendered to PNG by _get_pdf_page_*) so printed copies show the actual e-signature image
D12 Roles seeded 4 roles (noupdate=1): role_committee_chair, role_committee_member, role_hospital_signer, role_vendor_signer. All with partner_resolution='expression' so the bridge can auto-fill from the parent record (e.g. {{object.chair_id.id}})
D13 Sequence sign_oca ships its own ir.sequence for secure_sequence_number — we do not add a parallel one. Parent record's existing reference (AI/2026/00001, SC/2026/00001) is what humans see

3. Data model

No new business models. Two bridges only:

hemms.asset.inspection (INHERIT)
  ├─ sign_request_ids        One2many  ('sign.oca.request', compute via
  │                            record_ref domain, sudo)
  ├─ sign_request_count      Integer    (computed, stored, sudo)
  ├─ roots_signed_pdf        Binary     (final composited PDF after all signed)
  └─ roots_signed_pdf_filename  Char    (e.g. "AI-2026-00001-signed.pdf")

  + method action_send_for_signature()    → opens sign.oca.request form
  + method action_view_sign_requests()    → list domain'd to this record

hemms.service.contract (INHERIT, Q4.4)
  ├─ sign_request_ids        One2many  (same shape)
  ├─ sign_request_count      Integer
  ├─ roots_signed_pdf        Binary
  └─ roots_signed_pdf_filename  Char

  + same two methods

sign.oca.request (INHERIT, Q4.5)
  └─ _check_signed() extended:
       super(); if state == '2_signed' and record_ref:
           model, rid = record_ref._name, record_ref.id
           if model in {'hemms.asset.inspection', 'hemms.service.contract'}:
               record_ref.sudo().write({
                   'roots_signed_pdf': self.data,
                   'roots_signed_pdf_filename': f'{record_ref.display_name}-signed.pdf',
               })
               record_ref.message_post(
                   body=_("✅ Document signed by all parties — see attachment"),
                   attachments=[(filename, self.data)],
               )

4. Module layout

addons/roots/roots_hemms_signature/
  __init__.py
  __manifest__.py                  # LGPL-3, depends on sign_oca + roots_hemms_asset_inspection + roots_hemms_service_contracts
  README.rst
  models/
    __init__.py
    hemms_asset_inspection.py      # Q4.3 — bridge
    hemms_service_contract.py      # Q4.4 — bridge (blocked by Q3)
    sign_oca_request.py            # Q4.5 — _check_signed extension
  views/
    hemms_asset_inspection_views.xml   # smart button + send-for-sig button
    hemms_service_contract_views.xml   # smart button + send-for-sig button
  data/
    sign_oca_role_data.xml         # 4 roles, noupdate=1
  demo/
    sign_oca_template_inspection.xml   # 1 template + field placements
    sign_oca_template_contract.xml     # 1 template + field placements
    pdf/
      inspection_template.pdf      # placeholder ปจป. PDF (binary, base64-encoded in XML)
      contract_template.pdf        # placeholder MA contract PDF
  report/
    hemms_asset_inspection_report_override.xml   # Q4.3 — qweb rewire (fallback empty boxes vs embed signed image)
  security/
    ir.model.access.csv            # mostly inherited
  tests/
    __init__.py
    test_inspection_bridge.py      # Q4.6 — 5 tests
    test_contract_bridge.py        # Q4.6 — 5 tests
    test_check_signed_hook.py      # Q4.6 — 4 tests
  i18n/
    th.po                          # wrapper strings only

5. UX touchpoints

5.1 hemms.asset.inspection form (Q4.3)

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

[ Statbox ]
  [ N Equipment ] [ M Lines ] [ 📝 K Sign Requests ⭐ NEW ]
                                ↑ visible if sign_request_count > 0

[ Form body ] …unchanged…

Clicking "Send for Signature": 1. Render the existing ปจป. PDF via the current qweb report 2. Create sign.oca.request with 3 signers (chair + 2 members) resolved from chair_id, committee_member_1_id, committee_member_2_id 3. Open the new request form

5.2 hemms.service.contract form (Q4.4)

Same shape, but signers are: partner_id (vendor — portal) + roots_hospital_signer_id (hospital authorized — internal Odoo user).

5.3 sign.oca.request opened from a bridge

Standard sign_oca UI — no customisation. Configure document button → drag signature fields onto pages → Send → signers click pencil systray icon (internal) or email link (portal).

5.4 ปจป. PDF report after signing

When all 3 committee members have signed:

report/hemms_asset_inspection_report_override.xml  (Q4.3)

  <template id="report_hemms_asset_inspection" inherit_id="roots_hemms_asset_inspection.report_hemms_asset_inspection">
    <xpath expr="//table[@class='signature-block']" position="replace">
      <t t-if="doc.roots_signed_pdf">
        <!-- embed signed PDF page image -->
        <img t-att-src="'data:application/pdf;base64,%s' % doc.roots_signed_pdf.decode()"
             style="width:100%; max-height: 200px;"/>
      </t>
      <t t-else="">
        <!-- existing 3-box empty signature block (paper fallback) -->
        <table class="signature-block"> … existing markup … </table>
      </t>
    </xpath>
  </template>

6. UX features captured

Feature From analysis In which Q4 step
Vendor sign_oca core OCA repo Q4.1
Wrapper skeleton Project pattern Q4.2
4 sign.oca.role seeds (chair / member / hospital / vendor) Roles inventory Q4.2
Inspection bridge — 4 fields + 2 methods T3 Q4.3
Smart button "📝 N Sign Requests" on inspection T3 Q4.3
Demo template — ปจป. inspection (PDF + 3 signatures + 2 signed_on dates) OQ "demo templates pre-built" Q4.3
qweb fallback (empty boxes ↔ embedded signed PDF) D11 Q4.3
Contract bridge — 4 fields + 2 methods T4/T5 Q4.4
Demo template — MA contract (PDF + 2 signatures) OQ "demo templates pre-built" Q4.4
Portal signing for vendor (email link) sign_oca + portal dep Q4.4
_check_signed hook attaches final PDF to chatter OQ4 Q4.5
Bilingual (TH) .po for wrapper strings Standard Q4.6
4-page docs (overview / setup / inspection guide / contract guide) Standard Q4.6

7. Test Cases (14)

Unit (test_inspection_bridge.py) — 5 tests

# Test Verifies
U1 test_sign_request_count_initially_zero New inspection → sign_request_count == 0, smart button invisible
U2 test_action_send_for_signature_creates_request Calling the method on submitted inspection → creates one sign.oca.request with record_ref pointing back, 3 signers resolved from chair_id + committee_member_1_id + committee_member_2_id
U3 test_action_send_blocked_when_inspection_draft Calling on state=='draft' raises UserError("Submit the inspection first")
U4 test_sign_request_count_reflects_active_only Two requests created, one cancelled → count == 1 (active filter)
U5 test_action_send_for_signature_button_invisible_when_count_gt_zero Existing request → "Send for Signature" header button is invisible (view attrs)

Unit (test_contract_bridge.py) — 5 tests

# Test Verifies
U6 test_contract_sign_request_count_initially_zero New contract → sign_request_count == 0
U7 test_contract_action_send_creates_request_with_vendor_and_signer Contract w/ partner_id (vendor, portal) + roots_hospital_signer_id → request with 2 signers, vendor's role = role_vendor_signer, hospital signer role = role_hospital_signer
U8 test_contract_action_send_requires_hospital_signer roots_hospital_signer_id blank → UserError("Set hospital authorized signer")
U9 test_contract_action_send_requires_vendor_email Vendor partner without email → UserError("Vendor needs an email address for portal signing")
U10 test_contract_state_NOT_gated_by_signed Contract can transition draft→active even with sign_request_count==0 (OQ3 lock)

Integration (test_check_signed_hook.py) — 4 tests

# Test Verifies
I1 test_check_signed_attaches_pdf_to_inspection Create request on inspection → all 3 signers sign → parent roots_signed_pdf populated, roots_signed_pdf_filename = AI-2026-00001-signed.pdf, chatter message posted with attachment
I2 test_check_signed_attaches_pdf_to_contract Same flow for contract — vendor signs via portal, hospital signs in-app → final PDF lands on contract.roots_signed_pdf
I3 test_check_signed_does_NOT_advance_parent_state After all signed, inspection.state unchanged (still submitted); contract.state unchanged (still whatever it was). Matches OQ3 + OQ2 locks
I4 test_check_signed_ignores_unrelated_record_ref Request created with record_ref pointing to e.g. res.partner → signing completes normally, our hook does NOT crash, parent record untouched

Acceptance smoke (manual)

  1. Fresh DB → install roots_hemms_signature → verify 4 roles + 2 templates appear under Sign / Settings
  2. Open seeded inspection AI/2026/00001 (state=submitted) → header shows "Send for Signature"; click it → sign.oca.request form opens with 3 signers pre-filled
  3. Click "Configure Document" → drag 3 signature fields onto the PDF preview → Send
  4. Three committee users see pencil systray icon → click → sign canvas → submit
  5. After 3rd signer signs → inspection chatter shows "✅ Document signed by all parties" + signed PDF attachment; roots_signed_pdf populated; inspection state unchanged (still submitted)
  6. Print ปจป. report → signature block now shows embedded signed PDF (not empty boxes)
  7. Open seeded contract SC/2026/00001 → header shows "Send for Signature"; click it → request opens with vendor (portal) + hospital signer (internal)
  8. Vendor email log shows "You have a document to sign" link → click → portal page → sign → submit
  9. Hospital signer (internal Odoo user) → pencil systray → sign
  10. Contract chatter shows "✅ Document signed by all parties" + signed PDF; contract state unchanged (OQ3)
  11. Cancel a request mid-flow → sign_request_count decrements; parent record's roots_signed_pdf still blank
  12. Try "Send for Signature" twice in a row on same inspection → second attempt creates a second request (no uniqueness constraint — design choice; revisit Q5+)

8. Open questions — resolved 2026-05-22

All 5 locked at plan drafting. Decisions captured for posterity.

# Question Answer
OQ1 Ward nurse signs herself (portal user) or BME Manager signs on her behalf with witness audit note? Manual sign offline for now — internal sign-offs (T1/T2/T6/T7) stay on paper for V1. Defer e-sign for those to Q5+
OQ2 ปจป. inspection — sequential signing (chair last) or parallel (any order)? No workflow integration — out of scope. All signers can sign in any order. sign_oca's default behaviour
OQ3 Should Q3 contract state=active require signing complete? No. Contract lifecycle stays independent of e-sign. Signing is optional / parallel
OQ4 Hook point for "all signed → attach PDF" — action_sign override or _check_signed extension? _check_signed extension — cleaner, doesn't touch the AGPL core's transition logic
OQ5 Q4 in scope for e-Signature only, or also the contract-related Q4 items (multi-stage renewal, SLA breach, vendor scorecard)? e-Signature only. Contract-related items move to Q5+ to avoid scope sprawl

Secondary clarifications (lower stakes, can lock now or in code review):

  • Should "Send for Signature" be hidden once a non-cancelled request exists, to prevent accidental duplicates? — Plan: show always but reorder so existing requests are easier to see. Uniqueness constraint is Q5+ polish (test U5 captures the show-always default; revisit if noisy in demo)
  • Should the demo PDFs ship as real bilingual ปจป.-styled / MA-styled documents, or generic placeholders? — Plan: placeholders adequate for demo; real-styled PDFs are a polish pass once we know what the pilot hospital wants
  • Should we add an ir.attachment lifecycle hook so the signed PDF appears in the parent's Documents tab (not just chatter)? — Plan: chatter only for V1, Documents tab is Q5+

9. Resume checklist

When next session starts:

  1. Read this plan (§2 decisions, §4 module layout, §7 test list)
  2. Verify Q3 status — if Q3.x is still in progress, start Q4.1–Q4.3 only; pause before Q4.4 until Q3 ships
  3. Run git status — confirm clean tree from Q3.1 baseline
  4. Execute Q4.1: chore: vendor sign_oca from OCA/sign 18.0
  5. clone OCA/sign tag 18.0 to a temp dir
  6. copy sign_oca/ into addons/oca/sign_oca/
  7. update docs/developer/architecture.md Layer-2 vendored list
  8. commit
  9. Q4.2: module skeleton + manifest + 4 sign.oca.role seeds
  10. Q4.3: inspection bridge + qweb override + demo template (1 PDF + field placements XML)
  11. STOP — wait for Q3 done before Q4.4
  12. After Q3 ships: Q4.4 contract bridge + demo template
  13. Q4.5: _check_signed extension
  14. Q4.6: tests + 4-page docs (docs/modules/signature/)

10. Commit sequence

# Commit Notes
Q4.1 chore: vendor sign_oca from OCA/sign 18.0 Adds addons/oca/sign_oca/
Q4.2 feat(sign): roots_hemms_signature skeleton + 4 role seeds Manifest LGPL-3, depends on sign_oca + roots_hemms_asset_inspection
Q4.3 feat(sign): bridge on hemms.asset.inspection + qweb signature-block rewire + demo template T3 done end-to-end
Q4.4 feat(sign): bridge on hemms.service.contract + demo template Blocked by Q3 done. T4/T5 done end-to-end
Q4.5 feat(sign): _check_signed extension attaches signed PDF to parent chatter Hook point per OQ4
Q4.6 test(sign): 14 unit+integration tests + docs(sign): 4-page docs under docs/modules/signature/ Wrap-up

Plan status: Final 1.0 — Approved 2026-05-22, ready to execute Q4.1.