Appearance
PRD: Kho vật tư — Quản lý định lượng & giá vật tư dịch vụ
Feature slug: material-warehouseVersion: 3.0 Ngày: 2026-03-27 Profile: L (Large) Tác giả: PO/BA Design doc: docs/superpowers/specs/2026-03-26-material-warehouse-design.md
Executive Summary (TL;DR)
- Kho vật tư kết nối kho chi nhánh — Tạo module Kho vật tư (Material Warehouse) per chi nhánh, kết nối kho chi nhánh qua phiếu chuyển kho (tận dụng
inventory_documenthiện tại). Manager chọn SP từ kho chi nhánh → nhập SL (đơn vị mua: chai) → hệ thống tự quy đổi sang đơn vị kho (ml) + tạo lô FIFO. Hỗ trợ nhập tay/Excel làm fallback cho chi nhánh data chưa chuẩn. Kiến trúc 4 cấp: NCC → Kho tổng → Kho chi nhánh → Kho vật tư. - Chi phí vật tư trên subtask + auto-deduct — KTV ghi nhận vật tư thực tế trên subtask (chọn ĐVT, nhập SL), hệ thống tự động tính chi phí (snapshot giá × SL) và trừ tồn kho khi subtask hoàn thành. Undo/cancel tự động reverse.
- Tồn kho + kiểm kê + cảnh báo — Kiểm kê + điều chỉnh chênh lệch, cảnh báo tồn kho thấp + HSD sắp hết hạn realtime gửi Manager/Admin. Chi phí vật tư tổng hợp vào sidebar tài chính đơn hàng.
Milestones
| Milestone | Nội dung | Target Date | Owner | Status |
|---|---|---|---|---|
| M1 | DB migration: 5 bảng mới (incl. material_batch) + ALTER project_task_material + view + indexes | T+2d | Backend Dev | Pending |
| M2 | Hasura metadata: 5 bảng + permissions per role + relationships | T+3d | Backend Dev | Pending |
| M3 | FE: SCR-01 Trang danh sách kho vật tư + SCR-02 Form cấu hình | T+7d | Frontend Dev | Pending |
| M4 | FE: SCR-03 Chi tiết + lịch sử giá/movement | T+9d | Frontend Dev | Pending |
| M5a | FE: SCR-08 Chuyển kho chi nhánh → vật tư ★ NEW v3.0 | T+11d | Frontend Dev | Pending |
| M5b | FE: SCR-06 Nhập kho manual (fallback) + Excel import + SCR-07 Kiểm kê | T+13d | Frontend Dev | Pending |
| M6a | BE: Action transferToMaterialWarehouse + Event trigger kết nối inventory_document ★ NEW v3.0 | T+10d | Backend Dev | Pending |
| M6b | BE: Event trigger materialAutoDeduct + materialAutoReverse | T+13d | Backend Dev | Pending |
| M7 | FE: Sửa MaterialForm (SCR-04) + Aggregate (SCR-05) thêm cột giá/thành tiền | T+14d | Frontend Dev | Pending |
| M8 | BE: Low stock notification + Sidebar tài chính kết nối | T+12d | Backend Dev | Pending |
| M9 | QA testing (~30-40 test cases) | T+17d | QA | Pending |
| M10 | Deploy staging → production | T+20d | DevOps | Pending |
Trạng thái Sign-off
| Domain | Người review | Status | Ngày |
|---|---|---|---|
| Business (PO) | PO/BA | Approved | 2026-03-26 |
| UX/UI | — | Pending (cần UI Designer review wireframes SCR-01 → SCR-07) | — |
| Technical (Tech Lead) | — | Pending (cần review data model + event trigger) | — |
| QA | — | Pending (cần QA review test plan) | — |
| Ops | — | Pending (cần verify migration strategy staging) | — |
Action Items — Unblock Readiness
| ID | Item | Owner | Deadline | Status |
|---|---|---|---|---|
| AI-01 | UI Designer review wireframes (SCR-01 → SCR-08), confirm layout kho vật tư + form chuyển kho (SCR-08 v3.0) | UI/UX Designer | T+3d | Open |
| AI-02 | Tech Lead review data model (material_price_config versioning, material_stock_movement advisory lock pattern) | Tech Lead | T+3d | Open |
| AI-03 | QA review test plan + confirm seed data cho 30-40 test cases | QA Lead | T+3d | Open |
| AI-04 | DevOps verify migration strategy trên staging (5 bảng mới + ALTER + 1 VIEW) | DevOps | T+2d | Open |
| AI-05 | PO xác nhận danh sách vật tư mẫu cho seed data (ít nhất 10 sản phẩm ngành spa) | PO | T+3d | Open |
| AI-06 | Xác nhận format file Excel import tồn kho (cột, validation rules) | PO + Frontend Dev | T+5d | Open |
Pending Decisions
| ID | Quyết định | Recommendation | Owner | Deadline | Status |
|---|---|---|---|---|---|
| — | Tất cả PDs đã resolved thành DEC-D01 → DEC-D27 (D04/D17 superseded) | — | — | — | All Resolved |
RACI
| Deliverable | PO/BA | Frontend Dev | Backend Dev | QA | UI/UX | DevOps |
|---|---|---|---|---|---|---|
| prd.md | R/A | C | C | I | C | — |
| ui-spec.md | R | C | — | I | A | — |
| dev-spec.md | C | R | A | I | — | C |
| qa-test-plan.md | C | I | I | R/A | — | — |
| go-live-checklist.md | C | I | I | C | — | R/A |
| DB Migration (5 bảng + ALTER + VIEW) | I | — | R/A | I | — | C |
| Hasura Metadata | I | — | R/A | I | — | C |
| Event triggers (auto-deduct/reverse) | I | — | R/A | C | — | — |
| SCR-01 → SCR-03 (Kho vật tư pages) | I | R/A | — | C | C | — |
| SCR-06, SCR-07 (Nhập kho, Kiểm kê) | I | R/A | — | C | C | — |
| SCR-04, SCR-05 (MaterialForm + Aggregate sửa) | I | R/A | — | C | — | — |
| Sidebar tài chính kết nối | I | R/A | C | C | — | — |
| Low stock notification | I | — | R/A | C | — | — |
| Deploy staging | I | C | C | A | — | R |
| Deploy production | A | I | I | C | — | R |
R = Responsible, A = Accountable, C = Consulted, I = Informed
Backlog Phase sau
| ID | Item | Priority | Dependency |
|---|---|---|---|
| — | |||
| PH2-02 | — | ||
| PH2-03 | Override giá per branch (UI form) | Medium | Nhu cầu thực tế phát sinh |
| PH2-04 | Material recipe / BOM (pha trộn vật tư) | Low | Spa có nghiệp vụ pha trộn |
| PH2-05 | Chuyển kho vật tư giữa chi nhánh | Medium | Multi-branch operation |
| PH2-06 | Báo cáo chi phí vật tư (theo KTV, dịch vụ, chi nhánh) | High | Data đủ 1-2 tháng |
| PH2-07 | Export báo cáo Excel | Medium | PH2-06 |
| PH2-08 | Partition material_stock_movement nếu > 10M records | Medium | Monitor 6 tháng |
| PH2-09 | Optimistic locking cho concurrent editing kiểm kê | Low | Nếu phát sinh conflict |
| PH2-10 | Nhắc nhở kiểm kê định kỳ (notification weekly/monthly) | High | Cần notification-api scheduler |
| PH2-11 | Nút "Thay thế vật tư" trên form subtask | High | FR-009 (MaterialForm) |
Z) Decision Log
Decision Log LUÔN ĐẶT ĐẦU TIÊN. Mọi section downstream tham chiếu DEC-ID.
Z1 — Business Decisions
| ID | Quyết định | Lý do | Ngày | Status |
|---|---|---|---|---|
| DEC-D03 | wastage_rate (hao hụt hệ thống) tính vào stock_unit_price luôn | Không tính → báo cáo chi phí luôn thấp hơn thực tế 2-5%. PO ra quyết định sai. | 2026-03-26 | Locked |
| DEC-D09 | Giá chung toàn hệ thống (branch_id = NULL), field branch_id sẵn cho override per branch | Chuỗi spa VN mua hàng tập trung (1 supplier). Override per branch khi cần = thêm record branch_id cụ thể. | 2026-03-26 | Locked |
| DEC-D10 | Ẩn cột giá/thành tiền với Staff (KTV) | KTV không cần biết giá vốn vật tư. Giá là thông tin quản lý (Manager/Admin). | 2026-03-26 | Locked |
| DEC-D11 | Product chưa có config → vẫn thêm vào subtask, hiện "(chưa có giá)" | Không block KTV ghi nhận vật tư khi Admin chưa kịp config giá. | 2026-03-26 | Locked |
| Superseded by DEC-D29 (v3.0). Kết nối kho chi nhánh với dual-input (transfer + manual fallback). | 2026-03-26 | Superseded | ||
| DEC-D13 | Nhập tay/Excel là fallback, không phải luồng chính (v3.0) | Luồng chính: chuyển kho từ kho chi nhánh. Fallback cho chi nhánh data chưa chuẩn. | 2026-03-27 | Locked |
| DEC-D29 | Kết nối kho chi nhánh → kho vật tư. Kiến trúc 4 cấp: NCC→Kho tổng→Kho chi nhánh→Kho vật tư. Tận dụng inventory_document hiện tại. | Kho chi nhánh đã có sẵn danh mục SP + giá nhập + tồn kho. Config 100-300 SP bằng tay không khả thi. Giảm 80%+ nhập liệu. | 2026-03-27 | Locked |
| DEC-D30 | Dual-input: (1) Chuyển kho từ kho chi nhánh (primary), (2) Nhập tay/Excel (fallback). | Data kho chính chưa chuẩn ở một số chi nhánh. Cả 2 nguồn → không block go-live. | 2026-03-27 | Locked |
| DEC-D31 | Không cần duyệt chuyển kho chi nhánh → vật tư (cùng chi nhánh). Manager tự thực hiện. | Cùng 1 chi nhánh, hàng di chuyển nội bộ. Thêm duyệt = chậm + không cần thiết. | 2026-03-27 | Locked |
| DEC-D35 | Data cleanup kho chính là prerequisite song song. Chi nhánh data chưa chuẩn → dùng nhập tay. | PO xác nhận: data kho chính chưa tốt ở một số chi nhánh. Cần plan dọn data riêng, không block go-live. | 2026-03-27 | Locked |
| DEC-D36 | Chọn lô nguồn thủ công khi chuyển kho. Manager chọn cụ thể lô nào từ kho chi nhánh (lot_number, expiry, price, qty_remain). Mỗi lô chuyển = 1 material_batch. Reuse ProductLotNumberSelect. | Kho chi nhánh có nhiều lô khác giá/HSD. Không chọn rõ → giá vốn + HSD lệch audit. Hệ thống đã có sẵn UI chọn lô. | 2026-03-27 | Locked |
| DEC-D37 | Transfer transaction strategy: transferToMaterialWarehouse action chạy trong single PostgreSQL transaction vì ecommerce schema (inventory_document, product_supplying) và material schema (material_batch, material_stock_movement) cùng 1 PostgreSQL instance. Go handler dùng 1 DB connection + BEGIN/COMMIT. Nếu tương lai tách DB → chuyển sang Outbox pattern + compensation. | Cả 2 bên ghi nhận cùng 1 transaction → không xảy ra trường hợp trừ kho chi nhánh mà không tạo batch, hoặc ngược lại. | 2026-03-27 | Locked |
| DEC-D17 | 2026-03-26 | Superseded | ||
| DEC-D19 | Subtask canceled sau done → exclude khỏi material_cost trong sidebar tài chính | Tồn kho đã reverse nhưng chi phí vẫn hiển thị = data misleading cho Manager. | 2026-03-27 | Locked |
| DEC-D21 | Manager được xem chi phí vật tư trong sidebar tài chính đơn hàng (branch mình) | Pain point chính là Manager không biết chi phí → phải giải quyết. | 2026-03-27 | Locked |
| DEC-D23 | Không cho phép tồn kho âm. remaining_qty >= 0 (CHECK constraint) | Team Kho yêu cầu. Muốn xuất phải nhập trước. | 2026-03-27 | Locked |
| DEC-D24 | Hạn sử dụng (HSD) per lô: cảnh báo 3 tháng trước, auto-lock khi hết hạn | Team Kho đang phải lọc thủ công hàng tháng. | 2026-03-27 | Locked |
| DEC-D25 | Nhập cùng giá cùng ngày vẫn tách lô | Có thể khác mã lô NCC, khác HSD. | 2026-03-27 | Locked |
| DEC-D26 | Lô lỗi/thu hồi → xuất hủy (disposal) + ghi chú | Giữ audit trail, không xóa lô. | 2026-03-27 | Locked |
Z2 — UX Decisions
| ID | Quyết định | Lý do | Ngày | Status |
|---|---|---|---|---|
| DEC-D08 | UI đặt tại Kho > Kho vật tư (trang riêng trong sidebar) | Module mới, không phải tab phụ trên product. Admin cần nhìn tổng danh sách vật tư. Phase B mở rộng tự nhiên. | 2026-03-26 | Locked |
| DEC-D20 | Wastage KTV (wastage_quantity) mặc định ẩn trên form subtask. Admin bật qua Settings nếu cần. | KTV không đo được hao hụt chính xác, sẽ ghi 0 → data rác. | 2026-03-27 | Locked |
Z3 — Technical Decisions
| ID | Quyết định | Lý do | Ngày | Status |
|---|---|---|---|---|
| DEC-D01 | NUMERIC(15,4) cho đơn giá nội bộ, BIGINT cho amount cuối | Tránh mất precision khi chia (VD: 890,000/30 = 29,666.666đ). Round 1 lần duy nhất ở amount. | 2026-03-26 | Locked |
| DEC-D02 | Price versioning qua effective_from/to, tạo version mới không sửa cũ | Audit trail + báo cáo so sánh giá theo thời gian. Admin cần biết "chi phí tăng vì giá lên hay dùng nhiều hơn?" | 2026-03-26 | Locked |
| DEC-D04 | costing_method sẵn cho FIFO/WA | product_supplying integration — chưa làm. | 2026-03-26 | Superseded |
| DEC-D05 | Two-Phase Pricing (Ref: DEC-D28): Phase 1 (save) = giá ước tính từ lô FIFO cũ nhất (display only). Phase 2 (done) = giá thực tế từ FIFO deduct (final, locked). | Batch-based FIFO nên giá chỉ chính xác khi biết lô nào thực sự xuất. | 2026-03-27 | Locked |
| DEC-D06 | stock_unit lock khi đã có material_usage_unit reference | Đổi stock_unit → tất cả usage_unit + task_material hỏng. Muốn đổi = tạo config mới. | 2026-03-26 | Locked |
| DEC-D07 | to_stock_factor > 0, is_discrete validate số nguyên | Ngăn config sai (factor=0 → giá=0, factor<0 → giá âm). Mask = miếng → không dùng 0.5 miếng. | 2026-03-26 | Locked |
| DEC-D14 | Tự động trừ kho khi subtask status → done (event trigger) | Giảm thao tác KTV, data realtime. Giống pattern invoice_complete đã có. | 2026-03-26 | Locked |
| DEC-D15 | v2.0+: Batch-level tracking. material_batch.remaining_qty = tồn per lô. material_stock_movement.batch_remaining_after = tồn lô SAU giao dịch. Tồn kho tổng = SUM(remaining_qty) qua material_stock_balance VIEW. running_balance global — superseded) | Batch FIFO cần track per lô. remaining_qty trên batch là nguồn sự thật. Movement ghi batch_remaining_after cho audit trail. | 2026-03-27 | Locked |
| DEC-D16 | source_type enum có 'transfer' — dùng ngay cho chuyển kho chi nhánh → kho vật tư (v3.0, DEC-D29) | Schema đã sẵn sàng. Không cần migration thêm. | 2026-03-27 | Locked |
| DEC-D32 | Nhập theo đơn vị mua khi chuyển kho. Manager gõ "2 chai" → hệ thống quy đổi 2 × 500ml = 1,000ml → tạo lô. | Thực tế kho: người ta bê nguyên chai/hộp, không đong ml. Kiểm kê khớp: kệ kho thiếu đúng 2 chai. | 2026-03-27 | Locked |
| DEC-D33 | Behavior mới cho inventory_document: export_material_warehouse + import_material_warehouse. Extend hệ thống hiện tại. | inventory_document đã có đầy đủ status flow, product_supplying, notification. Reuse ~60%. | 2026-03-27 | Locked |
| DEC-D34 | Config quy đổi 1 lần per product: "1 chai = 500ml". Lưu trong material_price_config.source_quantity. | Admin config 1 lần, sau đó Manager chỉ nhập số chai. Giá nhập lấy từ kho chi nhánh, không config tay. | 2026-03-27 | Locked |
| DEC-D18 | Undo subtask done → reverse stock movement | Data consistency: trừ kho sai phải cộng lại. Tạo movement mới +quantity, không xóa movement cũ. | 2026-03-26 | Locked |
| DEC-D22 | Batch-based FIFO: mỗi lần nhập = 1 lô, xuất lô cũ nhất trước, giá theo lô | Team Kho xác nhận: kho chính đã quản lý theo lô. FIFO vì dùng lô cũ trước để tránh hết hạn. | 2026-03-27 | Locked |
| DEC-D27 | FIFO split: 1 lần xuất vượt 2+ lô → tách nhiều movements, mỗi movement gắn 1 lô | Chi phí chính xác theo giá từng lô. | 2026-03-27 | Locked |
| DEC-D28 | Two-Phase Pricing: Phase 1 (save) = giá ước tính từ FIFO batch hiện tại; Phase 2 (done) = giá thực tế từ FIFO deduct (bình quân gia quyền nếu split). Sau done = locked. | Batch-based FIFO nên giá chỉ chính xác khi biết lô nào thực sự xuất. Giữa save và done, lô có thể thay đổi (lô cũ hết, lô mới nhập). | 2026-03-27 | Locked |
Z4 — QA Decisions
| ID | Quyết định | Lý do | Ngày | Status |
|---|---|---|---|---|
| DEC-Q01 | Test concurrent auto-deduct: 2 KTV trừ kho cùng product cùng lúc | Verify pg_advisory_xact_lock serialize đúng. running_balance phải nhất quán. | 2026-03-26 | Locked |
| DEC-Q02 | Verify flow: KTV ghi → trừ kho → không đủ → block + thông báo "Không đủ tồn kho". | 2026-03-27 | Locked | |
| DEC-Q03 | Test price versioning: đơn cũ giữ snapshot, đơn mới lấy giá mới | Verify FORMULA-003: amount snapshot không đổi khi admin cập nhật giá. | 2026-03-26 | Locked |
A1) Blueprint
| Field | Value |
|---|---|
| Feature Name | Kho vật tư — Quản lý định lượng & giá vật tư dịch vụ |
| Feature Slug | material-warehouse |
| Type | New Module |
| Profile | L (Large) |
| Platform | Web (diva-admin) |
| Module chính | Kho > Kho vật tư (NEW), Projects > TaskForm/TaskDetail (MODIFY) |
| DB Domain | project (5 bảng mới incl. material_batch, 1 ALTER, 1 VIEW) |
| Related Services | Hasura Controller, ecommerce-api (product reference), notification-api |
| Branch Scope | material_warehouse.branch_id → 1 chi nhánh = 1 kho vật tư |
| Dependency | subtask-material (feature đã có: project_task_material table) |
| Effort Estimate | ~33 man-days (BE 10.5d + FE 16.5d + QA 5d + Hasura 1.5d). v3.0: +4d cho transfer flow (SCR-08, BE action/event, QA transfer TCs) |
A2) Context — Tổng quan dễ hiểu
1. Vấn đề hiện tại + hệ quả
Hệ thống spa Diva hiện có feature "Vật tư trên subtask" (subtask-material) cho phép KTV ghi nhận vật tư dự kiến theo từng bước công việc. Tuy nhiên:
- Không có giá vốn vật tư — KTV ghi "3 giọt Serum" nhưng hệ thống không biết 1 giọt = bao nhiêu tiền. Manager không tính được chi phí vật tư per đơn hàng.
- Không có kho vật tư riêng — Kho chính (kho chi nhánh) quản lý theo đơn vị mua (chai, hộp), không phải đơn vị sử dụng (ml, giọt, miếng). Không kiểm soát được tồn kho vật tư thực tế.
- Không tự động trừ kho — Subtask hoàn thành nhưng vật tư không bị trừ khỏi kho. Admin phải kiểm kê thủ công.
- Chi phí vật tư thiếu trong tổng hợp tài chính — Sidebar tài chính đơn hàng có tiền tour, commission, nhưng thiếu chi phí vật tư → lợi nhuận hiển thị sai.
Hệ quả: Manager không biết chi phí thực per đơn hàng (sai lệch 5-15%). Admin không biết vật tư nào sắp hết. PO không có data để đàm phán giá supplier.
2. Giải pháp + metric
Tạo module Kho vật tư (Material Warehouse) kết nối kho chi nhánh, quản lý:
- Kết nối kho chính (v3.0) — Chuyển kho từ kho chi nhánh → kho vật tư, tận dụng
inventory_documenthiện tại. Nhập tay/Excel làm fallback. - Danh mục vật tư + giá versioned — Cấu hình quy đổi đơn vị (chai → ml → giọt), tính hao hụt hệ thống.
- Tồn kho + nhập/xuất/kiểm kê — Auto-deduct khi subtask done, kiểm kê điều chỉnh.
- Chi phí vật tư trên subtask — KTV chọn ĐVT, nhập SL, hệ thống tính
đơn giá × SL = thành tiền(snapshot giá). - Kết nối tổng hợp tài chính — Chi phí vật tư hiển thị trong sidebar tài chính đơn hàng.
Từ không có giá vốn, không có kho vật tư, không tự trừ kho → kho vật tư kết nối kho chi nhánh, giá từ FIFO batch, auto-deduct, chi phí vật tư trong tổng hợp tài chính.
Metric mục tiêu: 100% subtask có giá vật tư (sau config), tồn kho chênh lệch < 2% sau 1 tháng, chi phí vật tư hiển thị đúng trong sidebar tài chính.
3. Giao diện mô phỏng
SCR-01: Trang danh sách Kho vật tư:
| # | Vật tư | Mã | ĐV kho | Giá/ĐV | Tồn kho | TT |
|---|---|---|---|---|---|---|
| 1 | Serum Laser X | SP001 | ml | 4,082đ | 5,000ml | Xanh |
| 2 | Gel làm mát | SP042 | g | 2,000đ | 800g | Xanh |
| 3 | Mask collagen | SP018 | miếng | 30,000đ | 49 | Đỏ |
| 4 | Bông tẩy trang | SP023 | miếng | 500đ | 200 | Xanh |
SCR-04: Form subtask — Bảng vật tư (nâng cấp có giá):
| # | Vật tư | Mã | ĐVT | SL | Đơn giá | Thành tiền |
|---|---|---|---|---|---|---|
| 1 | Serum Laser X | SP001 | giọt | 3 | 204đ | 612đ |
| 2 | Gel làm mát | SP042 | tube | 1 | 15,000đ | 15,000đ |
| 3 | Mask dưỡng da | SP018 | miếng | 1 | 30,000đ | 30,000đ |
| Tổng: | 45,612đ |
Cột Đơn giá + Thành tiền: ẩn với Staff (KTV) (Ref: DEC-D10)
4. Cách hoạt động — Quy trình 6 bước (v3.0)
NCC → Kho tổng (HQ) → Kho chi nhánh → Kho vật tư → KTV sử dụng → Auto-deduct
↑
① Chuyển kho (primary, DEC-D29)
② Nhập tay (fallback, DEC-D30)Admin: Config quy đổi ĐVT (1 lần) → Manager: Chuyển kho từ kho chi nhánh (nhập SL chai)
↓
Hệ thống: Quy đổi chai → ml, tạo lô FIFO, trừ kho chi nhánh
↓
KTV: Ghi vật tư trên subtask → Chọn ĐVT → Nhập SL → Hệ thống tính giá (snapshot từ lô FIFO)
↓
Hệ thống: Subtask done → Auto-deduct tồn kho FIFO → Cảnh báo nếu thấp → Sidebar tài chính| Bước | Ai | Hành động | Dữ liệu | Tần suất |
|---|---|---|---|---|
| 1 | Hệ thống | Auto-create kho vật tư khi chi nhánh chưa có | material_warehouse | 1 lần/branch |
| 2 | Admin | Config quy đổi đơn vị per product: "1 chai = 500ml", ĐVT sử dụng, hao hụt | material_price_config + material_usage_unit | 1 lần/product, sửa khi cần |
| 3a | Manager | Chuyển kho (primary): Chọn SP từ kho chi nhánh → nhập SL (chai) → hệ thống trừ kho chi nhánh + tạo lô vật tư (ml) | inventory_document → material_batch + material_stock_movement | Khi cần bổ sung |
| 3b | Admin/Manager | Nhập tay (fallback): Nhập trực tiếp khi kho chính chưa có data chuẩn | material_batch + material_stock_movement (source_type='stock_in') | Fallback |
| 4 | KTV | Ghi nhận vật tư trên subtask (chọn ĐVT, nhập SL) | project_task_material (+unit_price, amount, stock_equivalent) | Mỗi subtask |
| 5 | Hệ thống | Auto-deduct FIFO khi subtask done, cảnh báo nếu tồn thấp + HSD | material_stock_movement (source_type='auto_deduct') | Tự động |
Quy tắc cốt lõi:
| # | Quy tắc | Ví dụ | Ref DEC |
|---|---|---|---|
| 1 | 1 chi nhánh = 1 kho vật tư | "Kho vật tư Quận 1", "Kho vật tư Quận 3" | — |
| 2 | 1 product chỉ có 1 config active (global hoặc per branch) | Serum X chỉ 1 giá active. Muốn đổi giá → version mới. | DEC-D02 |
| 3 | Snapshot giá khi thêm vật tư vào subtask | Tháng 1 giá 4,000đ/ml → subtask tháng 1 giữ 4,000đ dù tháng 3 đổi 5,000đ | DEC-D05 |
| 4 | Hao hụt hệ thống tính vào đơn giá | Serum 2% hao hụt → giá/ml = 4,082đ thay vì 4,000đ | DEC-D03 |
| 5 | stock_unit lock khi đã có usage unit reference | Serum đã config giọt/muỗng/ml → không đổi ml sang g | DEC-D06 |
| 6 | Không cho phép tồn kho âm, block deduct nếu không đủ | Muốn xuất phải nhập trước. remaining_qty >= 0 (CHECK constraint) | DEC-D23 |
| 7 | Lấy giá: branch config → fallback global config | Chi nhánh có override → dùng. Không có → dùng chung. | DEC-D09 |
| 8 | Auto-deduct khi subtask done, reverse khi undo | done → trừ kho, undo done → cộng lại | DEC-D14, DEC-D18 |
| 9 | material_stock_movement là immutable log | Không UPDATE, không DELETE. Sai → tạo movement mới điều chỉnh. | — |
5. Ví dụ thực tế
Case 1 — Happy path: Admin setup + KTV dùng + auto-deduct
Bối cảnh: Chi nhánh Quận 1, spa Diva. Admin Nguyễn Thị Mai setup kho vật tư.
| Bước | Ai | Hành động | Chi tiết |
|---|---|---|---|
| Setup | Admin Mai | Tạo kho "Kho vật tư Quận 1" | material_warehouse.branch_id = Q1 |
| Setup | Admin Mai | Config Serum Laser X: giá nhập 2,000,000đ/chai 500ml, hao hụt 2% | stock_unit_price = 2,000,000 / (500 × 0.98) = 4,081.63đ/ml |
| Setup | Admin Mai | Thêm ĐVT: giọt (0.05ml, 204.08đ), muỗng (5ml, 20,408.16đ) | material_usage_unit 3 records |
| Nhập kho | Admin Mai | Nhập tồn kho ban đầu: 5,000ml | movement: +5,000ml, balance = 5,000ml |
| Dịch vụ | KTV Trần Thúy Linh | Subtask "Laser sắc tố" — thêm 3 giọt Serum | amount = ROUND(204.08 × 3) = 612đ |
| Dịch vụ | KTV Linh | Subtask done | auto-deduct: -0.15ml (3 × 0.05), balance = 4,999.85ml |
| Tài chính | Hệ thống | Sidebar đơn hàng hiển thị | Chi phí vật tư: 612đ |
Case 2 — Edge case: Không đủ tồn kho + cảnh báo
| Bước | Hành động | Kết quả |
|---|---|---|
| 1 | Mask collagen tồn kho = 49 miếng (lô L001, remaining = 49), min_stock = 50 | Hiển thị đỏ "Dưới ngưỡng" |
| 2 | KTV ghi subtask dùng 3 miếng Mask | stock_equivalent = 3 miếng |
| 3 | Subtask done → auto-deduct từ lô L001 (FIFO) | L001.remaining_qty = 49 - 3 = 46 miếng |
| 4 | Notification gửi Manager Q1 + Admin | "[Quận 1] Mask collagen còn 46 miếng, dưới ngưỡng 50" |
| 5 | KTV ghi subtask dùng 50 miếng → subtask done | Block: "Không đủ tồn kho Mask collagen (cần 50, còn 46)". Subtask không chuyển done. (Ref: DEC-D23) |
Case 3 — Edge case: Admin đổi giá, subtask cũ giữ snapshot
| Thời điểm | Hành động | Giá/ml |
|---|---|---|
| 01/01/2026 | Admin config Serum: 2,000,000đ/500ml/2% | 4,081.63đ |
| 15/01/2026 | KTV tạo subtask A: 3 giọt × 204.08đ = 612đ | Snapshot: 204.08đ/giọt |
| 01/03/2026 | Admin cập nhật giá: 2,500,000đ/500ml/2% | 5,102.04đ (version mới) |
| 02/03/2026 | KTV tạo subtask B: 3 giọt × 255.10đ = 765đ | Snapshot: 255.10đ/giọt |
| Kết quả | Subtask A vẫn 612đ, subtask B là 765đ | Giá đúng per thời điểm |
Case 4 — Edge case: Kiểm kê phát hiện chênh lệch
| Khoản mục | Giá trị |
|---|---|
| Hệ thống: Serum Laser X | 4,999.75ml |
| Thực tế (đếm): | 4,850ml |
| Chênh lệch: | -149.75ml |
| Admin nhập lý do: | "Hao hụt bay hơi + sai đo" |
| Movement tạo: | source_type='adjustment', quantity_change=-149.75, running_balance=4,850 |
Case 5 — Edge case: Product chưa config giá
| Bước | Hành động | Kết quả |
|---|---|---|
| 1 | KTV thêm "Kem chống nắng" vào subtask | Sản phẩm chưa có material_price_config |
| 2 | Hiển thị | Tên SP, ĐVT mặc định, SL — cột giá hiện "(chưa có giá)" |
| 3 | Không block KTV | Vẫn lưu được. unit_price = NULL, amount = NULL |
| 4 | Subtask done | Không trừ kho (vì total_stock_equivalent IS NULL) |
| 5 | Admin config giá sau | Subtask cũ giữ NULL, subtask mới lấy giá mới |
6. Ai thấy gì + lý do
| Role | Thấy được | Không thấy | Lý do | Ref DEC |
|---|---|---|---|---|
| Staff (KTV) | Vật tư trên subtask mình (tên, mã, ĐVT, SL) | Cột giá/thành tiền, Menu Kho vật tư | KTV không cần biết giá vốn. Kho là nghiệp vụ quản lý. | DEC-D10 |
| Manager | Kho vật tư branch mình, cấu hình readonly, nhập kho, kiểm kê, lịch sử. Cột giá trên subtask. | Thêm/sửa config giá (Admin only) | Manager kiểm soát branch, nhưng giá do Admin tập trung. | DEC-D09 |
| Admin | Tất cả: config giá, mọi branch, sidebar tài chính | — | Full access | — |
7. Công thức tóm tắt
| Chỉ số | Công thức | Ví dụ |
|---|---|---|
| Giá/ĐV kho | source_price / (source_quantity × (1 - wastage_rate)) | 2,000,000 / (500 × 0.98) = 4,081.63đ/ml |
| Giá/ĐV sử dụng | stock_unit_price × to_stock_factor | 4,081.63 × 0.05 = 204.08đ/giọt |
| Chi phí per subtask | ROUND(unit_price × quantity) | 204.08 × 3 = 612đ |
| Quy đổi trừ kho | (quantity + wastage) × to_stock_factor | (3 + 1) × 0.05 = 0.20ml |
| Tổng chi phí đơn hàng | SUM(ptm.amount) WHERE subtask → order | 612 + 15,000 + 30,000 = 45,612đ |
Chi tiết: xem section Formulas (C3) trong dev-spec.md
8. Tình huống đặc biệt
| Tình huống | Hệ thống xử lý |
|---|---|
| Product chưa config giá | Thêm được, hiện "(chưa có giá)". unit_price = NULL, không trừ kho. (Ref: DEC-D11) |
| Product bị disable sau khi đã thêm | Badge "Ngưng KD", tồn kho giữ nguyên, không cho thêm mới. |
| Subtask done → undo done | Reverse: tạo movement +quantity. running_balance cộng lại. (Ref: DEC-D18) |
| Subtask canceled (chưa done) | Không trừ kho (chưa done = chưa auto-deduct). |
| Subtask canceled (đã done) | Reverse movement (giống undo done). |
| 2 KTV trừ kho cùng lúc cùng product | pg_advisory_xact_lock serialize. running_balance nhất quán. |
| Admin đổi giá supplier | Version mới, subtask cũ giữ snapshot giá cũ. (Ref: DEC-D02, DEC-D05) |
| Không đủ tồn kho | Block deduct, hiển thị lỗi "Không đủ tồn kho [tên vật tư]". Muốn xuất phải nhập trước. (Ref: DEC-D23) |
Đơn vị rời (Mask = miếng, is_discrete = true) | SL phải nguyên. FE block nhập 0.5 miếng. (Ref: DEC-D07) |
| Kiểm kê: thực tế khác hệ thống | Tạo movement source_type='adjustment' với chênh lệch. |
| Nhập Excel sai format | Hiển thị lỗi per dòng. Không import dòng lỗi, import dòng đúng. |
| Hao hụt kỹ thuật (KTV đổ bỏ) | Ghi wastage_quantity per subtask, tính vào total_stock_equivalent khi trừ kho. |
9. Lộ trình + tại sao
| Phase | Nội dung | Tại sao chia | Trạng thái |
|---|---|---|---|
| Phase 0 (đã xong) | subtask-material: ghi nhận vật tư trên subtask (không có giá, không có kho) | Giải quyết vấn đề cấp bách nhất: ghi nhận vật tư đúng subtask | Done |
| Phase 1 (hiện tại) | Kho vật tư: giá + quy đổi + tồn kho + auto-deduct + sidebar tài chính | Admin cần biết chi phí. Manager cần tổng hợp tài chính. KTV cần biết vật tư còn bao nhiêu. | In scope |
| Phase 2 (sau) | FIFO đã implement v2.0. Kết nối kho chính đã implement v3.0. Báo cáo cần data 1-2 tháng. | Backlog |
v3.0 — Tại sao kết nối kho chính ngay? Kho chi nhánh đã có sẵn danh mục 100-300 SP + giá nhập + tồn kho. Config thủ công từng SP không khả thi. Giải pháp: dual-input — chi nhánh data tốt → chuyển kho (primary), chưa tốt → nhập tay (fallback). Song song dọn data kho chính, dần chuyển tất cả sang transfer. (Ref: DEC-D29, DEC-D30, DEC-D35)
10. Giá trị mang lại
| Đối tượng | Trước | Sau | Số liệu cụ thể |
|---|---|---|---|
| Manager | Không biết chi phí vật tư per đơn hàng | Sidebar tài chính hiển thị chi phí vật tư chính xác | Từ 0% → 100% đơn hàng có chi phí vật tư |
| Admin | Không biết vật tư nào sắp hết, kiểm kê thủ công | Cảnh báo realtime + kiểm kê so sánh hệ thống vs thực tế | Giảm 80% thời gian kiểm kê (ước tính) |
| KTV | Chỉ ghi SL vật tư, không biết giá | Ghi SL, hệ thống tính giá tự động (KTV không thấy giá) | 0 thao tác thêm cho KTV |
| PO | Không có data chi phí vật tư để phân tích | Data chi phí per dịch vụ/KTV/chi nhánh cho quyết định mua hàng | Tiết kiệm 2-5% chi phí mua sắm (ước tính) |
| Tài chính | total_cost thiếu chi phí vật tư → sai lệch 5-15% | total_cost = tour_money + material_cost + commission + fixed_cost | Lợi nhuận chính xác hơn 5-15% |
A3) Goals & Metrics
| # | Goal | Metric | Target | Cách đo |
|---|---|---|---|---|
| G1 | 100% vật tư có giá (sau config) | % project_task_material có unit_price IS NOT NULL | > 90% (sau 2 tuần) | SQL query |
| G2 | Tồn kho chính xác | Chênh lệch hệ thống vs kiểm kê thực tế | < 2% (sau 1 tháng) | ABS(system_balance - actual) / actual × 100 |
| G3 | Chi phí vật tư hiển thị đúng trong sidebar tài chính | % đơn hàng có material_cost > 0 (khi có vật tư) | 100% | SQL query |
| G4 | Cảnh báo tồn kho kịp thời | Thời gian từ tồn kho ≤ min_stock → notification | < 5 phút | Notification log timestamp |
| G5 | Admin setup kho nhanh | Thời gian setup kho vật tư cho 1 chi nhánh (50 products) | < 2 giờ (Excel import) | Tracking |
A4) Personas
| Persona | Vai trò | Tần suất sử dụng | Nhu cầu chính | Screens chính |
|---|---|---|---|---|
| Admin Nguyễn Thị Mai | Admin hệ thống, quản lý giá vốn toàn chuỗi | Hàng ngày (config), hàng tuần (kiểm kê) | Cấu hình giá chính xác, xem lịch sử giá, export báo cáo | SCR-01, SCR-02, SCR-03, SCR-06, SCR-07 |
| Manager Lê Văn Hùng | Quản lý chi nhánh Q1, kiểm soát tồn kho | Hàng ngày (xem tồn), hàng tuần (nhập kho, kiểm kê) | Biết vật tư còn bao nhiêu, chi phí vật tư per đơn hàng | SCR-01, SCR-03, SCR-06, SCR-07, SCR-05 |
| KTV Trần Thúy Linh | Kỹ thuật viên thực hiện dịch vụ, ghi nhận vật tư | 10-30 subtask/ngày | Form nhanh, chọn ĐVT, nhập SL. Không cần biết giá. | SCR-04 (MaterialForm) |
| PO/Chủ spa | Xem tổng hợp tài chính, ra quyết định mua hàng | Hàng tuần | Chi phí vật tư per đơn hàng, so sánh chi phí theo thời gian | Sidebar tài chính đơn hàng |
A5) Functional Requirements
Nhóm 1: Kho vật tư per branch (FR-001 → FR-003)
FR-001: Tạo kho vật tư cho chi nhánh
Mô tả: Admin tạo kho vật tư cho mỗi chi nhánh. Mỗi chi nhánh chỉ có tối đa 1 kho vật tư.
AC:
- [ ] Admin vào Kho > Kho vật tư → hệ thống tự tạo kho cho branch hiện tại nếu chưa có
- [ ] Tên kho mặc định: "Kho vật tư [tên chi nhánh]", Admin có thể sửa
- [ ] Constraint:
UNIQUE(branch_id)— 1 chi nhánh = 1 kho vật tư - [ ] Kho có trạng thái
is_active(mặc định true). Khi inactive → ẩn khỏi danh sách, không cho nhập/xuất - [ ] Soft delete qua
deleted_at
Validation:
branch_idbắt buộc, NOT NULLnamebắt buộc, max 200 ký tự
Edge cases:
- Admin truy cập branch chưa có kho → hệ thống tự tạo kho mặc định (tên = 'Kho vật tư [tên chi nhánh]'). Không cần bấm nút.
- Admin cố tạo kho thứ 2 cho cùng branch → DB reject (unique constraint), hiển thị lỗi "Chi nhánh đã có kho vật tư"
- Branch bị inactive → kho vật tư vẫn tồn tại nhưng ẩn khỏi navigation
- Soft delete kho → tất cả data liên quan (movements, configs) giữ nguyên nhưng kho ẩn
- Restore kho (undelete) → data hiện lại. Admin cần confirm "Khôi phục kho vật tư?"
FR-002: Xem danh sách vật tư trong kho (Ref: DEC-D08, SCR-01)
Mô tả: Admin/Manager xem danh sách tất cả vật tư đã cấu hình trong kho chi nhánh, kèm giá và tồn kho.
AC:
- [ ] Hiển thị bảng: #, Vật tư (tên), Mã (SKU), ĐV kho, Giá/ĐV, Tồn kho, Trạng thái
- [ ] Tồn kho lấy từ view
material_stock_balance(O(1) query, Ref: DEC-D15) - [ ] Trạng thái tồn kho theo 4 mức: Xanh (> min×2), Vàng (> min), Đỏ (≤ min), Đỏ bold (= 0, Hết hàng)
- [ ] Tìm kiếm theo tên/SKU — debounce 300ms, min 2 ký tự
- [ ] Phân trang: 20 items/page mặc định, hỗ trợ 50/100
- [ ] Sort: theo tên (A-Z), giá, tồn kho, trạng thái
- [ ] Filter: trạng thái tồn kho (Tất cả / Dưới ngưỡng / Hết hàng)
- [ ] Buttons: [+ Thêm vật tư], [Nhập kho], [Kiểm kê], [Xuất Excel]
- [ ] "Xuất Excel" xuất danh sách hiện tại (max 5,000 dòng, đồng bộ)
- [ ] Expandable row (QTable expand): click ▶ mở rộng dòng, hiện danh sách ĐVT sử dụng + giá per ĐVT (tính từ lô FIFO cũ nhất active)
- [ ] Expandable row hiện danh sách lô active: Tên lô, tồn lô, giá/stock_unit, HSD. Lô FIFO cũ nhất đánh dấu "► đang xuất"
- [ ] Giá ĐVT trên expandable =
batch.unit_price × to_stock_factortừ lô FIFO cũ nhất (batch active, remaining_qty > 0, ORDER BY created_at ASC)
Edge cases:
- Kho chưa có vật tư → empty state: "Chưa có vật tư. Bấm [+ Thêm vật tư] để bắt đầu."
- Product chưa có tồn kho (chưa nhập) → cột tồn kho hiện "—" (dash)
- Product bị disable → badge "Ngưng KD" cạnh tên, vẫn hiện trong danh sách
500 vật tư → phân trang hoạt động bình thường, test performance < 2s load
- Admin chọn chi nhánh khác (dropdown) → data reload, 0 kết quả nếu branch chưa có kho
FR-003: Quản lý vật tư — thêm/ẩn (Ref: DEC-D08)
Mô tả: Admin thêm product vào kho vật tư (tạo material_price_config) hoặc ẩn vật tư (soft delete config).
AC:
- [ ] Button [+ Thêm vật tư] → mở dialog search product (từ bảng
productecommerce DB) - [ ] Chỉ hiện product chưa có active config trong kho → tránh trùng
- [ ] Chọn product → mở form SCR-02 (cấu hình giá)
- [ ] Ẩn vật tư → soft delete
material_price_config(deleted_at = now()). Tồn kho giữ nguyên. - [ ] Khi ẩn: confirm dialog "Ẩn vật tư [tên]? Tồn kho sẽ giữ nguyên. KTV không thêm được vật tư này vào subtask mới."
Edge cases:
- Product đã có config bị soft delete → cho phép tạo config mới (new version)
- Product có tồn kho > 0 mà ẩn → cảnh báo "Vật tư còn [X] [unit] trong kho. Bạn chắc chắn ẩn?"
- Product search trả về > 100 kết quả → phân trang trong dialog search
- Admin cố thêm product đã có active config → lỗi "Vật tư đã tồn tại trong kho"
- Product từ branch khác → không hiện (isolate per branch)
Nhóm 2: Cấu hình giá + đơn vị quy đổi (FR-004 → FR-006)
FR-004: Cấu hình giá vật tư per product (Ref: DEC-D01, DEC-D02, DEC-D03, DEC-D09, SCR-02)
Mô tả: Admin cấu hình giá nhập, đơn vị kho, hao hụt hệ thống, và ngưỡng cảnh báo tồn kho cho mỗi vật tư.
AC:
- [ ] Form gồm: Sản phẩm (readonly), Đơn vị kho (dropdown: ml, g, miếng, custom), Giá nhập (VND), SL/đơn vị mua, Hao hụt hệ thống (%), Ngưỡng cảnh báo
- [ ]
stock_unit_pricetự tính =source_price / (source_quantity × (1 - wastage_rate))— hiển thị realtime khi nhập (FORMULA-001) - [ ] Giá nhập
source_price:NUMERIC(15,2), min 0đ, max 999,999,999,999.99đ - [ ] SL/đơn vị mua
source_quantity:NUMERIC(12,4), must > 0 (DB CHECK constraint) - [ ] Hao hụt
wastage_rate: 0.0000 → 0.9999 (0% → 99.99%), mặc định 0 - [ ] Ngưỡng cảnh báo
min_stock: >= 0, NULL = không cảnh báo - [ ] Giá chung toàn hệ thống (
branch_id = NULL). Fieldbranch_idsẵn cho override (Ref: DEC-D09) - [ ] Khi giá là global (
branch_id = NULL): hiện confirm dialog "Giá này áp dụng cho TẤT CẢ chi nhánh. Bạn chắc chắn?" trước khi lưu - [ ] Khi lưu: validate + insert
material_price_config+ cascade tạo/updatematerial_usage_unit
Validation chi tiết:
source_quantity <= 0→ lỗi "SL/đơn vị mua phải > 0"wastage_rate >= 1→ lỗi "Hao hụt không được ≥ 100%"wastage_rate < 0→ lỗi "Hao hụt không được âm"source_price < 0→ lỗi "Giá nhập không được âm"source_price = 0→ cho phép (vật tư miễn phí / nội bộ)
Edge cases:
- Hao hụt = 0% →
stock_unit_price = source_price / source_quantity(không chia thêm) - Giá nhập = 0 → hợp lệ (
stock_unit_price = 0, vật tư miễn phí) - source_quantity rất lớn (VD: 1,000,000) →
stock_unit_pricerất nhỏ →NUMERIC(15,4)đủ chứa wastage_rate = 0.9999(99.99%) → giá tăng 10,000× → cảnh báo "Hao hụt > 50%, xác nhận?"- Admin sửa đơn vị kho → lock nếu đã có usage unit reference (Ref: DEC-D06)
FR-005: Cập nhật giá (versioning) (Ref: DEC-D02, DEC-D05, SCR-03)
Mô tả: Admin cập nhật giá mới cho vật tư. Hệ thống tạo version mới, giữ lịch sử giá cũ.
AC:
- [ ] Button [Cập nhật giá] trên SCR-03 → mở form pre-filled với giá hiện tại
- [ ] Khi lưu: set
effective_to = now()trên config cũ → insert config mớieffective_from = now(),effective_to = NULL - [ ] Clone
material_usage_unittừ config cũ sang config mới (giữ usage units, recalcusage_unit_price) - [ ] Subtask đã ghi nhận trước đó → giữ snapshot giá cũ (
price_snapshot_at,config_version_id) - [ ] Subtask mới sau khi cập nhật → lấy giá mới
- [ ] Lịch sử giá hiển thị trên SCR-03: bảng [Từ ngày | Đến ngày | Giá/ĐV kho | Người sửa]
Edge cases:
- Admin cập nhật giá 2 lần trong 1 phút → 2 version liên tiếp, hợp lệ
- Cập nhật giá trong khi KTV đang thêm vật tư → KTV lấy giá tại thời điểm save (snapshot)
- Rollback giá (quay về giá cũ) → tạo version mới với giá = giá cũ (không delete version giữa)
- Xem lịch sử giá: paginate 20 records/page, sort desc by
effective_from - Config cũ đã soft delete → không hiện trong lịch sử giá
FR-006: Cấu hình đơn vị quy đổi (Ref: DEC-D06, DEC-D07, SCR-02)
Mô tả: Admin thêm/sửa đơn vị sử dụng (usage unit) cho mỗi vật tư, với hệ số quy đổi về đơn vị kho.
AC:
- [ ] Bảng ĐVT trong SCR-02: Tên ĐVT, Hệ số (→ ĐV kho), Giá/ĐVT (auto-calc), Rời? (is_discrete), Mặc định?
- [ ]
to_stock_factor > 0(CHECK constraint). VD: 1 giọt = 0.05ml → factor = 0.05 - [ ]
usage_unit_price = stock_unit_price × to_stock_factor(FORMULA-002), auto-calc realtime - [ ]
is_discrete = true→ KTV chỉ được nhập SL nguyên (VD: Mask = miếng → 1, 2, 3... không 0.5) - [ ]
is_default = true→ ĐVT mặc định khi KTV thêm vật tư vào subtask. Chỉ 1 default per config. - [ ]
UNIQUE(config_id, usage_unit_name)→ không cho trùng tên ĐVT - [ ] Ít nhất 1 ĐVT per config (bắt buộc)
- [ ] Max 10 ĐVT per config (đủ cho mọi use case spa)
Edge cases:
to_stock_factor = 0→ DB CHECK block, FE validate "Hệ số phải > 0"to_stock_factorrất nhỏ (0.001) →usage_unit_pricerất nhỏ → hiển thị 4 decimal- Admin xóa ĐVT đã được reference bởi
project_task_material→ block, hiển thị "ĐVT đang được sử dụng bởi [N] subtask" - Admin sửa tên ĐVT → cho phép (soft rename, subtask cũ lưu snapshot tên cũ trong
usage_unitfield) - Admin set
is_defaultcho ĐVT B khi ĐVT A đang default → A tự động bỏ default, B thành default
Nhóm 3a: Chuyển kho chi nhánh → Kho vật tư (FR-022) — NEW v3.0
FR-022: Chuyển kho chi nhánh → Kho vật tư (★ NEW v3.0, Ref: DEC-D29, DEC-D31, DEC-D32, DEC-D36, SCR-08)
Mô tả: Manager chọn SP từ kho chi nhánh, nhập SL theo đơn vị mua (chai/hộp), hệ thống tự quy đổi sang đơn vị kho (ml/g) + trừ tồn kho chi nhánh + tạo lô FIFO trong kho vật tư. Không cần duyệt.
AC:
- [ ] Button [★ Chuyển kho] (primary) trên SCR-01 → mở form SCR-08
- [ ] Form: Từ kho (kho chi nhánh, readonly) + Đến kho (kho vật tư, readonly)
- [ ] Search SP từ kho chi nhánh: chỉ hiện SP có tồn > 0. Reuse
ProductLotNumberSelectcomponent (DEC-D36) - [ ] Chọn lô nguồn bắt buộc: Manager chọn cụ thể lô nào từ kho chi nhánh. Dropdown hiện: [Mã lô | NCC | HSD | Giá nhập | Tồn lô]. Validation: "Chọn mã lô hàng cho vật tư!" nếu chưa chọn.
- [ ] Bảng editable: [Sản phẩm | SKU | Lô nguồn ▼ | Tồn lô | SL chuyển | ĐV mua]
- [ ] Giá nhập + HSD: auto-fill từ lô nguồn đã chọn (
product_supplying.price,expiry_at), read-only trên UI, không phảicost_pricechung - [ ] Mỗi lô chuyển = 1
material_batchriêng (giữ nguyên giá + HSD từ lô nguồn → audit 1:1) - [ ] SL chuyển: > 0, ≤ tồn lô nguồn (quantity_remain). Vi phạm → "Tồn lô không đủ: cần X, lô chỉ còn Y"
- [ ] Yêu cầu SP đã có config quy đổi (
material_price_config.source_quantity). Chưa có → "Vui lòng cấu hình quy đổi đơn vị" + link SCR-02 - [ ] Hiển thị SL quy đổi realtime: "2 chai → 1,000ml" (= SL × source_quantity)
- [ ] Giá nhập: luôn lấy từ lô nguồn đã chọn (
product_supplying.price), không cho sửa thủ công. Nếuprice = 0/NULLthì hiển thị warning (EC-26) và vẫn cho phép tiếp tục theo giá nguồn; FE/BE/QA không implement manual override cho transfer - [ ] Canonical cost basis (DEC-D37b):
product_supplying.price= giá per đơn vị mua (VD: 500,000đ/chai, KHÔNG phải tổng tiền).material_batch.purchase_price= giá từproduct_supplying.pricecủa lô nguồn (chuẩn hóaNULL -> 0, còn0giữ nguyên).material_batch.unit_price=purchase_price / (source_quantity × (1 - wastage_rate))= giá per stock unit đã tính hao hụt. VD: 500,000đ / (500ml × 0.98) = 1,020.41đ/ml. Tất cả downstream pricing tính từunit_pricenày. - [ ] Khi xác nhận:
- Tạo
inventory_document(behavior:export_material_warehouse, status:released) - Tạo
product_supplyingrecords (type:export) → trừ tồn kho chi nhánh - Tạo
material_batchper dòng (purchase_price, purchase_quantity = SL quy đổi stock unit, status='active') - Tạo
material_stock_movement(source_type='transfer', source_reference_id=inventory_document.id)
- Tạo
- [ ] Không cần bước duyệt — Manager xác nhận = thực hiện luôn (DEC-D31)
- [ ] Serialize per (warehouse, product) bằng
pg_advisory_xact_lock - [ ] Tồn kho chi nhánh + tồn kho vật tư cập nhật realtime
Validation:
- SL chuyển > tồn kho chi nhánh → lỗi "Tồn kho chi nhánh không đủ: cần X, chỉ còn Y"
- SP chưa config quy đổi → block dòng đó, hiện link cấu hình
- cost_price = 0/NULL → warning "Giá nhập = 0, vui lòng kiểm tra" (cho phép tiếp tục)
- 0 dòng → disable button Xác nhận
Edge cases:
- 2 Manager chuyển cùng SP cùng lúc → advisory lock serialize, first-come-first-served (EC-25)
- SP không tồn tại trong kho chi nhánh → ẩn khỏi dropdown (EC-24)
- Hủy phiếu chuyển sau released → nếu batch chưa dùng: reverse cả 2 bên. Nếu batch đã auto-deduct → block hủy (EC-27)
Nhóm 3b: Nhập kho Fallback (FR-007 → FR-008)
FR-007: Nhập tồn kho manual — Fallback (Ref: DEC-D13, DEC-D30, SCR-06)
Mô tả: Admin/Manager nhập tồn kho vật tư bằng form nhập tay (fallback cho chi nhánh data kho chính chưa chuẩn), tạo material_batch + material_stock_movement với source_type = 'stock_in'.
AC:
- [ ] Button [Nhập tay] (secondary) trên SCR-01 → mở form SCR-06
- [ ] Form: Kho (readonly), bảng editable [Vật tư | ĐV kho | SL nhập | Giá nhập | HSD | Mã lô | Ghi chú]
- [ ] Giá nhập: > 0 (bắt buộc),
NUMERIC(15,2)— giá per đơn vị mua (chai/hộp) - [ ] HSD: date picker, optional (vật tư không có HSD thì bỏ trống)
- [ ] Mã lô: text, optional (mã lô nhà cung cấp)
- [ ] [+ Thêm dòng] → search vật tư từ danh mục kho
- [ ] SL nhập: > 0 (nhập kho luôn dương),
NUMERIC(12,4) - [ ] Khi lưu: cho mỗi dòng:
- INSERT
material_batch(status='active', initial_qty=SL, remaining_qty=SL, purchase_price, unit_price=purchase_price/SL_stock_units, expiry_date, batch_code) - INSERT
material_stock_movement(source_type='stock_in', batch_id=batch vừa tạo, quantity_change=+SL, note=ghi chú)
- INSERT
- [ ] Serialize per (warehouse, product) bằng
pg_advisory_xact_lock(Ref: DEC-D15) - [ ] Tồn kho cập nhật realtime trên SCR-01 sau khi nhập
- [ ] Manager: chỉ nhập kho branch mình
Validation:
- SL nhập ≤ 0 → lỗi "SL nhập phải > 0"
- SL nhập > 1,000,000 → cảnh báo "SL lớn bất thường, xác nhận?"
- Vật tư trùng trong 1 phiếu nhập → gộp hoặc lỗi "Vật tư đã có trong phiếu"
Edge cases:
- Nhập kho cho vật tư chưa có tồn kho → tạo lô mới,
remaining_qty = SL_nhập. Tồn kho tổng = SUM(remaining_qty) các lô active. - Nhập kho khi tồn kho = 0 → tạo lô mới,
remaining_qty = SL_nhập. Tồn kho tổng = SUM(remaining_qty) các lô active. - 2 Admin nhập cùng lúc cùng product → advisory lock serialize, balance đúng
- Nhập kho 0 dòng → disable button Xác nhận
- Network error khi lưu → retry, idempotency qua transaction
FR-008: Nhập tồn kho bằng Excel — Fallback (Ref: DEC-D13, DEC-D30, SCR-06)
Mô tả: Admin/Manager import file Excel để nhập tồn kho hàng loạt.
AC:
- [ ] Button [Import Excel] trên SCR-06 → upload file .xlsx/.csv
- [ ] Template Excel: [Mã SP (SKU) | SL nhập | Giá nhập | HSD (dd/mm/yyyy) | Mã lô | Ghi chú] — có link download template mẫu
- Giá nhập: bắt buộc, > 0 — giá per đơn vị mua (chai/hộp)
- HSD: optional — để trống nếu vật tư không có HSD
- Mã lô: optional — mã lô nhà cung cấp
- [ ] Validate per dòng:
- SKU không tồn tại trong danh mục kho → lỗi dòng đó
- SL ≤ 0 → lỗi dòng đó
- SL > 1,000,000 → warning dòng đó
- [ ] Hiển thị preview: dòng hợp lệ (xanh) + dòng lỗi (đỏ + lý do)
- [ ] Admin confirm → import dòng hợp lệ, bỏ qua dòng lỗi
- [ ] Mỗi dòng tạo 1
material_batch(status='active') + 1material_stock_movement(source_type='stock_in', batch_id gắn lô vừa tạo) - [ ] Mỗi lần import tạo 1
batch_import_id(UUID) gom tất cả batches + movements - [ ] Lịch sử nhập kho hiển thị theo batch, button "Hủy phiếu nhập" tạo reverse movements cho toàn batch
- [ ] Max 500 dòng/file. > 500 → "File quá lớn, vui lòng chia nhỏ."
- [ ] Xử lý đồng bộ (< 500 dòng → < 10 giây)
Edge cases:
- File rỗng → "File không có dữ liệu"
- File sai format (không phải xlsx/csv) → "Chỉ hỗ trợ file .xlsx hoặc .csv"
- SKU trùng trong 1 file → gộp SL (cộng dồn) hoặc lỗi per dòng (chọn 1 — recommend gộp)
- File có header sai → "Cột [X] không đúng format. Tải template mẫu."
- Import thành công 450/500 dòng → hiển thị "Đã nhập 450 dòng thành công. 50 dòng lỗi." + link download lỗi
Nhóm 4: Ghi nhận vật tư trên subtask — giá + ĐVT (FR-009 → FR-011)
FR-009: Chọn đơn vị sử dụng khi thêm vật tư (Ref: DEC-D05, DEC-D07, SCR-04)
Mô tả: KTV chọn ĐVT (giọt, muỗng, ml...) khi thêm vật tư vào subtask. Hệ thống tính giá tự động.
AC:
- [ ] Cột ĐVT trên MaterialForm → dropdown danh sách
material_usage_unitcủa product đó - [ ] Mặc định chọn ĐVT có
is_default = true - [ ] Khi chọn ĐVT → cập nhật:
unit_price=usage_unit_price(snapshot tại thời điểm thêm, FORMULA-002)to_stock_factor= snapshot hệ sốamount=ROUND(unit_price × quantity)(FORMULA-003)stock_equivalent=quantity × to_stock_factor(FORMULA-004)
- [ ] Nếu product chưa có config → ĐVT dropdown trống, hiện text "(chưa có giá)" (Ref: DEC-D11)
- [ ] Cột Đơn giá + Thành tiền: ẩn với Staff (KTV) (Ref: DEC-D10)
Validation:
quantity ≤ 0→ FE block "SL phải > 0"is_discrete = true AND quantitycó phần thập phân → FE block "Đơn vị rời, SL phải nguyên"quantity > 10,000→ cảnh báo "SL lớn bất thường, xác nhận?"
Edge cases:
- Product có 3 ĐVT: giọt, muỗng, ml → dropdown 3 options
- KTV đổi ĐVT sau khi nhập SL → recalc unit_price, amount, stock_equivalent
- Product chỉ có 1 ĐVT → auto-select, không hiện dropdown
- ĐVT bị xóa sau khi đã lưu subtask → subtask cũ giữ snapshot tên ĐVT, không crash
FR-010: Ước tính giá khi lưu subtask + Finalize giá khi done (Ref: DEC-D05, DEC-D02, DEC-D22, DEC-D28)
Mô tả: Giá vật tư trải qua 2 phase:
- Phase 1 (save subtask): Hệ thống ước tính giá từ lô FIFO cũ nhất hiện tại → ghi vào
project_task_materialvới tag "ước tính". KTV thấy giá ngay nhưng đây chưa phải giá cuối. - Phase 2 (done subtask): Hệ thống deduct FIFO thực tế → tính giá bình quân gia quyền từ các lô đã xuất → UPDATE
project_task_materialvới giá cuối cùng. Tag "ước tính" biến mất. - Sau done: Giá là FINAL, không thay đổi dù admin đổi config hoặc nhập lô mới.
AC:
- [ ] Phase 1 — Khi lưu subtask (save/update):
- Lấy lô FIFO cũ nhất có
status='active'ANDremaining_qty > 0cho product unit_price=batch.unit_price × to_stock_factor(giá ước tính)amount=ROUND(unit_price × quantity)(ước tính)stock_equivalent=quantity × to_stock_factortotal_stock_equivalent=(quantity + wastage_quantity) × to_stock_factorprice_snapshot_at=now()- UI hiển thị giá với badge "(ước tính)" nếu subtask chưa done
- Lấy lô FIFO cũ nhất có
- [ ] Phase 2 — Khi subtask done (auto-deduct FR-020):
- Deduct FIFO thực tế → nếu split 2+ lô → tính bình quân gia quyền
- UPDATE
project_task_materialSETunit_price= giá FIFO thực tế,amount= chi phí thực tế,price_snapshot_at= now() - Badge "(ước tính)" biến mất
- [ ] Sau done:
unit_pricevàamountkhông thay đổi nữa, kể cả admin đổi giá config - [ ] Nếu product chưa có lô nào (tồn kho = 0) →
unit_price = NULL,amount = NULL, hiện "(chưa có giá)" - [ ] Subtask sửa lại SL → recalc amount, stock_equivalent, total_stock_equivalent (re-estimate từ lô FIFO hiện tại)
- [ ] Subtask sửa ĐVT → recalc toàn bộ (lấy giá ước tính mới theo ĐVT mới)
- [ ] Sau subtask done: mỗi dòng vật tư có nút [chi tiết] expandable, hiện danh sách lô đã xuất FIFO: Tên lô, SL xuất từ lô, giá/stock_unit của lô, trạng thái lô sau xuất (active/depleted)
- [ ] FIFO split: nếu xuất vượt 2+ lô, hiện 2+ dòng chi tiết. Giá bình quân gia quyền = tổng cost / tổng SL
- [ ] Chỉ hiện [chi tiết] khi subtask đã done (Phase 2 DEC-D28). Chưa done → hiện "Giá ước tính từ lô FIFO"
- [ ] Data từ
material_stock_movement WHERE source_type='auto_deduct' AND source_reference_id = ptm.id
Edge cases:
- Admin đổi giá 1 giây trước KTV save → KTV lấy giá ước tính từ lô FIFO hiện tại (race condition accepted)
- KTV mở form → đợi 30 phút → save → lấy giá ước tính tại thời điểm save (không phải thời điểm mở)
- Config bị delete (soft) giữa lúc KTV đang edit → save fail, hiển thị "Giá vật tư đã thay đổi, vui lòng thử lại"
quantity = 0→ FE block. Nếu bypass →amount = 0(hợp lệ toán học nhưng vô nghĩa)wastage_quantity > quantity→ cho phép (KTV đổ bỏ nhiều hơn dùng), cảnh báo "Hao hụt > SL sử dụng"- Giá ước tính (Phase 1) khác giá thực tế (Phase 2) khi lô FIFO đã hết giữa save và done → giá Phase 2 là đúng, Phase 1 chỉ mang tính tham khảo
FR-011: Ghi nhận hao hụt kỹ thuật (Ref: DEC-D03, FORMULA-004, FORMULA-006)
Mô tả: KTV ghi nhận SL hao hụt kỹ thuật (đổ bỏ, dùng thừa) per vật tư per subtask.
AC:
- [ ] Cột "Hao hụt" (wastage_quantity) trên MaterialForm — optional, mặc định 0
- [ ]
wastage_quantitymặc định ẩn trên form. Admin bật qua Settings > Kho vật tư > Hiển thị hao hụt (Ref: DEC-D20) - [ ]
wastage_quantity >= 0, cùng ĐVT với quantity - [ ]
wastage_reason: text tự do, max 200 ký tự (VD: "Đổ bỏ do hết HSD", "Dùng thừa") - [ ]
total_stock_equivalent = (quantity + wastage_quantity) × to_stock_factor— dùng khi trừ kho - [ ]
wastage_cost = ROUND(unit_price × wastage_quantity)— FORMULA-006, tracking riêng cho báo cáo - [ ] Khi Settings tắt: cột hao hụt ẩn với tất cả roles trên form subtask.
wastage_quantity = 0mặc định.
Edge cases:
wastage_quantity = 0→total_stock_equivalent = quantity × factor(không hao hụt)wastage_quantity>quantity→ cho phép + cảnh báo "Hao hụt > SL sử dụng"is_discrete = true→wastage_quantitycũng phải nguyênunit_price = NULL(chưa config) →wastage_cost = NULL- Hao hụt trên product chưa config → ghi nhận được, không tính cost
Nhóm 5: Auto-deduct + reverse (FR-012 → FR-013)
FR-012: Auto-deduct khi subtask done (Ref: DEC-D14, DEC-D15, DEC-D22, DEC-D23, DEC-D18)
Mô tả: Khi subtask chuyển sang status done_*, hệ thống tự động trừ tồn kho vật tư theo total_stock_equivalent.
AC:
- [ ] Event trigger on
project_task.status_idchanged - [ ] Condition: new status contains
done_(VD:done_branch,done_all) - [ ] Handler:
materialAutoDeduct(taskId) - [ ] Cho mỗi
project_task_materialcủa subtask:- Nếu
total_stock_equivalent IS NULL→ SKIP (product chưa config) - Lấy warehouse từ
task.branch_id→material_warehouse.branch_id - Nếu warehouse không tồn tại → LOG warning, SKIP
pg_advisory_xact_lock(hashtext(warehouse_id || product_id))— serialize per productcurrentBalance = getLatestBalance(warehouse, product)newBalance = currentBalance - total_stock_equivalent- Insert
material_stock_movement:quantity_change = -total_stock_equivalentrunning_balance = newBalancesource_type = 'auto_deduct'source_reference_id = material.idsource_reference_type = 'project_task_material'
- Nếu
newBalance <= min_stock→ trigger low stock notification (FR-015)
- Nếu
- [ ] Không cho phép tồn kho âm (Ref: DEC-D23). Nếu
remaining_qtycủa lô FIFO không đủ cho BẤT KỲ vật tư nào → block toàn bộ transaction, rollback tất cả movements, subtask KHÔNG chuyển done. Hiển thị lỗi "Không đủ tồn kho [tên vật tư] (cần X, còn Y)". Không partial deduct (không trừ vật tư A rồi skip vật tư B). - [ ] Subtask có 5 vật tư → 5 movements trong 1 transaction. Nếu 1 vật tư fail → rollback cả 5.
Edge cases:
- Subtask done nhưng 0 vật tư → không tạo movement (hợp lệ)
- Subtask done nhưng warehouse chưa tạo → log warning, không crash, không trừ kho
- 2 subtask done cùng lúc trừ cùng product → advisory lock serialize, balance chính xác
- Subtask done → không đủ tồn kho cho 1 trong 5 vật tư → block toàn bộ transaction, rollback tất cả, subtask không chuyển done, hiển thị lỗi "Không đủ tồn kho [tên vật tư] (cần X, còn Y). Vui lòng chuyển kho hoặc nhập thêm." (Ref: DEC-D23)
- Product chưa config → skip (total_stock_equivalent IS NULL)
- Network error trong auto-deduct → retry mechanism (max 3 retries)
FR-013: Auto-reverse khi undo done (Ref: DEC-D18)
Mô tả: Khi subtask chuyển từ done_* sang trạng thái khác (undo done, canceled sau done), hệ thống tự động reverse (cộng lại) tồn kho.
AC:
- [ ] Event trigger on
project_task.status_idchanged - [ ] Condition: old status contains
done_AND new status NOT containsdone_ - [ ] Handler:
materialAutoReverse(taskId) - [ ] Tìm movements
source_type = 'auto_deduct'liên quan đến subtask - [ ] Cho mỗi movement:
pg_advisory_xact_lockper (warehouse, product)currentBalance = getLatestBalance(warehouse, product)- Insert
material_stock_movement:quantity_change = +ABS(original_quantity_change)running_balance = currentBalance + ABS(original_quantity_change)source_type = 'auto_reverse'source_reference_id = original_material_idnote = 'Reverse: subtask undo/canceled'
- [ ] Immutable log: KHÔNG delete movement cũ. Tạo movement mới +quantity.
- [ ] Reverse chỉ xảy ra 1 lần per auto-deduct movement (idempotent check)
Edge cases:
- Subtask done → undo → done lại → 3 movements: deduct(-X), reverse(+X), deduct(-X) — balance đúng
- Subtask done → undo → sửa SL → done lại → deduct cũ + reverse + deduct mới (SL mới)
- Reverse movement không tìm thấy (data inconsistent) → log error, không crash
- Subtask canceled từ in_progress (chưa done) → không reverse (chưa có auto-deduct)
- Double-trigger (event fire 2 lần) → idempotent: check "đã có auto_reverse cho movement này chưa"
Nhóm 6: Kiểm kê + cảnh báo (FR-014 → FR-015)
FR-014: Kiểm kê + điều chỉnh (Ref: SCR-07)
Mô tả: Admin/Manager kiểm kê tồn kho thực tế, so sánh với hệ thống, và điều chỉnh chênh lệch.
AC:
- [ ] Button [Kiểm kê] trên SCR-01 → mở form SCR-07
- [ ] Form: Kho (readonly), Ngày kiểm kê (readonly = today), bảng [Vật tư | ĐV kho | Hệ thống | Thực tế (input) | Chênh lệch (auto) | Lý do (input)]
- [ ] "Hệ thống" =
running_balancetừmaterial_stock_balance - [ ] "Chênh lệch" = Thực tế - Hệ thống, auto-calc
- [ ] Khi confirm:
- Dòng có chênh lệch ≠ 0 → tạo
material_stock_movement:quantity_change = chênh lệchrunning_balance = Thực tếsource_type = 'adjustment'note = lý do
- Dòng chênh lệch = 0 → skip
- Dòng có chênh lệch ≠ 0 → tạo
- [ ] Manager: chỉ kiểm kê branch mình
- [ ] Sau kiểm kê:
running_balance= Thực tế (exactly) - [ ] Batch-level adjustment (v3.0): Chênh lệch PHẢI phân bổ về batch cụ thể, không chỉ cập nhật aggregate:
- Chênh lệch < 0 (thực tế < hệ thống): trừ FIFO từ lô cũ nhất active (ORDER BY created_at ASC). Nếu trừ hết 1 lô → SET status='depleted', tiếp tục lô kế.
- Chênh lệch > 0 (thực tế > hệ thống): cộng vào lô mới nhất active (ORDER BY created_at DESC). Nếu không có lô active → tạo lô mới (batch_code='ADJ-{timestamp}', purchase_price=0, note='Điều chỉnh kiểm kê').
- Mỗi batch bị điều chỉnh → 1
material_stock_movementriêng (source_type='adjustment', batch_id, batch_remaining_after)
- [ ] Audit trail: Kiểm kê tạo 1 nhóm movements cùng
source_reference_id= kiểm kê ID. Có thể truy vết "lần kiểm kê này điều chỉnh những lô nào".
Validation:
- "Thực tế" >= 0 (sau kiểm kê, tồn kho phải >= 0)
- "Lý do" bắt buộc khi chênh lệch > 10% → "Chênh lệch > 10%, vui lòng nhập lý do"
- "Lý do" tùy chọn khi chênh lệch ≤ 10%
Edge cases:
- Tất cả dòng chênh lệch = 0 → "Không có chênh lệch. Không cần điều chỉnh."
- Kiểm kê khi KTV đang trừ kho → advisory lock đảm bảo không race condition
- Kiểm kê > 500 vật tư → phân trang trong form kiểm kê (ít xảy ra)
- Admin kiểm kê 2 lần/ngày → hợp lệ, tạo 2 bộ adjustment movements
- Chênh lệch lớn (> 50%) → cảnh báo "Chênh lệch bất thường, xác nhận?"
FR-015: Cảnh báo tồn kho thấp (Ref: DEC-D23, DEC-D24)
Mô tả: Khi tồn kho vật tư giảm xuống ≤ min_stock, hệ thống gửi notification cho Manager branch + Admin.
AC:
- [ ] Trigger: sau
auto_deducthoặcadjustmentkhirunning_balance <= min_stock - [ ] Template:
"[{branch_name}] {product_name} còn {balance} {stock_unit}, dưới ngưỡng {min_stock}" - [ ] Kênh: In-app notification (notification-api)
- [ ] Người nhận: Manager của branch + Admin toàn hệ thống
- [ ] Dedupe: 1 notification / product / warehouse / ngày (check before send)
- [ ] Ví dụ:
"[Quận 1] Mask collagen còn 49 miếng, dưới ngưỡng 50" - [ ] 4 mức hiển thị trên SCR-01:
> min_stock × 2: Xanh (Đủ)> min_stock: Vàng (Sắp hết)≤ min_stock: Đỏ (Dưới ngưỡng)= 0: Đỏ bold (Hết hàng) — không cho phép âm (Ref: DEC-D23)
- [ ]
min_stock = NULL→ không cảnh báo, hiển thị "—" ở cột trạng thái
Edge cases:
- Tồn kho dao động quanh min_stock (49 → 51 → 48) → dedupe chỉ gửi 1 notification/ngày
- 10 products đồng thời dưới ngưỡng → 10 notifications riêng biệt
- Admin chưa set min_stock → không trigger notification cho product đó
- Balance = 0 → gửi notification "Hết hàng", block deduct cho đến khi nhập thêm (Ref: DEC-D23)
- Notification-api unavailable → log error, retry 3 lần, queue cho lần sau
Nhóm 7: Kết nối tổng hợp tài chính (FR-016)
FR-016: Chi phí vật tư trong sidebar tài chính đơn hàng (Ref: FORMULA-005)
Mô tả: Hiển thị tổng chi phí vật tư trong sidebar tổng hợp tài chính đơn hàng dịch vụ.
AC:
- [ ] Sidebar tài chính đơn hàng thêm dòng "Chi phí vật tư" (material_cost)
- [ ] Công thức:
material_cost = SUM(ptm.amount) WHERE ptm.task_id IN ( SELECT id FROM project_task WHERE parent_id IN ( SELECT id FROM project_task WHERE order_id = $order_id ) ) AND ptm.amount IS NOT NULL AND subtask.status_id NOT LIKE 'canceled%' - [ ] Subtask canceled → exclude khỏi SUM (Ref: DEC-D19)
- [ ] Tổng chi phí đơn hàng cập nhật:
total_cost = tour_money + material_cost + commission + fixed_cost - [ ] Backward compatibility:
- Đơn mới (có
project_task_material.amount): dùng FORMULA-005 - Đơn cũ (có
inventory_document.capture): dùngSUM(capture.Price × capture.Quantity) - Không có data:
material_cost = 0
- Đơn mới (có
- [ ] Quyền xem: Admin (all) + Manager (branch mình) (Ref: DEC-D21)
- [ ] Realtime: cập nhật khi subtask save (vật tư mới/sửa SL)
Edge cases:
- Đơn hàng có cả subtask cũ (không có giá) + subtask mới (có giá) → cộng dồn cả 2 nguồn
- Tất cả vật tư chưa config giá →
material_cost = 0(tất cả amount = NULL) - Đơn hàng 0 subtask →
material_cost = 0 - Đơn hàng chỉ có subtask canceled →
material_cost = 0(tất cả excluded, Ref: DEC-D19) - Undo subtask done → amount giữ nguyên (amount = chi phí ghi nhận, không liên quan trừ kho)
Nhóm 8: Import cấu hình (FR-017)
FR-017: Import Excel cấu hình giá + đơn vị quy đổi
Mô tả: Admin import file Excel để cấu hình giá và đơn vị quy đổi hàng loạt cho nhiều vật tư cùng lúc.
AC:
- [ ] Button [Import cấu hình] trên SCR-01 → upload file .xlsx/.csv
- [ ] Template Excel: [SKU | Đơn vị kho | Giá nhập | SL/đơn vị mua | Hao hụt % | Ngưỡng cảnh báo | ĐVT1 | Factor1 | ĐVT2 | Factor2] — có link download template mẫu
- [ ] Validate per dòng:
- SKU không tồn tại trong
product→ lỗi dòng đó - Giá nhập < 0 → lỗi dòng đó
- SL/đơn vị mua ≤ 0 → lỗi dòng đó
- Hao hụt % < 0 hoặc ≥ 100 → lỗi dòng đó
- Factor ≤ 0 → lỗi dòng đó
- SKU không tồn tại trong
- [ ] Hiển thị preview: dòng hợp lệ (xanh) + dòng lỗi (đỏ + lý do)
- [ ] Admin confirm → import dòng hợp lệ: tạo
material_price_config+material_usage_unitper dòng - [ ] SKU đã có config active → tạo version mới (close config cũ, Ref: DEC-D02)
- [ ] Max 500 dòng/file. > 500 → "File quá lớn, vui lòng chia nhỏ."
- [ ] Xử lý đồng bộ (< 500 dòng → < 10 giây)
Edge cases:
- Cột ĐVT2/Factor2 trống → chỉ tạo 1 usage unit (ĐVT1)
- SKU trùng trong 1 file → lỗi "SKU [X] xuất hiện nhiều lần, chỉ giữ dòng đầu"
- File rỗng → "File không có dữ liệu"
- File sai format → "Chỉ hỗ trợ file .xlsx hoặc .csv"
- Import thành công 480/500 dòng → hiển thị "Đã import 480 dòng thành công. 20 dòng lỗi." + link download lỗi
Nhóm 9: Quản lý lô hàng + FIFO (FR-018 → FR-021) — NEW v2.0
FR-018: Quản lý lô hàng (batch) (Ref: DEC-D22, DEC-D25)
Mô tả: Mỗi lần nhập kho tạo 1 lô (batch) mới với giá nhập riêng, hạn sử dụng (HSD), mã lô nhà cung cấp. Trạng thái lô theo lifecycle.
AC:
- [ ] Mỗi
material_stock_movementvớisource_type IN ('stock_in', 'import', 'transfer')tạo 1 recordmaterial_batch - [ ] Thông tin lô:
batch_code(mã lô NCC, optional),purchase_price(giá nhập per stock unit),expiry_date(HSD, optional),initial_qty,remaining_qty - [ ] Trạng thái lô (
status):active: còn hàng, đang sử dụng (remaining_qty > 0AND chưa hết hạn AND không bị hủy)depleted: đã dùng hết (remaining_qty = 0)locked: hết hạn sử dụng, auto-lock bởi scheduler (Ref: DEC-D24). Không cho xuất.disposed: đã hủy (lỗi, thu hồi). Ref: DEC-D26
- [ ] Nhập cùng giá cùng ngày vẫn tách lô riêng (Ref: DEC-D25) — vì có thể khác mã lô NCC, khác HSD
- [ ]
remaining_qty >= 0(CHECK constraint, Ref: DEC-D23) - [ ] SCR-03 (Chi tiết vật tư): thêm tab "Danh sách lô" hiển thị [Mã lô | Giá nhập | HSD | SL ban đầu | SL còn | Trạng thái]
- [ ] Sort lô:
created_at ASC(lô cũ nhất trên cùng — FIFO order)
Validation:
initial_qty > 0(nhập kho luôn dương)remaining_qty >= 0(CHECK constraint)purchase_price >= 0(cho phép 0 = vật tư miễn phí)expiry_datenếu có phải > ngày nhập (created_at)
Edge cases:
- Nhập 2 lần cùng ngày cùng giá → 2 lô riêng biệt, FIFO theo thứ tự nhập (Ref: DEC-D25)
- Lô không có HSD (
expiry_date = NULL) → không bao giờ auto-lock, trạng thái chỉ chuyển active → depleted - Lô depleted → không cho xuất thêm. Nếu reverse (undo subtask) →
remaining_qtytăng lại, status chuyểnactive - Import Excel: mỗi dòng = 1 lô mới (có thể thêm cột
batch_code,expiry_datevào template) - Lô có
remaining_qty = 0.0001(gần hết) → vẫn active, FIFO sẽ dùng trước
FR-019: Hạn sử dụng (HSD) — cảnh báo + auto-lock (Ref: DEC-D24)
Mô tả: Cảnh báo Manager 3 tháng trước HSD. Scheduler auto-lock lô hết hạn, không cho xuất.
AC:
- [ ] Scheduler chạy hàng ngày (00:00 UTC+7): quét tất cả lô
status = 'active'cóexpiry_date - [ ] Lô hết hạn (
expiry_date < today) → auto updatestatus = 'locked', ghilocked_reason = 'expired' - [ ] Lô sắp hết hạn (≤ 90 ngày) → gửi notification cho Manager branch + Admin:
- Template:
"[{branch_name}] Lô {batch_code} — {product_name} hết hạn ngày {expiry_date} (còn {days_left} ngày)" - Dedupe: 1 notification / lô / tuần (không spam hàng ngày)
- Template:
- [ ] Lô
locked→ không tham gia FIFO xuất kho. Auto-deduct bỏ qua lô locked. - [ ] SCR-01: badge "Sắp hết HSD" (vàng) nếu có lô sắp hết hạn ≤ 90 ngày. Badge "Hết HSD" (đỏ) nếu có lô locked.
- [ ] SCR-03 tab "Danh sách lô": cột HSD hiển thị đỏ nếu ≤ 90 ngày hoặc đã hết hạn
Edge cases:
- Lô không có HSD → scheduler bỏ qua
- Lô locked (hết hạn) nhưng còn hàng (
remaining_qty > 0) → Manager có thể hủy lô (FR-021) hoặc Admin unlock thủ công nếu HSD gia hạn - Lô sắp hết hạn nhưng đã depleted → không cảnh báo (không còn hàng)
- 10 lô sắp hết hạn cùng lúc → 10 notifications riêng biệt (mỗi lô 1 notification)
- Admin sửa
expiry_datethành ngày xa hơn → nếu lô đang locked (expired) → chuyển lạiactive(nếu remaining_qty > 0)
FR-020: FIFO xuất kho (Ref: DEC-D22, DEC-D27, DEC-D23)
Mô tả: Auto-deduct khi subtask done: lấy từ lô cũ nhất (FIFO). Nếu 1 lần xuất vượt 2+ lô → tách nhiều movements, mỗi movement gắn 1 lô. Giá snapshot = bình quân gia quyền.
AC:
- [ ] Khi auto-deduct (FR-012), thay vì trừ trực tiếp
running_balance, áp dụng FIFO:- Lấy danh sách lô
status = 'active'ANDremaining_qty > 0cho (warehouse, product), ORDER BYcreated_at ASC(lô cũ nhất trước) - Duyệt từng lô theo thứ tự FIFO:
- Nếu
remaining_qty >= qty_needed→ trừ hết từ lô này. Tạo 1 movement gắnbatch_id. Done. - Nếu
remaining_qty < qty_needed→ trừ hết lô này (remaining_qty → 0, status →depleted).qty_needed -= remaining_qty. Tạo movement gắn lô này. Tiếp lô sau. (FIFO split, Ref: DEC-D27)
- Nếu
- Nếu hết lô mà
qty_needed > 0→ block deduct, rollback transaction, hiển thị lỗi "Không đủ tồn kho" (Ref: DEC-D23)
- Lấy danh sách lô
- [ ] Mỗi movement lưu
batch_id→ truy vết giá theo lô - [ ] Giá snapshot cho subtask: bình quân gia quyền (weighted average) khi FIFO split:
snapshot_price = SUM(qty_from_batch_i × batch_i.purchase_price) / total_qty - [ ]
amount = ROUND(snapshot_price × total_qty)— BIGINT - [ ] Subtask
project_task_materiallưu:unit_price = snapshot_price(bình quân gia quyền nếu split)
Validation:
- Tổng
remaining_qtytất cả lô active <total_stock_equivalent→ block deduct - Mỗi movement:
quantity_change <= batch.remaining_qty(không được trừ quá lô)
Edge cases:
- Xuất 10 đơn vị, lô A còn 3, lô B còn 20 → 2 movements: M1 (lô A, -3), M2 (lô B, -7). Lô A → depleted.
- Xuất vượt 3 lô liên tiếp → 3 movements. Hiếm nhưng hợp lệ.
- Lô cũ nhất bị locked (hết HSD) → bỏ qua, lấy lô active tiếp theo
- Tất cả lô còn hàng bị locked → block deduct "Tất cả lô đã hết hạn hoặc bị khóa"
- Concurrent deduct:
pg_advisory_xact_lockper (warehouse, product) serialize — FIFO order nhất quán - Reverse (FR-013): cộng lại
remaining_qtycho từng lô đã trừ (theobatch_idtrong movement). Lô depleted → chuyển lạiactive.
FR-021: Hủy lô — disposal (Ref: DEC-D26)
Mô tả: Manager/Admin hủy lô lỗi, thu hồi, hoặc hết hạn. Tạo disposal movement + ghi chú lý do. Giữ audit trail.
AC:
- [ ] Button [Hủy lô] trên SCR-03 tab "Danh sách lô" → mở dialog confirm
- [ ] Dialog: Lô [mã lô] — [tên vật tư] — SL còn: [remaining_qty] [stock_unit]
- [ ] Field bắt buộc:
disposal_reason(dropdown + text tự do):- Dropdown: "Hết hạn sử dụng" | "Lỗi sản phẩm" | "Thu hồi NCC" | "Hư hỏng" | "Khác"
- Text tự do: max 500 ký tự (bắt buộc khi chọn "Khác")
- [ ] Khi confirm:
- Update batch
status = 'disposed',disposed_at = now(),disposed_by = current_user - Tạo
material_stock_movement:quantity_change = -remaining_qty(trừ hết SL còn lại)source_type = 'disposal'batch_id = batch.idnote = disposal_reason
- Update batch
remaining_qty = 0
- Update batch
- [ ] Quyền: Manager (branch mình) + Admin (tất cả)
- [ ] Sau hủy: lô hiển thị với badge "Đã hủy" (xám), không cho xuất, không tham gia FIFO
Edge cases:
- Hủy lô đã depleted (
remaining_qty = 0) → cho phép (chỉ đổi status, không tạo movement vì qty = 0) - Hủy lô đang được FIFO deduct (concurrent) → advisory lock serialize, hủy chờ deduct xong
- Manager cố hủy lô branch khác → block (permission check)
- Admin hủy lô → ghi
disposed_by = admin_user_idcho audit - Undo hủy lô → không hỗ trợ (disposal là irreversible). Nếu cần → nhập lô mới.
- Hủy lô có HSD xa (chưa hết hạn) → cho phép + confirm "Lô chưa hết hạn, bạn chắc chắn hủy?"
A6) Assumptions
| ID | Assumption | Impact nếu sai | Owner xác nhận |
|---|---|---|---|
| ASM-01 | Trung bình 3-5 vật tư/subtask, 5K-30K subtask/ngày toàn hệ thống | Scale material_stock_movement: 15K-150K movements/ngày. Cần partition sớm hơn nếu > 50K. | PO + Tech Lead |
| ASM-02 | Supplier đổi giá 1-2 lần/năm → Batch-based FIFO theo giá từng lô nhập | Nếu giá đổi hàng tuần → FIFO split nhiều lô, chi phí tính toán phức tạp hơn. | PO |
| ASM-03 | 1 chi nhánh trung bình 30-100 loại vật tư | > 500 vật tư → cần tối ưu search/filter trên SCR-01. | PO |
| PO + Admin | |||
| ASM-05 | Manager tin tưởng số liệu nhập tay ban đầu (kiểm kê thực tế trước nhập) | Nếu nhập sai → chênh lệch từ ngày 1 → giảm tin tưởng. Mitigation: kiểm kê sau 1 tuần. | Manager |
| ASM-06 | Product hiếm khi đổi tên/SKU → denormalize product_name/sku chấp nhận được | Nếu đổi nhiều → stale data hiển thị → cần sync job. | PO + Tech Lead |
| ASM-07 | Concurrent editing kiểm kê (last-write-wins) chấp nhận cho Phase 1 | Nếu 2 Manager kiểm kê cùng lúc → data sai. Mitigation: lock form kiểm kê per warehouse. | PO |
A7) Risks
| ID | Risk | Likelihood | Impact | Severity | Mitigation |
|---|---|---|---|---|---|
| RSK-01 | material_stock_movement grow nhanh (15K-150K records/ngày) | High | Medium | High | Indexes đã plan. Partition by created_at monthly khi > 5M records (Phase 2). |
| RSK-02 | Running balance sai nếu advisory lock fail | Low | High | Medium | Retry mechanism + audit: SUM(quantity_change) = running_balance check hàng đêm. |
| RSK-03 | Admin setup sai giá/hệ số → chi phí sai hàng loạt | Medium | High | High | Confirm dialog "Giá mới ≠ giá cũ > 20%, xác nhận?". Versioning cho rollback. |
| RSK-04 | Import Excel sai data → tồn kho sai từ ngày 1 | Medium | High | High | Preview trước import. Kiểm kê sau 1 tuần sử dụng. |
| RSK-05 | Event trigger auto-deduct fail → tồn kho không trừ | Low | High | Medium | Dead letter queue + retry 3 lần. Alert Admin nếu fail 3 lần. |
| RSK-06 | KTV ghi nhận sai SL → chi phí sai | Medium | Medium | Medium | Supervisor review (Manager xem aggregate). Phase 2: so sánh dự kiến vs thực tế. |
| RSK-07 | Performance: SCR-01 chậm khi > 500 vật tư | Low | Medium | Low | Phân trang + indexes. Test load 500 vật tư < 2 giây. |
| RSK-08 | Không đủ tồn kho block subtask done → gián đoạn KTV | Medium | Medium | Medium | Cảnh báo tồn kho thấp sớm (FR-015). Manager nhập kho kịp thời. UI hiển thị rõ lý do block. (Ref: DEC-D23) |
| RSK-09 | FIFO split nhiều lô → performance chậm nếu nhiều lô nhỏ | Low | Low | Low | Tối ưu query: index (warehouse_id, product_id, status, created_at). Thực tế spa ít khi > 10 lô/product. |
| RSK-10 | HSD auto-lock lô đang dùng dở → KTV bất ngờ bị block | Medium | Medium | Medium | Cảnh báo 3 tháng trước HSD (FR-019). Manager chủ động hủy/dùng hết lô sắp hết hạn. |
A8) Success Metrics
| Metric | Baseline (hiện tại) | Target (sau 1 tháng) | Target (sau 3 tháng) | Cách đo |
|---|---|---|---|---|
| % subtask có giá vật tư (unit_price NOT NULL) | 0% | > 80% (sau config) | > 95% | SQL: COUNT(unit_price IS NOT NULL) / COUNT(*) |
| Chênh lệch tồn kho (hệ thống vs kiểm kê) | N/A | < 5% | < 2% | ABS(system - actual) / actual × 100 trung bình per product. Đo được khi Manager kiểm kê. Backlog Phase 2: nhắc nhở kiểm kê định kỳ (PH2-10). |
| % đơn hàng có chi phí vật tư trong sidebar | 0% | > 70% | > 90% | SQL: COUNT(material_cost > 0) / COUNT(orders_with_materials) |
| Thời gian từ tồn thấp → notification | N/A | < 5 phút | < 1 phút | Notification log: created_at - movement.created_at |
| Số incident "tồn kho sai" | N/A | < 3/tháng | < 1/tháng | Jira/feedback tracking |
| Thời gian Admin setup 1 branch (50 products) | N/A | < 2 giờ | < 1 giờ | Manual tracking |
A9) Glossary — Thuật ngữ
| Thuật ngữ (VI) | Thuật ngữ (EN) | Định nghĩa | Phân biệt với |
|---|---|---|---|
| Kho vật tư | Material Warehouse / Backbar | Kho chứa vật tư dùng cho dịch vụ, tồn kho theo đơn vị sử dụng (ml, g, miếng). Mỗi chi nhánh 1 kho. Nhận hàng từ kho chi nhánh qua chuyển kho (v3.0, DEC-D29). | ≠ Kho chi nhánh (kho chính, đơn vị mua: chai/hộp). Kết nối qua phiếu chuyển kho. |
| Đơn vị kho (vật tư) | Stock Unit | Đơn vị tính tồn kho trong kho vật tư (ml, g, miếng) | ≠ Đơn vị mua (chai, hộp, tube) |
| Đơn vị sử dụng | Usage Unit | Đơn vị KTV dùng khi làm dịch vụ (giọt, muỗng, miếng) | ≠ Đơn vị kho (có thể khác, VD: giọt vs ml) |
| Hệ số quy đổi | Conversion Factor (to_stock_factor) | 1 usage unit = X stock units. VD: 1 giọt = 0.05ml | Chiều: usage → stock |
| Giá đơn vị kho | Stock Unit Price | Giá per đơn vị kho, đã tính hao hụt hệ thống. = source_price / (source_quantity × (1 - wastage_rate)) | ≠ Giá nhập (source_price) |
| Giá đơn vị sử dụng | Usage Unit Price | Giá per ĐVT sử dụng. = stock_unit_price × to_stock_factor | ≠ Giá đơn vị kho |
| Hao hụt hệ thống | System Wastage Rate | % vật tư mất do bay hơi, đáy chai, etc. Tính vào đơn giá. Config per product. | ≠ Hao hụt kỹ thuật (KTV đổ bỏ, ghi per subtask) |
| Hao hụt kỹ thuật | Technical Wastage | SL vật tư hao hụt do KTV (đổ bỏ, dùng thừa). Ghi trên subtask, tính vào trừ kho. | ≠ Hao hụt hệ thống (% config) |
| Snapshot giá | Price Snapshot | Giá + hệ số quy đổi copy vào subtask tại thời điểm thêm vật tư. Đơn cũ giữ giá cũ dù admin đổi giá. | ≠ Giá hiện tại (latest price) |
| Running balance | Tồn kho lũy kế | Số dư tồn kho sau mỗi giao dịch (movement). Lưu trực tiếp trên movement → query O(1). | ≠ Computed balance (SUM movements, O(n)) |
| Auto-deduct | Tự động trừ kho | Hệ thống trừ tồn kho vật tư khi subtask chuyển status → done_*. | ≠ Manual deduct (phase sau) |
| Auto-reverse | Tự động hoàn kho | Hệ thống cộng lại tồn kho khi subtask undo done hoặc canceled sau done. | ≠ Auto-deduct |
| ≠ FIFO (đã áp dụng), ≠ Weighted Average | |||
| Kiểm kê | Stock Check / Inventory Count | Admin/Manager đếm thực tế, so sánh với hệ thống, điều chỉnh chênh lệch. | ≠ Nhập kho (thêm hàng mới) |
| Movement | Giao dịch kho | 1 record nhập/xuất/điều chỉnh trong material_stock_movement. Immutable (không sửa, không xóa). | ≠ Stock balance (tồn kho hiện tại) |
| Config active | Cấu hình đang hoạt động | material_price_config có effective_to IS NULL AND deleted_at IS NULL | ≠ Config cũ (đã có effective_to) |
| Sidebar tài chính | Financial Summary Sidebar | Panel bên phải trong chi tiết đơn hàng, hiển thị tổng hợp tài chính: tour, vật tư, commission, etc. | — |
| Lô (Batch) | Batch | Mỗi lần nhập kho = 1 lô riêng, có giá nhập + HSD riêng. Record trong material_batch. | ≠ Stock movement (log giao dịch) |
| FIFO | FIFO (First In First Out) | Xuất lô cũ nhất (nhập trước) trước. Áp dụng cho auto-deduct khi subtask done. | ≠ LIFO, ≠ WAC |
| HSD | Expiry Date | Hạn sử dụng của lô hàng. Cảnh báo 3 tháng trước, auto-lock khi hết hạn. | ≠ Ngày nhập kho |
| Hủy lô | Disposal | Hủy lô lỗi/thu hồi/hết hạn, chuyển status disposed. Tạo disposal movement. Irreversible. | ≠ Xuất kho (deduct) |
| FIFO Split | FIFO Split | 1 lần dùng vượt qua 2+ lô, tách thành nhiều giao dịch (movements). Mỗi giao dịch gắn 1 lô riêng. | Giá snapshot = bình quân gia quyền |
Non-goals (Explicit)
| # | Non-goal | Lý do loại | Phase dự kiến |
|---|---|---|---|
| NG-02 | FIFO đã vào scope. WAC nếu cần sau. | ||
| NG-03 | Override giá per branch (UI form) | Chưa có nhu cầu thực tế. Field branch_id sẵn. Ref: DEC-D09 | Phase 2 |
| NG-04 | Material recipe / BOM (pha trộn) | Spa chưa có nghiệp vụ pha trộn phức tạp | Phase 3 |
| NG-05 | Chuyển kho vật tư giữa chi nhánh | Multi-branch operation chưa cần | Phase 2 |
| NG-06 | Báo cáo chi phí vật tư (report module) | Cần data 1-2 tháng trước khi có ý nghĩa | Phase 2 |
| NG-07 | Export báo cáo vật tư (Excel) | Phụ thuộc NG-06 | Phase 2 |
| NG-08 | Mobile support | Ưu tiên desktop trước | Phase 3 |
| NG-09 | Audit log cho material config changes | Phase 1 không yêu cầu | Phase 2 |
| NG-10 | Optimistic locking cho concurrent editing | Last-write-wins chấp nhận Phase 1 | Phase 2 |
Changelog
| Version | Ngày | Thay đổi | Tác giả |
|---|---|---|---|
| 1.0 | 2026-03-26 | Initial PRD — 18 DECs, 16 FRs, 6 Formulas, 13 Business Rules, 15+ Edge Cases. Dựa trên design doc v1.1. | PO/BA + AI |
| 1.1 | 2026-03-27 | Fix review: +DEC-D19~D21, +FR-017 (import config Excel), sửa FR-004 (confirm global price), FR-008 (batch_import_id), FR-011 (wastage mặc định ẩn), FR-016 (exclude canceled + Manager quyền xem), sync milestones T+20d, +PH2-10/PH2-11. | PO/BA |
| 2.0 | 2026-03-27 | Chuyển sang Batch-based FIFO (DEC-D22~D27). +FR-018~FR-021 (batch, HSD, FIFO xuất kho, hủy lô). Supersede DEC-D04 (Latest Price → FIFO), DEC-D17 (tồn kho âm → block). Sửa FR liên quan. Cập nhật Glossary, Risks, Non-goals. | PO/BA |
| 2.1 | 2026-03-27 | Post-review fixes: +DEC-D28 (Two-Phase Pricing: save=ước tính, done=final FIFO). Sửa FR-010 (Two-Phase Pricing rewrite). FR-007 thêm batch fields (Giá nhập, HSD, Mã lô) + source_type='stock_in'. FR-008 Excel template thêm cột batch. FR-001 chọn auto-create. Bỏ trạng thái tồn kho < 0 (FR-002). | PO/BA |
| 3.0 | 2026-03-27 | Major: Kết nối kho chính. Supersede DEC-D12 → DEC-D29 (kết nối kho chi nhánh). +DEC-D29→D36 (8 decisions: transfer flow, unit conversion, no approval, dual-input, data cleanup, lot selection). Kiến trúc 4 cấp: NCC→Kho tổng→Kho chi nhánh→Kho vật tư. +FR-022 (chuyển kho, Nhóm 3a). PH2-01/NG-01→scope. ASM-04 superseded. Milestones +M5a/M6a. Quy trình 5→6 bước. Executive Summary rewrite. Fix DEC-D05 (Two-Phase Pricing), effort 23d→33d, RACI/AI-04 số bảng, Phase 2 roadmap, FR-018 source_type. | PO/BA |
Hướng dẫn đọc (RACI-based)
| Bạn là... | Đọc gì | Bỏ qua |
|---|---|---|
| PO / Manager / Sếp | Executive Summary → A2 Context (10 mục) → A5 FRs (overview) → Milestones → Backlog Phase sau | dev-spec.md (C sections), qa-test-plan.md |
| Frontend Dev | Z Decision Log → A5 FRs (chi tiết AC + edge cases) → ui-spec.md (SCR-01→SCR-07, wireframes, permission, state matrix) → dev-spec.md (C4 Data Model, C5 API, C11 Tasks) | qa-test-plan.md |
| Backend Dev | Z Decision Log (Z3 Technical) → A5 FRs (FR-012, FR-013 auto-deduct/reverse) → dev-spec.md (C4 Data Model, C5 API, C6 Event Triggers, C7 Migration) | ui-spec.md (B sections) |
| QA | Z Decision Log (Z4) → A5 FRs (tất cả AC + edge cases) → A2 Ví dụ thực tế → qa-test-plan.md | dev-spec.md (C4-C7) |
| UI/UX Designer | A2 Context (giao diện mô phỏng) → ui-spec.md (B1-B9, ASCII wireframes, permission matrix, state matrix, copy text) | dev-spec.md, qa-test-plan.md |
| Sếp / Stakeholder | Executive Summary only (3 bullet points, đủ hiểu feature) | Tất cả sections khác |