Service Contracts: Workflows¶
Lifecycle states¶
draft ─── date_start ≤ today ≤ date_end ───► active
│ │
│ date_end < today │
▼ ▼
cancelled ◄─── manual only (sticky) expired
Four states, two automatic transitions, one manual.
| State | Set by | Meaning |
|---|---|---|
draft | initial computed (and manual reset) | future or incomplete contract |
active | daily cron + initial compute | within validity window |
expired | daily cron + initial compute | past date_end |
cancelled | user action only | terminated early; sticky |
Drafting a contract¶
- Maintenance → Service Contracts → :guilabel:
New - Fill in Name, Vendor, Contract Type, Date Start, Date End
- Add Equipment in the Equipment tab
- Add Coverage lines per maintenance kind
- Save — the contract is assigned a reference (
SC/YYYY/NNNNN) and the state is computed from the dates
Single equipment → vendor auto-fills
If you add exactly one equipment to a fresh contract while the Vendor field is empty, the equipment's assigned_partner_id fills it automatically.
Activation¶
There are two activation paths:
Path 1 — Auto (daily cron)¶
If date_start ≤ today ≤ date_end when the contract is saved, the initial compute sets state to active immediately. The daily cron re-evaluates every contract once a day so that drafts whose start date arrives later get promoted without intervention.
Path 2 — Manual¶
Click any state on the status bar to jump directly to it — useful for back-dating a contract or returning a cancelled contract to draft.
Coverage banner behaviour¶
When a maintenance.request is opened on covered equipment, a banner appears above the title with a copy that follows this matrix:
| Contract on equipment? | Kind on request? | Covered? | Billable? | Banner |
|---|---|---|---|---|
| ❌ | — | — | True | ⚠ Out of contract — hospital pays |
| ✅ | ❌ | n/a | True | ✅ Covered by SC/X — hospital billable |
| ✅ | ✅ | ❌ | True | ⚠ Covered by SC/X but kind not in coverage |
| ✅ | ✅ | ✅, billable=True | True | ✅ Covered by SC/X — hospital billable |
| ✅ | ✅ | ✅, billable=False | False | ✅ Covered by SC/X (vendor) — vendor pays |
The contract picked for the request is the equipment's roots_active_contract_id — the most recent active contract by date_start. If two contracts cover the same equipment with overlapping windows, the newer-started wins. The technician can manually override.
Renewal workflow¶
date_end - 60d date_end date_end + 1
│ │ │
▼ ▼ ▼
Alert On Renewal target Auto-expire
│ │ │
└─ cron fires ├─ procurement └─ state=expired
mail.activity │ finalises renewal cron sets
on Responsible │ (extends contract roots_renewal_
+ sets │ or signs new one) alerted intact
roots_renewal_ │
alerted=True │
▼
┌──────────────┐
│ Option A │ Option B
│ Extend │ Replace
│ existing │ with new
│ contract │ contract
│ │
│ Edit dates, │ Create new
│ reset │ contract,
│ roots_ │ leave old
│ renewal_ │ one to
│ alerted=False│ auto-expire
└──────────────┘
Option A — Extend (most common for warranty)¶
- Open the contract form
- Push Date End forward (e.g. add 12 months)
- Reset Renewal Alerted to False (in the Validity group)
- Save — the contract stays active,
Alert Onrecomputes, and the next renewal cycle is armed
Option B — Replace (most common for MA)¶
- Create a new contract starting on the day after the old one ends
- Add the same equipment to the new contract
- Copy the coverage lines (or refine them based on renegotiation)
- Leave the old contract alone — it auto-expires when its
date_endpasses - The equipment's
roots_active_contract_idwill switch to the new contract automatically (newer-started wins)
Cancellation¶
To terminate a contract early (e.g. equipment decommissioned, vendor default):
- Open the contract form
- Click :guilabel:
Cancel Contractin the header - Confirm
The state becomes cancelled (sticky — date-based recompute will not un-cancel it). The equipment's roots_active_contract_id drops this contract (cancelled is not active). Requests already linked to the cancelled contract keep their link unless re-computed.
To revert a cancelled contract back into the date-based lifecycle:
- Open the contract form (still in
cancelledstate) - Click :guilabel:
Reset to Draft - The compute re-runs based on dates — the contract becomes draft, active, or expired as the calendar dictates
Multi-contract scenarios¶
One equipment covered by two contracts (warranty + MA)¶
Common scenario: 2-year manufacturer warranty + a 5-year extended MA purchased separately, both covering the same MRI.
- Add the equipment to both contracts
- Both contracts will be
activeif their windows include today - The equipment's
roots_active_contract_idresolves to the newer-started one (most recentdate_start) - Requests auto-link to the newer contract
- The technician can manually override to the warranty contract if a defect should be claimed against it
One contract covering many equipment (fleet MA)¶
Common scenario: one MA covering all 4 ventilators in the ICU.
- Create one contract, add all 4 equipment
- Each ventilator's
roots_active_contract_idresolves to this same contract - Each new request auto-links to it
- Renewal alert fires once for the contract (not 4×)
Spare-parts-only contract sharing equipment with a labor MA¶
Common scenario: spare parts supplied by a specialist vendor, labor covered by a separate firm.
- Create two contracts, both covering the same equipment
- The newer-started one wins for auto-link
- Coverage banner reflects the auto-linked contract — technician manually switches when working on a "the other half" job
- The decision-matrix logic (Q4) for routing pickings to RFQ instead of stock is not implemented in Q3 — only the banner reflects the scenario today
Renewal-alert de-duplication¶
If the daily cron fires once, the activity is created and roots_renewal_alerted flips to True. On every subsequent run, the cron sees True and skips. To re-arm the alert (e.g. you extended the contract and want a fresh reminder near the new date_end):
- Open the contract form
- In the Validity group, uncheck Renewal Alerted
- Save — next cron run will fire a fresh activity if
today >= Alert On
This is the only manual reset on a contract; everything else is date-driven.