---
date: 2026-05-21
type: epic
tags: [epic, product, structure, material, mes]
epic_id: EPIC-004
title: Structure Material — Vật tư cố định theo kết cấu
status: planning
---

# EPIC-004 — Structure Material (Vật tư cố định theo kết cấu)

## Problem Statement

Mỗi **kết cấu sản phẩm (Structure)** đòi hỏi một bộ vật tư tiêu hao cố định để sản xuất — ví dụ: keo dán, đinh vít, bản lề, sơn phủ. Hiện tại hệ thống **không có nơi lưu danh sách vật tư này theo từng kết cấu**, khiến:

- Kỹ thuật viên và quản lý sản xuất phải ghi chép vật tư ngoài hệ thống (file Excel, giấy tờ)
- Khi tạo BOM/Work Order, không có cơ sở để tự động tính định mức vật tư tiêu hao cho kết cấu đó
- Thay đổi kết cấu không kéo theo cập nhật danh sách vật tư → dễ nhầm lẫn, thiếu nguyên liệu tại xưởng

**Người dùng bị ảnh hưởng:** Kỹ thuật viên (người định nghĩa kết cấu), Quản lý sản xuất (người cần biết vật tư cần chuẩn bị), Thủ kho (người cấp phát nguyên vật liệu).

## Business Value

| Lợi ích | Đo lường |
|---------|----------|
| Loại bỏ ghi chép vật tư ngoài hệ thống | 100% danh sách vật tư cố định được lưu trong ERP |
| Nền tảng tính định mức vật tư khi tạo Work Order | Tech Lead có thể dùng danh sách này để tự động populate vật tư tiêu hao |
| Tra cứu nhanh "kết cấu X cần vật tư gì" | Giảm thời gian chuẩn bị NVL trước khi bắt đầu sản xuất |

## Target User

| Người dùng | Vai trò | Nhu cầu |
|-----------|---------|---------|
| Kỹ thuật viên | Quản lý kết cấu | Thêm/sửa/xóa vật tư trong danh sách cố định của kết cấu |
| Quản lý sản xuất | Lập kế hoạch SX | Xem vật tư cần chuẩn bị theo kết cấu được chọn |
| Thủ kho | Cấp phát NVL | Tra cứu định mức để cấp phát đúng, đủ |

## Scope

### In Scope

- Entity mới **StructureMaterial** — bảng liên kết giữa `ProductStructure` và `Material`, có thêm trường `quantity` (định lượng) và `unitId` (đơn vị tính)
- CRUD StructureMaterial:
  - Thêm vật tư vào danh sách của một kết cấu
  - Cập nhật số lượng / đơn vị
  - Xóa vật tư khỏi danh sách
- API liệt kê danh sách vật tư theo `structureId`
- API thêm / cập nhật / xóa một entry vật tư
- Web UI: tab/section "Vật tư" trong màn hình chi tiết kết cấu (nằm trong tab Kết cấu của sản phẩm)
- Validation: không cho thêm trùng cùng một `materialId` trong cùng kết cấu

### Out of Scope

- Tự động tính vật tư khi tạo BOM / Work Order (sẽ là epic riêng)
- Versioning danh sách vật tư (changelog)
- Import từ Excel
- Tích hợp với workstation-inventory hoặc goods-receipt
- Mobile app (app-kingston)

## User Stories

| ID | Story | Acceptance Criteria (summary) | Priority |
|----|-------|-------------------------------|----------|
| US-01 | Là kỹ thuật viên, tôi muốn thêm vật tư vào danh sách cố định của kết cấu để hệ thống biết kết cấu này cần NVL gì | Thêm được StructureMaterial với materialId, quantity, unitId; không trùng lặp materialId trong cùng structure | MUST |
| US-02 | Là kỹ thuật viên, tôi muốn cập nhật số lượng / đơn vị của vật tư trong danh sách để điều chỉnh định mức | Cập nhật quantity và unitId thành công; không ảnh hưởng dữ liệu khác | MUST |
| US-03 | Là kỹ thuật viên, tôi muốn xóa vật tư khỏi danh sách khi không còn dùng nữa | Xóa thành công; không xóa Material gốc | MUST |
| US-04 | Là quản lý sản xuất, tôi muốn xem đầy đủ danh sách vật tư của một kết cấu trước khi tạo Work Order | Hiển thị list vật tư kèm tên, mã, số lượng, đơn vị; empty state rõ ràng | MUST |
| US-05 | Là kỹ thuật viên, tôi không muốn thêm trùng cùng một vật tư vào kết cấu | Hệ thống trả lỗi validation khi materialId đã tồn tại trong structure đó | MUST |

## Acceptance Criteria (chi tiết)

### EPIC-004-AC01 — Thêm vật tư vào kết cấu
**Given** kỹ thuật viên đang xem màn hình chi tiết kết cấu  
**When** nhấn "Thêm vật tư", chọn vật tư từ danh sách Material, nhập số lượng (> 0), chọn đơn vị, nhấn Lưu  
**Then** StructureMaterial mới được tạo và hiển thị trong danh sách vật tư của kết cấu đó

**Error cases:**
- Số lượng ≤ 0 → lỗi "Số lượng phải lớn hơn 0"
- Không chọn vật tư → lỗi "Vui lòng chọn vật tư"
- Vật tư đã tồn tại trong kết cấu → lỗi "Vật tư này đã có trong danh sách"

### EPIC-004-AC02 — Cập nhật vật tư
**Given** danh sách vật tư của kết cấu đang hiển thị  
**When** nhấn Edit trên một dòng, sửa quantity hoặc unitId, nhấn Lưu  
**Then** dữ liệu được cập nhật, danh sách refresh

### EPIC-004-AC03 — Xóa vật tư
**Given** danh sách vật tư của kết cấu đang hiển thị  
**When** nhấn Delete trên một dòng, xác nhận  
**Then** dòng bị xóa; bản ghi `Material` gốc không bị ảnh hưởng

### EPIC-004-AC04 — Xem danh sách vật tư
**Given** người dùng mở màn hình chi tiết kết cấu  
**When** chuyển sang tab/section "Vật tư"  
**Then** hiển thị danh sách với columns: Mã vật tư, Tên vật tư, Số lượng, Đơn vị; hiển thị empty state nếu chưa có vật tư nào

## Affected Areas

| Surface | Thay đổi |
|---------|----------|
| **api-kingston** | Entity mới `StructureMaterial`; use case CRUD; API endpoints mới dưới `/product-structures/:id/materials` |
| **web-kingston** | Tab/section "Vật tư" trong màn hình chi tiết kết cấu (nằm trong Product detail → tab Kết cấu → detail kết cấu) |
| **Database** | Bảng `structure_material` mới (structureId, materialId, quantity, unitId, timestamps) |
| **app-kingston** | Không thuộc scope |

## Dependencies

| Phụ thuộc | Trạng thái | Owner | Ghi chú |
|-----------|-----------|-------|---------|
| EPIC-001 — ProductStructure entity đã tồn tại | ✅ Done | Nghĩa | API `/product-structures` đã có; entity đã deployed |
| Material entity đã tồn tại | ✅ Done | Nghĩa | `/materials` endpoint đã có; dùng để lookup |
| MasterData cho unitId | ✅ Done | Nghĩa | Unit đã có trong master-data, cùng pattern với Material.unitId |

## Epic Phases

| Phase | Artifact | Status |
|-------|----------|--------|
| Planning (PO) | `EPIC-004.md` | 🟡 In progress |
| PRD | `PRD.md` | ⬜ Chưa bắt đầu |
| Tech Design | `TECH-DESIGN.md`, `IMPL-PLAN-API.md`, `IMPL-PLAN-WEB.md` | ⬜ Chưa bắt đầu |
| Implementation API | `impl-report-api.md` | ⬜ Chưa bắt đầu |
| Implementation Web | `impl-report-web.md` | ⬜ Chưa bắt đầu |

## Risks & Mitigations

| Rủi ro | Mức độ | Mitigation |
|--------|--------|-----------|
| Unique constraint materialId+structureId có thể bị vi phạm nếu migrate dữ liệu cũ | Thấp | Không có dữ liệu cũ cần migrate (entity hoàn toàn mới) |
| Frontend cần lazy-load danh sách Material khi mở dropdown | Thấp | Dùng paginated search API đã có của `/materials` |
| unitId có thể khác với unitId của Material gốc (người dùng dùng đơn vị đặc thù cho kết cấu) | Trung bình | Cho phép override unitId độc lập với Material.unitId; document rõ trong PRD |
