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 <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-18 09:33:41 +08:00
1 parent 8e689ee771
commit 5fc460c69a
1 file changed
+434
@@ -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<PageList<QcMaterialBatch>>({
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
<div
class="panel-pill-tabs panel-pill-tabs--toolbar list-toolbar-stock-pills"
role="tablist"
aria-label="库存状态"
>
<div class="panel-pill-tabs__track">
<button
v-for="opt in STOCK_STATUS_PILLS"
:key="opt.value"
type="button"
role="tab"
class="panel-pill-tabs__btn"
:class="{ 'is-active': stockStatusFilter === opt.value }"
:aria-selected="stockStatusFilter === opt.value"
@click="handleStockStatusChange(opt.value)"
>
{{ opt.label }}
</button>
</div>
</div>
```
- [ ] **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<StockStatusFilter>('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 未传时后端行为等同全部(兼容旧客户端);页面始终显式传参。