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:
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 未传时后端行为等同全部(兼容旧客户端);页面始终显式传参。
|
||||
Reference in new issue
Block a user