แผน Q6 — roots_hemms_mass_import (Final 1.0)¶
เวอร์ชันแผน: Final 1.0 — อนุมัติแล้ว, พร้อมดำเนินการ อนุมัติ: 2026-05-22 Module ID: roots_hemms_mass_import Layer: 3 (เลนส์โรงพยาบาล) — depends on Q1 (PM, สำหรับ ref (มาตรฐาน Odoo, unique ต่อ constraint ref_uniq ของ roots_hemms_asset_master)), Q5 (HA Report, สำหรับ hemms.ha.device.class + ฟิลด์คะแนน EM บนเครื่องมือแพทย์) การ implement: Q6.1–Q6.6 ship ตามลำดับ, ไม่มี blocker ภายนอก (Q5 ลำดับก่อน Q6)
0. บทสรุปผู้บริหาร¶
สิ่งที่เราสร้าง: โมดูล Layer 3 ใหม่ roots_hemms_mass_import ที่ นำเข้าระเบียน maintenance.equipment, res.partner (vendor), และ hr.department (สถานที่) จากเทมเพลต XLSX การนำเข้าทุกครั้ง รัน การทดลองรันก่อน พร้อมคำตัดสิน OK / WARN / FAIL ต่อแถว, แล้ว commit ในธุรกรรมเดียว ทุกระเบียนที่นำเข้าถูกแท็กด้วย import_batch_id เพื่อให้ลบชุดล่าสุดเป็นหน่วยได้ การนำเข้าซ้ำ idempotent — Odoo external_id ชนะ, ไม่งั้น fallback business-key ต่อ object (เครื่องมือแพทย์ตาม ref (มาตรฐาน Odoo, unique ต่อ constraint ref_uniq ของ roots_hemms_asset_master), vendor ตาม vat/name+country, สถานที่ตาม complete_name) การจับคู่แบบ exact เข้มงวดกับ seed MoPH 82-device ของ Q5 ป้องกันการจัดประเภทผิดเงียบ ๆ ของรายงาน HA
ทำไม: โรงพยาบาล 200 เตียงมีเครื่องมือแพทย์ 800–2,000 ชิ้นในทะเบียน Excel ที่มีอยู่ หากไม่มีการนำเข้าจำนวนมาก, HEMMS go-live = หลายสัปดาห์ ของการพิมพ์ด้วยมือ = ข้อผิดพลาดการคัดลอก = รายงาน HA เสียหาย (Q5) base_import ที่มีในตัวของ Odoo ทั่วไปเกินไปสำหรับเจ้าหน้าที่ biomed ที่ไม่เชี่ยวชาญ Odoo — ไม่มีการตรวจสอบ MoPH, ไม่มีตัวอย่างการทดลองรัน, ไม่มี undo ตามชุด Q6 คือ ฟีเจอร์ที่ทำให้การนำ HEMMS มาใช้ขยายขนาดได้
ความพยายาม: 6 commit / ~1 เซสชันพัฒนา, เป้าหมายการทดสอบ 16 รายการ, เอกสาร 4 หน้า, 1 ชุดสาธิต (24 เครื่องมือแพทย์ / 5 vendor / 3 แผนก)
ชัยชนะหลัก (จัดลำดับตามผลกระทบ):
- ⭐ การทดลองรันแบบจำลองเต็มรูปแบบ (Q6.3) — parse → resolve FK → ตรวจความไม่ซ้ำ → จับคู่ MoPH → คำตัดสินต่อแถวพร้อมเหตุผล, ก่อน เขียน DB ใด ๆ
- ⭐ การลบที่แท็กตามชุด (Q6.5) — ทุกระเบียนที่นำเข้ามี
import_batch_id; ผู้ใช้ undo ชุดล่าสุดในคลิกเดียวได้ (ภายใน 7 วัน, ถ้าไม่มีเหตุการณ์ PM ของ Q1 หรือสัญญา Q3 อ้างถึง) - ⭐ การนำเข้าซ้ำแบบ idempotent (Q6.3) — external_id ก่อน, fallback business-key นำเข้าไฟล์เดิมซ้ำ = 0 ระเบียนใหม่, ไม่ใช่ 800 รายการ ซ้ำ
- ⭐ การจับคู่ MoPH 82-class เข้มงวด (Q6.3) — จับคู่ exact กับ seed Q5.2 (ชื่อ TH หรือ EN); การจับคู่แบบ fuzzy ถูกปฏิเสธพร้อมข้อผิดพลาดชัดเจน ปกป้องคุณภาพข้อมูลรายงาน HA
- ⭐ ส่งออก XLSX ข้อผิดพลาด (Q6.5) — แถวที่ล้มเหลวดาวน์โหลดเป็น XLSX พร้อมคอลัมน์เหตุผล; ผู้ใช้แก้ + รันใหม่เฉพาะข้อผิดพลาด
License: LGPL-3, เขตแดนเดียวกับ Q5 / Q3 / Q4
สถานะของคำถามที่ยังเปิด: ทั้ง 6 ข้อแก้ไขแล้ว 2026-05-22 — OQ-A ข้อตกลง header xml_id; OQ-B business-key ของ vendor vat แล้ว name+country; OQ-C ไม่ตรวจจับชื่อ sheet อัตโนมัติ; OQ-D session cache ตัวอย่าง 1 ชั่วโมง; OQ-E หน้าต่างลบชุด 7 วันพร้อม guard เหตุการณ์ที่เกี่ยวข้อง; OQ-F XLSX ข้อผิดพลาด sheet เดียว ดู §8
1. วัตถุประสงค์¶
การนำ HEMMS มาใช้ในโรงพยาบาลล้มเหลวตั้งแต่วันแรกหากโหลดทะเบียน เครื่องมือแพทย์เร็วไม่ได้ โรงพยาบาล 200 เตียงมีเครื่องมือแพทย์ 800–2,000 ชิ้น, บวก 30–80 vendor, บวก 15–40 แผนก / สถานที่ การป้อนด้วยมือ ไม่สมจริงที่ขนาดนั้นและสร้างข้อผิดพลาดการคัดลอกที่ ทำให้รายงาน HA เสียหายเงียบ ๆ (Q5)
สมมติฐานที่สมเหตุสมผลคือโรงพยาบาลมีข้อมูลนี้ใน Excel แล้ว (หรือระบบ legacy ที่ส่งออก Excel) Q6 เปลี่ยน "ส่งไฟล์ Excel ของคุณมา" เป็นการดำเนินการ 5 นาทีที่สร้างระเบียนที่สะอาด, ตรวจสอบแล้ว พร้อมร่องรอยการตรวจสอบเต็มรูปแบบ
ขอบเขต — ที่รวม¶
- Q6.1: โครงโมดูล, manifest, โมเดล
hemms.import.batch,ir.sequence(IMP/%(year)s/00001), security ACL - Q6.2: ตัวสร้างเทมเพลต XLSX ฝั่งเซิร์ฟเวอร์ (3 object: เครื่องมือแพทย์ / vendor / สถานที่); header คอลัมน์ TH+EN; รายการ dropdown สำหรับชื่อ MoPH 82-class + ระดับความเสี่ยง + ช่วงคะแนน EM; action ดาวน์โหลดบน วิซาร์ด
- Q6.3: เอนจิน parser + validator:
- openpyxl XLSX reader (.xlsx เท่านั้น)
- การ normalise header (alias TH+EN → field key)
- การวนแถวพร้อมเพดาน 5,000 แถว (system parameter, ค่าเริ่มต้น 5000)
- ตัว resolve external_id ทางเลือก — คอลัมน์
id/External ID; ถ้า ให้มา, มีลำดับก่อน; ถ้าว่าง, Odoo สร้าง entryir.model.dataอัตโนมัติต่อระเบียนใหม่ (พฤติกรรม Odoo มาตรฐาน) - ตัว resolve business-key — เส้นทาง idempotency หลัก (เครื่องมือแพทย์:
ref(มาตรฐาน Odoo, unique ต่อ constraintref_uniqของroots_hemms_asset_master); vendor:vat→name+country_id→name; สถานที่:complete_name) - การ resolve FK (equipment→vendor, equipment→department, equipment →device_class)
- ตัวจับคู่ MoPH 82-class เข้มงวด กับ seed Q5.2 (
name_enหรือname_th, case-sensitive, ตัด whitespace) - การตรวจความไม่ซ้ำ (ไม่มีสองแถวใหม่ที่มี business key เดียวกัน)
- คำตัดสินต่อแถว (OK / WARN / FAIL) พร้อมคอลัมน์
reason - คืนโครงสร้างข้อมูล
ImportPreviewที่ cache ใน session (TTL 1 ชั่วโมง) - Q6.4: วิซาร์ดนำเข้า (3 ขั้น):
- อัปโหลด XLSX + เลือกชนิด object (Equipment / Vendor / Location)
- ตัวอย่างการทดลองรัน — แสดง 50 แถวแรกพร้อมป้ายคำตัดสิน + จำนวนสรุป (✅ N OK / ⚠️ M WARN / ❌ K FAIL); ปุ่ม "Download error XLSX" แสดงถ้ามี FAIL
- Commit — ธุรกรรมเดียว, ทั้งหมดหรือไม่เลย; เมื่อสำเร็จ, เปิด ระเบียน
hemms.import.batchใหม่ (ห่วงโซ่ audit) - Q6.5: การลบที่แท็กตามชุด + ส่งออก XLSX ข้อผิดพลาด:
hemms.import.batch.action_delete_batch()— ตรวจหน้าต่าง 7 วัน, ไม่มีเหตุการณ์ PM ของ Q1 ที่เกี่ยวข้อง, ไม่มีสัญญา Q3 ที่เกี่ยวข้อง; ลบ ระเบียนทั้งหมดที่แท็กimport_batch_idนี้hemms.import.batch.action_download_error_xlsx()— สร้าง XLSX ของแถว FAIL จากตัวอย่างการทดลองรันล่าสุด (คอลัมน์:row_num,field,severity,message,raw_value)- Q6.6: การทดสอบ (6 unit + 10 integration = 16 การทดสอบ), เอกสาร 4 หน้า ใต้
docs/modules/mass-import/, ชุดสาธิต + แผนนี้
ขอบเขต — ที่ตัดออก (เลื่อนไปตามต่อ Q6.x)¶
- ❌ การนำเข้าอะไหล่ Q2 (
stock.lot,product.productสำหรับชิ้นส่วน) — ความซับซ้อนของ lot/serial, ตามต่อ Q6.x - ❌ การนำเข้าสัญญาบริการ Q3 (
hemms.service.contract) — กรณีขอบ การ derive date-state, ตามต่อ Q6.x - ❌ แนบรูป / คู่มือ / ใบรับรองจำนวนมาก (ZIP + การจับคู่ชื่อไฟล์) — แนบด้วยมือหลังนำเข้าสำหรับ v1
- ❌ การ rollback เต็มรูปแบบ (undo การแก้ไข, ไม่ใช่แค่ลบ) — การลบที่แท็ก ตามชุดครอบคลุม 90% ของกรณีด้วยต้นทุน 10%
- ❌ การรองรับรูปแบบ CSV — XLSX เท่านั้น; CSV ชวนให้เกิดความวุ่นวายของ encoding สำหรับ TH
- ❌
.xlslegacy (xlrd) — XLSX เท่านั้น; openpyxl อยู่ใน dep ของ Odoo แล้ว - ❌ การนำเข้าตามเวลา (cron) จาก dropbox ของ filesystem — อัปโหลดด้วยมือเท่านั้น
- ❌ ตรวจจับชนิด object อัตโนมัติจากชื่อ sheet — ตัวเลือกชัดเจน (OQ-C)
- ❌ XLSX หลาย sheet ที่มีทั้ง 3 object ในไฟล์เดียว — หนึ่ง object ต่อไฟล์ บังคับลำดับ FK (vendor → สถานที่ → เครื่องมือแพทย์)
- ❌ Webhook / API การนำเข้าจำนวนมาก — วิซาร์ด UI เท่านั้น
- ❌ มุมมอง diff (เทียบระเบียนที่นำเข้า vs ที่มีอยู่) — คำตัดสินก่อน commit เพียงพอสำหรับ v1
2. การตัดสินใจด้านการออกแบบ (ล็อกแล้ว)¶
| # | การตัดสินใจ | ตัวเลือก |
|---|---|---|
| D1 | ขอบเขต object ใน v1 | เครื่องมือแพทย์ + vendor + สถานที่ เท่านั้น อะไหล่ Q2 + สัญญา Q3 + lot/serial + การแนบจำนวนมาก เลื่อนไป Q6.x ขอบเขตเล็กกว่า (เครื่องมือแพทย์อย่างเดียว) ทำให้การ migrate ทำได้ครึ่งเดียว; ขอบเขตใหญ่กว่าสืบทอดกรณีขอบของ Q2/Q3 |
| D2 | กลยุทธ์เทมเพลต | สร้างฝั่งเซิร์ฟเวอร์ตามต้องการ action ดาวน์โหลดของวิซาร์ดสร้าง XLSX สดจาก schema โมเดลปัจจุบัน หลีกเลี่ยง drift ระหว่างเทมเพลต-vs-schema; รวมฟิลด์ใหม่ที่โมดูลในอนาคตเพิ่มให้เครื่องมือแพทย์อัตโนมัติ |
| D3 | ไลบรารี XLSX | openpyxl (อยู่ในแผนผัง dependency ของ Odoo แล้ว) ไม่รองรับ xlrd / .xls legacy; XLSX-only โดยการออกแบบ |
| D4 | ความลึกของการทดลองรัน | จำลองเต็มรูปแบบ — parse, resolve FK, ตรวจความไม่ซ้ำ, จับคู่ MoPH, OK/WARN/FAIL ต่อแถว ไม่ใช่ระดับคอลัมน์อย่างเดียว |
| D5 | Idempotency | Business-key ก่อน; external_id ทางเลือก เมื่อนำเข้าซ้ำ ตัว resolve จับคู่ตาม business key ต่อ object คอลัมน์ id / External ID เป็นทางเลือก — Odoo สร้าง external_id ใน ir.model.data อัตโนมัติทุกระเบียนใหม่ (พฤติกรรมการนำเข้า Odoo มาตรฐาน), ผู้ใช้จึง ไม่ ต้องให้มา ถ้าผู้ใช้ ให้ id, มันมีลำดับก่อนการจับคู่ business-key |
| D6 | Business keys | เครื่องมือแพทย์: ref (มาตรฐาน Odoo, unique ต่อ constraint ref_uniq ของ roots_hemms_asset_master) (เลขครุภัณฑ์ — unique ต่อ Q1) Vendor: fallback แบบเป็นชั้นเพื่อความง่ายในการ onboard: vat ถ้าไม่ว่าง → name+country_id ถ้ามีประเทศ → name อย่างเดียว ความเสี่ยงการชนกันบน name-only ยอมรับสำหรับ v1 (ดู §9 gotcha) สถานที่: complete_name |
| D7 | หน้าต่างการลบที่แท็กตามชุด | 7 วัน + ไม่มีเหตุการณ์ที่เกี่ยวข้อง วิซาร์ดปฏิเสธการลบถ้าชุดเก่ากว่า 7 วัน, หรือถ้าระเบียนที่แท็กมี maintenance.request ของ Q1 หรือ hemms.service.contract ของ Q3 ที่ลิงก์ หลัง 7 วัน, ต้อง "force delete" ชัดเจนพร้อมการยืนยัน (admin เท่านั้น) |
| D8 | การจับคู่ MoPH 82-class | จับคู่ exact เข้มงวด กับ name_en หรือ name_th (ตัด whitespace, case-sensitive) Fuzzy / Levenshtein ถูกปฏิเสธ เหตุผล: การจัดประเภทผิดเงียบ ๆ ทำให้รายงาน HA เสียหาย; ล้มเหลวให้ดังกว่า |
| D9 | การแปล TH | context lang ที่ active (idiom ของ Odoo) วิซาร์ดรันในภาษา session ของผู้ใช้; ป้าย header คอลัมน์การแปลคือชื่อ TH/EN ของฟิลด์ ไม่มีคอลัมน์ name_th ขนาน |
| D10 | เพดานแถว | 5,000 ต่อไฟล์ (system parameter roots_hemms_mass_import.row_cap, ค่าเริ่มต้น 5000) 5,001+ raise ข้อผิดพลาดชัดเจนก่อน parse โรงพยาบาลแบ่งทะเบียนใหญ่ข้ามหลายไฟล์ |
| D11 | การบังคับลำดับ | หนึ่ง object ต่อไฟล์ วิซาร์ดต้องการการเลือก object ชัดเจน; ถ้าไฟล์เครื่องมือแพทย์อ้าง vendor / แผนกที่ไม่รู้จัก, การทดลองรันธง FAIL ผู้ใช้นำเข้า vendor → สถานที่ → เครื่องมือแพทย์ ตามลำดับ |
| D12 | รายงานข้อผิดพลาด | ทั้งสอง — 50 ข้อผิดพลาดแรก inline ในวิซาร์ด, ชุดเต็มดาวน์โหลดเป็น XLSX (sheet เดียว, คอลัมน์: row_num, field, severity, message, raw_value) |
| D13 | โมเดล state ของชุด | draft → previewed → committed → (archived|deleted) การ preview สร้างชุด draft พร้อมฟิลด์ data_json ที่ cache; commit เลื่อนเป็น committed การลบเขียน deleted_at |
| D14 | ความปลอดภัยของธุรกรรม | ธุรกรรมเดียวต่อ commit ใช้ cr.savepoint() รอบลูปการสร้าง line แถว FAIL ใด ๆ → commit ทั้งหมด rollback, ชุดคงอยู่ใน previewed ทั้งหมดหรือไม่เลย |
| D15 | session cache ตัวอย่าง | hemms.import.batch.data_json ฟิลด์ Json เก็บแถวที่ parse + validate; เลื่อนเป็นระเบียน committed เมื่อ commit TTL 1 ชั่วโมงผ่าน cron cleanup ของชุด previewed ที่เก่ากว่า 1 ชั่วโมง |
| D16 | License / สถานะ | LGPL-3, Alpha, maintainer jakapolr |
3. โมเดลข้อมูล¶
3.1 โมเดลใหม่¶
hemms.import.batch (Q6.1)
├─ name Char readonly, ir.sequence IMP-YYYY-#####
├─ object_type Selection equipment / vendor / location
├─ state Selection draft / previewed / committed / archived / deleted
├─ file_name Char original .xlsx filename
├─ file_data Binary uploaded XLSX (kept for audit)
├─ data_json Json parsed rows + verdicts (preview cache)
├─ row_count_total Integer computed, stored
├─ row_count_ok Integer computed, stored
├─ row_count_warn Integer computed, stored
├─ row_count_fail Integer computed, stored
├─ records_created Integer computed post-commit, stored
├─ records_updated Integer computed post-commit, stored (idempotent upsert hits)
├─ committed_at Datetime
├─ committed_by_id M2O res.users
├─ deleted_at Datetime
├─ deleted_by_id M2O res.users
├─ delete_blocked_reason Char set when delete attempt fails the D7 check
└─ active Boolean
+ mail.thread + mail.activity.mixin
(no hemms.import.batch.line model — rows live in data_json blob; per-row
verdicts are read-only after preview, not Odoo records)
3.2 โมเดลที่ inherit — แท็ก import_batch_id¶
maintenance.equipment (INHERIT, Q6.1)
└─ roots_hemms_import_batch_id M2O hemms.import.batch (null = manual entry)
res.partner (INHERIT, Q6.1)
└─ roots_hemms_import_batch_id M2O hemms.import.batch
hr.department (INHERIT, Q6.1)
└─ roots_hemms_import_batch_id M2O hemms.import.batch
ฟิลด์ใช้ namespace roots_hemms_import_batch_id เพื่อเลี่ยงการชนกัน กับโมดูลอื่นที่เพิ่ม import_batch_id ให้โมเดลที่แชร์เดียวกัน
4. เลย์เอาต์โมดูล¶
addons/roots/roots_hemms_mass_import/
__init__.py
__manifest__.py
README.rst
data/
ir_sequence_data.xml # IMP-YYYY-#####
ir_config_parameter_data.xml # row_cap default 5000
ir_cron_data.xml # cleanup previewed >1h
demo/
hemms_mass_import_demo.xml # Q6.6 — 1 demo batch
models/
__init__.py
hemms_import_batch.py # Q6.1 — batch model + actions
maintenance_equipment.py # Q6.1 — import_batch_id field
res_partner.py # Q6.1 — import_batch_id field
hr_department.py # Q6.1 — import_batch_id field
services/
__init__.py
template_generator.py # Q6.2 — XLSX schema → workbook
parser.py # Q6.3 — XLSX → row dicts
resolver.py # Q6.3 — external_id + business-key
validator.py # Q6.3 — FK + uniqueness + MoPH
committer.py # Q6.4 — transactional commit
error_export.py # Q6.5 — error XLSX generator
wizard/
__init__.py
hemms_mass_import_wizard.py # Q6.4 — 3-step wizard
hemms_mass_import_wizard_views.xml
views/
hemms_import_batch_views.xml # list/form + delete + download
menus.xml
security/
ir.model.access.csv
mass_import_security.xml # restrict to BME / admin
tests/
__init__.py
test_parser.py # Q6.6 — U1, U2
test_resolver.py # Q6.6 — U3, U4
test_moph_match.py # Q6.6 — U5
test_row_cap.py # Q6.6 — U6
test_dry_run.py # Q6.6 — I1, I2
test_commit.py # Q6.6 — I3, I4
test_idempotency.py # Q6.6 — I5
test_order.py # Q6.6 — I6
test_delete.py # Q6.6 — I7, I8
test_error_xlsx.py # Q6.6 — I9
test_wizard_render.py # Q6.6 — I10
i18n/
th.po
5. จุดสัมผัส UX¶
5.1 วิซาร์ดการนำเข้าจำนวนมาก (Q6.4)¶
ขั้น 1 — อัปโหลด + เลือก object:
┌─ Mass Import — Step 1: Upload ──────────────────────────────────┐
│ Object Type [ Equipment ▼ ] │
│ ( Equipment / Vendor / Location ) │
│ │
│ XLSX File [ Choose file... hospital_register_2026.xlsx ] │
│ │
│ [ Download Template ] ← server-generates fresh XLSX │
│ │
│ [ Cancel ] [ Next: Preview ]│
└──────────────────────────────────────────────────────────────────┘
ขั้น 2 — ตัวอย่างการทดลองรัน:
┌─ Mass Import — Step 2: Preview ─────────────────────────────────┐
│ Batch: IMP-2026-00042 (draft) │
│ File: hospital_register_2026.xlsx │
│ │
│ Summary: ✅ 794 OK ⚠️ 4 WARN ❌ 2 FAIL │
│ │
│ First 50 rows: │
│ | Row | Verdict | Asset No | Name (TH/EN) | Reason | │
│ | 2 | ✅ OK | A-0001 | เครื่อง... / Anesth | — | │
│ | 3 | ⚠️ WARN | A-0002 | (blank EN) | EN name... | │
│ | 7 | ❌ FAIL | A-0006 | เครื่อง... / X-ray | Vendor not | │
│ | | | | | found | │
│ ... │
│ │
│ [ ⬇ Download error XLSX ] (visible if FAIL > 0) │
│ [ Cancel ] [ ← Back ] [ Commit Batch ]│
└──────────────────────────────────────────────────────────────────┘
ปุ่ม Commit ปิดใช้งานถ้า row_count_fail > 0 AND ผู้ใช้ไม่ใช่ admin (admin commit บางส่วนได้; non-admin ต้องทำความสะอาดก่อน)
ขั้น 3 — ผลลัพธ์:
┌─ Mass Import — Step 3: Result ──────────────────────────────────┐
│ Batch: IMP-2026-00042 (committed) │
│ │
│ Records created: 790 │
│ Records updated: 4 ← idempotent upsert hits │
│ Records failed: 0 │
│ │
│ Committed at: 2026-05-22 14:32:11 │
│ Committed by: jakapol@roots.tech │
│ │
│ [ Open Batch ] [ Done ] │
└──────────────────────────────────────────────────────────────────┘
5.2 มุมมองรายการ hemms.import.batch¶
| Reference | Object | State | OK | WARN | FAIL | Created | Committed by |
| IMP-2026-00042 | Equipment | Committed | 794| 4 | 0 | 790 | jakapol |
| IMP-2026-00041 | Vendor | Committed | 24 | 0 | 0 | 24 | jakapol |
| IMP-2026-00040 | Equipment | Previewed | 100| 2 | 8 | — | — |
| IMP-2026-00039 | Equipment | Deleted | 50 | 0 | 0 | 50→0 | (jakapol) |
5.3 ฟอร์ม hemms.import.batch¶
[ Header ]
IMP-2026-00042 [ Committed ]
[ Header buttons ]
[ ⬇ Download Original XLSX ]
[ ⬇ Download Error XLSX ] ← if row_count_fail > 0
[ 🗑 Delete Batch ] ← if state='committed' AND <7d AND no related events
[ Notebook tabs ]
- Summary (counts, file, audit timeline)
- Preview Rows (read-only table from data_json)
- Delete Block Reason (visible if delete attempted + blocked)
[ Chatter ]
Standard mail.thread — commit + delete events post here
6. ฟีเจอร์ UX ที่บันทึกไว้¶
| ฟีเจอร์ | จากการกำหนดขอบเขต | อยู่ในขั้นตอน Q6 ใด |
|---|---|---|
ir.sequence IMP-YYYY-##### | รูปแบบ Q3 / Q5 | Q6.1 |
โมเดล audit hemms.import.batch | การกำหนดขอบเขต OQ | Q6.1 |
roots_hemms_import_batch_id แบบ namespace บนโมเดลที่แชร์ | ความปลอดภัยจากการชนกัน | Q6.1 |
| เทมเพลต XLSX ที่สร้างฝั่งเซิร์ฟเวอร์ | D2 | Q6.2 |
| header คอลัมน์ TH+EN + dropdown | D9 | Q6.2 |
| parser openpyxl, .xlsx เท่านั้น | D3 | Q6.3 |
| เพดาน 5,000 แถวพร้อม config param | D10 | Q6.3 |
External_id (xml_id) ชนะ business-key | D5 | Q6.3 |
| fallback business-key ต่อ object | D6 | Q6.3 |
| จับคู่ MoPH 82-class เข้มงวด | D8 | Q6.3 |
| คำตัดสิน OK/WARN/FAIL ต่อแถว | D4 | Q6.3 |
| cache ตัวอย่าง 1 ชั่วโมง + cron cleanup | D15 | Q6.1 + Q6.3 |
| วิซาร์ด 3 ขั้น (upload → preview → commit) | UX | Q6.4 |
| commit ธุรกรรมเดียว (ทั้งหมดหรือไม่เลย) | D14 | Q6.4 |
| idempotent upsert (ตัวนับ records_updated) | D5 | Q6.4 |
| การลบที่แท็กตามชุดพร้อม guard 7 วัน + เหตุการณ์ | D7 | Q6.5 |
| ส่งออก XLSX ข้อผิดพลาด (sheet เดียว) | D12 / OQ-F | Q6.5 |
| 16 การทดสอบบน DB คู่ | รูปแบบ Q3 / Q4 / Q5 | Q6.6 |
| เอกสาร 4 หน้า | Q5 ที่ย่อขนาด | Q6.6 |
| 1 ชุดสาธิต (24/5/3) | รูปแบบ Q5 | Q6.6 |
7. กรณีทดสอบ (16)¶
Unit tests (6)¶
| # | ไฟล์ | การทดสอบ | ตรวจสอบ |
|---|---|---|---|
| U1 | test_parser.py | test_xlsx_header_aliases | header TH "ชื่อเครื่อง" และ header EN "Name (EN)" ทั้งคู่แมปไปยัง field key ที่ถูกต้อง |
| U2 | test_parser.py | test_blank_row_skip | แถวที่ว่างทั้งหมดกลางไฟล์ถูกข้ามเงียบ ๆ; ไม่มี FAIL |
| U3 | test_resolver.py | test_external_id_when_provided_wins | เมื่อผู้ใช้ให้คอลัมน์ id / External ID, มันมีลำดับก่อนคอลัมน์ business-key แม้ business key ไม่ตรงกัน |
| U4 | test_resolver.py | test_business_key_layered_fallback | เส้นทางหลัก: ไม่มี external_id → vendor จับคู่ตาม vat ถ้ามี → ไม่งั้น name+country_id ถ้ามีประเทศ → ไม่งั้น name อย่างเดียว; เครื่องมือแพทย์ตาม ref (มาตรฐาน Odoo, unique ต่อ constraint ref_uniq ของ roots_hemms_asset_master); รายงานชั้นที่ถูกต้องบนคำตัดสินต่อแถว |
| U5 | test_moph_match.py | test_strict_match_exact_only | name_en หรือ name_th exact จาก seed Q5.2 → OK; ตัด whitespace ท้าย; near-match ("Anesthesia Unit" vs "Anesthesia Units") → FAIL พร้อมข้อความชัดเจน |
| U6 | test_row_cap.py | test_5001_rows_raises_before_parse | ไฟล์ 5,001 แถว raise UserError ก่อน parse ระเบียนใด ๆ; เคารพ config param row_cap ถ้า override |
Integration tests (10)¶
| # | ไฟล์ | การทดสอบ | ตรวจสอบ |
|---|---|---|---|
| I1 | test_dry_run.py | test_preview_writes_no_db_records | หลังการทดลองรัน, จำนวน maintenance.equipment / res.partner ไม่เปลี่ยน; สร้างเฉพาะ hemms.import.batch (state=previewed) |
| I2 | test_dry_run.py | test_per_row_verdict_counts | ไฟล์ 10 แถวผสม (7 valid, 2 warn, 1 fail) → จำนวนชุด row_count_ok=7, row_count_warn=2, row_count_fail=1; เติมเหตุผลใน data_json |
| I3 | test_commit.py | test_commit_creates_records_with_batch_tag | commit สร้างระเบียนเครื่องมือแพทย์ + vendor; ทุกระเบียนใหม่มี roots_hemms_import_batch_id == batch.id; state ชุด → committed |
| I4 | test_commit.py | test_fk_orphan_row_fails_savepoint_rollback | แถวเครื่องมือแพทย์อ้าง vendor ที่ไม่รู้จัก → แถว FAIL; savepoint D14 rollback ชุดทั้งหมด; ชุดกลับเป็น previewed (ไม่มี state บางส่วน) |
| I5 | test_idempotency.py | test_reimport_same_file_upserts_no_duplicates | นำเข้าไฟล์ → สร้าง 24; นำเข้าไฟล์เดิม → สร้าง 0, updated 24 (จับคู่ตาม business-key); จำนวน maintenance.equipment ไม่เปลี่ยน |
| I6 | test_order.py | test_equipment_before_vendor_fails | ไฟล์เครื่องมือแพทย์อ้าง vendor "Acme" → FAIL เมื่อไม่มี Acme; หลังนำเข้า vendor สร้าง Acme, ไฟล์เครื่องมือแพทย์เดิม → OK |
| I7 | test_delete.py | test_batch_delete_removes_only_tagged | ลบชุด #1 → ระเบียนที่แท็กชุด #1 ถูกลบ; ระเบียนชุด #2 ยังอยู่; state ชุด #1 → deleted, ตั้ง deleted_at |
| I8 | test_delete.py | test_delete_blocked_by_related_pm_or_contract | เครื่องมือแพทย์ในชุดมี maintenance.request ลิงก์ (Q1 PM) → ลบถูกบล็อก, เติม delete_blocked_reason, ไม่มีระเบียนถูกลบ |
| I9 | test_error_xlsx.py | test_error_xlsx_contains_fail_rows | หลังการทดลองรันที่มีแถว FAIL, action_download_error_xlsx() คืน XLSX ที่มีหนึ่งแถวต่อ FAIL ประกอบด้วย row_num, field, severity, message, raw_value |
| I10 | test_wizard_render.py | test_wizard_renders_all_3_steps | ฟอร์มวิซาร์ด render ขั้น 1 (upload), ขั้น 2 (preview พร้อม 50 แถวแรก), ขั้น 3 (result) โดยไม่มี QWeb error; ปุ่มถูกต้องตาม state |
โครงสร้างพื้นฐานการทดสอบ¶
- ทั้งหมด
TransactionCase, ไม่มีHttpCase - Fixtures: ไฟล์ XLSX ขั้นต่ำใน
tests/fixtures/(valid_equipment.xlsx, fk_orphan.xlsx, oversized_5001.xlsx, mixed_verdicts.xlsx) - ใช้
openpyxlโดยตรงเพื่อสร้าง fixture แบบ inline (เลี่ยงไฟล์ binary ใน git เท่าที่เป็นไปได้) - Green gate: รันบน
hemms_demoANDhemms_private— gate DB คู่เดียวกัน กับ Q3 / Q4 / Q5
เป้าหมายความครอบคลุม¶
| ระบบย่อย | ความครอบคลุม |
|---|---|
| parser XLSX (Q6.3) | U1 + U2 |
| ตัว resolve External_id / business-key (Q6.3) | U3 + U4 |
| จับคู่ MoPH เข้มงวด (Q6.3) | U5 |
| เพดานแถว (Q6.3) | U6 |
| เอนจินการทดลองรัน (Q6.3) | I1 + I2 |
| commit ธุรกรรม (Q6.4) | I3 + I4 |
| Idempotency (Q6.3 + Q6.4) | I5 |
| การบังคับลำดับ (Q6.3) | I6 |
| การลบที่แท็กตามชุด (Q6.5) | I7 + I8 |
| ส่งออก XLSX ข้อผิดพลาด (Q6.5) | I9 |
| UI วิซาร์ด (Q6.4) | I10 |
8. คำถามที่ยังเปิด — แก้ไขแล้ว¶
| # | คำถาม | การแก้ไข |
|---|---|---|
| OQ-A | header คอลัมน์ External_id — คอลัมน์ที่จำเป็น หรือทางเลือก? | ทางเลือก การนำเข้าที่มีในตัวของ Odoo สร้าง external_id ใน ir.model.data อัตโนมัติทุกระเบียนใหม่ (รูปแบบ __import__.X) Q6 สืบทอดพฤติกรรมนี้ — ผู้ใช้ ไม่ ต้องให้คอลัมน์ id Idempotency เมื่อนำเข้าซ้ำได้ผ่านการจับคู่ business-key (D5) ถ้าผู้ใช้ ให้ คอลัมน์ id / External ID (เช่น กำลัง migrate จาก Odoo อื่น), มันมีลำดับก่อน รับ header คอลัมน์ Odoo มาตรฐาน id หรือ External ID |
| OQ-B | business-key ของ vendor — vat อย่างเดียว, name อย่างเดียว, หรือ composite? | fallback แบบเป็นชั้น — vat → name+country_id → name ความง่ายในการ onboard มาก่อน: ข้อมูล vendor ของไม่ทุกโรงพยาบาลมี VAT หรือประเทศกรอก การจับคู่ name เดียวเป็นความเสี่ยงการชนกันที่รู้กัน (เช่น "Siemens" สองรายการ) และบันทึกใน §9 gotchas, แต่การบล็อกการนำเข้าเพราะขาด VAT จะทำลายจุดประสงค์ของการนำเข้าจำนวนมาก โรงพยาบาล dedupe หลังนำเข้าได้ผ่านวิซาร์ด merge res.partner มาตรฐาน |
| OQ-C | ตรวจจับชนิด object อัตโนมัติจากชื่อ sheet (เช่น แท็บ "Equipment")? | ไม่ — ตัวเลือกชัดเจน การตรวจจับอัตโนมัติชวนให้กำกวม (ถ้า sheet ตั้งชื่อผิดล่ะ?) ผู้ใช้เลือก Equipment / Vendor / Location ในขั้น 1; หนึ่งไฟล์ = หนึ่ง object |
| OQ-D | TTL session cache ตัวอย่าง — เก็บชุดที่ preview นานเท่าใด? | 1 ชั่วโมง นานพอให้ผู้ใช้แบ่งทะเบียนใหญ่เป็นชิ้น (เช่น 2K + 2K + 1K) และนำเข้าตามลำดับในหนึ่ง session cron กวาดชุด previewed ที่เก่ากว่า 1 ชั่วโมงเพื่อไม่ให้ blob data_json สะสม กำหนดค่าได้ผ่าน roots_hemms_mass_import.preview_ttl_hours |
| OQ-E | หน้าต่างการลบชุด — gate "ล่าสุด" เข้มงวดแค่ไหน? | 7 วัน + ไม่มีเหตุการณ์ PM ของ Q1 + ไม่มีสัญญา Q3 ที่เกี่ยวข้อง หลัง 7 วัน หรือถ้ามีเหตุการณ์, ต้อง admin force-delete พร้อมการยืนยันชัดเจน ป้องกันการ undo ชุดที่ตอนนี้รับน้ำหนักข้อมูลปฏิบัติการ |
| OQ-F | รูปแบบ XLSX ข้อผิดพลาด — sheet แยกต่อข้อผิดพลาด vs sheet เดียว? | sheet เดียว, 5 คอลัมน์ (row_num, field, severity, message, raw_value) หนึ่งแถวต่อข้อผิดพลาด กรอง / เรียงใน Excel หลาย sheet เปลืองความสนใจของผู้ใช้ |
9. ข้อควรระวังในการดำเนินแผน¶
- Q6.1
import_batch_idแบบ namespace —maintenance.equipment,res.partner,hr.departmentเป็นโมเดล Odoo ที่แชร์; โมดูลอื่น อาจกำหนดimport_batch_idด้วย ใช้ namespace เป็นroots_hemms_import_batch_idเพื่อเลี่ยงการเขียนทับเงียบ ๆ - Q6.1 ส่วนขยาย
res.partnerบน multi-company — ตรวจให้ฟิลด์ใหม่ รับรู้company_id(หรือcompany_dependent=False); ข้อมูลสาธิต HEMMS เป็น single-company แต่res.partnerแชร์ข้ามบริษัท - Q6.2 การสร้างเทมเพลตรันในภาษาผู้ใช้ — XLSX ที่ render ฝั่งเซิร์ฟเวอร์ ต้องใช้
res.langของผู้ใช้สำหรับ header คอลัมน์เพื่อให้ผู้ใช้ TH เห็น header ไทย, ผู้ใช้ EN เห็นอังกฤษ ใช้with_context(lang=...)อย่างชัดเจน - Q6.2 รายการ dropdown — dropdown การ validate ข้อมูล XLSX สำหรับ ชื่อ class MoPH ระเบิดที่ 82 entry บน Excel บางเวอร์ชัน; cap การแสดง dropdown ที่ 50 แรก + ให้ sheet อ้างอิงเต็ม (
__moph_classes) ใน workbook เดียวกัน - Q6.3 openpyxl โหมด read-only — สำหรับไฟล์ 5K แถว, ใช้
openpyxl.load_workbook(path, read_only=True, data_only=True)เพื่อ เลี่ยงการโหลดสูตร + style เข้า memory - Q6.3 whitespace + zero-width characters — Excel ไทยมักส่งออก พร้อม non-breaking space (U+00A0) และ zero-width space (U+200B); normalise ด้วย
unicodedata.normalize('NFC', s).strip()ก่อนการ เทียบจับคู่ MoPH มิฉะนั้น "เครื่องดมยาสลบ" ที่มี NBSP แอบแฝงล้มเหลว การจับคู่เข้มงวด - Q6.3 จับคู่ MoPH — case sensitivity — D8 บอก case-sensitive, แต่ อักขระ TH ไม่มี case ชื่อ EN มี คง case-sensitive สำหรับ EN; บันทึกว่า "anesthesia units" ≠ "Anesthesia Units"
- Q6.3 ลำดับการตรวจจับ FK orphan — resolve external_id ก่อน (ถ้าให้คอลัมน์
id), แล้วจึง business-key แบบเป็นชั้น, ก่อนการ validate FK มิฉะนั้นแถวที่ vendor ระบุด้วย external_id (และมีอยู่) จะถูกธงผิด ๆ เพราะ fallback business-key หาไม่เจอ - Q6.3 การชนกันของ vendor
name-only (D6 / OQ-B) — ถ้าสอง ระเบียนres.partnerที่มีอยู่แชร์nameเดียวกัน (เช่น Siemens ทางภาพการแพทย์ และ Siemens อุปกรณ์สำนักงาน), ตัว resolve จับคู่ รายการแรกและสร้างการจัดประเภทผิดเงียบ ๆ WARN ทุก การจับคู่name-only (ไม่ใช่ FAIL), แสดง candidate ID ใน เหตุผลคำตัดสิน, และบันทึกเส้นทาง dedupe หลังนำเข้า (วิซาร์ด mergeres.partnerมาตรฐานของ Odoo) ในเอกสารผู้ใช้ การแลกเปลี่ยน ที่ยอมรับได้สำหรับ v1; กลับมาทบทวนใน Q6.x หากโรงพยาบาลรายงานปัญหา - Q6.3 ความไม่ซ้ำในไฟล์เดียวกัน — ไฟล์ที่มีแถว 5 และ 17 ทั้งคู่มี asset_no=A-0042 ต้อง FAIL แถว 17 (ซ้ำ), ไม่ใช่ last-wins เงียบ ๆ ติดตาม business key ที่เห็นระหว่าง parse pass
- Q6.4 semantics ของ
cr.savepoint()—self.env.cr.savepoint()ของ Odoo เป็น context manager; ตรวจให้ห่อลูปการสร้างระเบียนทั้งหมด, รวม side-effect ของฟิลด์คำนวณ การทดสอบ I4 ตรวจสอบ rollback อย่างชัดเจน - Q6.4 commit + การ invalidate ฟิลด์คำนวณ — ระเบียนเครื่องมือแพทย์ ที่สร้างในชุดทริกเกอร์การ recompute waterfall risk-tier ของ Q5; นี่ คาดไว้และเพิ่มเวลา commit ~30% บนชุด 1000 แถว วางแผน ตามนั้น; พิจารณา
recompute=Falseสำหรับการ insert ชุด + เรียก_recompute_done()อย่างชัดเจนภายหลัง - Q6.5 การบล็อกการลบ — ตรวจผ่าน search count, ไม่ใช่ลูป — สำหรับ ชุด 800 ระเบียน, ใช้
self.env['maintenance.request'].search_count([('equipment_id', 'in', ids)])ครั้งเดียวเพื่อตรวจเหตุการณ์ PM ที่เกี่ยวข้อง ลูปต่อระเบียนบนชุด 800 เป็น กับดักด้าน perf - Q6.5 XLSX ข้อผิดพลาดในภาษาผู้ใช้ — เหมือนการสร้างเทมเพลต; header คอลัมน์ใน
res.langของผู้ใช้ - Q6.6 gate DB คู่ — Q5 เพิ่ม MoPH seed; Q6 ขึ้นกับ Q5 อย่างแข็ง ทั้ง
hemms_demoและhemms_privateต้องติดตั้ง Q5 ก่อน การทดสอบ Q6 รัน - Q6.6 การจัดลำดับเอกสาร —
docs/modules/mass-import/index.mdต้อง เพิ่มใน nav ของmkdocs.yml; มิฉะนั้นจะไม่ render - Q6.6 ข้อมูลสาธิต — 24 เครื่องมือแพทย์ / 5 vendor / 3 แผนก เป็นชุด เดียวที่ commit ไว้ล่วงหน้าทำให้วิซาร์ดค้นพบได้; ผู้ใช้เปิด ระเบียนชุดและเห็นร่องรอยการตรวจสอบ
10. ลำดับ commit (Q6.1 → Q6.6)¶
| # | สถานะ | ข้อความ commit | หัวเรื่อง |
|---|---|---|---|
| Q6.1 | ✅ f8dc4fa | chore(import): skeleton + batch model + sequence [Q6.1] | โมดูลติดตั้งบน DB ทั้งสอง; โมเดล hemms.import.batch, sequence IMP/YYYY/#####, roots_hemms_import_batch_id แบบ namespace บน equipment/partner/department; cron GC รายชั่วโมง stub; config params สำหรับ row_cap + preview_ttl_hours; มุมมอง list/form/search + เมนู; CSV access + record rule ที่ scope ผู้อัปโหลด |
| Q6.2 | ✅ 8a55386 | feat(import): XLSX template generator [Q6.2] | ตัวสร้าง workbook openpyxl ฝั่งเซิร์ฟเวอร์สำหรับ 3 ชนิด object; header TH/EN เลือกจากภาษา active; dropdown MoPH 82-class ผ่าน sheet อ้างอิงซ่อน __moph_classes; วิซาร์ด Download Template เดี่ยว + เมนู |
| Q6.3 | ✅ 0f23354 | feat(import): parser + validator + dry-run engine [Q6.3] | parser openpyxl read-only พร้อม normalise NFC/whitespace; ตัว resolve external_id ทางเลือก (D5); business-key vendor แบบเป็นชั้น vat→name+country→name (D6, WARN บน name-only); เครื่องมือแพทย์ตาม ref; FK + ความไม่ซ้ำ + จับคู่ MoPH เข้มงวด (D8); คำตัดสิน OK/WARN/FAIL ต่อแถวใน data_json; ความไม่ซ้ำข้ามแถวใน orchestrator ของชุด; เปิดใช้งาน cron GC cleanup จริง |
| Q6.4 | ✅ 96dbf4e | feat(import): 3-step wizard + transactional commit [Q6.4] | hemms.mass.import.wizard (Upload → Preview → Result) พร้อมตาราง preview HTML + ป้ายคำตัดสิน; commit ห่อ cr.savepoint() (D14, ทั้งหมดหรือไม่เลย); idempotent upsert; ตัวนับ records_created / records_updated; ปุ่ม Download Template ในขั้นตอน; ลงทะเบียน external_id อัตโนมัติผ่าน ir.model.data แก้กลางทาง: ลบ default supplier_rank=1 (ฟิลด์ต้องการโมดูล purchase ที่ไม่อยู่ใน dep ของ HEMMS) |
| Q6.5 | ✅ fea13a0 | feat(import): batch-tagged delete + error XLSX [Q6.5] | action_delete_batch() พร้อม guard D7 เต็มรูปแบบ (หน้าต่าง 7 วัน + guard เหตุการณ์ PM ของ Q1 + guard สัญญา Q3 ผ่านการตรวจ in self.env ตอน runtime + guard สถานที่→เครื่องมือแพทย์); force-delete เฉพาะ manager ที่ผ่าน guard ใด ๆ; unlink ห่อ savepoint action_download_error_xlsx() ตัวส่งออก sheet เดียว (OQ-F) ปุ่ม header ของมุมมอง form พร้อมไดอะล็อกยืนยัน |
| Q6.6 | ✅ ecb34e5 | test+docs(import): 16 tests + 4-page docs + demo [Q6.6] | 6 unit + 10 integration tests (16/16 เขียวบน hemms_demo + hemms_private); docs/modules/mass-import/ (4 หน้า) + nav mkdocs; ชุด vendor IMP/2026/DEMO1 ที่ commit ไว้ล่วงหน้าพร้อม 3 partner สาธิตที่แท็ก (Acme Medical / Meditek Imaging / BioMed Service) แก้กลางทาง: เขียน I8 ใหม่เพื่อทดสอบ _check_delete_blockers โดยตรง (record-rule AccessError บัง guard); เปลี่ยนการแท็กสาธิตจากแบบ <function> เป็นฟิลด์ M2O โดยตรง; แก้ syntax XML-comment double-hyphen |
11. คำกระตุ้นเพื่อทำงานต่อ¶
เมื่อผู้ใช้พูด "เริ่ม Q6.1" / "start Q6" / "ship Q6" / ทำแผนนี้ต่อ:
- อ่านแผนนี้ซ้ำ +
project_q6_status.mdเพื่อ context เต็ม - ยืนยันว่า dependency ติดตั้งสะอาด:
- Q1 (
roots_hemms_pm) — สำหรับการรวมเหตุการณ์ PM roots_hemms_asset_master— สำหรับ SQL constraintref_uniqบนmaintenance.equipment.ref(business key ของเครื่องมือแพทย์ตาม D6)- Q5 (
roots_hemms_ha_report) — สำหรับ seed MoPHhemms.ha.device.class- ฟิลด์คะแนน EM บนเครื่องมือแพทย์
- ดำเนินการ Q6.1 — โครง + โมเดลชุด + sequence + cron stub (Ship 2026-05-22 — ติดตั้งสะอาดบน
hemms_demo+hemms_private) - หลังแต่ละ commit:
pre-commit run --all-files; รันการทดสอบโมดูล (หลัง Q6.6); smoke-install บนทั้งhemms_demoและhemms_private - ตรวจขั้นสุดท้ายที่ Q6.6: สาธิต end-to-end เต็มรูปแบบ — เปิดวิซาร์ด → ดาวน์โหลด เทมเพลต → กรอก 10 แถว → อัปโหลด → preview แสดง 10 OK → commit → ลบชุด → ยืนยันระเบียนหายไป