- Given Kế toán đang ở trang Kho → Danh mục với quyền
inventory.manage - When Nhập mã hàng, tên hàng, đơn vị, danh mục, giá bán, giá vốn, mức cảnh báo tồn và gửi
- Then Mặt hàng được lưu ở trạng thái hoạt động, mã hàng là duy nhất trong phạm vi trung tâm, và mặt hàng xuất hiện trong danh sách chọn khi phát hàng ở mọi chi nhánh.
PRD 08 · Kho
Sách, tài liệu và vật phẩm được lưu kho theo từng chi nhánh, phát cho học viên gắn với một dòng học phí hoặc một lượt phát tài liệu, kèm cảnh báo sắp hết hàng. Mô-đun thuộc Giai đoạn 2; hiện chưa có backend.
Xuất xứ / Prompt / Giới hạn
Tài liệu do trợ lý AI soạn trong một phiên làm việc ngày 10/09/2026, theo yêu cầu gốc được trích nguyên văn ở trang mục lục; mục Thiết kế đề xuất được bổ sung ngày 16/09/2026. Đây là bản đề xuất để đội thảo luận, chưa phải quyết định của đội sản phẩm.
Nguồn
- Nghiên cứu công khai về đối thủ cạnh tranh (PSE ONE, pse4.one), mã nguồn
center-crm-frontendvà đặc tả OpenAPI/v3trên staging (182 endpoint), tính đến ngày 10/09/2026. - Nhãn existing / extend / new trong trang được xác định bằng cách đối chiếu với đặc tả
/v3trên staging tại thời điểm trên.
Giới hạn
- Cần xác minh khi khảo sát trực tiếp trong ứng dụng, vì đăng nhập tự động vào sản phẩm tham chiếu chưa được cấp phép trong phiên soạn:
- Cách đối thủ liên kết một lượt phát đồ ("phát đồ dùng học tập") với một chứng từ thu: cùng một phiếu hay tách riêng.
- Tồn kho được theo dõi theo chi nhánh hay theo toàn trung tâm, và có tồn tại chức năng chuyển kho hay không.
- Các trường chính xác trong danh mục sản phẩm của họ (đơn vị, giá vốn, giá bán, nhà cung cấp) và cảnh báo sắp hết hàng là một báo cáo hay một thông báo.
- Việc trả lại và hàng hỏng có được mô hình hoá hay không.
Thiết kế đề xuất
Kho là một trang duy nhất dưới mục Tài chính với bốn tab (Tồn kho · Nhập xuất · Danh mục · Sắp hết); mọi thay đổi tồn kho đều đi qua phiếu (nhập, chuyển, điều chỉnh) hoặc lượt phát cho học viên, và điều quan trọng nhất người đọc nên thấy là không có ô "số lượng" nào sửa trực tiếp được.








1Mục đích
Trung tâm bán hoặc phát sách giáo khoa, sách bài tập, đồng phục và văn phòng phẩm cho học viên. Hiện tại việc này được theo dõi bằng Excel hoặc không được theo dõi, và kế toán không thể đối soát tồn kho với chứng từ thu. Trường Số duy trì một sổ kho theo từng chi nhánh, cho phép nhân viên phát mặt hàng cho học viên trong một bước, và hiển thị những mặt hàng đang sắp hết. Mô-đun này được thiết kế cố tình đơn giản: đây là một sổ kho có danh mục hàng hoá, không phải một hệ thống POS bán lẻ.
Mục tiêu
- Mọi mặt hàng xuất khỏi kho đều được gắn với một học viên, một lượt phát cho lớp, một lượt chuyển kho hoặc một lượt xoá sổ.
- Tồn kho theo từng chi nhánh luôn có thể tính được từ sổ kho; không có trường "số lượng" nào có thể sửa trực tiếp.
- Phát một mặt hàng có tính phí sẽ tạo ra một dòng tài chính (PRD 05) để doanh thu và tồn kho khớp với nhau.
Không thuộc phạm vi
- Đơn mua hàng từ nhà cung cấp, quét mã vạch, số sê-ri, nhiều kho trong cùng một chi nhánh.
- Kế toán giá vốn (FIFO/bình quân). Chỉ lưu một giá vốn tham chiếu để phục vụ báo cáo lợi nhuận.
- Bán hàng cho đối tượng không phải học viên. Đối tượng nhận của một lượt phát luôn là một học viên, một lớp hoặc một chi nhánh.
2Đối tượng sử dụng và vai trò
| Đối tượng | Mã vai trò | Dùng kho cho |
|---|---|---|
| Kế toán | accountant | Nhập kho, kiểm kê, báo cáo giá trị tồn kho, đối soát với chứng từ thu. |
| Quản lý chi nhánh | branch_manager | Duyệt xoá sổ và chuyển kho, theo dõi danh sách hàng sắp hết. |
| Giáo vụ | academic_admin | Phát sách cho học viên mới nhập học, phát tài liệu cho lớp. |
| Chủ trung tâm | owner | Xem tồn kho và lợi nhuận trên toàn bộ chi nhánh. |
| Giáo viên | teacher | Chỉ xem: thấy được học viên nào trong lớp đã nhận sách. |
3User story và tiêu chí chấp nhận
- Given Một mặt hàng đang hoạt động và một chi nhánh
- When Ghi sổ một phiếu nhập kho có một hoặc nhiều dòng (mặt hàng, số lượng > 0, giá vốn đơn vị, tham chiếu)
- Then Một phát sinh kho loại
inđược ghi cho mỗi dòng, tồn kho của chi nhánh đó tăng lên, và phiếu chuyển sang chỉ đọc.
- Given Một học viên có lượt ghi danh đang hoạt động và tồn kho ≥ số lượng yêu cầu
- When Chọn học viên, mặt hàng, số lượng và cờ "tính phí"
- Then Một phát sinh loại
issueđược ghi, tồn kho giảm, hồ sơ học viên hiển thị mặt hàng trong mục Tài liệu, và nếu có tính phí, một dòng học phí loạimaterialđược tạo trong PRD 05 với số tiền = số lượng × giá bán. - Given Tồn kho thấp hơn số lượng yêu cầu
- When Xác nhận
- Then Biểu mẫu bị chặn với thông báo "Không đủ tồn kho (còn N)" và không có phát sinh nào được ghi.
- Given Một lớp có N học viên đang học
- When Chọn lớp, mặt hàng, và bỏ chọn những học viên đã nhận rồi
- Then Một phát sinh cho mỗi học viên được chọn sẽ được ghi trong một giao dịch duy nhất; nếu tồn kho không đủ cho tất cả, hộp thoại hiển thị số lượng còn thiếu và cho phép chọn chỉ phát cho K học viên đầu tiên hoặc huỷ.
- Given Người dùng có quyền
inventory.managetrên cả hai chi nhánh, hoặc là chủ trung tâm - When Ghi sổ một phiếu chuyển kho
- Then Một cặp phát sinh
transfer_outtại A vàtransfer_intại B được ghi đồng thời với cùng mộtdocument_id.
- Given Một phiếu kiểm kê ghi số lượng đã kiểm
- When Ghi sổ phiếu kiểm kê
- Then Với mỗi mặt hàng có số lượng kiểm khác với tồn kho, một phát sinh
adjustvới mức chênh lệch (có dấu) và lý do bắt buộc được ghi; các điều chỉnh vượt quá 10 đơn vị hoặc 2.000.000 VND cần quản lý chi nhánh phê duyệt (trạng tháipendingcho đến khi được duyệt).
- Given Mức cảnh báo tồn đã được thiết lập cho một mặt hàng
- When Tồn kho tại một chi nhánh ≤ mức cảnh báo tồn
- Then Mặt hàng xuất hiện trong tab Sắp hết với nhãn Sắp hết, và thông báo trong ứng dụng lúc 08:00 hằng ngày liệt kê tất cả các mặt hàng này cho người dùng có quyền
inventory.manage.
- Given Một phát sinh
issueđã có sẵn - When Chọn Trả lại trên dòng đó và chọn tình trạng sách (mới / đã dùng)
- Then Một phát sinh
returntham chiếu đến phát sinh gốc được ghi; sách trả lại còn mới làm tăng tồn kho, sách trả lại đã dùng được chuyển vào số lượngdamaged; nếu phát sinh gốc có tính phí, dòng tài chính được đánh dấu để ghi giảm trong PRD 05 (không tự động hoàn tiền).
4Màn hình
Bộ chọn chi nhánh (mặc định là chi nhánh của người dùng; chủ trung tâm thấy "Tất cả chi nhánh"). Ba ô KPI: Mặt hàng đang tồn kho, Giá trị tồn kho (tổng tồn kho × giá vốn), Mặt hàng sắp hết. Các tab: Tồn kho · Nhập xuất · Danh mục · Sắp hết.
Bảng tồn kho
| Cột | Nguồn | Ghi chú |
|---|---|---|
| SKU | item.sku | monospace, có thể sắp xếp |
| Tên hàng | item.name | liên kết đến bảng chi tiết mặt hàng |
| Danh mục | item.category | chip lọc |
| Đơn vị | item.unit | |
| Tồn | tổng các phát sinh kho theo từng chi nhánh | canh phải; màu đỏ khi ≤ mức cảnh báo tồn |
| Hỏng | số lượng hỏng | ẩn khi bằng 0 |
| Giá bán | item.sale_price | VND, có dấu phân cách nghìn |
| Giá trị tồn | tồn kho × giá vốn (cost_price) | chỉ chủ trung tâm/kế toán |
| Thao tác | Nhập · Xuất · Điều chỉnh |
Bộ lọc: tìm kiếm (theo mã hàng hoặc tên), danh mục, cờ "chỉ hàng sắp hết". Trạng thái trống: "Chưa có hàng hoá. Tạo danh mục trước, sau đó nhập kho." với nút chính Tạo mặt hàng. Phân trang 50 dòng.
Bảng phát sinh kho
| Cột | Ghi chú |
|---|---|
| Thời gian | created_at, dd/MM/yyyy HH:mm |
| Loại | chip: Nhập (ok) · Xuất (exc) · Chuyển (grey) · Điều chỉnh (late) · Trả lại (grey) |
| Mặt hàng | tên + SKU |
| SL | số lượng có dấu |
| Đối tượng | tên học viên / lớp / chi nhánh / tham chiếu nhà cung cấp |
| Chứng từ | mã chứng từ, liên kết |
| Người thực hiện | tên hiển thị (display_name) của người thực hiện |
Bộ lọc: khoảng thời gian, loại, mặt hàng, học viên. Xuất CSV (quyền inventory.export).
| Trường | Kiểu dữ liệu | Kiểm tra hợp lệ |
|---|---|---|
| Mã hàng (sku) | text 3–32 | bắt buộc, duy nhất trong phạm vi trung tâm, chỉ chữ hoa A–Z0–9- |
| Tên hàng (name) | text 2–120 | bắt buộc |
| Danh mục (category) | chọn từ danh sách của trung tâm; thêm nhanh tại chỗ | bắt buộc |
| Đơn vị (unit) | chọn: cuốn, bộ, cái, hộp | bắt buộc |
| Giá bán (sale_price) | tiền tệ VND | ≥ 0 |
| Giá vốn (cost_price) | tiền tệ VND | ≥ 0; ẩn với vai trò academic_admin |
| Mức cảnh báo (reorder_level) | số nguyên | ≥ 0, mặc định là 5 |
| Tính phí khi phát (chargeable_default) | công tắc bật/tắt | mặc định bật đối với sách |
| Gắn với khoá (course_ids) | chọn nhiều khoá học | không bắt buộc; quyết định danh sách "tài liệu gợi ý" khi ghi danh |
| Trạng thái | active / inactive | mặt hàng ở trạng thái inactive không thể được phát nhưng vẫn giữ lại lịch sử |
Lỗi: mã hàng trùng hiển thị thông báo tại chỗ "Mã hàng đã tồn tại". Không có tùy chọn xoá; chỉ có thể vô hiệu hoá.
Phần đầu: loại phiếu, chi nhánh (từ/đến cho phiếu chuyển kho), ngày (mặc định là hiện tại, không thể chọn ngày trong tương lai), tham chiếu (văn bản tự do, ví dụ hoá đơn nhà cung cấp), ghi chú. Các dòng: chọn mặt hàng (tìm theo mã hàng/tên), số lượng, giá vốn đơn vị (chỉ với phiếu nhập kho), lý do (chỉ với phiếu điều chỉnh, chọn: hỏng, mất, kiểm kê, khác). Phần chân: số dòng, tổng số lượng, tổng giá trị. Nút Ghi sổ để ghi phiếu; sau khi ghi, phiếu chuyển sang chỉ đọc và hiển thị mã NK-YYYYMM-#### / CK-… / DC-….
Các trường: học viên (tìm kiếm; điền sẵn theo ngữ cảnh), mặt hàng, số lượng (mặc định 1), công tắc tính phí (mặc định lấy theo mặt hàng), giá (có thể sửa khi tính phí, quyền inventory.override_price), ghi chú. Hiển thị tồn kho ngay bên cạnh mặt hàng. Khi xác nhận, hiển thị thông báo nổi "Đã phát 1 cuốn Family and Friends 2 cho Nguyễn An" kèm liên kết đến dòng tài chính khi được tạo.
Bảng: ngày, mặt hàng, số lượng, tính phí, trạng thái thanh toán (theo dòng tài chính), đã trả lại. Thao tác Trả lại trên từng dòng. Trạng thái trống: "Học viên chưa nhận tài liệu nào."
5Quy tắc nghiệp vụ
- Tồn kho(chi nhánh, mặt hàng) = Σ số lượng của các phát sinh khớp chi nhánh đó; không có cột số lượng nào được lưu trữ và có thể sửa trực tiếp.
- Phiếu đã ghi sổ là bất biến. Các điều chỉnh sai sót là các phát sinh
adjustmới, tham chiếu đến phát sinh gốc. - Một lượt phát (
issue) yêu cầu học viên có trạng thái Đang học, Học thử hoặc Bảo lưu (PRD 02); học viên đã nghỉ học không thể nhận mặt hàng. - Một lượt phát có tính phí tạo ra chính xác một dòng tài chính; việc xoá dòng đó bị chặn khi phát sinh vẫn còn tồn tại, và một lượt trả lại sẽ đánh dấu dòng đó thay vì xoá.
- Các điều chỉnh vượt ngưỡng (có thể cấu hình theo từng trung tâm, mặc định 10 đơn vị hoặc 2.000.000 VND) cần được phê duyệt bởi
branch_managerhoặcowner. - Tồn kho âm không bao giờ được phép; API sẽ từ chối với lỗi
INSUFFICIENT_STOCK. - Giá vốn chỉ hiển thị cho
ownervàaccountant. - Phiếu được đánh số theo từng trung tâm, theo từng tháng; được phép có khoảng trống trong dãy số (do bản nháp bị huỷ).
6Mô hình dữ liệu
| Thực thể | Trường | Kiểu dữ liệu | Ghi chú |
|---|---|---|---|
| inventory_item | id | uuid | PK |
| tenant_id | int64 | = school_id | |
| sku | text | unique (tenant_id, sku) | |
| name | text | ||
| category_id | uuid | FK inventory_category | |
| unit | text | ||
| sale_price | numeric(14,0) | VND | |
| cost_price | numeric(14,0) | ||
| reorder_level | int | ||
| chargeable_default | bool | ||
| course_ids | uuid[] | FK course (PRD 03) | |
| is_active, created_at, updated_at | |||
| inventory_category | id | uuid | |
| tenant_id | int64 | ||
| name | text | unique per tenant | |
| position | int | ||
| inventory_document | id | uuid | |
| tenant_id | int64 | ||
| code | text | NK/XK/CK/DC-YYYYMM-#### | |
| type | enum in|issue|transfer|adjust|return | ||
| branch_id | uuid | FK branch (PRD 10); đối với phiếu chuyển kho là chi nhánh nguồn (from) | |
| to_branch_id | uuid null | chỉ dùng cho phiếu chuyển kho | |
| status | enum draft|pending|posted|cancelled | ||
| reference, note | text | ||
| approved_by, approved_at | có thể null | ||
| created_by, posted_at | |||
| inventory_movement | id | uuid | |
| document_id | uuid | FK | |
| item_id | uuid | FK | |
| branch_id | uuid | chi nhánh có tồn kho bị thay đổi | |
| quantity | int | có dấu; in/transfer_in/return(new) > 0; issue/transfer_out/adjust(-) < 0 | |
| unit_cost | numeric | chỉ dùng cho phiếu nhập kho | |
| student_id | int64 null | FK student (issue/return) | |
| class_id | int64 null | phát cho lớp | |
| tuition_line_id | uuid null | FK dòng tài chính khi có tính phí | |
| condition | enum new|damaged null | dùng cho trả lại | |
| reason, created_by, created_at |
Chỉ mục: (tenant_id, branch_id, item_id) trên bảng movement để tính tổng tồn kho; có thể dùng một materialized view inventory_onhand được làm mới mỗi khi có ghi mới.
7Hợp đồng API
Tất cả endpoint đều là NEW (mới). Tiền tố /v3/school/inventory; header xác thực giống các endpoint /v3 hiện có (Auth Token); X-Locale được tôn trọng cho nhãn hiển thị. Response sử dụng các envelope DataResponse / ListResponse hiện có kèm Pagination.
| Method | Path | Request | Response | Status |
|---|---|---|---|---|
| GET | /v3/school/inventory/items | q, category_id, is_active, branch_id (thêm on_hand), page, limit | danh sách InventoryItem {..., on_hand, damaged, low_stock} | new |
| POST | /v3/school/inventory/items | {sku,name,category_id,unit,sale_price,cost_price,reorder_level,chargeable_default,course_ids} | InventoryItem | new |
| PATCH | /v3/school/inventory/items/{id} | một phần | InventoryItem | new |
| GET/POST | /v3/school/inventory/categories | {name} | Category[] | new |
| GET | /v3/school/inventory/stock | branch_id (bắt buộc trừ khi là owner), q, low_only | [{item, on_hand, damaged, value}] | new |
| GET | /v3/school/inventory/documents | type, branch_id, from, to, status, page | Document[] kèm số lượng dòng | new |
| POST | /v3/school/inventory/documents | {type, branch_id, to_branch_id?, reference, note, lines:[{item_id, quantity, unit_cost?, reason?, student_id?, class_id?, chargeable?, price?}]} | Document kèm movements; 409 INSUFFICIENT_STOCK {item_id, on_hand} | new |
| POST | /v3/school/inventory/documents/{id}/approve | Document (pending → posted) | new | |
| POST | /v3/school/inventory/documents/{id}/cancel | {reason} | chỉ áp dụng cho draft/pending | new |
| POST | /v3/school/inventory/issue | {branch_id, student_id | class_id, item_id, quantity, chargeable, price?, exclude_student_ids?} | {document, movements[], tuition_line_ids[]} | new |
| POST | /v3/school/inventory/movements/{id}/return | {condition, note} | Movement | new |
| GET | /v3/school/inventory/movements | branch_id, item_id, student_id, type, from, to, page | Movement[] | new |
| GET | /v3/school/students/{id}/materials | Movement[] kết hợp với trạng thái dòng học phí | new | |
| GET | /v3/school/inventory/export | cùng bộ lọc như stock hoặc movements, format=csv | tệp | new |
Các endpoint hiện có được tái sử dụng: GET /v3/teacher/school-finance/students và /v3/teacher/students/quick-search cho ô chọn học viên; GET /v3/teacher/classes cho ô chọn lớp; việc tạo dòng tài chính đi qua endpoint học phí của PRD 05 (POST /v3/teacher/school-finance/tuitions, EXISTING) với title = item name cho đến khi có một dòng chi tiết (line item) có kiểu riêng.
8Phân quyền
| Mã quyền | owner | branch_manager | accountant | academic_admin | admissions | teacher |
|---|---|---|---|---|---|---|
inventory.view | có | có | có | có | không | theo lớp |
inventory.manage (danh mục, nhập kho, điều chỉnh, chuyển kho) | có | có | có | không | không | không |
inventory.issue | có | có | có | có | không | không |
inventory.approve | có | có | không | không | không | không |
inventory.override_price | có | có | có | không | không | không |
inventory.view_cost | có | không | có | không | không | không |
inventory.export | có | có | có | không | không | không |
Phạm vi theo chi nhánh: mọi quyền được đánh giá dựa trên các chi nhánh mà người dùng được gán (PRD 10). Chủ trung tâm mặc định có quyền trên tất cả chi nhánh.
9Sự kiện phân tích
| Sự kiện | Thuộc tính |
|---|---|
| inventory_item_created | tenant_id, category, chargeable_default |
| inventory_document_posted | type, line_count, total_qty, total_value, branch_id |
| inventory_issue | target (student|class), item_id, quantity, chargeable, created_finance_line |
| inventory_issue_blocked | reason (insufficient_stock|student_status) |
| inventory_return | condition |
| inventory_low_stock_viewed | count |
| inventory_export | scope, rows |
10Trường hợp biên
- Hai giáo vụ cùng phát cuốn sách cuối cùng vào cùng lúc: API tuần tự hoá theo (chi nhánh, mặt hàng) và lệnh gọi thứ hai nhận về mã lỗi 409.
- Mặt hàng bị vô hiệu hoá trong khi một phiếu nháp đang tham chiếu đến nó: việc ghi sổ thất bại với lỗi
ITEM_INACTIVEvà dòng liên quan được đánh dấu nổi bật. - Học viên chuyển sang chi nhánh khác sau khi đã nhận sách: phát sinh vẫn giữ nguyên chi nhánh ban đầu; tab Tài liệu hiển thị tên chi nhánh đó.
- Giá thay đổi sau khi đã phát hàng: dòng tài chính vẫn giữ giá tại thời điểm phát hàng.
- Phát cho lớp có 40 học viên nhưng tồn kho chỉ còn 38: hệ thống đề xuất phát một phần, sắp xếp theo tên hiển thị của học viên; số lượng còn thiếu được liệt kê trong thông báo nổi.
- Chuyển kho đến cùng một chi nhánh bị từ chối ở cả phía client và phía server.
- Nhập số dư đầu kỳ: sử dụng một phiếu nhập kho với tham chiếu "Số dư đầu kỳ"; việc nhập số dư đầu kỳ bằng CSV là một phần của bộ công cụ di chuyển dữ liệu trong PRD 11.
11Câu hỏi còn mở
- Doanh thu bán tài liệu có nên chịu thuế VAT tách riêng với học phí không (sách 5%, học phí miễn thuế)? Ảnh hưởng đến loại dòng trong PRD 05.
- Trung tâm có cần bước trung gian phát theo giáo viên (giáo viên nhận 10 cuốn để mang lên lớp) hay chỉ cần phát ở cấp độ lớp là đủ?
- Có cần danh sách nhà cung cấp ở giai đoạn 2 không, hay chỉ cần một tham chiếu dạng văn bản tự do là đủ?
Trường Số (truongso.net) · Nội bộ FLYER