From f7ee984d048c17e32efbe16b877f963d9b20940d Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Wed, 19 Aug 2026 12:50:46 +0800 Subject: [PATCH] docs: add qc material month import design and implementation plan Co-authored-by: Cursor --- .../2026-08-19-qc-material-month-import.md | 810 ++++++++++++++++++ ...6-08-19-qc-material-month-import-design.md | 235 +++++ 2 files changed, 1045 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-19-qc-material-month-import.md create mode 100644 docs/superpowers/specs/2026-08-19-qc-material-month-import-design.md diff --git a/docs/superpowers/plans/2026-08-19-qc-material-month-import.md b/docs/superpowers/plans/2026-08-19-qc-material-month-import.md new file mode 100644 index 00000000..bd7b38ce --- /dev/null +++ b/docs/superpowers/plans/2026-08-19-qc-material-month-import.md @@ -0,0 +1,810 @@ +# 质检物料库 · 月初固定导入 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在质检物料库提供可维护的「月初入库模板」与「月初导入」预览确认入库,减少每月固定物品的重复录入。 + +**Architecture:** 新增租户级模板表与 CRUD/preview/confirm API;confirm 同事务 `ensure` 物资后复用现有 `batch_stock_in`。前端在批次列表工具栏增加两个入口,弹窗拆成独立组件,避免继续膨胀 `index.vue`。 + +**Tech Stack:** FastAPI / SQLAlchemy / Pydantic v2 / Vue 3 / Element Plus;权限沿用 `biz:qcMaterial:list`。 + +**Spec:** `docs/superpowers/specs/2026-08-19-qc-material-month-import-design.md` + +--- + +## File map + +| File | Responsibility | +|------|----------------| +| Create: `docs/sql/2026-08-19-qc-material-month-template.sql` | 建表 SQL | +| Modify: `backend/app/models/qc_material.py` | 增加 `BizQcMaterialMonthTemplate` | +| Create: `backend/app/crud/qc_material_stock_in_no.py` | 入库编号生成纯函数 + 查库取下一序号 | +| Create: `backend/tests/test_qc_material_stock_in_no.py` | 编号生成单测 | +| Create: `backend/app/crud/qc_material_month_template.py` | 模板 CRUD、preview、confirm | +| Create: `backend/tests/test_qc_material_month_template.py` | 模板校验 / ensure 物资纯逻辑单测 | +| Modify: `backend/app/crud/qc_material.py` | `get_material_by_code`;`create_material(..., commit=True)` 支持不提交 | +| Modify: `backend/app/api/qc.py` | 模板 list/create/update/delete/preview/confirm 路由与 Body | +| Create: `web-admin/src/api/qc-material-month-template.ts` | 前端 API | +| Create: `web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthTemplateDialog.vue` | 维护模板弹窗 | +| Create: `web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthImportDialog.vue` | 月初导入预览弹窗 | +| Modify: `web-admin/src/views/quality-inspection/qc-material/index.vue` | 工具栏按钮 + 挂载两弹窗 | + +--- + +### Task 1: SQL 建表 + +**Files:** +- Create: `docs/sql/2026-08-19-qc-material-month-template.sql` + +- [ ] **Step 1: Write SQL** + +```sql +-- 质检物料库:月初入库模板 +-- MySQL 5.7 / utf8mb4;可重复执行需人工判断表是否已存在 + +CREATE TABLE IF NOT EXISTS `biz_qc_material_month_template` ( + `id` varchar(64) NOT NULL COMMENT '主键ID', + `material_id` varchar(64) DEFAULT NULL COMMENT '关联 biz_qc_material.id,可空', + `material_code` varchar(64) NOT NULL COMMENT '物料编码', + `material_name` varchar(200) NOT NULL COMMENT '物料名称', + `spec_model` varchar(200) DEFAULT NULL COMMENT '规格型号', + `unit` varchar(32) DEFAULT NULL COMMENT '单位', + `catalog_no` varchar(128) NOT NULL COMMENT '货号', + `batch_no` varchar(128) NOT NULL COMMENT '批号', + `manufacturer` varchar(200) NOT NULL COMMENT '厂家', + `unit_price` decimal(18,2) NOT NULL COMMENT '单价(元)', + `amount` decimal(18,2) NOT NULL COMMENT '金额(元)', + `in_qty` decimal(18,4) NOT NULL COMMENT '默认入库数量', + `expire_date` date NOT NULL COMMENT '默认过期日期', + `tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', + `order_num` int(11) DEFAULT 0 COMMENT '排序号', + `remark` varchar(512) DEFAULT NULL COMMENT '备注', + `create_by` varchar(64) DEFAULT NULL COMMENT '创建人id', + `create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` varchar(64) DEFAULT NULL COMMENT '更新人id', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_qc_mmt_tenant` (`tenant_no`, `del_flag`, `order_num`), + KEY `idx_qc_mmt_material` (`material_id`, `del_flag`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='质检物料月初入库模板'; +``` + +- [ ] **Step 2: Commit** + +```bash +git add docs/sql/2026-08-19-qc-material-month-template.sql +git commit -m "$(cat <<'EOF' +docs(sql): add qc material month import template table + +EOF +)" +``` + +--- + +### Task 2: SQLAlchemy 模型 + +**Files:** +- Modify: `backend/app/models/qc_material.py` +- Modify: `backend/app/models/models.py` + +- [ ] **Step 1: Append model class**(放在 `BizQcMaterial` 之后、`BizQcMaterialStockBatch` 之前) + +```python +class BizQcMaterialMonthTemplate(Base): + __tablename__ = "biz_qc_material_month_template" + + id = Column(String(64), primary_key=True, comment="主键ID") + material_id = Column(String(64), nullable=True, comment="关联物料ID") + material_code = Column(String(64), nullable=False, comment="物料编码") + material_name = Column(String(200), nullable=False, comment="物料名称") + spec_model = Column(String(200), nullable=True, comment="规格型号") + unit = Column(String(32), nullable=True, comment="单位") + catalog_no = Column(String(128), nullable=False, comment="货号") + batch_no = Column(String(128), nullable=False, comment="批号") + manufacturer = Column(String(200), nullable=False, comment="厂家") + unit_price = Column(Numeric(18, 2), nullable=False, comment="单价(元)") + amount = Column(Numeric(18, 2), nullable=False, comment="金额(元)") + in_qty = Column(Numeric(18, 4), nullable=False, comment="默认入库数量") + expire_date = Column(Date, nullable=False, comment="默认过期日期") + tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") + order_num = Column(Integer, nullable=True, comment="排序号") + remark = Column(String(512), nullable=True, comment="备注") + create_by = Column(String(64), nullable=True, comment="创建人id") + create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID") + create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间") + update_by = Column(String(64), nullable=True, comment="更新人id") + update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间") + del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记") +``` + +- [ ] **Step 2: 在 `backend/app/models/models.py` 末尾 import 旁补充导出** + +找到: + +```python +from app.models.qc_material import BizQcMaterial, BizQcMaterialStockBatch, BizQcMaterialUsage # noqa: E402,F401 +``` + +改为: + +```python +from app.models.qc_material import ( # noqa: E402,F401 + BizQcMaterial, + BizQcMaterialMonthTemplate, + BizQcMaterialStockBatch, + BizQcMaterialUsage, +) +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/app/models/qc_material.py backend/app/models/models.py +git commit -m "$(cat <<'EOF' +feat(backend): add BizQcMaterialMonthTemplate model + +EOF +)" +``` + +--- + +### Task 3: 入库编号生成(TDD) + +**Files:** +- Create: `backend/app/crud/qc_material_stock_in_no.py` +- Create: `backend/tests/test_qc_material_stock_in_no.py` + +- [ ] **Step 1: Write the failing test** + +```python +# backend/tests/test_qc_material_stock_in_no.py +"""质检物料入库编号:RK + YYYYMMDD + 3位序号。""" +import unittest +from datetime import date + +from app.crud.qc_material_stock_in_no import ( + format_stock_in_no, + max_seq_from_stock_in_nos, + next_stock_in_nos, +) + + +class TestFormatStockInNo(unittest.TestCase): + def test_format(self): + self.assertEqual(format_stock_in_no(date(2026, 8, 19), 1), "RK20260819001") + self.assertEqual(format_stock_in_no(date(2026, 8, 19), 12), "RK20260819012") + self.assertEqual(format_stock_in_no(date(2026, 8, 19), 999), "RK20260819999") + + +class TestMaxSeq(unittest.TestCase): + def test_empty(self): + self.assertEqual(max_seq_from_stock_in_nos("RK20260819", []), 0) + + def test_parses_matching_prefix(self): + nos = ["RK20260819001", "RK20260819007", "OTHER", "RK20260818099"] + self.assertEqual(max_seq_from_stock_in_nos("RK20260819", nos), 7) + + def test_ignores_bad_suffix(self): + nos = ["RK20260819ABC", "RK20260819100"] + self.assertEqual(max_seq_from_stock_in_nos("RK20260819", nos), 100) + + +class TestNextStockInNos(unittest.TestCase): + def test_allocates_contiguous(self): + got = next_stock_in_nos(date(2026, 8, 19), count=3, existing_nos=["RK20260819002"]) + self.assertEqual(got, ["RK20260819003", "RK20260819004", "RK20260819005"]) + + def test_zero_count(self): + self.assertEqual(next_stock_in_nos(date(2026, 8, 19), count=0, existing_nos=[]), []) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd backend && source venv/bin/activate && pytest tests/test_qc_material_stock_in_no.py -v +``` + +Expected: FAIL(模块不存在) + +- [ ] **Step 3: Implement** + +```python +# backend/app/crud/qc_material_stock_in_no.py +"""质检物料入库编号生成:RK + YYYYMMDD + 3位序号。""" +from __future__ import annotations + +import re +from datetime import date +from typing import Iterable, List + +from sqlalchemy.orm import Session + +from app.models.qc_material import BizQcMaterialStockBatch + +_SUFFIX_RE = re.compile(r"^(\d{3})$") + + +def format_stock_in_no(d: date, seq: int) -> str: + if seq < 1 or seq > 999: + raise ValueError(f"入库编号序号超出范围: {seq}") + return f"RK{d.strftime('%Y%m%d')}{seq:03d}" + + +def prefix_for_date(d: date) -> str: + return f"RK{d.strftime('%Y%m%d')}" + + +def max_seq_from_stock_in_nos(prefix: str, nos: Iterable[str]) -> int: + max_seq = 0 + for raw in nos: + s = (raw or "").strip() + if not s.startswith(prefix): + continue + suffix = s[len(prefix) :] + m = _SUFFIX_RE.match(suffix) + if not m: + continue + max_seq = max(max_seq, int(m.group(1))) + return max_seq + + +def next_stock_in_nos( + d: date, + *, + count: int, + existing_nos: Iterable[str], +) -> List[str]: + if count <= 0: + return [] + prefix = prefix_for_date(d) + start = max_seq_from_stock_in_nos(prefix, existing_nos) + 1 + if start + count - 1 > 999: + raise ValueError("当日入库编号序号已用尽(上限 999)") + return [format_stock_in_no(d, start + i) for i in range(count)] + + +def list_existing_stock_in_nos_for_prefix( + db: Session, *, tenant_no: str, prefix: str +) -> List[str]: + like = f"{prefix}%" + rows = ( + db.query(BizQcMaterialStockBatch.stock_in_no) + .filter( + BizQcMaterialStockBatch.tenant_no == tenant_no, + BizQcMaterialStockBatch.del_flag == 0, + BizQcMaterialStockBatch.stock_in_no.like(like), + ) + .all() + ) + return [r[0] for r in rows if r[0]] + + +def allocate_stock_in_nos( + db: Session, + *, + tenant_no: str, + stock_in_date: date, + count: int, +) -> List[str]: + prefix = prefix_for_date(stock_in_date) + existing = list_existing_stock_in_nos_for_prefix(db, tenant_no=tenant_no, prefix=prefix) + return next_stock_in_nos(stock_in_date, count=count, existing_nos=existing) +``` + +- [ ] **Step 4: Run test to verify it passes** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_qc_material_stock_in_no.py -v +``` + +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/crud/qc_material_stock_in_no.py backend/tests/test_qc_material_stock_in_no.py +git commit -m "$(cat <<'EOF' +feat(backend): add RK+date stock-in number allocator for qc materials + +EOF +)" +``` + +--- + +### Task 4: 物资 ensure 前置 — create_material(commit=False) + +**Files:** +- Modify: `backend/app/crud/qc_material.py` + +- [ ] **Step 1: Add `get_material_by_code`** + +在 `get_material` 旁增加: + +```python +def get_material_by_code( + db: Session, material_code: str, *, tenant_no: str +) -> Optional[BizQcMaterial]: + code = (material_code or "").strip() + if not code: + return None + return ( + db.query(BizQcMaterial) + .filter( + BizQcMaterial.tenant_no == tenant_no, + BizQcMaterial.material_code == code, + BizQcMaterial.del_flag == 0, + ) + .first() + ) +``` + +- [ ] **Step 2: `create_material` 增加 `commit: bool = True`** + +`db.add(obj)` 后改为: + +```python + if commit: + db.commit() + db.refresh(obj) + else: + db.flush() + return obj +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/app/crud/qc_material.py +git commit -m "$(cat <<'EOF' +feat(backend): support non-committing qc material create and lookup by code + +EOF +)" +``` + +--- + +### Task 5: 模板 CRUD + preview + confirm + +**Files:** +- Create: `backend/app/crud/qc_material_month_template.py` +- Create: `backend/tests/test_qc_material_month_template.py` + +- [ ] **Step 1: Write unit tests for field validation** + +测试 `_require_template_fields`:缺货号、数量≤0、缺编码/名称 → `ValueError`;合法 payload 返回规范化 dict。 + +- [ ] **Step 2: Implement CRUD module** + +关键签名(须完整实现): + +```python +# backend/app/crud/qc_material_month_template.py +"""质检物料月初入库模板 CRUD / preview / confirm。""" + +def _row_to_dict(row: BizQcMaterialMonthTemplate) -> dict: ... + +def list_templates(db, *, tenant_no: str) -> list: ... +def create_template(db, *, data: dict, tenant_no: str, user_id: Optional[str], create_dept_id: str = "") -> dict: ... +def update_template(db, row_id: str, *, data: dict, tenant_no: str, user_id: Optional[str]) -> dict: ... +def soft_delete_template(db, row_id: str, *, tenant_no: str) -> None: ... + +def preview_templates(db, *, tenant_no: str, stock_in_date: date) -> list: + """加载模板行,分配默认 stock_in_no,返回预览明细。""" + +def ensure_material_id( + db, + *, + tenant_no: str, + user_id: Optional[str], + material_id: Optional[str], + material_code: str, + material_name: str, + spec_model: Optional[str], + unit: Optional[str], +) -> str: + """ + 1) material_id 有值:存在且未删;status!=1 则 ValueError「物资已停用」 + 2) 否则按 code 查找;找到则返回 id(不覆盖主数据名称) + 3) 否则 create_material(..., commit=False) + """ + +def confirm_month_import( + db, + *, + tenant_no: str, + user_id: Optional[str], + stock_in_date: date, + items: list, + remark: Optional[str] = None, + operate_time: Optional[datetime] = None, +) -> int: + """ensure 物资后调用 batch_stock_in;失败由其 rollback,未提交 insert 一并回滚。""" +``` + +`preview_templates` 返回驼峰字段:`templateId`, `materialId`, `materialCode`, `materialName`, `specModel`, `unit`, `stockInNo`, `stockInDate`, `expireDate`, `catalogNo`, `batchNo`, `manufacturer`, `unitPrice`, `amount`, `qty`, `remark`。 + +- [ ] **Step 3: Run tests** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_qc_material_month_template.py tests/test_qc_material_stock_in_no.py -v +``` + +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add backend/app/crud/qc_material_month_template.py backend/tests/test_qc_material_month_template.py +git commit -m "$(cat <<'EOF' +feat(backend): add qc material month template CRUD preview and confirm + +EOF +)" +``` + +--- + +### Task 6: API 路由 + +**Files:** +- Modify: `backend/app/api/qc.py` + +- [ ] **Step 1: 在 `BatchStockInBody` 附近增加 Pydantic models** + +```python +class QcMonthTemplateBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + material_id: Optional[str] = Field(None, alias="materialId") + material_code: str = Field(..., alias="materialCode") + material_name: str = Field(..., alias="materialName") + spec_model: Optional[str] = Field(None, alias="specModel") + unit: Optional[str] = None + catalog_no: str = Field(..., alias="catalogNo") + batch_no: str = Field(..., alias="batchNo") + manufacturer: str + unit_price: float = Field(..., alias="unitPrice") + amount: float + in_qty: float = Field(..., alias="inQty", gt=0) + expire_date: str = Field(..., alias="expireDate") + order_num: Optional[int] = Field(None, alias="orderNum") + remark: Optional[str] = None + + +class QcMonthTemplatePreviewBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + stock_in_date: str = Field(..., alias="stockInDate") + + +class QcMonthTemplateConfirmItem(BaseModel): + model_config = ConfigDict(populate_by_name=True) + material_id: Optional[str] = Field(None, alias="materialId") + material_code: str = Field(..., alias="materialCode") + material_name: str = Field(..., alias="materialName") + spec_model: Optional[str] = Field(None, alias="specModel") + unit: Optional[str] = None + stock_in_no: str = Field(..., alias="stockInNo") + expire_date: str = Field(..., alias="expireDate") + catalog_no: str = Field(..., alias="catalogNo") + batch_no: str = Field(..., alias="batchNo") + manufacturer: str + unit_price: float = Field(..., alias="unitPrice") + amount: float + qty: float = Field(..., gt=0) + remark: Optional[str] = None + + +class QcMonthTemplateConfirmBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + stock_in_date: str = Field(..., alias="stockInDate") + items: List[QcMonthTemplateConfirmItem] = Field(..., min_length=1) + remark: Optional[str] = None + stock_in_time: Optional[str] = Field(None, alias="stockInTime") +``` + +- [ ] **Step 2: 增加 6 个路由**(均 `require_menu_code("biz:qcMaterial:list")`) + +| Method | Path | +|--------|------| +| GET | `/qc-material-month-templates` | +| POST | `/qc-material-month-templates` | +| PUT | `/qc-material-month-templates/{id}` | +| DELETE | `/qc-material-month-templates/{id}` | +| POST | `/qc-material-month-templates/preview` | +| POST | `/qc-material-month-templates/confirm` | + +`confirm`:解析日期 → `confirm_month_import` → `success_response({"count": n})`。金额字段与批量入库一致调用 `_resolve_stock_in_amounts`(若适用)。 + +- [ ] **Step 3: Commit** + +```bash +git add backend/app/api/qc.py +git commit -m "$(cat <<'EOF' +feat(api): expose qc material month template endpoints + +EOF +)" +``` + +--- + +### Task 7: 前端 API 模块 + +**Files:** +- Create: `web-admin/src/api/qc-material-month-template.ts` + +- [ ] **Step 1: Implement** + +```typescript +import request from '@/utils/http' + +export interface QcMonthTemplateRow { + id: string + materialId?: string + materialCode: string + materialName: string + specModel?: string + unit?: string + catalogNo: string + batchNo: string + manufacturer: string + unitPrice: number + amount: number + inQty: number + expireDate: string + orderNum?: number + remark?: string +} + +export interface QcMonthImportPreviewLine { + templateId?: string + materialId?: string + materialCode: string + materialName: string + specModel?: string + unit?: string + stockInNo: string + stockInDate: string + expireDate: string + catalogNo: string + batchNo: string + manufacturer: string + unitPrice: number + amount: number + qty: number + remark?: string +} + +export function fetchQcMonthTemplateList() { + return request.get({ + url: '/web/admin/qc-material-month-templates' + }) +} + +export function fetchCreateQcMonthTemplate(data: Record) { + return request.post({ + url: '/web/admin/qc-material-month-templates', + data + }) +} + +export function fetchUpdateQcMonthTemplate(id: string, data: Record) { + return request.put({ + url: `/web/admin/qc-material-month-templates/${id}`, + data + }) +} + +export function fetchDeleteQcMonthTemplate(id: string) { + return request.delete({ + url: `/web/admin/qc-material-month-templates/${id}` + }) +} + +export function fetchPreviewQcMonthImport(stockInDate: string) { + return request.post({ + url: '/web/admin/qc-material-month-templates/preview', + data: { stockInDate } + }) +} + +export function fetchConfirmQcMonthImport(data: { + stockInDate: string + items: QcMonthImportPreviewLine[] + remark?: string + stockInTime?: string +}) { + return request.post<{ count: number }>({ + url: '/web/admin/qc-material-month-templates/confirm', + data + }) +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add web-admin/src/api/qc-material-month-template.ts +git commit -m "$(cat <<'EOF' +feat(web-admin): add qc month template API client + +EOF +)" +``` + +--- + +### Task 8: 维护模板弹窗组件 + +**Files:** +- Create: `web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthTemplateDialog.vue` + +- [ ] **Step 1: 实现弹窗** + +要点: + +- `el-dialog`:`class="dialog-form-layout"`、`append-to-body`、`modal-class="protocol-dialog-mask"`、宽约 `960px`、`align-center` +- `v-model`;打开时 `fetchQcMonthTemplateList` +- 表格 + 新增/编辑子表单(`label-position="top"`,`form-grid-2`) +- 可选关联启用物资(`fetchQcMaterialOptions`) +- 字段:物资、编码、名称、规格、单位、货号、批号、厂家、单价、金额、数量、过期日期、备注 +- 金额默认 `unitPrice * inQty`(未手工改时联动) +- 删除:`ElMessageBox.confirm` 后调删除 API +- 底栏 Close / Check 图标;空列表 `el-empty` +- Props/Emits:`modelValue` / `update:modelValue` / `saved` + +- [ ] **Step 2: Commit** + +```bash +git add web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthTemplateDialog.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add qc month template maintenance dialog + +EOF +)" +``` + +--- + +### Task 9: 月初导入预览弹窗 + +**Files:** +- Create: `web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthImportDialog.vue` + +- [ ] **Step 1: 实现弹窗** + +要点: + +- 打开后调 `fetchPreviewQcMonthImport(stockInDate)` +- 顶部入库日期变更时重新 preview(保证序号与库一致) +- 明细可编辑;行删除仅本地;不提供预览新增行 +- 确认:`fetchConfirmQcMonthImport`;成功后 `emit('success')` +- 前端校验对齐批量入库必填 + +- [ ] **Step 2: Commit** + +```bash +git add web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthImportDialog.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add qc month import preview dialog + +EOF +)" +``` + +--- + +### Task 10: 挂到质检物料库页 + +**Files:** +- Modify: `web-admin/src/views/quality-inspection/qc-material/index.vue` + +- [ ] **Step 1: 工具栏增加按钮**(建议在批量废弃与导出之间) + +```vue + + + 月初导入 + + + + 维护月初模板 + +``` + +显式 import `Calendar`、`Setting`(若页面未自动导入)。 + +- [ ] **Step 2: `handleMonthImport`** + +先拉模板列表;空则 `ElMessage.warning('请先维护月初模板')` 并打开维护弹窗;否则打开导入弹窗。解包方式与本页现有 `request` 返回习惯一致。 + +- [ ] **Step 3: 挂载组件** + +```vue + + +``` + +成功回调刷新批次列表(现有 `loadData` / 等价函数)。 + +- [ ] **Step 4: 本地 UI 冒烟** + +1. 打开质检物料库 +2. 维护模板 1~2 行 +3. 月初导入 → 改日期看编号 → 确认 +4. 列表出现新批次;同月再导一次仍成功 + +- [ ] **Step 5: 更新图谱** + +```bash +./scripts/update-graph.sh all +``` + +- [ ] **Step 6: Commit** + +```bash +git add web-admin/src/views/quality-inspection/qc-material/index.vue \ + web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthTemplateDialog.vue \ + web-admin/src/views/quality-inspection/qc-material/QcMaterialMonthImportDialog.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): wire month import into qc material library toolbar + +EOF +)" +``` + +--- + +### Task 11: 验收对照 Spec + +- [ ] **Step 1: 对照清单** + +| Spec 项 | 验证 | +|---------|------| +| 独立模板表 | SQL + model | +| 维护弹窗 CRUD | Task 8 | +| 两步预览确认 | Task 9–10 | +| RK 编号 | Task 3 + preview | +| 自动建物资 | confirm ensure | +| 同月可多次 | 手工冒烟 | +| 复用批量入库 | confirm → batch_stock_in | +| 仅质检 | 无其它模块改动 | + +- [ ] **Step 2: 跑测试** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_qc_material_stock_in_no.py tests/test_qc_material_month_template.py -v +``` + +- [ ] **Step 3: 全部通过后可将 Spec 状态改为「已实现」** + +--- + +## Spec coverage self-check + +| Spec 章节 | Plan Task | +|-----------|-----------| +| §3 模板表 | Task 1–2 | +| §4.1 维护 | Task 5–6, 8, 10 | +| §4.2 导入预览确认 | Task 5–6, 9–10 | +| §4.3 编号 | Task 3 | +| §5 confirm 薄封装 | Task 5–6 | +| §6 工具栏 / 弹窗 | Task 8–10 | +| §7 停用物资 / 回滚 | Task 5 ensure + batch_stock_in | +| §8 测试 | Task 3, 5, 11 | +| 非目标 | 无对应 Task(刻意) | + +## Type consistency + +- 预览/确认:`stockInNo`, `expireDate`, `catalogNo`, `batchNo`, `unitPrice`, `qty` +- 模板数量字段:`inQty`;确认映射为 `qty` diff --git a/docs/superpowers/specs/2026-08-19-qc-material-month-import-design.md b/docs/superpowers/specs/2026-08-19-qc-material-month-import-design.md new file mode 100644 index 00000000..a702dac9 --- /dev/null +++ b/docs/superpowers/specs/2026-08-19-qc-material-month-import-design.md @@ -0,0 +1,235 @@ +# 质检物料库 · 月初固定导入设计 + +> 日期: 2026-08-19 +> 状态: 已实现(待库表执行与联调验收) +> 父模块: 质量检测 → 质检物料库 +> 相关: `docs/superpowers/specs/2026-06-11-qc-material-batch-inbound-design.md` + +## 1. 背景与目标 + +### 现状 + +质检物料库(`/#/quality-group/quality-base/qc-material`)已支持: + +- 单条入库 / 批量入库(入库批次:编号、日期、货号、批号、厂家、单价、金额、数量、过期日期等) +- 批量出库、批量废弃 +- 批次列表、变动记录、库存盘点等 + +业务侧每月初会固定入库一批物品,字段大体相同,目前需反复手工录入,效率低。 + +### 目标 + +1. 提供租户可维护的 **月初入库模板**(固定那一批物品,用户可自行增删改) +2. 提供 **月初导入**:按模板展开预览 → 用户可改 → 确认后走现有批量入库链路,生成入库批次与变动流水 +3. 预览时按规则 **默认生成入库编号**(可改);物资不存在时可按模板信息 **自动创建** + +### 已确认决策 + +| 问题 | 决策 | +|------|------| +| 导入本质 | 模板驱动的批量入库(非 Excel 文件导入) | +| 模板字段完整度 | 尽量带齐现有入库字段;每月主要改入库日期 / 编号,其余可微调 | +| 维护入口 | 仍在质检物料库页:工具栏「月初导入」+「维护月初模板」弹窗 | +| 同月重复导入 | 允许随时多次执行,系统不拦截 | +| 入库编号 | 预览时默认生成 `RK + YYYYMMDD + 3位序号`,用户可改(现网手工录入,本期补生成能力) | +| 物资关系 | 可关联已有物资;也可只填物资信息,确认入库时若不存在则自动创建 | +| 执行交互 | 两步:打开预览(可改)→ 确认才真正入库 | +| 本期范围 | 仅质检物料库 | +| 实现路径 | 独立模板表 + 复用现有批量入库 API | + +### 非目标 + +- 定时/cron 自动入库 +- 生产物料库 / 市场物料库同步改造 +- 同月幂等或「每月只能导一次」 +- 改造现有单条入库弹窗的编号生成(可后续复用同一生成函数) +- Excel 文件上传导入 +- 模板行携带质检附件 URL + +--- + +## 2. 架构与数据流 + +``` +维护月初模板 (CRUD) + ↓ +biz_qc_material_month_template(租户级模板行) + ↓ +月初导入预览(选入库日期 → 展开 + 生成编号 → 可改可删行) + ↓ +现有批量入库 API(必要时同事务 ensure 物资) + ↓ +biz_qc_material_stock_batch + biz_qc_material.stock_qty + biz_qc_material_usage +``` + +原则: + +- **模板不直接改库存**;只有确认入库才写批次与流水 +- **预览中的增删改不回写模板**(仅影响本次导入) +- 库存一致性完全依赖现有 `batch_stock_in` 事务语义(任一行失败整单回滚) + +--- + +## 3. 数据模型 + +### 3.1 新表 `biz_qc_material_month_template` + +一行 = 模板中的一条固定入库明细。字段对齐入库批次(不含入库编号 / 入库日期)。 + +| 字段 | 说明 | +|------|------| +| `id` | VARCHAR(64) UUID PK | +| `material_id` | 可空;关联已有 `biz_qc_material.id` | +| `material_code` | 物资编码(关联时冗余;待建物资时必填) | +| `material_name` | 物资名称 | +| `spec_model` | 规格型号 | +| `unit` | 单位 | +| `catalog_no` | 货号(必填,与入库一致) | +| `batch_no` | 批号(必填) | +| `manufacturer` | 厂家(必填) | +| `unit_price` | 单价(元,必填) | +| `amount` | 金额(元,必填;维护时可按单价×数量默认) | +| `in_qty` | 默认入库数量(>0) | +| `expire_date` | 默认过期日期(必填,与现网批次一致) | +| `remark` | 备注 | +| 公共字段 | `tenant_no`, `order_num`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` | + +**规则:** + +- 租户隔离:所有读写按当前用户 `tenant_no` 过滤 / 写入 +- 软删除:`del_flag=1` +- 列表默认按 `order_num ASC, create_time ASC` +- 若填写 `material_id`:保存时校验物资存在且未删除;建议同步冗余编码/名称/规格/单位(物资主数据变更后,模板冗余可被用户手动刷新或下次编辑覆盖) +- 若未填 `material_id`:`material_code`、`material_name` 必填;导入确认时按 `tenant_no + material_code` 查找启用物资,找不到则创建(`stock_qty=0`,`status=1`) + +SQL 脚本落在 `docs/sql/2026-08-19-qc-material-month-template.sql`(实现阶段)。 + +--- + +## 4. 业务流程 + +### 4.1 维护月初模板 + +入口:质检物料库 · 批次列表工具栏「维护月初模板」。 + +- 弹窗内表格展示当前租户全部模板行 +- 支持新增一行、编辑、删除(软删)、调整顺序(`order_num`) +- 选择已有物资:下拉选启用物资,带出编码/名称/规格/单位 +- 手填新物资:清空 `material_id`,填编码/名称等;编码在模板内建议唯一(同租户模板行不重复 `material_code`,避免混淆) +- 不维护入库编号、入库日期 + +### 4.2 月初导入 + +1. 用户点击「月初导入」 +2. 若模板行数为 0:提示先维护模板,并引导打开维护弹窗;不打开空预览 +3. 打开预览弹窗: + - 顶部「本次入库日期」默认今天,可改 + - 调用 preview:按模板行展开明细,并为每行生成默认 `stock_in_no` + - 用户可改任意字段、删除本行(不回写模板);**本期不要求在预览中新增模板外行**(需要额外物资用普通入库/批量入库) +4. 确认入库: + - 对每行:`ensure_material`(已有 id 则用;否则按编码查/建) + - 组装与现有批量入库相同的 payload + - 调用现有批量入库逻辑(同一事务) +5. 成功:关闭弹窗、刷新批次列表、提示成功条数 + +### 4.3 入库编号生成 + +格式:`RK` + `YYYYMMDD`(取自本次入库日期)+ **3 位十进制序号**(左补零),例:`RK20260819001`。 + +算法(preview 与最终提交前均可调用): + +1. 取租户内当日(按入库日期对应的 `YYYYMMDD`)已存在且匹配 `RK{YYYYMMDD}%` 的 `stock_in_no`,解析最大序号 +2. 从 `max+1` 起为预览中的 N 行连续分配 +3. 提交时仍做租户内唯一校验;若冲突(并发入库),返回明确错误,用户改编号后重试 + +说明:现网单条/批量入库仍为手工录入;本能力仅服务月初导入预览默认值。生成函数宜放在后端可复用处,便于日后单条入库复用。 + +--- + +## 5. API 设计 + +前缀与现有质检物料一致:`/web/admin/...`,经现有鉴权 / 租户中间件。 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/qc-material-month-templates` | 当前租户模板列表 | +| POST | `/qc-material-month-templates` | 新增一行 | +| PUT | `/qc-material-month-templates/{id}` | 更新 | +| DELETE | `/qc-material-month-templates/{id}` | 软删 | +| POST | `/qc-material-month-templates/preview` | Body: `{ stockInDate }` → 展开行 + 默认编号 | +| POST | `/qc-materials/batch-stock-in`(已有) | 确认入库;前端也可走等价 `/qc-material-batches/batch-stock-in` | + +**确认入库包装(推荐实现方式):** + +- 新增薄封装 `POST /qc-material-month-templates/confirm`:接收预览编辑后的明细列表 + `stockInDate`;服务端 `ensure_material` 后调用已有 `batch_stock_in` +- 或:前端先调 ensure/创建物资 API 再调批量入库——**不推荐**(非原子) + +本设计采用 **`confirm` 薄封装**,保证「建物资 + 入库」同事务。 + +**权限:** 与质检物料库入库相同(能入库即可维护模板与月初导入);不新增独立菜单。按钮可见性跟随现有入库能力。 + +--- + +## 6. 前端交互 + +页面:`web-admin/src/views/quality-inspection/qc-material/index.vue`(批次列表工具栏)。 + +- 工具栏顺序建议:已选 N 项 → **入库**(唯一 `type="primary"`)→ 批量出库 / 批量废弃(现有)→ **月初导入** → **维护月初模板** +- 两个弹窗:对齐现有批量入库弹窗风格(`label-position="top"` / 表格明细 / 无 backdrop blur) +- 维护弹窗:保存单行即时调 API,或底部统一保存——实现阶段优先 **单行保存 + 删除确认**,与现有 CRUD 习惯一致 +- 预览弹窗:金额随单价×数量联动规则对齐现有入库(未手工改金额则重算) +- 按钮须带 Element Plus 图标(工程 UI 约定) + +界面模式:对照批量入库弹窗(P 系列选型在实现计划中引用 `docs/web-admin-ui-pattern-index.md`)。 + +--- + +## 7. 校验与错误处理 + +| 场景 | 行为 | +|------|------| +| 模板为空点导入 | 警告提示,不打开预览 | +| 模板行缺必填 | 保存模板时拦截 | +| 关联物资已删除/停用 | 维护保存时警告;预览时标出该行,确认前须改绑或去掉该行 | +| 待建物资编码已存在 | `ensure` 时绑定已有物资,不重复创建 | +| 待建物资编码冲突名称不同 | 绑定已有物资并以已有主数据为准;备注中可不强制覆盖名称(避免静默改主数据) | +| 入库编号重复 | 整单失败回滚,提示冲突编号 | +| 数量 ≤ 0 / 缺必填批次字段 | 与现有批量入库一致,整单失败 | +| 并发同日多用户预览 | 允许;提交时唯一校验兜底 | + +`source_type`:确认入库产生的流水仍为现有手动入库类型(如 `manual_in`);若需区分「月初导入」,可新增 `month_template_in`——**本期默认仍用 `manual_in`**,避免报表分叉;若产品后续要统计,再加枚举。 + +--- + +## 8. 测试要点 + +- 模板 CRUD 租户隔离与软删 +- 空模板导入被拦截 +- 预览编号:同日多行连续;跨日日期切换后编号日期段正确 +- 关联已有物资导入成功,库存与流水正确 +- 仅填编码的新物资:首次导入自动创建并入库;第二次导入复用同一物资 +- 预览删行不影响模板 +- 人为制造编号冲突时整单回滚、库存不变 +- 停用物资关联行:确认被拒绝或强制用户处理 + +--- + +## 9. 组件边界(便于实现拆分) + +| 单元 | 职责 | 依赖 | +|------|------|------| +| 模板 CRUD API/CRUD | 模板行持久化 | `BizQcMaterialMonthTemplate` | +| preview 服务 | 展开 + 编号生成 | 模板表 + 已有 `stock_in_no` | +| confirm 服务 | ensure 物资 + 调 `batch_stock_in` | `qc_material` + `qc_material_stock_batch` | +| 前端维护弹窗 | 模板编辑 UX | 模板 API | +| 前端导入弹窗 | 预览编辑 UX | preview + confirm API | + +--- + +## 10. 开放问题(实现前可再定,有默认) + +| 项 | 默认 | +|----|------| +| 预览是否允许临时加行 | 否(用普通批量入库) | +| 流水 `source_type` | 继续 `manual_in` | +| 单条入库是否同步默认编号 | 否(仅月初导入) |