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

แผน 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 แผนก)

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

  1. การทดลองรันแบบจำลองเต็มรูปแบบ (Q6.3) — parse → resolve FK → ตรวจความไม่ซ้ำ → จับคู่ MoPH → คำตัดสินต่อแถวพร้อมเหตุผล, ก่อน เขียน DB ใด ๆ
  2. การลบที่แท็กตามชุด (Q6.5) — ทุกระเบียนที่นำเข้ามี import_batch_id; ผู้ใช้ undo ชุดล่าสุดในคลิกเดียวได้ (ภายใน 7 วัน, ถ้าไม่มีเหตุการณ์ PM ของ Q1 หรือสัญญา Q3 อ้างถึง)
  3. การนำเข้าซ้ำแบบ idempotent (Q6.3) — external_id ก่อน, fallback business-key นำเข้าไฟล์เดิมซ้ำ = 0 ระเบียนใหม่, ไม่ใช่ 800 รายการ ซ้ำ
  4. การจับคู่ MoPH 82-class เข้มงวด (Q6.3) — จับคู่ exact กับ seed Q5.2 (ชื่อ TH หรือ EN); การจับคู่แบบ fuzzy ถูกปฏิเสธพร้อมข้อผิดพลาดชัดเจน ปกป้องคุณภาพข้อมูลรายงาน HA
  5. ส่งออก 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 สร้าง entry ir.model.data อัตโนมัติต่อระเบียนใหม่ (พฤติกรรม Odoo มาตรฐาน)
  • ตัว resolve business-key — เส้นทาง idempotency หลัก (เครื่องมือแพทย์: ref (มาตรฐาน Odoo, unique ต่อ constraint ref_uniq ของ roots_hemms_asset_master); vendor: vatname+country_idname; สถานที่: 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
  • .xls legacy (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_demo AND hemms_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 แบบเป็นชั้นvatname+country_idname ความง่ายในการ 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 แบบ namespacemaintenance.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 หลังนำเข้า (วิซาร์ด merge res.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 แบบเป็นชั้น vatname+countryname (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" / ทำแผนนี้ต่อ:

  1. อ่านแผนนี้ซ้ำ + project_q6_status.md เพื่อ context เต็ม
  2. ยืนยันว่า dependency ติดตั้งสะอาด:
  3. Q1 (roots_hemms_pm) — สำหรับการรวมเหตุการณ์ PM
  4. roots_hemms_asset_master — สำหรับ SQL constraint ref_uniq บน maintenance.equipment.ref (business key ของเครื่องมือแพทย์ตาม D6)
  5. Q5 (roots_hemms_ha_report) — สำหรับ seed MoPH hemms.ha.device.class
    • ฟิลด์คะแนน EM บนเครื่องมือแพทย์
  6. ดำเนินการ Q6.1 — โครง + โมเดลชุด + sequence + cron stub (Ship 2026-05-22 — ติดตั้งสะอาดบน hemms_demo + hemms_private)
  7. หลังแต่ละ commit: pre-commit run --all-files; รันการทดสอบโมดูล (หลัง Q6.6); smoke-install บนทั้ง hemms_demo และ hemms_private
  8. ตรวจขั้นสุดท้ายที่ Q6.6: สาธิต end-to-end เต็มรูปแบบ — เปิดวิซาร์ด → ดาวน์โหลด เทมเพลต → กรอก 10 แถว → อัปโหลด → preview แสดง 10 OK → commit → ลบชุด → ยืนยันระเบียนหายไป