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:
hemms.asset.inspection— Committee chair + 2 members sign the annual ปจป. report inside Odoo instead of on paperhemms.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):
- ⭐ Vendor signs contracts via email link (Q4.4) — kills the paper chase; vendor receives email, signs in browser, hospital sees signed PDF in chatter
- ⭐ 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
- ⭐ 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_ocaunderaddons/oca/sign_oca - Q4.2:
roots_hemms_signaturemodule skeleton (manifest, security, data, demo scaffolding) — LGPL-3, depends onsign_oca+roots_hemms_asset_inspection - Q4.3: Bridge on
hemms.asset.inspection: sign_request_ids(o2m viarecord_refdomain)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_pdfpresent, 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_signedextension onsign.oca.request: - When state transitions to
2_signedANDrecord_refis one of our two bridged models → write signed PDF to parent'sroots_signed_pdfroots_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)¶
- Fresh DB → install
roots_hemms_signature→ verify 4 roles + 2 templates appear under Sign / Settings - 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 - Click "Configure Document" → drag 3 signature fields onto the PDF preview → Send
- Three committee users see pencil systray icon → click → sign canvas → submit
- After 3rd signer signs → inspection chatter shows "✅ Document signed by all parties" + signed PDF attachment;
roots_signed_pdfpopulated; inspection state unchanged (stillsubmitted) - Print ปจป. report → signature block now shows embedded signed PDF (not empty boxes)
- Open seeded contract
SC/2026/00001→ header shows "Send for Signature"; click it → request opens with vendor (portal) + hospital signer (internal) - Vendor email log shows "You have a document to sign" link → click → portal page → sign → submit
- Hospital signer (internal Odoo user) → pencil systray → sign
- Contract chatter shows "✅ Document signed by all parties" + signed PDF; contract state unchanged (OQ3)
- Cancel a request mid-flow →
sign_request_countdecrements; parent record'sroots_signed_pdfstill blank - 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.attachmentlifecycle 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:
- Read this plan (§2 decisions, §4 module layout, §7 test list)
- Verify Q3 status — if Q3.x is still in progress, start Q4.1–Q4.3 only; pause before Q4.4 until Q3 ships
- Run
git status— confirm clean tree from Q3.1 baseline - Execute Q4.1:
chore: vendor sign_oca from OCA/sign 18.0 - clone OCA/sign tag
18.0to a temp dir - copy
sign_oca/intoaddons/oca/sign_oca/ - update
docs/developer/architecture.mdLayer-2 vendored list - commit
- Q4.2: module skeleton + manifest + 4 sign.oca.role seeds
- Q4.3: inspection bridge + qweb override + demo template (1 PDF + field placements XML)
- STOP — wait for Q3 done before Q4.4
- After Q3 ships: Q4.4 contract bridge + demo template
- Q4.5:
_check_signedextension - 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.