|
|
@@ -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<QcMonthTemplateRow[]>({
|
|
|
+ url: '/web/admin/qc-material-month-templates'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+export function fetchCreateQcMonthTemplate(data: Record<string, any>) {
|
|
|
+ return request.post<QcMonthTemplateRow>({
|
|
|
+ url: '/web/admin/qc-material-month-templates',
|
|
|
+ data
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+export function fetchUpdateQcMonthTemplate(id: string, data: Record<string, any>) {
|
|
|
+ return request.put<QcMonthTemplateRow>({
|
|
|
+ url: `/web/admin/qc-material-month-templates/${id}`,
|
|
|
+ data
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+export function fetchDeleteQcMonthTemplate(id: string) {
|
|
|
+ return request.delete<void>({
|
|
|
+ url: `/web/admin/qc-material-month-templates/${id}`
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+export function fetchPreviewQcMonthImport(stockInDate: string) {
|
|
|
+ return request.post<QcMonthImportPreviewLine[]>({
|
|
|
+ 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
|
|
|
+<el-button class="btn-ghost" @click="handleMonthImport">
|
|
|
+ <el-icon><Calendar /></el-icon>
|
|
|
+ 月初导入
|
|
|
+</el-button>
|
|
|
+<el-button class="btn-ghost" @click="monthTemplateVisible = true">
|
|
|
+ <el-icon><Setting /></el-icon>
|
|
|
+ 维护月初模板
|
|
|
+</el-button>
|
|
|
+```
|
|
|
+
|
|
|
+显式 import `Calendar`、`Setting`(若页面未自动导入)。
|
|
|
+
|
|
|
+- [ ] **Step 2: `handleMonthImport`**
|
|
|
+
|
|
|
+先拉模板列表;空则 `ElMessage.warning('请先维护月初模板')` 并打开维护弹窗;否则打开导入弹窗。解包方式与本页现有 `request` 返回习惯一致。
|
|
|
+
|
|
|
+- [ ] **Step 3: 挂载组件**
|
|
|
+
|
|
|
+```vue
|
|
|
+<QcMaterialMonthTemplateDialog v-model="monthTemplateVisible" />
|
|
|
+<QcMaterialMonthImportDialog
|
|
|
+ v-model="monthImportVisible"
|
|
|
+ @success="handleMonthImportSuccess"
|
|
|
+/>
|
|
|
+```
|
|
|
+
|
|
|
+成功回调刷新批次列表(现有 `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`
|