|
|
@@ -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:** 待审核 Tab 默认只展示质检侧与生产侧均「已完成」的入库放行记录;用「展示全部待审核」复选框替换原质检下拉,勾选后展示全部待审核。
|
|
|
+
|
|
|
+**Architecture:** 列表 API 新增可选布尔参数 `readyForReviewOnly`;为 `true` 时在 `list_reviews` 上对质检/生产未完成状态做双 `NOT EXISTS`(无工单算该侧完成)。前端仅在待审核展示复选框,默认/重置/切回待审核均为不勾选,并停止传 `qcWorkOrderCompleted`。
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI + SQLAlchemy(backend)、Vue 3 + Element Plus(web-admin)、pytest
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-07-29-storage-release-pending-ready-filter-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File Map
|
|
|
+
|
|
|
+| 文件 | 职责 |
|
|
|
+|------|------|
|
|
|
+| `backend/tests/test_storage_release_review_ready_filter.py` | **新建** `normalize_ready_for_review_only` 单测 |
|
|
|
+| `backend/app/crud/storage_release_review.py` | normalize + 生产未完成 EXISTS + `list_reviews` 过滤 |
|
|
|
+| `backend/app/api/storage_release_review.py` | Query `readyForReviewOnly` |
|
|
|
+| `web-admin/src/api/storage-release-review.ts` | 列表 params 增加 `readyForReviewOnly?: boolean` |
|
|
|
+| `web-admin/src/views/quality-inspection/storage-release-review/index.vue` | 复选框替换下拉;默认/重置/切 Tab;请求传参 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: Backend — normalize + 双 NOT EXISTS 过滤 + API
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/tests/test_storage_release_review_ready_filter.py`
|
|
|
+- Modify: `backend/app/crud/storage_release_review.py`
|
|
|
+- Modify: `backend/app/api/storage_release_review.py`
|
|
|
+
|
|
|
+- [x] **Step 1: 写失败单测(规范化函数)**
|
|
|
+
|
|
|
+创建 `backend/tests/test_storage_release_review_ready_filter.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+from app.crud.storage_release_review import normalize_ready_for_review_only
|
|
|
+
|
|
|
+
|
|
|
+def test_normalize_ready_for_review_only_true_values():
|
|
|
+ assert normalize_ready_for_review_only(True) is True
|
|
|
+ assert normalize_ready_for_review_only("true") is True
|
|
|
+ assert normalize_ready_for_review_only("TRUE") is True
|
|
|
+ assert normalize_ready_for_review_only("1") is True
|
|
|
+ assert normalize_ready_for_review_only("yes") is True
|
|
|
+ assert normalize_ready_for_review_only(" YES ") is True
|
|
|
+
|
|
|
+
|
|
|
+def test_normalize_ready_for_review_only_non_true_is_none():
|
|
|
+ """Only explicit true enables the filter; false/unknown/absent → no filter."""
|
|
|
+ assert normalize_ready_for_review_only(None) is None
|
|
|
+ assert normalize_ready_for_review_only(False) is None
|
|
|
+ assert normalize_ready_for_review_only("") is None
|
|
|
+ assert normalize_ready_for_review_only("false") is None
|
|
|
+ assert normalize_ready_for_review_only("0") is None
|
|
|
+ assert normalize_ready_for_review_only("no") is None
|
|
|
+ assert normalize_ready_for_review_only("maybe") is None
|
|
|
+```
|
|
|
+
|
|
|
+- [x] **Step 2: 跑单测确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_ready_filter.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(`normalize_ready_for_review_only` 未定义或导入失败)
|
|
|
+
|
|
|
+- [x] **Step 3: 实现 normalize、生产未完成 EXISTS,并接入 `list_reviews`**
|
|
|
+
|
|
|
+在 `backend/app/crud/storage_release_review.py`,于 `normalize_qc_work_order_completed` **之后**增加:
|
|
|
+
|
|
|
+```python
|
|
|
+def normalize_ready_for_review_only(raw) -> Optional[bool]:
|
|
|
+ """Return True when ready-only filter should apply; None means no filter."""
|
|
|
+ if raw is None:
|
|
|
+ return None
|
|
|
+ if isinstance(raw, bool):
|
|
|
+ return True if raw else None
|
|
|
+ s = str(raw).strip().lower()
|
|
|
+ if s in ("true", "1", "yes"):
|
|
|
+ return True
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _production_work_order_unfinished_exists_for_review_sample(tenant_no: str):
|
|
|
+ """True if sample has any unfinished inbound-side production WO (dpz/jxz)."""
|
|
|
+ conds = [
|
|
|
+ BizProductionWorkOrder.tenant_no == tenant_no,
|
|
|
+ BizProductionWorkOrder.del_flag == 0,
|
|
|
+ BizProductionWorkOrder.sample_code.isnot(None),
|
|
|
+ BizProductionWorkOrder.sample_code != "",
|
|
|
+ eq_unicode(
|
|
|
+ BizProductionWorkOrder.sample_code, BizStorageReleaseReview.sample_code
|
|
|
+ ),
|
|
|
+ BizStorageReleaseReview.sample_code.isnot(None),
|
|
|
+ BizStorageReleaseReview.sample_code != "",
|
|
|
+ BizProductionWorkOrder.work_order_status.in_(("dpz", "jxz")),
|
|
|
+ ]
|
|
|
+ return (
|
|
|
+ exists()
|
|
|
+ .where(and_(*conds))
|
|
|
+ .correlate(BizStorageReleaseReview)
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+(`BizProductionWorkOrder`、`eq_unicode`、`exists`、`and_` 已在文件中导入,无需新增 import。)
|
|
|
+
|
|
|
+在 `list_reviews` 签名中增加参数(放在 `qc_work_order_completed` 之后):
|
|
|
+
|
|
|
+```python
|
|
|
+ ready_for_review_only=None,
|
|
|
+```
|
|
|
+
|
|
|
+在现有 `qc_done = normalize_qc_work_order_completed(...)` 块**之后**、`if user_info:` **之前**插入:
|
|
|
+
|
|
|
+```python
|
|
|
+ if normalize_ready_for_review_only(ready_for_review_only) is True:
|
|
|
+ # 无未完成质检/生产工单 ⇒ 该侧完成(含零工单)
|
|
|
+ q0 = q0.filter(
|
|
|
+ ~_qc_work_order_exists_for_review_sample(
|
|
|
+ tenant_no, unfinished_only=True
|
|
|
+ ),
|
|
|
+ ~_production_work_order_unfinished_exists_for_review_sample(tenant_no),
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+保留现有 `qc_work_order_completed` 逻辑不动(本页前端停用;API 兼容)。
|
|
|
+
|
|
|
+- [x] **Step 4: API 增加 Query 并下传**
|
|
|
+
|
|
|
+修改 `backend/app/api/storage_release_review.py` 的 `list_storage_release_reviews`:
|
|
|
+
|
|
|
+在 `qcWorkOrderCompleted: Optional[bool] = Query(None),` 后增加:
|
|
|
+
|
|
|
+```python
|
|
|
+ readyForReviewOnly: Optional[bool] = Query(None),
|
|
|
+```
|
|
|
+
|
|
|
+在 `srr_svc.list_reviews(...)` 调用中增加:
|
|
|
+
|
|
|
+```python
|
|
|
+ ready_for_review_only=readyForReviewOnly,
|
|
|
+```
|
|
|
+
|
|
|
+- [x] **Step 5: 跑单测确认通过**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_ready_filter.py tests/test_storage_release_review_qc_completed.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS(新旧 normalize 单测均绿)
|
|
|
+
|
|
|
+- [x] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/tests/test_storage_release_review_ready_filter.py \
|
|
|
+ backend/app/crud/storage_release_review.py \
|
|
|
+ backend/app/api/storage_release_review.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(storage-release): add readyForReviewOnly list filter
|
|
|
+
|
|
|
+Default pending list can exclude rows with unfinished QC or production work orders via SQL NOT EXISTS.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: Frontend — API 类型 + 复选框替换下拉
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/api/storage-release-review.ts`
|
|
|
+- Modify: `web-admin/src/views/quality-inspection/storage-release-review/index.vue`
|
|
|
+
|
|
|
+- [x] **Step 1: API params 增加 `readyForReviewOnly`**
|
|
|
+
|
|
|
+修改 `web-admin/src/api/storage-release-review.ts` 中 `fetchStorageReleaseReviewList`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+export function fetchStorageReleaseReviewList(params?: {
|
|
|
+ page?: number
|
|
|
+ pageSize?: number
|
|
|
+ receptionSource?: string
|
|
|
+ reviewConclusion?: string
|
|
|
+ storageStatus?: string
|
|
|
+ keyword?: string
|
|
|
+ reviewPhase?: 'pending' | 'reviewed'
|
|
|
+ qcWorkOrderCompleted?: boolean
|
|
|
+ readyForReviewOnly?: boolean
|
|
|
+}) {
|
|
|
+ return request.get<PageList<StorageReleaseReview>>({
|
|
|
+ url: '/web/admin/storage-release-reviews',
|
|
|
+ params
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+(保留 `qcWorkOrderCompleted` 类型以免其它调用方类型报错;本页不再传。)
|
|
|
+
|
|
|
+- [x] **Step 2: 模板 — 去掉质检下拉与 5 列栅格,加复选框**
|
|
|
+
|
|
|
+在 `index.vue` 模板中:
|
|
|
+
|
|
|
+1) 将 `filter-grid` 改回普通 4 列(去掉 `storage-release-filter-grid--5` 与内联 `gridTemplateColumns`):
|
|
|
+
|
|
|
+```vue
|
|
|
+ <div class="filter-grid">
|
|
|
+```
|
|
|
+
|
|
|
+2) **删除**整块:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <el-form-item v-if="activeTab === 'pending'" label="质检工单是否完成">
|
|
|
+ ...
|
|
|
+ </el-form-item>
|
|
|
+```
|
|
|
+
|
|
|
+3) 在 `filter-actions-row` 内、`.filter-actions-end` **之前**插入(仅待审核):
|
|
|
+
|
|
|
+```vue
|
|
|
+ <div class="filter-actions-row">
|
|
|
+ <label
|
|
|
+ v-if="activeTab === 'pending'"
|
|
|
+ class="storage-release-ready-check"
|
|
|
+ >
|
|
|
+ <el-checkbox v-model="searchForm.showAllPending" />
|
|
|
+ <span class="storage-release-ready-check__copy">
|
|
|
+ <span class="storage-release-ready-check__title">展示全部待审核</span>
|
|
|
+ <span class="storage-release-ready-check__hint">
|
|
|
+ 默认不勾选:仅展示质检与生产工单均为完成的记录;勾选后展示全部。
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ <div class="filter-actions-end">
|
|
|
+ ...
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+```
|
|
|
+
|
|
|
+(若项目 `filter-actions-row` 默认把内容推到右侧,给 `.storage-release-ready-check` 设 `margin-right: auto`,见 Step 4 样式。)
|
|
|
+
|
|
|
+- [x] **Step 3: script — 状态、请求、重置、切 Tab**
|
|
|
+
|
|
|
+删除:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ const qcWorkOrderCompletedOptions = [ ... ]
|
|
|
+```
|
|
|
+
|
|
|
+将 `searchForm` 改为:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ const searchForm = reactive({
|
|
|
+ receptionSource: '',
|
|
|
+ reviewConclusion: '',
|
|
|
+ storageStatus: '',
|
|
|
+ keyword: '',
|
|
|
+ showAllPending: false
|
|
|
+ })
|
|
|
+```
|
|
|
+
|
|
|
+`loadData` 中替换 `qcParam` 逻辑:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ async function loadData() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const readyOnly =
|
|
|
+ activeTab.value === 'pending' && !searchForm.showAllPending ? true : undefined
|
|
|
+ const res = await fetchStorageReleaseReviewList({
|
|
|
+ page: pagination.page,
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
+ receptionSource: searchForm.receptionSource || undefined,
|
|
|
+ reviewConclusion: searchForm.reviewConclusion || undefined,
|
|
|
+ storageStatus: searchForm.storageStatus || undefined,
|
|
|
+ keyword: searchForm.keyword || undefined,
|
|
|
+ reviewPhase: activeTab.value,
|
|
|
+ readyForReviewOnly: readyOnly
|
|
|
+ })
|
|
|
+ tableData.value = res.list || []
|
|
|
+ pagination.total = res.total ?? 0
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+`handleTabChange`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ function handleTabChange(name: ReviewTab) {
|
|
|
+ if (activeTab.value === name) return
|
|
|
+ activeTab.value = name
|
|
|
+ pagination.page = 1
|
|
|
+ selectedRows.value = []
|
|
|
+ if (name === 'pending') {
|
|
|
+ searchForm.showAllPending = false
|
|
|
+ }
|
|
|
+ loadData()
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+`handleReset`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ function handleReset() {
|
|
|
+ searchForm.receptionSource = ''
|
|
|
+ searchForm.reviewConclusion = ''
|
|
|
+ searchForm.storageStatus = ''
|
|
|
+ searchForm.keyword = ''
|
|
|
+ searchForm.showAllPending = false
|
|
|
+ pagination.page = 1
|
|
|
+ loadData()
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+说明:`watchDebounced(searchForm, ...)` 已存在;勾选复选框会自动触发搜索,无需额外 `@change`。
|
|
|
+
|
|
|
+- [x] **Step 4: 样式 — 复选框行 + 去掉 5 列窄屏规则**
|
|
|
+
|
|
|
+在 `<style scoped>` 中:
|
|
|
+
|
|
|
+删除(或整段替换)针对 `storage-release-filter-grid--5` 的 `@media` 规则。
|
|
|
+
|
|
|
+增加:
|
|
|
+
|
|
|
+```scss
|
|
|
+ .filter-actions-row {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .storage-release-ready-check {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 10px;
|
|
|
+ margin-right: auto;
|
|
|
+ max-width: 520px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ border: 1px dashed #a7f3d0;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: var(--primary-bg, #ecfdf5);
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .storage-release-ready-check__copy {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .storage-release-ready-check__title {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--text, #1e293b);
|
|
|
+ line-height: 1.4;
|
|
|
+ }
|
|
|
+
|
|
|
+ .storage-release-ready-check__hint {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--text-muted, #64748b);
|
|
|
+ line-height: 1.45;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+若全局已有 `.filter-actions-row` 布局,只补复选框相关类,避免重复覆盖冲突;以页面可对齐 Demo 为准。
|
|
|
+
|
|
|
+- [x] **Step 5: 手工验收**
|
|
|
+
|
|
|
+启动后端与 `web-admin`(`pnpm dev`),打开
|
|
|
+`http://localhost:3006/#/market/cell-storage/storage-release-review`:
|
|
|
+
|
|
|
+1. 进入待审核:复选框未勾选;无「质检工单是否完成」下拉;默认列表不含质检 `dpz`/`ypz` 或生产 `dpz`/`jxz` 的行;双无工单行可见
|
|
|
+2. 勾选「展示全部待审核」→ 出现原先被隐藏的未完成行;`total` 变大
|
|
|
+3. 取消勾选 / 重置 → 回到默认过滤
|
|
|
+4. 切到已审核:复选框消失;切回待审核:复选框不勾选
|
|
|
+5. Network:未勾选时请求带 `readyForReviewOnly=true`;勾选或不在待审核时不带该参数;不再带 `qcWorkOrderCompleted`
|
|
|
+
|
|
|
+- [x] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/api/storage-release-review.ts \
|
|
|
+ web-admin/src/views/quality-inspection/storage-release-review/index.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): pending storage-release ready checkbox filter
|
|
|
+
|
|
|
+Replace QC-completed select with show-all checkbox; request readyForReviewOnly when unchecked.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 文档状态(可选收尾)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `docs/superpowers/specs/2026-07-29-storage-release-pending-ready-filter-design.md`(头部状态可改为「已实现」)
|
|
|
+- Modify: `docs/superpowers/plans/2026-07-29-storage-release-pending-ready-filter.md`(本文件,勾选完成项)
|
|
|
+
|
|
|
+- [x] **Step 1: 实现完成后更新 spec 状态为「已实现」并 commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/specs/2026-07-29-storage-release-pending-ready-filter-design.md \
|
|
|
+ docs/superpowers/plans/2026-07-29-storage-release-pending-ready-filter.md
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+docs: mark storage-release ready filter spec as implemented
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec Coverage Checklist
|
|
|
+
|
|
|
+| Spec 要求 | Task |
|
|
|
+|-----------|------|
|
|
|
+| 新参数 `readyForReviewOnly` + SQL 双 NOT EXISTS | Task 1 |
|
|
|
+| 无工单算完成;质检 dpz/ypz;生产 dpz/jxz | Task 1 Step 3 |
|
|
|
+| 复选框替换质检下拉;默认不勾选 | Task 2 |
|
|
|
+| 仅待审核;重置/切回恢复不勾选 | Task 2 Step 3 |
|
|
|
+| 本页停用 `qcWorkOrderCompleted` | Task 2 Step 3 |
|
|
|
+| 分页 total 正确 | Task 1(过滤在 COUNT 前) |
|
|
|
+| 保留旧 qc 参数兼容 | Task 1(不删旧逻辑) |
|
|
|
+
|
|
|
+## Self-Review Notes
|
|
|
+
|
|
|
+- 无 TBD/占位步骤;normalize 对 `false` 返回 `None`,与 spec「仅 true 启用」一致。
|
|
|
+- 复用现有 `_qc_work_order_exists_for_review_sample(..., unfinished_only=True)`,避免重复质检 EXISTS。
|
|
|
+- `watchDebounced` 会因 `showAllPending` 变化自动刷新;`handleReset`/`handleTabChange` 仍显式 `loadData`,与现页一致。
|