From 5fc460c69acc84d0cb4415bb401c6beef14617d9 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Tue, 18 Aug 2026 09:33:41 +0800 Subject: [PATCH] docs: add QC material remaining-stock filter implementation plan. Break the approved design into backend stockStatus filtering and frontend toolbar capsule tasks. Co-authored-by: Cursor --- ...8-18-qc-material-remaining-stock-filter.md | 434 ++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-18-qc-material-remaining-stock-filter.md diff --git a/docs/superpowers/plans/2026-08-18-qc-material-remaining-stock-filter.md b/docs/superpowers/plans/2026-08-18-qc-material-remaining-stock-filter.md new file mode 100644 index 00000000..17e0dfd1 --- /dev/null +++ b/docs/superpowers/plans/2026-08-18-qc-material-remaining-stock-filter.md @@ -0,0 +1,434 @@ +# 质检物料库 · 剩余库存胶囊过滤 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:** 后端 `list_batches` 增加 `stock_status`(API 驼峰 `stockStatus`),按 `remaining_qty > 0` / `= 0` 过滤;前端工具栏三胶囊驱动该参数。纯函数规范化参数取值,便于单测。 + +**Tech Stack:** FastAPI / SQLAlchemy / Vue 3 / TypeScript / Element Plus;设计依据见 Spec。 + +**Spec:** `docs/superpowers/specs/2026-08-18-qc-material-remaining-stock-filter-design.md` + +--- + +## File map + +| File | Responsibility | +|------|----------------| +| Create: `backend/app/crud/qc_material_batch_stock_status.py` | `normalize_batch_stock_status`、`batch_matches_stock_status` 纯函数 | +| Create: `backend/tests/test_qc_material_batch_stock_status.py` | 纯函数单测 | +| Modify: `backend/app/crud/qc_material_stock_batch.py` | `list_batches` 接收并应用 `stock_status` | +| Modify: `backend/app/api/qc.py` | `GET /qc-material-batches` 增加 Query `stockStatus` 并下传 | +| Modify: `web-admin/src/api/qc-material.ts` | `fetchQcMaterialBatchList` 参数增加 `stockStatus` | +| Modify: `web-admin/src/views/quality-inspection/qc-material/index.vue` | 工具栏胶囊 UI、默认 `available`、请求/重置/导出传参 | + +--- + +### Task 1: 后端纯函数 — stockStatus 语义 + +**Files:** +- Create: `backend/app/crud/qc_material_batch_stock_status.py` +- Create: `backend/tests/test_qc_material_batch_stock_status.py` + +- [ ] **Step 1: Write the failing test** + +```python +# backend/tests/test_qc_material_batch_stock_status.py +"""质检物料批次列表:剩余库存过滤语义。""" +import unittest +from decimal import Decimal + +from app.crud.qc_material_batch_stock_status import ( + batch_matches_stock_status, + normalize_batch_stock_status, +) + + +class TestNormalizeBatchStockStatus(unittest.TestCase): + def test_available_and_exhausted(self): + self.assertEqual(normalize_batch_stock_status("available"), "available") + self.assertEqual(normalize_batch_stock_status("exhausted"), "exhausted") + self.assertEqual(normalize_batch_stock_status(" Available "), "available") + + def test_all_empty_invalid_map_to_none(self): + self.assertIsNone(normalize_batch_stock_status(None)) + self.assertIsNone(normalize_batch_stock_status("")) + self.assertIsNone(normalize_batch_stock_status("all")) + self.assertIsNone(normalize_batch_stock_status("bogus")) + + +class TestBatchMatchesStockStatus(unittest.TestCase): + def test_all_always_true(self): + self.assertTrue(batch_matches_stock_status(0, "all")) + self.assertTrue(batch_matches_stock_status(1, None)) + + def test_available_gt_zero(self): + self.assertTrue(batch_matches_stock_status(Decimal("0.1"), "available")) + self.assertFalse(batch_matches_stock_status(0, "available")) + self.assertFalse(batch_matches_stock_status(Decimal("0"), "available")) + + def test_exhausted_eq_zero(self): + self.assertTrue(batch_matches_stock_status(0, "exhausted")) + self.assertTrue(batch_matches_stock_status(Decimal("0.0000"), "exhausted")) + self.assertFalse(batch_matches_stock_status(1, "exhausted")) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd backend && source venv/bin/activate && pytest tests/test_qc_material_batch_stock_status.py -v +``` + +Expected: FAIL(模块不存在 / import error) + +- [ ] **Step 3: Write minimal implementation** + +```python +# backend/app/crud/qc_material_batch_stock_status.py +"""质检物料入库批次:按剩余数量过滤的语义辅助。""" +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Optional + + +def normalize_batch_stock_status(stock_status: Optional[str]) -> Optional[str]: + """返回 'available' | 'exhausted';全部 / 空 / 非法 → None(不加剩余数量条件)。""" + s = (stock_status or "").strip().lower() + if s in ("available", "exhausted"): + return s + return None + + +def batch_matches_stock_status(remaining_qty: Any, stock_status: Optional[str]) -> bool: + """纯函数判定(供单测);list_batches 用 SQL 等价条件。""" + mode = normalize_batch_stock_status(stock_status) + if mode is None: + return True + qty = Decimal(str(remaining_qty if remaining_qty is not None else 0)) + if mode == "available": + return qty > 0 + return qty == 0 +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +cd backend && source venv/bin/activate && pytest tests/test_qc_material_batch_stock_status.py -v +``` + +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/crud/qc_material_batch_stock_status.py backend/tests/test_qc_material_batch_stock_status.py +git commit -m "$(cat <<'EOF' +feat(backend): add QC material batch stockStatus helpers. + +Normalize available/exhausted filter values and document remaining_qty matching for list filtering. +EOF +)" +``` + +--- + +### Task 2: 后端 list_batches + API 传参 + +**Files:** +- Modify: `backend/app/crud/qc_material_stock_batch.py`(`list_batches`) +- Modify: `backend/app/api/qc.py`(`list_qc_material_batches`) + +- [ ] **Step 1: 扩展 `list_batches` 签名与过滤** + +在 `list_batches` 增加参数 `stock_status: Optional[str] = None`。在 `_filtered()` 内、现有 `status` 过滤之后、`user_info` 数据权限之前加入: + +```python +from app.crud.qc_material_batch_stock_status import normalize_batch_stock_status + +# ... inside _filtered(), after status filter: +mode = normalize_batch_stock_status(stock_status) +if mode == "available": + q0 = q0.filter(BizQcMaterialStockBatch.remaining_qty > 0) +elif mode == "exhausted": + q0 = q0.filter(BizQcMaterialStockBatch.remaining_qty == 0) +``` + +函数签名变为: + +```python +def list_batches( + db: Session, + *, + page: int = 1, + page_size: int = 10, + keyword: Optional[str] = None, + material_id: Optional[str] = None, + status: Optional[str] = None, + stock_status: Optional[str] = None, + tenant_no: Optional[str] = None, + user_info=None, +) -> Tuple[List[Dict[str, Any]], int]: +``` + +- [ ] **Step 2: API 增加 Query 并下传** + +在 `backend/app/api/qc.py` 的 `list_qc_material_batches`: + +```python +@router.get("/qc-material-batches") +async def list_qc_material_batches( + page: int = Query(1, ge=1), + pageSize: int = Query(10, ge=1, le=100), + keyword: Optional[str] = None, + materialId: Optional[str] = None, + status: Optional[str] = None, + stockStatus: Optional[str] = None, + db: Session = Depends(get_db), + user: dict = Depends(require_menu_code("biz:qcMaterial:list")), +): + try: + tenant_no = _get_tenant_no(db, user) + items, total = batch_svc.list_batches( + db, + page=page, + page_size=pageSize, + keyword=keyword, + material_id=materialId, + status=status, + stock_status=stockStatus, + tenant_no=tenant_no, + user_info=user, + ) + # ... unchanged +``` + +- [ ] **Step 3: 快速回归纯函数测试仍通过** + +Run: + +```bash +cd backend && source venv/bin/activate && pytest tests/test_qc_material_batch_stock_status.py -v +``` + +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add backend/app/crud/qc_material_stock_batch.py backend/app/api/qc.py +git commit -m "$(cat <<'EOF' +feat(backend): filter QC material batches by stockStatus. + +Support available (remaining_qty > 0) and exhausted (remaining_qty = 0) on the batch list API. +EOF +)" +``` + +--- + +### Task 3: 前端 API 类型 + +**Files:** +- Modify: `web-admin/src/api/qc-material.ts` + +- [ ] **Step 1: 扩展列表请求参数** + +```typescript +export function fetchQcMaterialBatchList(params?: { + page?: number + pageSize?: number + keyword?: string + materialId?: string + status?: string + stockStatus?: 'all' | 'available' | 'exhausted' +}) { + return request.get>({ + url: '/web/admin/qc-material-batches', + params + }) +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add web-admin/src/api/qc-material.ts +git commit -m "$(cat <<'EOF' +feat(web-admin): add stockStatus to QC material batch list API. +EOF +)" +``` + +--- + +### Task 4: 物料列表工具栏胶囊 + 请求联动 + +**Files:** +- Modify: `web-admin/src/views/quality-inspection/qc-material/index.vue` + +- [ ] **Step 1: 模板 — 工具栏三胶囊** + +在 `list-toolbar-row` 内,「已选 N 项」之后、「入库」按钮之前插入(复用全局 `panel-pill-tabs`,加紧凑修饰类): + +```vue +
+
+ +
+
+``` + +- [ ] **Step 2: script — 状态、切换、请求、重置、导出** + +在 `QC_MATERIAL_TABS` 附近增加: + +```typescript +const STOCK_STATUS_PILLS = [ + { value: 'all', label: '全部' }, + { value: 'available', label: '未用完' }, + { value: 'exhausted', label: '已用完' } +] as const + +type StockStatusFilter = (typeof STOCK_STATUS_PILLS)[number]['value'] + +const stockStatusFilter = ref('available') + +function handleStockStatusChange(value: StockStatusFilter) { + if (stockStatusFilter.value === value) return + stockStatusFilter.value = value + pagination.page = 1 + loadData() +} +``` + +`loadData` 传参: + +```typescript +const res = await fetchQcMaterialBatchList({ + page: pagination.page, + pageSize: pagination.pageSize, + keyword: searchForm.keyword || undefined, + status: searchForm.status || undefined, + stockStatus: stockStatusFilter.value +}) +``` + +`loadAllQcMaterialBatchesForExport` 同样传入 `stockStatus: stockStatusFilter.value`。 + +`handleReset`: + +```typescript +function handleReset() { + searchForm.keyword = '' + searchForm.status = '' + stockStatusFilter.value = 'available' + pagination.page = 1 + loadData() +} +``` + +- [ ] **Step 3: 紧凑样式(scoped)** + +在该页 `style` 中增加工具栏胶囊尺寸,避免与顶栏大 Tab 抢视觉: + +```scss +.list-toolbar-stock-pills.panel-pill-tabs--toolbar { + margin-right: 8px; + + .panel-pill-tabs__btn { + min-width: 64px; + padding: 6px 12px; + font-size: 12px; + } +} +``` + +若 `list-toolbar-row` 为 flex,确认胶囊落在「已选」与「入库」之间,且未用 `space-between` 把按钮推到行末(保持现有工具栏约定)。 + +- [ ] **Step 4: 手动验收清单(本机)** + +1. 打开 `/#/quality-group/quality-base/qc-material`,默认「未用完」,行内 `剩余数量` 均 > 0。 +2. 点「已用完」,仅见剩余 = 0;点「全部」与旧行为一致(仍可叠加关键词/状态)。 +3. 切换胶囊后页码回到 1,total 变化合理。 +4. 点「重置」:关键词/状态清空,胶囊回到「未用完」。 +5. 导出结果与当前胶囊一致。 +6. 「变动记录」「库存盘点」无异常。 + +- [ ] **Step 5: Commit** + +```bash +git add web-admin/src/views/quality-inspection/qc-material/index.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add remaining-stock pills on QC material list. + +Filter batches via stockStatus capsules before stock-in; default to available stock. +EOF +)" +``` + +--- + +### Task 5: 图谱增量更新(若本会话改了代码) + +**Files:** 无业务源码;跑脚本 + +- [ ] **Step 1: 更新图谱** + +```bash +./scripts/update-graph.sh all +``` + +- [ ] **Step 2: 若有 graphify 变更则单独提交** + +```bash +git add backend/graphify-out web-admin/graphify-out +git commit -m "$(cat <<'EOF' +chore: sync graphify after QC material stockStatus filter. +EOF +)" +``` + +若脚本无变更可跳过 commit。 + +--- + +## Spec coverage checklist + +| Spec 项 | Task | +|---------|------| +| 工具栏三胶囊位置与文案 | Task 4 | +| 默认未用完 | Task 4 | +| remaining > 0 / = 0 | Task 1–2 | +| 与关键词/状态叠加 | Task 2 + 4 | +| 切换回第 1 页 | Task 4 | +| 导出尊重过滤 | Task 4 | +| 分页 total 一致 | Task 2(服务端过滤) | +| API `stockStatus` | Task 2–3 | +| 不改其它 Tab / 其它物料库 | 范围外,无 Task | + +## Self-review notes + +- 无 TBD/placeholder。 +- 前端传 `all` 时后端 `normalize` → None,不加条件,与 Spec「全部」一致。 +- API 未传时后端行为等同全部(兼容旧客户端);页面始终显式传参。