แผน Q3 — roots_hemms_service_contracts (Final 1.0)¶
เวอร์ชันแผน: Final 1.0 — อนุมัติแล้ว, พร้อมดำเนินการ อนุมัติ: 2026-05-22 Module ID: roots_hemms_service_contracts Layer: 3 (เลนส์โรงพยาบาล) — ห่อหุ้มโมดูล OCA Layer 2 maintenance_partner (vendor แล้ว) จงใจ ไม่ vendor OCA maintenance_equipment_contract — ดู D1 ใน §2 การ implement: 🎉 commit ครบทั้ง 12 รายการแล้ว 2026-05-22 Q3.1–Q3.12 commit แล้ว การทดสอบ 24/24 ผ่านบน hemms_demo (โมดูลเดียว) และบน hemms_private (พร้อมติดตั้ง Q2 + โหลดข้อมูลสาธิต Q3) เอกสาร 5 หน้าที่ docs/modules/service-contracts/ สาธิต: 3 vendor × 5 สัญญา × 13 line ความครอบคลุม แบนเนอร์ความครอบคลุมแบบรหัสสี + ป้าย state
0. บทสรุปผู้บริหาร¶
สิ่งที่เราสร้าง: โมดูลสัญญาบริการสไตล์โรงพยาบาลที่เพิ่ม ระเบียน hemms.service.contract แบบเบาซึ่งเชื่อม maintenance.equipment ↔ vendor (res.partner) ↔ ชนิดสัญญา ↔ ขอบเขตความครอบคลุม ↔ วันที่เริ่ม/สิ้นสุด ↔ ระดับ SLA สัญญาที่ใช้งานอยู่ จะถูก ลิงก์อัตโนมัติ เข้าทุก maintenance.request ใหม่ของเครื่องมือแพทย์นั้น, flag การเรียกเก็บเงิน จะแพร่ไปยังคำขอ, และ กิจกรรมการต่ออายุ จะทำงาน 60 วันก่อน end_date รายการสัญญาสองภาษา พร้อม workflow แนบเอกสารช่วยให้ UX ของผู้จัดการ BME สมบูรณ์
ทำไม: OCA maintenance_equipment_contract เป็น m2m wrapper 2 ฟิลด์ บน contract.contract ที่ลากเอา stack การเรียกเก็บเงินประจำของ contract ทั้งหมดเข้ามา (depends on account) โรงพยาบาล HA Thailand ไม่ได้ลงบัญชี GL สำหรับสัญญารับประกัน/MA — พวกเขาติดตามความครอบคลุม, วันต่ออายุ, และระดับ SLA การสร้างโมเดล Layer 3 แบบเบาบน maintenance_partner ที่ vendor แล้ว เร็วกว่า, เบากว่า, และใกล้ โดเมนจริง มากกว่าการ vendor contract + maintenance_equipment_contract เราหยิบรูปแบบสองฟิลด์ของโมดูล OCA (m2m equipment ↔ contract, ปุ่มสถิติ) มาโดยไม่ต้องแบกภาระ
ความพยายาม: 12 commit / ~3 เซสชันพัฒนา, เป้าหมายการทดสอบ 24 รายการ, เอกสาร 5 หน้า
ชัยชนะหลัก (จัดลำดับตามผลกระทบ):
- ⭐ ลิงก์สัญญาที่ใช้งานอยู่อัตโนมัติบน maintenance.request (Q3.5) — ช่างเห็นความครอบคลุมและ flag การเรียกเก็บเงินเมื่อสร้างคำขอ โดยไม่ต้องค้นหา
- ⭐ กิจกรรมแจ้งเตือนการต่ออายุ (Q3.7) — 60 วันก่อน
end_date, กิจกรรมทำงานสำหรับ BME Manager + Procurement; ไม่มีสัญญาใดหมดอายุเงียบ ๆ - ⭐ คำศัพท์ชนิดสัญญา + ขอบเขตความครอบคลุม (Q3.3 + Q3.4) — กรอง "อะไรครอบคลุมจริง" ต่อชนิด PM ในคลิกเดียว
License: LGPL-3 เหนือ dependency แบบ AGPL-3 (maintenance_partner) — เข้ากันได้ที่การแจกจ่าย
สถานะของคำถามที่ยังเปิด: ทั้ง 4 ข้อแก้ไขแล้ว 2026-05-22 — D1 ยืนยัน Path A (Layer 3 แบบเบา, ไม่มี OCA maintenance_equipment_contract); Q1 threshold 60 วันเดียว; Q2 SLA แสดงผลอย่างเดียว; Q3 ค่าเริ่มต้นการเรียกเก็บเงิน = True เมื่อไม่มีสัญญา ดู §8 สำหรับคำตอบที่ล็อกแล้ว
1. วัตถุประสงค์¶
ผู้จัดการ BME และฝ่ายจัดซื้อของโรงพยาบาลจำเป็นต้องรู้ว่า เครื่องมือแพทย์ใด อยู่ภายใต้สัญญาใดกับ vendor รายใดเป็นเวลานานเท่าใด — และได้รับการเตือน ก่อนสัญญาใดหมดอายุ ช่างเทคนิคต้องรู้ทันทีที่ เปิดคำขอแก้ไขว่า vendor จะครอบคลุมงานนี้ ภายใต้สัญญาหรือโรงพยาบาลจะถูกเรียกเก็บเงิน วันนี้สิ่งนี้ อยู่ในสเปรดชีต, แชต Line, และในหัวของวิศวกรอาวุโสคนหนึ่ง
ขอบเขต — ที่รวม¶
- ใช้ OCA
maintenance_partnerซ้ำ (vendor แล้ว) —assigned_partner_idบนเครื่องมือแพทย์ +partner_idบนคำขอ - โมเดลใหม่
hemms.service.contract(header) +hemms.service.contract.coverage.line(ความครอบคลุมต่อชนิด) - โมเดลใหม่
hemms.contract.type(ตารางหลักขนาดเล็ก: Warranty / Extended Warranty / MA / Spare-only / Labor-only / Custom) nameสองภาษา +roots_name_thบนชนิดสัญญา + บนตัวสัญญาเอง- m2o
partner_idบนสัญญา = vendor (supplier_rank > 0) - m2m
equipment_idsบนสัญญา (สะท้อนรูปแบบ OCA; หนึ่งสัญญาครอบคลุมเครื่องมือแพทย์หลายชิ้นได้) - ฟิลด์วันที่:
date_start,date_end,roots_renewal_alert_days(ค่าเริ่มต้น 60, กำหนดค่าได้ต่อสัญญา) - Selection
state: draft → active → expired / cancelled (คำนวณจากวันที่, แทนที่ด้วยมือได้) - Selection
roots_sla_tier: bronze / silver / gold / platinum +roots_sla_response_hours(Float) - o2m ความครอบคลุมต่อชนิด: แต่ละ line =
maintenance.kind×included_in_contractBoolean ×billable_to_hospitalBoolean - ลิงก์สัญญาที่ใช้งานอยู่อัตโนมัติ เข้า
maintenance.requestใหม่ผ่านequipment_id→ สัญญาที่ใช้งานอยู่ล่าสุด (Q3.5) - แพร่ flag การเรียกเก็บเงิน จาก line ความครอบคลุมที่ตรงกับ
request.maintenance_kind_id→request.roots_is_billable_to_hospital - กิจกรรมแจ้งเตือนการต่ออายุ บนผู้รับผิดชอบสัญญา (ค่าเริ่มต้น =
partner_id.user_idfallback กลุ่ม BME Manager), ทำงานโดย cron เมื่อtoday >= date_end - roots_renewal_alert_daysAND state=active - แนบเอกสารสัญญาผ่าน
mail.thread+ir.attachment(PDF ที่เซ็นแล้ว) - ปุ่มสถิติบนฟอร์มเครื่องมือแพทย์: "X Contracts" → เปิดรายการสัญญาด้วย domain
equipment_ids in [equipment.id] - ปุ่มสถิติบนฟอร์มสัญญา: "Y Equipment" → เปิดรายการเครื่องมือแพทย์
- ปุ่มสถิติบนฟอร์มสัญญา: "Z Requests" → คำขอซ่อมบำรุงต่อเครื่องมือแพทย์ที่ครอบคลุมในช่วงสัญญา
- เมนูเฉพาะ: Maintenance → Contracts → All Contracts / Active / Expiring Soon (≤90d) / Expired
- ตัวกรองค้นหา: ตาม vendor / ชนิดสัญญา / ระดับ SLA / state / เครื่องมือแพทย์
- มุมมองรายการสองภาษา + จัดกลุ่มตาม vendor / ชนิดสัญญา / state
ขอบเขต — ที่ตัดออก (เลื่อนออกไป)¶
- ❌ การเรียกเก็บเงินประจำ / ออกใบแจ้งหนี้อัตโนมัติ — Q5+ (ต้องใช้ OCA
contractหรือaccount) - ❌ เทมเพลตสัญญา + วิซาร์ดคัดลอก — Q4
- ❌ มูลค่าสัญญาหลายสกุลเงิน — Q5
- ❌ scorecard ผลงาน vendor (dashboard KPI: อัตราละเมิด SLA, MTTR) — Q4
- ❌ สร้าง RFQ อัตโนมัติเมื่อสัญญาหมดอายุ — Q5 (ต้องใช้
purchase) - ❌ สัญญา Spare-only ลิงก์ไปยัง
stock.pickingอัตโนมัติ (override cron Q2.6) — Q4 - ❌ สัญญาหลายปีพร้อมการปรับราคากลางเทอม — Q5
- ❌ การเข้าถึง portal ของ vendor (vendor เห็นคำขอของตน) — Q6+
- ❌ การติดตามการเบิกใช้ / การใช้มูลค่าสัญญา — Q5
- ❌ ร่องรอยการตรวจสอบการแก้ไขสัญญาเกินกว่า
mail.thread— Q6+ - ❌ การ integrate กับ
roots_hemms_workflowเพื่อกั้นการเปลี่ยน stage ตามความครอบคลุมสัญญา — Q4
2. การตัดสินใจด้านการออกแบบ (ล็อกแล้ว)¶
| # | การตัดสินใจ | ตัวเลือก |
|---|---|---|
| D1 | Vendor OCA maintenance_equipment_contract หรือไม่? | ไม่ — มันลากเอา OCA contract + account เข้ามา ทั้งคู่หนัก ในขณะที่โดเมนของเราต้องการการติดตามการรับประกัน/SLA ไม่ใช่การออกใบแจ้งหนี้ สร้างโมเดล Layer 3 แบบเบาที่ สะท้อนรูปแบบฟิลด์ของมัน (m2m equipment ↔ contract, ปุ่มสถิติ) โดยไม่มี dep กลับมาทบทวนใน Q5 หากการเรียกเก็บเงินประจำจริงกลายเป็น quick-win |
| D2 | ใช้ maintenance_partner ซ้ำ (vendor แล้ว)? | ใช่ — assigned_partner_id บนเครื่องมือแพทย์กลายเป็น ค่าเริ่มต้น สำหรับสัญญาใหม่ (default= บน partner_id); partner_id ของคำขอคือฝ่ายลูกค้า, partner_id ของสัญญาคือ vendor (ต้องสอดคล้องกับ supplier_rank > 0) |
| D3 | ชนิดสัญญา — Selection vs Model? | Model (hemms.contract.type, ~5 แถว seed, noupdate=1) — สองภาษา, admin ขยายได้, พร้อมสำหรับพฤติกรรมเฉพาะชนิดในอนาคต (เช่น spare-only กำหนดเส้นทางต่างกันใน Q4) สะท้อนรูปแบบ maintenance.kind จาก Q1 |
| D4 | ความละเอียดของความครอบคลุม | ต่อ maintenance.kind เป็น line o2m (hemms.service.contract.coverage.line) — included_in_contract Boolean × billable_to_hospital Boolean หลีกเลี่ยงความซับซ้อนต่อ product; ต่อ product เป็น Q5 |
| D5 | ลิงก์สัญญาเข้าคำขออัตโนมัติ | บนส่วนขยาย _compute_partner_id — เมื่อตั้ง equipment_id, คำนวณ roots_service_contract_id = สัญญา active ล่าสุดที่ equipment_ids รวมเครื่องมือแพทย์นั้น stored, readonly=False, ช่างจึง override ได้ |
| D6 | การแพร่ flag การเรียกเก็บเงิน | ฟิลด์คำนวณบนคำขอ roots_is_billable_to_hospital, depends on roots_service_contract_id + maintenance_kind_id ค่าเริ่มต้น True (โรงพยาบาลจ่าย) เมื่อไม่มีสัญญาที่ใช้งานอยู่ — ดู Q3 ใน §8 เพื่อยืนยัน |
| D7 | จังหวะแจ้งเตือนการต่ออายุ | threshold เดียว, ค่าเริ่มต้น 60 วัน, override ต่อสัญญา (roots_renewal_alert_days, Integer, ค่าเริ่มต้น 60) cron รายวันตรวจ today >= date_end - days AND state=active AND not already_alerted Boolean roots_renewal_alerted ป้องกันกิจกรรมซ้ำ หลายขั้น (90/60/30) เลื่อนไป Q4 — ดู Q1 ใน §8 |
| D8 | การเปลี่ยน state | draft → active → expired คำนวณจากวันที่โดย cron รายวัน; admin ตั้ง cancelled ด้วยมือได้ ไม่มีวิซาร์ด confirm สำหรับ Q3 — รักษาให้ลูปกระชับ |
| D9 | การตั้งชื่อฟิลด์ | prefix roots_* บนโมเดลที่ inherit (maintenance.equipment, maintenance.request, res.partner); ชื่อล้วนบนโมเดลใหม่ hemms.* ตรงกับ Q2 D4 |
| D10 | License | LGPL-3 (roots) เหนือ AGPL-3 (maintenance_partner) |
| D11 | ระดับ SLA — Selection vs Model? | Selection ด้วย 4 ค่า (bronze, silver, gold, platinum) + Float roots_sla_response_hours แบบอิสระ การตรวจจับการละเมิด SLA อยู่นอกขอบเขต Q3 (แสดงผลอย่างเดียว); การคำนวณการละเมิดเป็น Q4 — ดู Q2 ใน §8 |
| D12 | ข้อมูลสาธิต | 3 vendor × 5 สัญญา ครอบคลุม 3 คลาสเครื่องช่วยหายใจ/MRI/เครื่องกระตุกหัวใจที่ seed ใน Q2.11 — ให้ smoke test มีอะไรให้เดินผ่านครบ end-to-end |
3. โมเดลข้อมูล¶
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. แผนการพัฒนา (12 ขั้นตอน)¶
| # | ขั้นตอน | Commit prefix | ผลลัพธ์สำคัญ |
|---|---|---|---|
| Q3.1 ✅ | โครงโมดูล (manifest + โมเดลว่าง + security/ir.model.access.csv + dependency บน maintenance_partner, roots_hemms_pm, roots_hemms_asset_master, mail) | feat(contract): add roots_hemms_service_contracts module skeleton [Q3.1] ← commit 7094011 | ติดตั้งได้สะอาด; ยังไม่มีฟีเจอร์ |
| Q3.2 ✅ | โมเดล hemms.contract.type + ข้อมูล seed (5 ชนิด) + มุมมอง list/form สองภาษา + เมนูใต้ Configuration | feat(contract): contract-type vocabulary with bilingual seed [Q3.2] ← commit b3e1fd8 | Admin เห็น Warranty / Extended Warranty / MA / Spare-only / Labor-only |
| Q3.3 ✅ | โมเดล header hemms.service.contract + ข้อมูล ir.sequence + มุมมอง form/list/kanban + เมนูใต้ Operations | feat(contract): contract header model with running reference [Q3.3] ← commit 2848c66 | สร้างร่างสัญญา → SC/2026/00001 |
| Q3.4 ✅ | o2m hemms.service.contract.coverage.line + มุมมองแก้ไข inline บนฟอร์มสัญญา | feat(contract): per-kind coverage lines with billable flag [Q3.4] ← commit 2e5c186 | เพิ่มแถวความครอบคลุม: Calibration ✅ included ❌ billable; GPM ✅ included ✅ billable |
| Q3.5 ⭐ ✅ | Inherit maintenance.equipment → roots_active_contract_id (computed+stored, active ล่าสุดตาม date_start); inherit maintenance.request → roots_service_contract_id (เติมอัตโนมัติจากเครื่องมือแพทย์), roots_is_billable_to_hospital (จาก line ความครอบคลุม), roots_contract_warning (ข้อความแบนเนอร์) | feat(contract): auto-link active contract + billable propagation [Q3.5] ← commit ba511f3 | เปิดคำขอแก้ไขใหม่ → เห็นแบนเนอร์ความครอบคลุม, ตั้ง flag การเรียกเก็บเงิน |
| Q3.6 ✅ | Inherit res.partner → roots_service_contract_ids + ปุ่มสถิติบนฟอร์ม partner ของ vendor ปุ่มสถิติบนฟอร์มเครื่องมือแพทย์ ("X Contracts") และฟอร์มสัญญา ("Y Equipment", "Z Requests") | feat(contract): partner/equipment/contract stat buttons [Q3.6] ← commit 8c33f8f | เจาะดูในคลิกเดียวจาก vendor → สัญญา → เครื่องมือแพทย์ → คำขอ |
| Q3.7 ⭐ ✅ | State cron (draft → active → expired ตามวันที่) + cron แจ้งเตือนการต่ออายุ (mail.activity บน user_id เมื่อ today >= renewal_alert_date AND state=active AND not roots_renewal_alerted) — ทั้งคู่อยู่ในเมธอด _cron_update_state_and_alert เดียว, รายวัน | feat(contract): daily state cron + renewal alert activity [Q3.7] ← commit adba729 | ตั้งสัญญาทดสอบ end_date=today+59 → cron ทำงานกิจกรรมแท็ก "Renewal due" |
| Q3.8 ✅ | โครงสร้างเมนูเฉพาะ (All / Active / Expiring Soon / Expired) + ตัวกรองค้นหา (vendor, ชนิด, ระดับ SLA, state) + แกนจัดกลุ่ม + kanban ตาม state + name_get สองภาษา (EN / TH) | feat(contract): contract tracking menu + bilingual search [Q3.8] ← commit d10167f | ผู้จัดการ BME เปิด "Expiring Soon" → เห็นเฉพาะสัญญาภายใน 90 วันก่อนสิ้นสุด |
| Q3.9 ✅ | การทดสอบ (8 unit + 16 integration = 24); แก้ไขที่รวมไว้: state default ชนะ compute, Odoo 18 name_get→display_name, I2 onchange partner จากเครื่องมือแพทย์, สตริงวันที่ Expiring Soon, ir.cron drop numbercall/user_id, drop domain supplier_rank, U8 ใช้ IntegrityError+mute_logger | test(contract): add 8 unit + 16 integration tests [Q3.9] ← commit d5dfa79 | 24/24 ผ่าน บน hemms_demo, 0 fail, 0 error |
| Q3.10 ✅ | เอกสารโมดูล (5 หน้า: index/get-started/configuration/workflows/reporting) ใต้ docs/modules/service-contracts/ + อัปเดต nav | รวมเข้าใน 48a1199 โดยเซสชันคู่ขนาน + ffebcc0 ตามต่อหน้า landing | mkdocs serve สะอาด |
| Q3.11 ✅ | ข้อมูลสาธิต — 3 vendor (GE Healthcare, Drägerwerk, Mindray) × 5 สัญญา (1 expired, 2 active, 1 expiring-soon, 1 draft) บนกองเครื่องมือหลายแบรนด์ที่แยกออก (Ventilator V500 / MRI Signa Voyager / Defib BeneHeart) — อยู่ในตัวเอง (ไม่ขึ้นกับ roots_hemms_spare_parts) | chore(contract): demo data for 3 vendors × 5 contracts [Q3.11] ← commit 8a2cf80 | smoke test ผ่านบน hemms_private; cron แจ้งเตือนการต่ออายุมีอะไรให้ทำงาน; โหลด 13 line ความครอบคลุม |
| Q3.12 ✅ | ขัดเกลา UX — แบนเนอร์ความครอบคลุมแบบรหัสสี (alert-success / alert-info / alert-warning ผ่าน roots_contract_warning_level ที่คำนวณ) + ป้าย state พร้อมสีต่อ state บนรายการสัญญา + date_end สีเหลืองอำพันเมื่อภายใน 90 วัน / สีแดงเมื่อหมดอายุ | feat(contract): UX polish — colour-coded banner + state badges [Q3.12] ← commit 0a64fe5 | สัญญาณที่เห็นได้ทันทีบนทุกคำขอที่เปิด |
Dependencies: maintenance_partner (OCA — vendor แล้ว), roots_hemms_pm (สำหรับ maintenance.kind), roots_hemms_asset_master (สำหรับข้อมูลหลักเครื่องมือแพทย์), mail
5. ข้อกำหนดมุมมอง (รายละเอียด Q3.5 + Q3.8)¶
5.1 โครงสร้างเมนู¶
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 มุมมองรายการสัญญา (ค่าเริ่มต้น = All Contracts)¶
| คอลัมน์ | แหล่งที่มา |
|---|---|
| Reference | reference (e.g. SC/2026/00001) |
| Name | name (+ roots_name_th เป็นคำบรรยายรองแบบ muted ผ่าน name_get) |
| Vendor | partner_id |
| Type | contract_type_id (ป้าย, สีตาม code) |
| Equipment | equipment_count (ลิงก์) |
| Start | date_start |
| End | date_end (สีแดงถ้า state=expired, สีเหลืองอำพันถ้าภายใน roots_renewal_alert_days) |
| SLA | roots_sla_tier (ป้าย: ไล่สี bronze→silver→gold→platinum) |
| State | state (ป้าย: draft=เทา, active=เขียว, expired=แดง, cancelled=muted) |
- จัดกลุ่มเริ่มต้นตาม: State
- การตกแต่ง: แถวเป็นสีแดงเมื่อ
state=expired; สีเหลืองอำพันเมื่ออยู่ในช่วง threshold การต่ออายุ
5.3 เลย์เอาต์มุมมอง form สัญญา¶
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 มุมมอง kanban สัญญา¶
- คอลัมน์ตาม
state: Draft / Active / Expired / Cancelled (ซ่อน Cancelled โดยค่าเริ่มต้น) - การ์ด: โลโก้ vendor, reference สัญญา, ป้ายชนิด, จำนวนเครื่องมือแพทย์, วันสิ้นสุด (พร้อม widget นับถอยหลังเมื่ออยู่ในช่วง threshold), ป้ายระดับ SLA
5.5 ตัวกรองค้นหาบนรายการสัญญา¶
- ☐ 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) - จัดกลุ่มตาม: State / Vendor / Contract Type / SLA Tier / End Date (ตามเดือน)
5.6 ฟอร์มเครื่องมือแพทย์ — ปุ่มสถิติที่เพิ่ม¶
เพิ่มปุ่มสถิติที่ 4 (หลัง "Pending Items" ของ Q2):
เปิดรายการ hemms.service.contract ที่กรองเฉพาะที่มี equipment_ids in (active_id,) การคลิกการ์ดเครื่องมือแพทย์ยังแสดง roots_active_contract_id และ roots_under_warranty เป็นป้าย readonly ในส่วนหัวด้วย
5.7 ฟอร์มคำขอซ่อมบำรุง — แบนเนอร์ความครอบคลุม (Q3.5 + Q3.12)¶
แทรก เหนือ ชื่อคำขอผ่านส่วนขยาย <header> บนมุมมอง form:
┌──────────────────────────────────────────────────────────┐
│ ✅ Covered by SC/2026/00003 (GE Healthcare — 5yr MA) │
│ Kind: Calibration → vendor pays │
└──────────────────────────────────────────────────────────┘
หรือ เมื่อไม่มีสัญญาที่ใช้งานอยู่:
┌──────────────────────────────────────────────────────────┐
│ ⚠ Out of contract — hospital pays │
└──────────────────────────────────────────────────────────┘
รองรับด้วย roots_contract_warning ที่คำนวณ (Char) + สีผ่าน decoration-success / decoration-warning บน Selection helper ที่ไม่จัดเก็บ
5.8 ฟอร์ม partner ของ vendor — ปุ่มสถิติที่เพิ่ม¶
เพิ่ม 1 ปุ่มข้าง "Equipments" + "Maintenances" ของ maintenance_partner:
เปิดรายการ hemms.service.contract ที่กรอง partner_id=active_id
6. ฟีเจอร์ UX ที่บันทึกไว้¶
| ฟีเจอร์ | จากการวิเคราะห์ | อยู่ในขั้นตอน Q3 ใด |
|---|---|---|
| คำศัพท์ชนิดสัญญา (5 ชนิด, สองภาษา, admin ขยายได้) | โดเมนโรงพยาบาล | Q3.2 |
| ขอบเขตความครอบคลุมต่อชนิดงานซ่อมบำรุง | โดเมนโรงพยาบาล | Q3.4 |
| ชื่อสัญญาสองภาษา + name_get | ข้อกำหนดหลัก | Q3.3 + Q3.8 |
m2o vendor ผ่านการใช้ maintenance_partner ซ้ำ | ใช้ Layer 2 ซ้ำ | Q3.3 (D2) |
| หลายเครื่องมือแพทย์ต่อสัญญา (m2m) | สะท้อนรูปแบบ OCA | Q3.3 (D1) |
| ลิงก์สัญญาที่ใช้งานอยู่อัตโนมัติบนคำขอ ⭐ | กำจัดจุดติดขัด UX | Q3.5 |
| แพร่ flag การเรียกเก็บเงินไปยังคำขอ ⭐ | สนับสนุนการตัดสินใจ | Q3.5 |
| แบนเนอร์ความครอบคลุมบนฟอร์มคำขอ ⭐ | มองเห็นได้ทันที | Q3.12 |
| กิจกรรมแจ้งเตือนการต่ออายุ (ค่าเริ่มต้น 60 วัน) ⭐ | ระบบอัตโนมัติสำคัญ | Q3.7 |
| State cron รายวัน (draft→active→expired) | ระบบอัตโนมัติวงจรชีวิต | Q3.7 |
| ปุ่มสถิติเครื่องมือแพทย์ "X Contracts" | เจาะดูในคลิกเดียว | Q3.6 |
| ปุ่มสถิติ vendor "Contracts" | เจาะดูในคลิกเดียว | Q3.6 |
| แนบเอกสารสัญญา (mail.thread) | การปฏิบัติตามข้อกำหนด / HA Thailand | Q3.3 (chatter ของฟอร์ม) |
| แสดงระดับ SLA (แสดงผลอย่างเดียว, ตรวจจับการละเมิด Q4) | การจัดการ vendor | Q3.3 + Q3.8 |
| เมนูเฉพาะ "Expiring Soon" | workflow ฝ่ายจัดซื้อ | Q3.8 |
reference สัญญารันนิ่ง SC/2026/00001 | การสืบย้อน (รูปแบบ Q2) | Q3.3 |
| ข้อมูลสาธิตสำหรับ smoke test เต็มรูปแบบ | การยอมรับ | Q3.11 |
7. กรณีทดสอบ (24)¶
Unit (test_contract_unit.py) — 8 การทดสอบ¶
| # | การทดสอบ | ตรวจสอบ |
|---|---|---|
| U1 | test_contract_type_bilingual_name_get | name_get() บน hemms.contract.type คืนค่า "EN / TH" เมื่อตั้ง roots_name_th |
| U2 | test_contract_reference_uses_ir_sequence_code | รหัส sequence = hemms.service.contract มีอยู่; prefix ไม่ฮาร์ดโค้ด |
| U3 | test_contract_reference_assigned_on_create | สัญญาใหม่ → reference = SC/2026/00001 |
| U4 | test_contract_state_computed_from_dates_active | สัญญาที่ 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 บล็อก 2 line ที่มี kind_id เดียวกันบนสัญญาเดียว |
Integration (test_contract_integration.py) — 16 การทดสอบ¶
| # | การทดสอบ | ตรวจสอบ |
|---|---|---|
| I1 | test_partner_id_domain_requires_supplier | การเลือก res.partner ที่ไม่ใช่ supplier สำหรับ partner_id raise |
| I2 | test_default_partner_from_equipment_assigned_partner | สร้างสัญญาด้วย equipment_id เดียว → partner_id ตั้งค่าเริ่มต้นเป็น equipment.assigned_partner_id |
| I3 | test_active_contract_auto_links_to_new_request | เครื่องมือแพทย์ภายใต้สัญญาที่ใช้งานอยู่ → คำขอแก้ไขใหม่ได้รับ roots_service_contract_id ตั้งไว้ |
| I4 | test_no_contract_leaves_request_uncovered | เครื่องมือแพทย์ไม่มีสัญญาที่ใช้งานอยู่ → roots_service_contract_id=False, roots_contract_warning="Out of contract" |
| I5 | test_billable_propagates_from_coverage_line | line ความครอบคลุม kind=Calibration, billable_to_hospital=False → คำขอ kind=Calibration มี roots_is_billable_to_hospital=False |
| I6 | test_billable_default_true_when_no_contract | คำขอไม่มีสัญญา → roots_is_billable_to_hospital=True (ขึ้นกับการทบทวน D6) |
| I7 | test_uncovered_kind_falls_back_to_billable | มีสัญญาที่ใช้งานอยู่แต่ kind ไม่อยู่ใน coverage_line_ids → roots_is_billable_to_hospital=True |
| I8 | test_request_contract_field_user_overridable | ช่าง override สัญญาที่ลิงก์อัตโนมัติได้; compute ถัดมาไม่ลบทิ้ง |
| I9 | test_multi_equipment_contract_links_to_each | สัญญาที่มี 3 เครื่องมือแพทย์ → คำขอบนแต่ละเครื่องได้สัญญาเดียวกันลิงก์อัตโนมัติ |
| I10 | test_two_overlapping_active_contracts_picks_most_recent | สองสัญญา active บนเครื่องมือแพทย์เดียว → เลือกที่ date_start ล่าสุด |
| I11 | test_renewal_cron_fires_activity_at_threshold | date_end = today + 60, roots_renewal_alert_days=60, cron รัน → สร้างกิจกรรมบน user_id, roots_renewal_alerted=True |
| I12 | test_renewal_cron_skips_already_alerted | สัญญาเดียวกันรอบ cron ที่สอง → ไม่มีกิจกรรมซ้ำ |
| I13 | test_renewal_cron_respects_custom_threshold | roots_renewal_alert_days=30, date_end=today+45 → ยังไม่มีกิจกรรม; date_end=today+29 → กิจกรรมทำงาน |
| I14 | test_state_cron_promotes_draft_to_active | date_start=today, state เคยเป็น draft → cron ตั้ง state=active |
| I15 | test_state_cron_demotes_active_to_expired | date_end=yesterday, state เคยเป็น active → cron ตั้ง state=expired |
| I16 | test_expiring_soon_filter_window | ตัวกรอง "Expiring Soon" คืนสัญญาที่ state=active AND date_end <= today+90 และไม่รวมที่หมดอายุแล้ว |
Acceptance smoke (ทำด้วยมือ)¶
- เปิด partner ของ vendor "GE Healthcare" (seed Q3.11) → คลิกสถิติ "Contracts" → เห็น 2 active + 1 expiring-soon
- เปิดสัญญา
SC/2026/00003(5yr MA บน MRI 1.5T) → แท็บ Coverage แสดง Calibration ✅ included, ❌ billable; General PM ✅ included, ✅ billable - เปิดเครื่องมือแพทย์ MRI 1.5T → ส่วนหัวแสดง "✅ Under MA — covered to 2031-04-15"; สถิติแสดง "2 Contracts"
- สร้างคำขอแก้ไขบน MRI 1.5T, kind=Calibration → แบนเนอร์ "✅ Covered by SC/2026/00003 — vendor pays"
- สร้างคำขอแก้ไขบน MRI 1.5T, kind=Software Update (ไม่อยู่ในความครอบคลุม) → แบนเนอร์ "✅ Covered by SC/2026/00003 — kind not in coverage, hospital billable"
- สร้างคำขอแก้ไขบนเครื่องกระตุกหัวใจที่ไม่มีสัญญา → แบนเนอร์ "⚠ Out of contract — hospital pays"
- เปิดเมนู "Expiring Soon" → เห็นสัญญา
SC/2026/00005(end_date=today+45) - รัน cron การต่ออายุด้วยมือ → กิจกรรมปรากฏบนเจ้าของสัญญา;
roots_renewal_alerted=True - แก้
date_endบนSC/2026/00001(การรับประกันหมดอายุแล้ว) → ป้าย state สีแดง; ปรากฏในเมนู Expired - เพิ่ม line ความครอบคลุมใหม่ (kind=Safety Inspection) บนสัญญาที่ใช้งานอยู่ → ร่างคำขอที่มีอยู่ ไม่ เปลี่ยน; คำขอใหม่เปลี่ยน (ไม่ recompute ย้อนหลัง)
8. คำถามที่ยังเปิด — แก้ไขแล้ว 2026-05-22¶
ทั้ง 4 ข้อล็อกเมื่ออนุมัติแผน บันทึกการตัดสินใจไว้ด้านล่างเพื่อเป็นหลักฐาน; ใช้ค่าเริ่มต้นตามที่แนะนำ
| # | คำถาม | คำแนะนำของฉัน |
|---|---|---|
| Q1 | จังหวะแจ้งเตือนการต่ออายุ — threshold เดียว (60d) หรือหลายขั้น (90/60/30/7)? หลายขั้นถูกต้องกว่าสำหรับความเป็นจริงของฝ่ายจัดซื้อ (90d=เริ่มหาแหล่ง, 30d=ตื่นตระหนก) แต่เพิ่มพื้นที่ทดสอบ 3 เท่าและ 3 boolean (alerted_90, alerted_60, alerted_30) | 60d เดียวใน Q3, หลายขั้นใน Q4 ล็อกเป็น D7 เว้นแต่คุณคัดค้าน |
| Q2 | ระดับ SLA — roots_sla_response_hours พอหรือไม่ หรือเราต้องตรวจจับการละเมิดเวลาตอบสนองเลย? การตรวจจับต้องเทียบ request.create_date กับ request.first_response_date (ฟิลด์ใหม่) เทียบกับ contract.roots_sla_response_hours ~4 commit เพิ่ม | แสดงผลอย่างเดียวใน Q3 การตรวจจับการละเมิดเป็น hook ของ scorecard vendor ใน Q4 |
| Q3 | เมื่อคำขอไม่มีสัญญาที่ใช้งานอยู่ ค่าเริ่มต้นการเรียกเก็บเงินคืออะไร? ตัวเลือก: (a) True — โรงพยาบาลจ่าย (ค่าเริ่มต้นปลอดภัย, ช่างยืนยันต้นทุน), (b) False — ไม่ทราบ, บังคับให้ช่างตั้งชัดเจน, © ตั้งค่าได้ต่อโรงพยาบาลบน res.company | (a) True โรงพยาบาลดำเนินงานบนหลัก "ถ้าไม่ครอบคลุม เราจ่าย" เป็นฐานการเงินที่สมจริง การบังคับตั้งชัดเจนทุกคำขอเป็นจุดติดขัด |
| Q4 | การสร้าง picking อัตโนมัติของอะไหล่ Q2 ควรรับรู้ความครอบคลุมของสัญญาหรือไม่? กล่าวคือ เมื่อสัญญาที่ใช้งานอยู่ contract_type_id.code='spare_only', กำหนดเส้นทางร่าง picking ไปยัง vendor เป็น RFQ แทนการใช้จากสต็อก | เลื่อนไป Q4 อย่างชัดเจน — อยู่ใน §1 Scope-out Q3 เพียงแสดงแบนเนอร์ "Covered by spare-only contract"; ไม่มีการเปลี่ยน picking |
การชี้แจงรอง (เดิมพันต่ำกว่า, ล็อกได้ตอนนี้หรือใน code review):
roots_under_warrantyควรแสดงในมุมมองรายการเครื่องมือแพทย์เป็นคอลัมน์ (ป้ายเหมาะกับมือถือ) หรือเฉพาะบนฟอร์ม? — แผนระบุ เฉพาะฟอร์ม; เพิ่มคอลัมน์ในการขัดเกลา Q3.12 หากถูก- สัญญาที่หมดอายุควรซ่อนจากสถิติ "Contracts" ของเครื่องมือแพทย์โดยค่าเริ่มต้นหรือไม่? — แผน: ไม่, เห็นประวัติเต็ม; สลับ "Active only" บนรายการ
- ผู้รับกิจกรรมการต่ออายุ —
contract.user_id(ค่าเริ่มต้น) หรือ CC กลุ่ม BME Manager ด้วย? — แผน:user_idเท่านั้น; การยกระดับ cc-group เป็น Q4
9. รายการตรวจสอบเพื่อทำงานต่อ¶
เมื่อเซสชันถัดไปเริ่ม:
- อ่านไฟล์นี้ (จากนั้นอ่านซ้ำที่ทำเครื่องหมายหลังอนุมัติเป็น Final 1.0)
- ยืนยันว่า Q2 ปิดแล้ว (
roots_hemms_spare_partsติดตั้งได้, การทดสอบ 26 รายการเขียว) — Q3 ขึ้นกับ semantics ของmaintenance.kindใน Q2 ที่เสถียร - ยืนยันว่า
addons/oca/maintenance_partner/ยังอยู่ (vendor ใน main ปัจจุบัน) — ไม่ต้องมีขั้น vendoring Q3.0 - ยืนยันว่า ไม่มี
addons/oca/maintenance_equipment_contract/— ตาม D1 เราไม่ vendor มัน - แก้คำถามที่ยังเปิด 4 ข้อใน §8 → ทำเครื่องหมายแผนเป็น Final 1.0 พร้อมประทับวันที่
- ทำตามตาราง 12 ขั้นอย่างเคร่งครัด; หนึ่ง commit ต่อหนึ่งแถว
- สำหรับ Q3.3: ไฟล์ข้อมูล
ir.sequenceต้องโหลดก่อนการทดสอบcreate()ของโมเดลใด ๆ ใช้ sequence — จัดไว้ก่อนไฟล์มุมมองในรายการdata:ของ__manifest__.py(บทเรียนจาก Q2.8) - สำหรับ Q3.5: เมื่อคำนวณ
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— มิฉะนั้น stored compute จะไม่รีเฟรชเมื่อสัญญาพลิกเป็น active ผ่าน cron - สำหรับ Q3.7:
_cron_update_state_and_alertของ cron ต้องประมวลผลการเปลี่ยน state ก่อน แล้วจึงแจ้งเตือนในธุรกรรมเดียว — มิฉะนั้นสัญญาที่เพิ่งกลายเป็น active จะไม่ได้รับการประเมินช่วงแจ้งเตือนจนกว่าจะถึงพรุ่งนี้