Q2 Plan — roots_hemms_spare_parts (Final 1.0)¶
Plan version: Final 1.1 — Q2.1–Q2.8 committed 2026-05-22 Approved: 2026-05-22 Module ID: roots_hemms_spare_parts Layer: 3 (Hospital lens) — wraps OCA Layer 2 module maintenance_stock Status: Q2.1–Q2.8 ✅ committed | Q2.9–Q2.11 pending
0. Executive Summary¶
What we build: A hospital-flavoured spare-parts module on top of OCA maintenance_stock that (a) lets each maintenance.equipment carry a filterable spare-parts template, (b) auto-creates a draft stock.picking when PM cron generates a request, and © gives technicians a dedicated "My Open Requests" view to track their pending pickings with a running Stock Request number.
Why: OCA maintenance_stock provides the picking↔request link but deliberately leaves three gaps that matter for hospital UX — no consumption template, no auto-picking, no maintenance-flavoured tracking. We fill all three at Layer 3.
Effort: 11 commits / ~3 dev sessions, 26 tests target, 5-page docs.
Keystone wins (impact-ranked):
- ⭐ Auto-draft picking on PM cron (Q2.6) — technician clicks Validate, not New
- ⭐ My Open Requests view (Q2.8) — self-service tracking with Running No
SR/2026/00001 - ⭐ PM-kind filter on template (Q2.4) — only relevant spares per maintenance kind
License: LGPL-3 over AGPL-3 dependency — compatible at distribution.
Status of open questions: None remaining (all locked in §2).
1. Purpose¶
Hospital BME technicians need to know which spare parts a given piece of equipment may require during repair/PM — and then track each request to completion — without memorising SKUs or asking senior engineers. The module gives equipment master data a configurable spare-part template that auto-flows into a draft picking when a maintenance request is opened, with a dedicated tracking view to follow up.
Scope — in¶
- Vendor OCA
maintenance_stock(Layer 2 prerequisite) - New model
hemms.equipment.spare.line(o2m onmaintenance.equipment) roots_is_critical_spare+roots_name_thonproduct.template- Bilingual
name_getso EN and TH names both searchable - Inline qty_available + critical badge in template view
- Auto-propagate equipment criticality red → line
roots_is_critical - Low-stock activity for critical spares
- Auto-create draft picking on PM cron, filtered by request's kind
- Draft picking remains fully editable (add / remove / change qty) until validated — confirmed in Q2.6 behaviour
- Spare summary stat button on equipment form
- Stock Request running no.
roots_request_noviair.sequencecodehemms.stock.request(prefix configurable through UI, defaultSR/%(year)s/padding 5 →SR/2026/00001) - Stock Request tracking views (List / Form / Kanban) with computed open/pending fields — default landing = "My Open Requests"
- "Pending Items" stat button on
maintenance.requestform
Scope — out (deferred)¶
- ❌ "Used last time" pin (F4) — Q3
- ❌ Single-page move-line embed in request form (S5) — Q3
- ❌ "Copy from similar equipment" wizard (S6) — Q3
- ❌ Compatibility guard via product brand m2m (S4) — Q3
- ❌ Auto-reserve stock on stage transition (A2) — Q3
- ❌ Auto-return picking on request cancel (A3) — Q3
- ❌ Track "manual vs template" line origin — Q3 analytics
- ❌ Qty auto-learn (A5), ward cost notification (A7) — Q4+
- ❌ Multi-warehouse routing, PO automation
2. Design decisions (locked)¶
| # | Decision | Choice |
|---|---|---|
| D1 | Template on equipment vs category | Equipment (category copy = Q3 wizard) |
| D2 | Auto-picking default | Opt-in per equipment, default True when allow_consumptions=True |
| D3 | Qty-over-default approval in Q2 | No — revisit Q3 after demo feedback |
| D4 | Field naming | roots_* prefix on inherited models; bare names on new hemms.* |
| D5 | License | LGPL-3 (roots) over AGPL-3 (maintenance_stock) |
| D6 | Stock Request sequence format | SR/%(year)s/00001 (year-based, padding 5) — admin-editable through Settings, NOT hardcoded in Python; lives in ir.sequence data |
| D7 | Default landing for tracking menu | My Open Requests (state ∉ done/cancel AND user_id=uid OR create_uid=uid) |
| D8 | Draft picking editability | Fully editable (add / remove / change qty) until Validate — relies on default Odoo stock behaviour |
3. Data model¶
maintenance.equipment (inherit)
├─ roots_auto_generate_picking (Boolean, default by allow_consumptions)
├─ roots_spare_line_ids (o2m → hemms.equipment.spare.line)
└─ roots_spare_line_count (Integer, computed)
hemms.equipment.spare.line (NEW)
├─ equipment_id (m2o → maintenance.equipment, ondelete=cascade)
├─ product_id (m2o → product.product, ondelete=restrict)
├─ default_qty (Float, default 1.0)
├─ kind_ids (m2m → maintenance.kind) ⭐ filter axis
├─ position_note (Char)
├─ roots_is_critical (Boolean, computed+stored, writable)
├─ qty_available (Float, related, readonly)
└─ sequence (Integer)
product.template (inherit)
├─ roots_is_critical_spare (Boolean)
└─ roots_name_th (Char)
stock.picking (inherit — Stock Request tracking)
├─ roots_request_no (Char, readonly, copy=False, index=True, tracking=True)
├─ roots_is_open (Boolean, computed+stored)
├─ roots_pending_line_count (Integer, computed)
├─ roots_qty_progress (Float, computed) sum(quantity)/sum(uom_qty)*100
├─ roots_pending_qty_total (Float, computed)
└─ roots_scheduled_overdue (Boolean, computed)
ir.sequence (data record, NOT hardcoded)
├─ code : "hemms.stock.request"
├─ name : "HEMMS Stock Request"
├─ prefix : "SR/%(year)s/"
├─ padding: 5
└─ company_id: False
4. Development Plan (11 steps)¶
| # | Step | Commit prefix | Key deliverable |
|---|---|---|---|
| Q2.1 ✅ | Vendor OCA maintenance_stock 18.0 → addons/oca/ | 3650615 | Installs cleanly |
| Q2.2 ✅ | Module skeleton (manifest + empty models + security) | 8332871 | Installs; no features |
| Q2.3 ✅ | roots_is_critical_spare + roots_name_th on product.template + bilingual _compute_display_name + EN/TH _name_search | ca55074 | "แบตเตอรี่" matches "Battery" |
| Q2.4 ✅ | New model hemms.equipment.spare.line + o2m on equipment + inline view (qty_available, critical badge, kind tags) | 5910394 | Admin sets template; technician sees filtered list per kind |
| Q2.5 ✅ | ir.cron for daily critical-spare low-stock activity | f1340c3 | Critical badge + activity flow |
| Q2.6 ✅ | maintenance.request.create() override → auto-create draft stock.picking from template filtered by maintenance_kind_id | ad0805d | PM request opens with draft picking pre-filled; lines remain editable |
| Q2.7 ✅ | Spare summary stat buttons on equipment form ("N Spare Parts · N Critical Low") | c2dc019 | One-click drill from equipment |
| Q2.8 ✅ ⭐ | Stock Request tracking — ir.sequence + roots_request_no + computed pending fields + List/Kanban views + search filters + "Pending SR" stat button on request form + menu | 3f36d84 | Technician opens "Stock Requests" menu, sees SR/2026/00001, pending count per picking |
| Q2.9 | Tests (9 unit + 17 integration = 26) | test(spare): add unit + integration tests [Q2.9] | Green CI |
| Q2.10 | Module docs (5 pages) under docs/modules/spare-parts/ | docs(spare): add module documentation [Q2.10] | mkdocs serve clean |
| Q2.11 | Demo data — ventilator/MRI/defibrillator with realistic spare templates | chore(spare): demo data for 3 critical equipment classes [Q2.11] | Smoke test passes on fresh DB |
Dependencies: maintenance_stock (OCA — to vendor), roots_hemms_pm, roots_hemms_asset_master, roots_hemms_criticality, stock, mail.
5. View specifications (Q2.8 detail)¶
5.1 Menu structure¶
Maintenance
└─ Operations
└─ Stock Requests (ใบเบิก)
├─ My Open Requests ← default landing (D7)
├─ All My Requests
├─ Team Open
└─ Backorders
└─ Configuration
└─ Stock Request Sequence ← ir.sequence shortcut
5.2 List view (default = My Open Requests)¶
| Column | Source |
|---|---|
| Request No. | roots_request_no |
| Equipment | equipment_id (with photo) |
| For Request | maintenance_request_id |
| State | state (badge) |
| Progress | roots_qty_progress (progress bar widget) |
| Pending Items | roots_pending_line_count |
| Scheduled | scheduled_date (red if overdue) |
- Default filter: state ∉ done/cancel AND mine
- Default group by: State
- Decoration: row in red when
roots_scheduled_overdue=True
5.3 Form view — "Pending Items" notebook tab¶
| Column | Source |
|---|---|
| Product | move.product_id |
| Ordered | move.product_uom_qty |
| Received | move.quantity |
| Pending | product_uom_qty - quantity |
| Available | product.qty_available (red badge if 0) |
| Critical | product.roots_is_critical_spare |
- Filter: shows only moves where
state ∉ (done, cancel) - Header shows
roots_request_noprominently + state badge + progress bar
5.4 Kanban view¶
- Columns by
state: Draft / Waiting / Ready / Partial / Done (Partial column hidden by default) - Card: equipment thumbnail, request no, item count, progress bar, overdue badge
5.5 Search filters¶
- ☑ Mine (
user_id=uid OR create_uid=uid) - ☑ Open Only (
state ∉ done, cancel) - ☐ Overdue (
scheduled_date < today AND roots_is_open) - ☐ Backorders (
backorder_id != False) - ☐ Has Critical Spare (
mapped("move_ids.product_id.roots_is_critical_spare")contains True) - ☐ Scheduled This Week
- Group by: State / Equipment / Kind / Scheduled Date
5.6 "Pending Items" stat button on maintenance.request¶
Adds a 4th stat button beside OCA's 3 (Picking list / Stock Moves / Product Moves):
Opens a list of stock.move where picking_id.maintenance_request_id=request.id AND state ∉ done, cancel.
6. UX features captured¶
| Feature | From analysis | In which Q2 step |
|---|---|---|
| Equipment spare template (data model A3) | Core req | Q2.4 |
| Auto-populate picking from template (F1) | Friction | Q2.6 |
| Pre-filter by PM kind (F2) | Friction | Q2.4 + Q2.6 |
| Inline qty_available + critical badge (F3) | Friction | Q2.4 view |
| Bilingual search (F5) | Friction | Q2.3 |
| Spare summary widget (S1) | Simplify | Q2.7 |
| Hide out-of-stock toggle (S3) | Simplify | Q2.4 view |
| Auto-create draft picking on PM cron (A1) | Automate ⭐ | Q2.6 |
| Low-stock activity (A4) | Automate | Q2.5 |
| Auto-propagate critical (A6) | Automate | Q2.5 |
| Stock Request running no. (NEW) | UX traceability | Q2.8 |
| My Open Requests view (NEW) | UX self-service | Q2.8 |
| Pending Items per picking (NEW) | UX visibility | Q2.8 |
| Editable draft picking (confirmed) | Reality of fieldwork | Q2.6 (default Odoo behavior, locked by tests) |
7. Test Cases (26)¶
Unit (test_spare_unit.py) — 9 tests¶
| # | Test | Verifies |
|---|---|---|
| U1 | test_critical_spare_default_false | Flag default = False |
| U2 | test_bilingual_name_get | name_get() returns "EN / TH" when roots_name_th set |
| U3 | test_bilingual_search_by_thai | name_search("แบตเตอรี่") finds product whose roots_name_th="แบตเตอรี่" |
| U4 | test_spare_line_default_qty | New line defaults qty = 1.0 |
| U5 | test_spare_line_critical_propagation | Equipment criticality=red → line roots_is_critical=True |
| U6 | test_auto_generate_flag_follows_allow_consumptions | allow_consumptions=True → roots_auto_generate_picking=True |
| U7 | test_request_no_assigned_on_create_with_maintenance_link | Picking with maintenance_request_id → roots_request_no populated |
| U8 | test_request_no_skipped_for_non_maintenance_picking | Picking without maintenance link → roots_request_no empty |
| U9 | test_request_no_uses_ir_sequence_code | Sequence code = hemms.stock.request exists; prefix NOT hardcoded in Python |
Integration (test_spare_integration.py) — 17 tests¶
| # | Test | Verifies |
|---|---|---|
| I1 | test_add_spare_line_to_equipment | Admin can add product + qty + kind |
| I2 | test_template_filtered_by_kind_on_request_view | Opening request kind=Calibration shows only lines tagged that kind |
| I3 | test_pm_cron_creates_draft_picking_from_template | PM cron → request + draft picking from template |
| I4 | test_pm_cron_picking_lines_filtered_by_kind | Auto-picking only includes template lines matching kind |
| I5 | test_validating_picking_reduces_qty_available | Validate picking → qty_available drops |
| I6 | test_critical_spare_below_min_creates_activity | Critical spare < min → activity for BME Manager |
| I7 | test_non_critical_spare_below_min_no_activity | Non-critical → no activity |
| I8 | test_equipment_summary_count_excludes_archived_products | Stat button count ignores archived |
| I9 | test_corrective_request_does_not_auto_generate | Non-PM request → no auto-picking |
| I10 | test_auto_generate_disabled_skips_picking | roots_auto_generate_picking=False → no draft |
| I11 | test_technician_can_add_non_template_line_to_draft_picking | Adding line outside template, validate succeeds |
| I12 | test_technician_can_remove_template_line_from_draft_picking | Removing template line, validate succeeds |
| I13 | test_technician_can_change_qty_on_template_line | Qty 1→2, qty_available drops 2 on validate |
| I14 | test_pending_line_count_excludes_done_moves | Pending count excludes moves in done/cancel |
| I15 | test_qty_progress_computes_correctly_on_partial_validation | Partial validation → progress=50%, state=partial |
| I16 | test_open_filter_excludes_done_and_cancel | "Open Only" filter excludes done + cancel |
| I17 | test_my_filter_includes_responsible_and_creator | "Mine" filter includes both user_id and create_uid |
Acceptance smoke (manual)¶
- Setup ventilator → criticality=red →
allow_consumptions=True→ add 3 template lines (Filter ×1 kind=Filter Replacement, Battery ×1 kind=Battery, Sensor ×1 kind=Calibration) - Attach
maintenance.plankind=Filter Replacement, interval=3 months - Run PM cron manually
- Verify: request in PM Scheduled stage; draft picking attached with
roots_request_no=SR/2026/00001containing only the Filter line - Open "My Open Requests" menu → see SR/2026/00001 with progress 0%
- Open picking → "Pending Items" tab → see Filter line with Pending=1
- Add an unscheduled Battery line → save → Pending=2
- Validate → state=done → row disappears from "My Open Requests"
- If Filter qty falls below min → activity on BME Manager
8. Open questions¶
None. Module name, scope, data model, design decisions (D1–D8), sequence format, and default landing view all confirmed 2026-05-22.
9. Resume checklist¶
Completed (as of 2026-05-22): Q2.1–Q2.8 all committed on main.
Next session starts at Q2.9:
- Read this file (v1.1)
- Run tests from §7 — implement
tests/test_spare_unit.py+tests/test_spare_integration.py - Commit as
test(spare): add unit + integration tests [Q2.9] - Proceed to Q2.10 (docs) and Q2.11 (demo data) to close out Q2