ข้ามไปที่เนื้อหา

แผน 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 หน้า

ชัยชนะหลัก (จัดลำดับตามผลกระทบ):

  1. ลิงก์สัญญาที่ใช้งานอยู่อัตโนมัติบน maintenance.request (Q3.5) — ช่างเห็นความครอบคลุมและ flag การเรียกเก็บเงินเมื่อสร้างคำขอ โดยไม่ต้องค้นหา
  2. กิจกรรมแจ้งเตือนการต่ออายุ (Q3.7) — 60 วันก่อน end_date, กิจกรรมทำงานสำหรับ BME Manager + Procurement; ไม่มีสัญญาใดหมดอายุเงียบ ๆ
  3. คำศัพท์ชนิดสัญญา + ขอบเขตความครอบคลุม (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_contract Boolean × billable_to_hospital Boolean
  • ลิงก์สัญญาที่ใช้งานอยู่อัตโนมัติ เข้า maintenance.request ใหม่ผ่าน equipment_id → สัญญาที่ใช้งานอยู่ล่าสุด (Q3.5)
  • แพร่ flag การเรียกเก็บเงิน จาก line ความครอบคลุมที่ตรงกับ request.maintenance_kind_idrequest.roots_is_billable_to_hospital
  • กิจกรรมแจ้งเตือนการต่ออายุ บนผู้รับผิดชอบสัญญา (ค่าเริ่มต้น = partner_id.user_id fallback กลุ่ม BME Manager), ทำงานโดย cron เมื่อ today >= date_end - roots_renewal_alert_days AND 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.equipmentroots_active_contract_id (computed+stored, active ล่าสุดตาม date_start); inherit maintenance.requestroots_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.partnerroots_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):

[ Inspections ] [ Requests ] [ Pending Items ] [ Contracts ⭐ ]
      8              12             5                  2

เปิดรายการ 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:

[ Equipments ] [ Maintenances ] [ Contracts ⭐ ]
      6               42                4

เปิดรายการ 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_endstate=active
U5 test_contract_state_computed_from_dates_expired date_end < todaystate=expired
U6 test_contract_state_computed_from_dates_draft date_start > todaystate=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 (ทำด้วยมือ)

  1. เปิด partner ของ vendor "GE Healthcare" (seed Q3.11) → คลิกสถิติ "Contracts" → เห็น 2 active + 1 expiring-soon
  2. เปิดสัญญา SC/2026/00003 (5yr MA บน MRI 1.5T) → แท็บ Coverage แสดง Calibration ✅ included, ❌ billable; General PM ✅ included, ✅ billable
  3. เปิดเครื่องมือแพทย์ MRI 1.5T → ส่วนหัวแสดง "✅ Under MA — covered to 2031-04-15"; สถิติแสดง "2 Contracts"
  4. สร้างคำขอแก้ไขบน MRI 1.5T, kind=Calibration → แบนเนอร์ "✅ Covered by SC/2026/00003 — vendor pays"
  5. สร้างคำขอแก้ไขบน MRI 1.5T, kind=Software Update (ไม่อยู่ในความครอบคลุม) → แบนเนอร์ "✅ Covered by SC/2026/00003 — kind not in coverage, hospital billable"
  6. สร้างคำขอแก้ไขบนเครื่องกระตุกหัวใจที่ไม่มีสัญญา → แบนเนอร์ "⚠ Out of contract — hospital pays"
  7. เปิดเมนู "Expiring Soon" → เห็นสัญญา SC/2026/00005 (end_date=today+45)
  8. รัน cron การต่ออายุด้วยมือ → กิจกรรมปรากฏบนเจ้าของสัญญา; roots_renewal_alerted=True
  9. แก้ date_end บน SC/2026/00001 (การรับประกันหมดอายุแล้ว) → ป้าย state สีแดง; ปรากฏในเมนู Expired
  10. เพิ่ม 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. รายการตรวจสอบเพื่อทำงานต่อ

เมื่อเซสชันถัดไปเริ่ม:

  1. อ่านไฟล์นี้ (จากนั้นอ่านซ้ำที่ทำเครื่องหมายหลังอนุมัติเป็น Final 1.0)
  2. ยืนยันว่า Q2 ปิดแล้ว (roots_hemms_spare_parts ติดตั้งได้, การทดสอบ 26 รายการเขียว) — Q3 ขึ้นกับ semantics ของ maintenance.kind ใน Q2 ที่เสถียร
  3. ยืนยันว่า addons/oca/maintenance_partner/ ยังอยู่ (vendor ใน main ปัจจุบัน) — ไม่ต้องมีขั้น vendoring Q3.0
  4. ยืนยันว่า ไม่มี addons/oca/maintenance_equipment_contract/ — ตาม D1 เราไม่ vendor มัน
  5. แก้คำถามที่ยังเปิด 4 ข้อใน §8 → ทำเครื่องหมายแผนเป็น Final 1.0 พร้อมประทับวันที่
  6. ทำตามตาราง 12 ขั้นอย่างเคร่งครัด; หนึ่ง commit ต่อหนึ่งแถว
  7. สำหรับ Q3.3: ไฟล์ข้อมูล ir.sequence ต้องโหลดก่อนการทดสอบ create() ของโมเดลใด ๆ ใช้ sequence — จัดไว้ก่อนไฟล์มุมมองในรายการ data: ของ __manifest__.py (บทเรียนจาก Q2.8)
  8. สำหรับ Q3.5: เมื่อคำนวณ roots_active_contract_id, depend on equipment_id.roots_service_contract_ids.state AND equipment_id.roots_service_contract_ids.date_start AND equipment_id.roots_service_contract_ids.date_end — มิฉะนั้น stored compute จะไม่รีเฟรชเมื่อสัญญาพลิกเป็น active ผ่าน cron
  9. สำหรับ Q3.7: _cron_update_state_and_alert ของ cron ต้องประมวลผลการเปลี่ยน state ก่อน แล้วจึงแจ้งเตือนในธุรกรรมเดียว — มิฉะนั้นสัญญาที่เพิ่งกลายเป็น active จะไม่ได้รับการประเมินช่วงแจ้งเตือนจนกว่าจะถึงพรุ่งนี้