Q3 Plan — roots_hemms_service_contracts (Final 1.0)¶
Plan version: Final 1.0 — Approved, ready to execute Approved: 2026-05-22 Module ID: roots_hemms_service_contracts Layer: 3 (Hospital lens) — wraps OCA Layer 2 module maintenance_partner (already vendored). Deliberately does NOT vendor OCA maintenance_equipment_contract — see D1 in §2 Implementation: 🎉 ALL 12 commits shipped 2026-05-22. Q3.1–Q3.12 committed. 24/24 tests green on hemms_demo (single-module) AND on hemms_private (with Q2 installed + Q3 demo data loaded). 5-page docs at docs/modules/service-contracts/. Demo: 3 vendors × 5 contracts × 13 coverage lines. Colour-coded coverage banner + state badges.
0. Executive Summary¶
What we build: A hospital-flavoured service-contracts module that adds a lean hemms.service.contract record linking maintenance.equipment ↔ vendor (res.partner) ↔ contract type ↔ coverage scope ↔ start/end dates ↔ SLA tier. The active contract is auto-linked onto every new maintenance.request for the equipment, its billable flag propagates onto the request, and a renewal activity fires 60 days before end_date. A bilingual contract list plus document attachment workflow rounds out the BME manager UX.
Why: OCA's maintenance_equipment_contract is a 2-field m2m wrapper on contract.contract that drags in the entire contract recurring- billing stack (depends on account). HA Thailand hospitals do not bill warranty/MA contracts to GL — they track coverage, renewal dates, and SLA tiers. Building a lean Layer 3 model on top of the already-vendored maintenance_partner is faster, lighter, and closer to the real domain than vendoring contract + maintenance_equipment_contract. We pick up the OCA module's two field shapes (m2m equipment ↔ contract, stat button) without its baggage.
Effort: 12 commits / ~3 dev sessions, 24 tests target, 5-page docs.
Keystone wins (impact-ranked):
- ⭐ Auto-link active contract on maintenance.request (Q3.5) — technician sees coverage & billable flag at request creation, no search
- ⭐ Renewal alert activity (Q3.7) — 60 days before
end_date, activity fires for BME Manager + Procurement; zero contracts expire silently - ⭐ Contract type vocabulary + coverage scope (Q3.3 + Q3.4) — filter "what's actually covered" per PM kind in one click
License: LGPL-3 over AGPL-3 dependency (maintenance_partner) — compatible at distribution.
Status of open questions: All 4 resolved 2026-05-22 — D1 confirmed Path A (lean Layer 3, no OCA maintenance_equipment_contract); Q1 single 60d threshold; Q2 SLA display-only; Q3 billable default = True when no contract. See §8 for the locked answers.
1. Purpose¶
Hospital BME managers and procurement need to know which equipment is under what contract with which vendor for how long — and to be reminded before any contract expires. Technicians need to know, the moment they open a corrective request, whether the vendor will cover this work under contract or whether the hospital will be billed. Today this lives in spreadsheets, Line chats, and the head of one senior engineer.
Scope — in¶
- Reuse OCA
maintenance_partner(already vendored) —assigned_partner_idon equipment +partner_idon request - New model
hemms.service.contract(header) +hemms.service.contract.coverage.line(per-kind coverage) - New model
hemms.contract.type(small master table: Warranty / Extended Warranty / MA / Spare-only / Labor-only / Custom) - Bilingual
name+roots_name_thon contract type + on the contract itself - m2o
partner_idon contract = the vendor (supplier_rank > 0) - m2m
equipment_idson contract (mirrors OCA shape; one contract can cover multiple equipment) - Date fields:
date_start,date_end,roots_renewal_alert_days(default 60, configurable per contract) - Selection
state: draft → active → expired / cancelled (computed from dates, manually overridable) - Selection
roots_sla_tier: bronze / silver / gold / platinum +roots_sla_response_hours(Float) - Per-kind coverage o2m: each line =
maintenance.kind×included_in_contractBoolean ×billable_to_hospitalBoolean - Auto-link active contract to new
maintenance.requestviaequipment_id→ most recent active contract (Q3.5) - Propagate billable flag from coverage line matching
request.maintenance_kind_id→request.roots_is_billable_to_hospital - Renewal alert activity on contract responsible (default =
partner_id.user_idfallback BME Manager group), fired by cron whentoday >= date_end - roots_renewal_alert_daysAND state=active - Contract document attachment via
mail.thread+ir.attachment(signed PDFs) - Stat button on equipment form: "X Contracts" → opens contract list domain
equipment_ids in [equipment.id] - Stat button on contract form: "Y Equipment" → opens equipment list
- Stat button on contract form: "Z Requests" → maintenance requests against covered equipment during contract window
- Dedicated menu: Maintenance → Contracts → All Contracts / Active / Expiring Soon (≤90d) / Expired
- Search filters: by vendor / contract type / SLA tier / state / equipment
- Bilingual list view + group-by vendor / contract type / state
Scope — out (deferred)¶
- ❌ Recurring billing / auto-invoicing — Q5+ (would require OCA
contractoraccount) - ❌ Contract template + copy wizard — Q4
- ❌ Multi-currency contract value — Q5
- ❌ Vendor performance scorecard (KPI dashboard: SLA breach rate, MTTR) — Q4
- ❌ Auto-create RFQ on contract expiry — Q5 (needs
purchase) - ❌ Spare-only contracts auto-link to
stock.picking(override Q2.6 cron) — Q4 - ❌ Multi-year contracts with mid-term price adjustments — Q5
- ❌ Vendor portal access (vendor sees their requests) — Q6+
- ❌ Contract value drawdown / utilisation tracking — Q5
- ❌ Contract amendment audit trail beyond
mail.thread— Q6+ - ❌ Integration with
roots_hemms_workflowto gate stage transitions on contract coverage — Q4
2. Design decisions (locked)¶
| # | Decision | Choice |
|---|---|---|
| D1 | Vendor OCA maintenance_equipment_contract? | No — it pulls in OCA contract + account, both heavyweight, when our domain needs warranty/SLA tracking not invoicing. Build a lean Layer 3 model that mirrors its field shape (m2m equipment ↔ contract, stat button) without the dep. Revisit in Q5 if true recurring billing becomes a quick-win |
| D2 | Reuse maintenance_partner (already vendored)? | Yes — assigned_partner_id on equipment becomes the default for new contracts (default= on partner_id); request partner_id is the customer-side party, contract partner_id is the vendor (must satisfy supplier_rank > 0) |
| D3 | Contract type — Selection vs Model? | Model (hemms.contract.type, ~5 seed rows, noupdate=1) — bilingual, admin-extensible, future-ready for type-specific behaviour (e.g. spare-only routes differently in Q4). Mirrors maintenance.kind pattern from Q1 |
| D4 | Coverage granularity | Per maintenance.kind o2m line (hemms.service.contract.coverage.line) — included_in_contract Boolean × billable_to_hospital Boolean. Avoids per-product complexity; per-product is Q5 |
| D5 | Auto-link contract to request | On _compute_partner_id extension — when equipment_id is set, compute roots_service_contract_id = most recent active contract whose equipment_ids includes the equipment. Stored, readonly=False, so technician can override |
| D6 | Billable propagation | Computed field on request roots_is_billable_to_hospital, depends on roots_service_contract_id + maintenance_kind_id. Defaults to True (hospital pays) when no active contract — see Q3 in §8 to confirm |
| D7 | Renewal alert cadence | Single threshold, default 60 days, per-contract override (roots_renewal_alert_days, Integer, default 60). Daily cron checks today >= date_end - days AND state=active AND not already_alerted. Boolean roots_renewal_alerted prevents duplicate activities. Multi-stage (90/60/30) deferred to Q4 — see Q1 in §8 |
| D8 | State transitions | draft → active → expired computed from dates by daily cron; admin can manually set cancelled. No confirm wizard for Q3 — keeps the loop tight |
| D9 | Field naming | roots_* prefix on inherited models (maintenance.equipment, maintenance.request, res.partner); bare names on new hemms.* models. Matches Q2 D4 |
| D10 | License | LGPL-3 (roots) over AGPL-3 (maintenance_partner) |
| D11 | SLA tier — Selection vs Model? | Selection with 4 values (bronze, silver, gold, platinum) + free-form roots_sla_response_hours Float. SLA breach detection is out of scope for Q3 (display only); breach computation is Q4 — see Q2 in §8 |
| D12 | Demo data | 3 vendors × 5 contracts covering the 3 ventilator/MRI/defib classes seeded in Q2.11 — gives the smoke test something to walk through end-to-end |
3. Data model¶
hemms.contract.type (NEW — master)
├─ name (Char, required, translate)
├─ roots_name_th (Char)
├─ code (Char, unique, required) e.g. "warranty", "ma", "ext_warranty"
├─ description (Text, translate)
├─ sequence (Integer)
└─ active (Boolean, default True)
hemms.service.contract (NEW — header, inherits mail.thread + mail.activity.mixin)
├─ name (Char, required) e.g. "MRI 1.5T — GE — 5yr MA 2026-2031"
├─ roots_name_th (Char)
├─ reference (Char, copy=False, default ir.sequence "hemms.service.contract" → "SC/2026/00001")
├─ partner_id (m2o → res.partner, required, domain=[('supplier_rank','>',0)])
├─ contract_type_id (m2o → hemms.contract.type, required)
├─ equipment_ids (m2m → maintenance.equipment, "hemms_contract_equipment_rel")
├─ equipment_count (Integer, computed)
├─ date_start (Date, required)
├─ date_end (Date, required)
├─ state (Selection: draft / active / expired / cancelled, computed+stored, readonly=False)
├─ roots_sla_tier (Selection: bronze/silver/gold/platinum)
├─ roots_sla_response_hours (Float)
├─ roots_renewal_alert_days (Integer, default 60)
├─ roots_renewal_alerted (Boolean, default False, copy=False)
├─ roots_renewal_alert_date (Date, computed = date_end - roots_renewal_alert_days)
├─ user_id (m2o → res.users, default=env.user — contract owner / BME side)
├─ coverage_line_ids (o2m → hemms.service.contract.coverage.line)
├─ notes (Html)
├─ company_id (m2o → res.company, default)
├─ request_count (Integer, computed) related requests during window
└─ active (Boolean, default True)
hemms.service.contract.coverage.line (NEW)
├─ contract_id (m2o → hemms.service.contract, ondelete=cascade)
├─ kind_id (m2o → maintenance.kind, required) ⭐ filter axis (mirrors Q2 D-pattern)
├─ included_in_contract (Boolean, default True)
├─ billable_to_hospital (Boolean, default False)
├─ notes (Char)
└─ sequence (Integer)
└─ _sql_constraints: unique(contract_id, kind_id)
maintenance.equipment (inherit)
├─ roots_service_contract_ids (m2m → hemms.service.contract, computed=NO — stored via m2m relation table)
├─ roots_active_contract_id (m2o → hemms.service.contract, computed+stored — most recent active)
├─ roots_service_contract_count (Integer, computed)
└─ roots_under_warranty (Boolean, computed — active contract whose type.code in ('warranty','ext_warranty'))
maintenance.request (inherit)
├─ roots_service_contract_id (m2o → hemms.service.contract, computed+stored, readonly=False)
├─ roots_is_billable_to_hospital (Boolean, computed+stored — depends on contract coverage line for kind)
└─ roots_contract_warning (Char, computed — "Out of contract" / "Covered, billable" / "Covered, vendor pays")
res.partner (inherit — vendor side)
├─ roots_service_contract_ids (o2m → hemms.service.contract via partner_id)
└─ roots_service_contract_count (Integer, computed)
ir.sequence (data record, NOT hardcoded)
├─ code : "hemms.service.contract"
├─ name : "HEMMS Service Contract"
├─ prefix : "SC/%(year)s/"
├─ padding: 5
└─ company_id: False
ir.cron (data record)
├─ name : "HEMMS Service Contract — daily state + renewal alert"
├─ model : hemms.service.contract
├─ method: _cron_update_state_and_alert
└─ interval: 1 day
4. Development Plan (12 steps)¶
| # | Step | Commit prefix | Key deliverable |
|---|---|---|---|
| Q3.1 ✅ | Module skeleton (manifest + empty models + security/ir.model.access.csv + dependency on maintenance_partner, roots_hemms_pm, roots_hemms_asset_master, mail) | feat(contract): add roots_hemms_service_contracts module skeleton [Q3.1] ← commit 7094011 | Installs cleanly; no features |
| Q3.2 ✅ | hemms.contract.type model + seed data (5 types) + bilingual list/form views + menu under Configuration | feat(contract): contract-type vocabulary with bilingual seed [Q3.2] ← commit b3e1fd8 | Admin sees Warranty / Extended Warranty / MA / Spare-only / Labor-only |
| Q3.3 ✅ | hemms.service.contract header model + ir.sequence data + form/list/kanban views + menu under Operations | feat(contract): contract header model with running reference [Q3.3] ← commit 2848c66 | Create draft contract → SC/2026/00001 |
| Q3.4 ✅ | hemms.service.contract.coverage.line o2m + inline editable view on contract form | feat(contract): per-kind coverage lines with billable flag [Q3.4] ← commit 2e5c186 | Add coverage rows: Calibration ✅ included ❌ billable; GPM ✅ included ✅ billable |
| Q3.5 ⭐ ✅ | Inherit maintenance.equipment → roots_active_contract_id (computed+stored, most recent active by date_start); inherit maintenance.request → roots_service_contract_id (auto-fills from equipment), roots_is_billable_to_hospital (from coverage line), roots_contract_warning (banner text) | feat(contract): auto-link active contract + billable propagation [Q3.5] ← commit ba511f3 | Open new corrective request → coverage banner visible, billable flag set |
| Q3.6 ✅ | Inherit res.partner → roots_service_contract_ids + stat button on vendor partner form. Stat buttons on equipment form ("X Contracts") and contract form ("Y Equipment", "Z Requests") | feat(contract): partner/equipment/contract stat buttons [Q3.6] ← commit 8c33f8f | One-click drilldown from vendor → contracts → equipment → requests |
| Q3.7 ⭐ ✅ | State cron (draft → active → expired based on dates) + renewal alert cron (mail.activity on user_id when today >= renewal_alert_date AND state=active AND not roots_renewal_alerted) — both in one _cron_update_state_and_alert method, daily | feat(contract): daily state cron + renewal alert activity [Q3.7] ← commit adba729 | Set test contract end_date=today+59 → cron fires activity tagged "Renewal due" |
| Q3.8 ✅ | Dedicated menu structure (All / Active / Expiring Soon / Expired) + search filters (vendor, type, SLA tier, state) + group-by axes + kanban by state + bilingual name_get (EN / TH) | feat(contract): contract tracking menu + bilingual search [Q3.8] ← commit d10167f | BME manager opens "Expiring Soon" → sees only contracts within 90d of end |
| Q3.9 ✅ | Tests (8 unit + 16 integration = 24); fixes folded in: state default beat compute, Odoo 18 name_get→display_name, I2 onchange partner from equipment, Expiring Soon date string, ir.cron drop numbercall/user_id, drop supplier_rank domain, U8 use IntegrityError+mute_logger | test(contract): add 8 unit + 16 integration tests [Q3.9] ← commit d5dfa79 | 24/24 pass on hemms_demo, 0 failed, 0 errors |
| Q3.10 ✅ | Module docs (5 pages: index/get-started/configuration/workflows/reporting) under docs/modules/service-contracts/ + nav update | rolled into 48a1199 by parallel session + ffebcc0 landing-page follow-up | mkdocs serve clean |
| Q3.11 ✅ | Demo data — 3 vendors (GE Healthcare, Drägerwerk, Mindray) × 5 contracts (1 expired, 2 active, 1 expiring-soon, 1 draft) on a distinct multi-brand fleet (Ventilator V500 / MRI Signa Voyager / Defib BeneHeart) — self-contained (no dep on roots_hemms_spare_parts) | chore(contract): demo data for 3 vendors × 5 contracts [Q3.11] ← commit 8a2cf80 | Smoke test passes on hemms_private; renewal-alert cron has something to fire; 13 coverage lines load |
| Q3.12 ✅ | UX polish — colour-coded coverage banner (alert-success / alert-info / alert-warning via computed roots_contract_warning_level) + state badges with per-state colour on contract list + date_end amber when within 90d / red when expired | feat(contract): UX polish — colour-coded banner + state badges [Q3.12] ← commit 0a64fe5 | Visible at-a-glance signal on every request opened |
Dependencies: maintenance_partner (OCA — already vendored), roots_hemms_pm (for maintenance.kind), roots_hemms_asset_master (for equipment master), mail.
5. View specifications (Q3.5 + Q3.8 detail)¶
5.1 Menu structure¶
Maintenance
└─ Operations
└─ Service Contracts
├─ All Contracts
├─ Active ← state=active
├─ Expiring Soon (≤90d) ← state=active AND date_end ≤ today+90
└─ Expired ← state=expired
└─ Configuration
├─ Contract Types ← hemms.contract.type
└─ Service Contract Sequence ← ir.sequence shortcut
5.2 Contract list view (default = All Contracts)¶
| Column | Source |
|---|---|
| Reference | reference (e.g. SC/2026/00001) |
| Name | name (+ roots_name_th as muted subtitle via name_get) |
| Vendor | partner_id |
| Type | contract_type_id (badge, colour by code) |
| Equipment | equipment_count (link) |
| Start | date_start |
| End | date_end (red if state=expired, amber if within roots_renewal_alert_days) |
| SLA | roots_sla_tier (badge: bronze→silver→gold→platinum colour ramp) |
| State | state (badge: draft=grey, active=green, expired=red, cancelled=muted) |
- Default group by: State
- Decoration: row in red when
state=expired; amber when within renewal threshold
5.3 Contract form view layout¶
Header: [Draft] → [Active] → [Expired] state widget (manual override allowed)
Title: reference + name + roots_name_th
[Stat buttons]
[ Equipment ] [ Requests ] [ Documents ]
3 12 4
Main:
┌─ Vendor & Type ────────────┬─ Validity ─────────────┐
│ partner_id (vendor) │ date_start │
│ contract_type_id │ date_end │
│ user_id (BME responsible) │ roots_renewal_alert_days│
│ roots_sla_tier │ roots_renewal_alert_date│
│ roots_sla_response_hours │ │
└────────────────────────────┴────────────────────────┘
Tabs:
[Equipment] m2m widget tags, photo thumbnails
[Coverage] o2m editable list:
| Kind | Included | Billable | Notes |
| Calibration | ✅ | ❌ | |
| Safety Inspection | ✅ | ❌ | |
| General PM | ✅ | ✅ | parts billed |
[Notes] Html
[Documents] attachment widget (signed PDF, addendum, etc.)
Chatter: mail.thread + activities
5.4 Contract kanban view¶
- Columns by
state: Draft / Active / Expired / Cancelled (Cancelled hidden by default) - Card: vendor logo, contract reference, type badge, equipment count, end date (with countdown widget when within threshold), SLA tier badge
5.5 Search filters on contract list¶
- ☐ My Contracts (
user_id=uid) - ☑ Active Only (
state=active) - ☐ Expiring Soon (
state=active AND date_end <= today+90) - ☐ Expired (
state=expired) - ☐ Under Warranty (
contract_type_id.code in ('warranty','ext_warranty')) - ☐ Gold/Platinum SLA
- ☐ Not Yet Alerted (
roots_renewal_alerted=False AND state=active) - Group by: State / Vendor / Contract Type / SLA Tier / End Date (by month)
5.6 Equipment form — added stat button¶
Adds a 4th stat button (after Q2's "Pending Items"):
Opens hemms.service.contract list filtered to those with equipment_ids in (active_id,). Click on equipment card also surfaces roots_active_contract_id and roots_under_warranty as readonly badges in the header.
5.7 Maintenance request form — coverage banner (Q3.5 + Q3.12)¶
Inserted above the request title via a <header> extension on the form view:
┌──────────────────────────────────────────────────────────┐
│ ✅ Covered by SC/2026/00003 (GE Healthcare — 5yr MA) │
│ Kind: Calibration → vendor pays │
└──────────────────────────────────────────────────────────┘
Or, when no active contract:
┌──────────────────────────────────────────────────────────┐
│ ⚠ Out of contract — hospital pays │
└──────────────────────────────────────────────────────────┘
Backed by computed roots_contract_warning (Char) + colour via decoration-success / decoration-warning on a non-stored Selection helper.
5.8 Vendor partner form — added stat button¶
Adds 1 button beside maintenance_partner's "Equipments" + "Maintenances":
Opens hemms.service.contract list filtered to partner_id=active_id.
6. UX features captured¶
| Feature | From analysis | In which Q3 step |
|---|---|---|
| Contract-type vocabulary (5 types, bilingual, admin-extensible) | Hospital domain | Q3.2 |
| Coverage scope per maintenance kind | Hospital domain | Q3.4 |
| Bilingual contract names + name_get | Core req | Q3.3 + Q3.8 |
Vendor m2o via maintenance_partner reuse | Layer 2 reuse | Q3.3 (D2) |
| Multi-equipment per contract (m2m) | Mirrors OCA shape | Q3.3 (D1) |
| Auto-link active contract on request ⭐ | UX friction-killer | Q3.5 |
| Propagate billable flag to request ⭐ | Decision-support | Q3.5 |
| Coverage banner on request form ⭐ | At-a-glance visibility | Q3.12 |
| Renewal alert activity (60 days default) ⭐ | Critical automation | Q3.7 |
| Daily state cron (draft→active→expired) | Lifecycle automation | Q3.7 |
| Equipment "X Contracts" stat button | One-click drilldown | Q3.6 |
| Vendor "Contracts" stat button | One-click drilldown | Q3.6 |
| Contract document attachment (mail.thread) | Compliance / HA Thailand | Q3.3 (form chatter) |
| SLA tier display (display-only, breach detection Q4) | Vendor management | Q3.3 + Q3.8 |
| "Expiring Soon" dedicated menu | Procurement workflow | Q3.8 |
Running Contract reference SC/2026/00001 | Traceability (Q2 pattern) | Q3.3 |
| Demo data for full smoke test | Acceptance | Q3.11 |
7. Test Cases (24)¶
Unit (test_contract_unit.py) — 8 tests¶
| # | Test | Verifies |
|---|---|---|
| U1 | test_contract_type_bilingual_name_get | name_get() on hemms.contract.type returns "EN / TH" when roots_name_th set |
| U2 | test_contract_reference_uses_ir_sequence_code | Sequence code = hemms.service.contract exists; prefix NOT hardcoded |
| U3 | test_contract_reference_assigned_on_create | New contract → reference = SC/2026/00001 |
| U4 | test_contract_state_computed_from_dates_active | Contract with date_start <= today <= date_end → state=active |
| U5 | test_contract_state_computed_from_dates_expired | date_end < today → state=expired |
| U6 | test_contract_state_computed_from_dates_draft | date_start > today → state=draft |
| U7 | test_renewal_alert_date_computed | roots_renewal_alert_date = date_end - roots_renewal_alert_days |
| U8 | test_coverage_line_unique_kind_per_contract | SQL constraint blocks 2 lines with same kind_id on one contract |
Integration (test_contract_integration.py) — 16 tests¶
| # | Test | Verifies |
|---|---|---|
| I1 | test_partner_id_domain_requires_supplier | Picking a non-supplier res.partner for partner_id raises |
| I2 | test_default_partner_from_equipment_assigned_partner | Creating contract with one equipment_id → partner_id defaults to equipment.assigned_partner_id |
| I3 | test_active_contract_auto_links_to_new_request | Equipment under active contract → new corrective request gets roots_service_contract_id set |
| I4 | test_no_contract_leaves_request_uncovered | Equipment without active contract → roots_service_contract_id=False, roots_contract_warning="Out of contract" |
| I5 | test_billable_propagates_from_coverage_line | Coverage line kind=Calibration, billable_to_hospital=False → request kind=Calibration has roots_is_billable_to_hospital=False |
| I6 | test_billable_default_true_when_no_contract | Request without contract → roots_is_billable_to_hospital=True (subject to D6 review) |
| I7 | test_uncovered_kind_falls_back_to_billable | Active contract but kind NOT in coverage_line_ids → roots_is_billable_to_hospital=True |
| I8 | test_request_contract_field_user_overridable | Technician can override auto-linked contract; subsequent compute does NOT clobber it |
| I9 | test_multi_equipment_contract_links_to_each | Contract with 3 equipment → request on each gets the same contract auto-linked |
| I10 | test_two_overlapping_active_contracts_picks_most_recent | Two active contracts on one equipment → picks the one with the latest date_start |
| I11 | test_renewal_cron_fires_activity_at_threshold | date_end = today + 60, roots_renewal_alert_days=60, cron runs → activity created on user_id, roots_renewal_alerted=True |
| I12 | test_renewal_cron_skips_already_alerted | Same contract second cron pass → no duplicate activity |
| I13 | test_renewal_cron_respects_custom_threshold | roots_renewal_alert_days=30, date_end=today+45 → no activity yet; date_end=today+29 → activity fires |
| I14 | test_state_cron_promotes_draft_to_active | date_start=today, state was draft → cron sets state=active |
| I15 | test_state_cron_demotes_active_to_expired | date_end=yesterday, state was active → cron sets state=expired |
| I16 | test_expiring_soon_filter_window | Filter "Expiring Soon" returns contracts with state=active AND date_end <= today+90 and excludes already-expired |
Acceptance smoke (manual)¶
- Open vendor partner "GE Healthcare" (seeded Q3.11) → click "Contracts" stat → see 2 active + 1 expiring-soon
- Open contract
SC/2026/00003(5yr MA on MRI 1.5T) → Coverage tab shows Calibration ✅ included, ❌ billable; General PM ✅ included, ✅ billable - Open MRI 1.5T equipment → header shows "✅ Under MA — covered to 2031-04-15"; stat shows "2 Contracts"
- Create corrective request on MRI 1.5T, kind=Calibration → banner "✅ Covered by SC/2026/00003 — vendor pays"
- Create corrective request on MRI 1.5T, kind=Software Update (not in coverage) → banner "✅ Covered by SC/2026/00003 — kind not in coverage, hospital billable"
- Create corrective request on un-contracted defibrillator → banner "⚠ Out of contract — hospital pays"
- Open "Expiring Soon" menu → see contract
SC/2026/00005(end_date=today+45) - Manually run renewal cron → activity appears on contract owner;
roots_renewal_alerted=True - Edit
date_endonSC/2026/00001(warranty already expired) → state badge red; appears in Expired menu - Add a new coverage line (kind=Safety Inspection) on active contract → existing draft requests do NOT change; new requests do (no retroactive recompute)
8. Open questions — resolved 2026-05-22¶
All 4 locked at plan approval. Decisions captured below for posterity; defaults were taken as recommended.
| # | Question | My recommendation |
|---|---|---|
| Q1 | Renewal alert cadence — single threshold (60d) or multi-stage (90/60/30/7)? Multi-stage is more correct for procurement reality (90d=start sourcing, 30d=panic) but is 3× the test surface and 3 booleans (alerted_90, alerted_60, alerted_30). | Single 60d in Q3, multi-stage in Q4. Locked as D7 unless you push back. |
| Q2 | SLA tier — is roots_sla_response_hours enough, or do we need response-time breach detection now? Detection requires comparing request.create_date to request.first_response_date (new field) against contract.roots_sla_response_hours. ~4 extra commits. | Display only in Q3. Breach detection is the Q4 vendor scorecard hook. |
| Q3 | When a request has no active contract, what is the billable default? Options: (a) True — hospital pays (safe default, technician confirms cost), (b) False — unknown, force technician to set explicitly, © configurable per-hospital setting on res.company. | (a) True. Hospitals run on "if uncovered, we pay" as the realistic financial baseline. Forcing explicit set on every request is friction. |
| Q4 | Should the Q2 spare-parts auto-picking be aware of contract coverage? i.e. when active contract contract_type_id.code='spare_only', route the draft picking to the vendor as an RFQ instead of consuming from stock. | Defer to Q4 explicitly — listed in §1 Scope-out. Q3 only displays "Covered by spare-only contract" banner; no picking change. |
Secondary clarifications (lower stakes, can lock now or in code review):
- Should
roots_under_warrantyshow in equipment list view as a column (mobile-friendly badge) or only on the form? — Plan says form only; add column in Q3.12 polish if cheap. - Should expired contracts be hidden from the equipment "Contracts" stat by default? — Plan: no, full history visible; "Active only" toggle on the list.
- Renewal activity assignee —
contract.user_id(default) or also CC the BME Manager group? — Plan:user_idonly; cc-group escalation is Q4.
9. Resume checklist¶
When next session starts:
- Read this file (then re-read post-approval marked as Final 1.0)
- Confirm Q2 is closed (
roots_hemms_spare_partsinstallable, 26 tests green) — Q3 depends on Q2'smaintenance.kindsemantics being stable - Confirm
addons/oca/maintenance_partner/still present (vendored in current main) — no Q3.0 vendoring step needed - Confirm no
addons/oca/maintenance_equipment_contract/exists — per D1 we are NOT vendoring it - Resolve the 4 open questions in §8 → mark plan as Final 1.0 with date stamp
- Follow the 12-step table strictly; one commit per row
- For Q3.3:
ir.sequencedata file MUST be loaded before any modelcreate()test uses the sequence — list it ahead of view files in__manifest__.pydata:list (lesson learned from Q2.8) - For Q3.5: when computing
roots_active_contract_id, depend onequipment_id.roots_service_contract_ids.stateANDequipment_id.roots_service_contract_ids.date_startANDequipment_id.roots_service_contract_ids.date_end— otherwise stored compute won't refresh when a contract flips to active via cron - For Q3.7: the cron's
_cron_update_state_and_alertmust process state transitions FIRST then alerts in a single transaction — otherwise a contract that just became active will not have its alert window evaluated until tomorrow