From d3cc76d5602fa9252ea7f29bda33a13072a7d935 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Wed, 29 Jul 2026 13:32:15 +0800 Subject: [PATCH] docs: add outbound release tabs/ready/void implementation plan Break down backend list filters and web-admin tabs/checkbox aligned with storage-release. Co-authored-by: Cursor --- ...-07-29-outbound-release-tabs-ready-void.md | 554 ++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-29-outbound-release-tabs-ready-void.md diff --git a/docs/superpowers/plans/2026-07-29-outbound-release-tabs-ready-void.md b/docs/superpowers/plans/2026-07-29-outbound-release-tabs-ready-void.md new file mode 100644 index 00000000..ad097080 --- /dev/null +++ b/docs/superpowers/plans/2026-07-29-outbound-release-tabs-ready-void.md @@ -0,0 +1,554 @@ +# 出库放行审核 · Tab + 就绪过滤 + 作废隐藏 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、待审核默认仅展示双工单侧完成、两 Tab 常驻隐藏「出库质检/生产全作废」行。 + +**Architecture:** 在 `list_reviews_paginated`(输注计划主表)增加 `review_phase`、`ready_for_review_only`,以及常驻 `NOT (qc_all_void OR production_all_void)`。工单经 `infusion_schedule_id` 关联出库质检/出库生产。前端同页组件加 Tab +「展示全部待审核」复选框,中试/基础实验/临床入口一并生效。 + +**Tech Stack:** FastAPI + SQLAlchemy、Vue 3 + Element Plus、pytest + +**Spec:** `docs/superpowers/specs/2026-07-29-outbound-release-tabs-ready-void-design.md` + +--- + +## File Map + +| 文件 | 职责 | +|------|------| +| `backend/tests/test_outbound_release_review_filters.py` | **新建** normalize / 谓词单测 | +| `backend/app/crud/outbound_release_review.py` | phase / ready / void EXISTS + `list_reviews_paginated` | +| `backend/app/api/outbound_release_review.py` | Query 透传 `reviewPhase`、`readyForReviewOnly` | +| `web-admin/src/api/outbound-release-review.ts` | 列表 params 类型 | +| `web-admin/src/views/pilot-service/outbound-release-review/index.vue` | Tab、复选框、请求、样式 | +| `docs/superpowers/specs/2026-07-29-outbound-release-tabs-ready-void-design.md` | 实现后标「已实现」 | + +不改:详情/审核/物料 API、卡片列表结构、表结构。 + +--- + +### Task 1: Backend — normalize、EXISTS、列表过滤、API + +**Files:** +- Create: `backend/tests/test_outbound_release_review_filters.py` +- Modify: `backend/app/crud/outbound_release_review.py` +- Modify: `backend/app/api/outbound_release_review.py` + +- [ ] **Step 1: 写失败单测** + +创建 `backend/tests/test_outbound_release_review_filters.py`: + +```python +from app.crud.outbound_release_review import ( + normalize_outbound_ready_for_review_only, + normalize_outbound_review_phase, + outbound_work_orders_all_void, +) + + +def test_normalize_outbound_review_phase(): + assert normalize_outbound_review_phase("pending") == "pending" + assert normalize_outbound_review_phase("REVIEWED") == "reviewed" + assert normalize_outbound_review_phase(" reviewed ") == "reviewed" + assert normalize_outbound_review_phase(None) is None + assert normalize_outbound_review_phase("") is None + assert normalize_outbound_review_phase("all") is None + + +def test_normalize_outbound_ready_for_review_only(): + assert normalize_outbound_ready_for_review_only(True) is True + assert normalize_outbound_ready_for_review_only("true") is True + assert normalize_outbound_ready_for_review_only("1") is True + assert normalize_outbound_ready_for_review_only("yes") is True + assert normalize_outbound_ready_for_review_only(False) is None + assert normalize_outbound_ready_for_review_only("false") is None + assert normalize_outbound_ready_for_review_only(None) is None + + +def test_outbound_all_void_predicate(): + assert outbound_work_orders_all_void([]) is False + assert outbound_work_orders_all_void([""]) is False + assert outbound_work_orders_all_void(["yzf"]) is True + assert outbound_work_orders_all_void(["yzf", "yzf"]) is True + assert outbound_work_orders_all_void([" YZF ", "yzf"]) is True + assert outbound_work_orders_all_void(["yzf", "ywc"]) is False + assert outbound_work_orders_all_void(["yzf", ""]) is False +``` + +- [ ] **Step 2: 跑单测确认失败** + +```bash +cd backend && source venv/bin/activate && PYTHONPATH=. pytest tests/test_outbound_release_review_filters.py -v +``` + +Expected: FAIL(导入失败) + +- [ ] **Step 3: 实现 normalize、谓词、EXISTS,并接入 `list_reviews_paginated`** + +在 `backend/app/crud/outbound_release_review.py`: + +1) 顶部将 sqlalchemy 导入改为: + +```python +from sqlalchemy import and_, exists, func, or_ +``` + +(已有 `and_`/`func`/`or_` 则只补 `exists`。) + +2) 在 `_total_pages` 之后(或 Helpers 区)增加: + +```python +def normalize_outbound_review_phase(review_phase: Optional[str]) -> Optional[str]: + p = (review_phase or "").strip().lower() + if p in ("pending", "reviewed"): + return p + return None + + +def normalize_outbound_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 outbound_work_orders_all_void(statuses) -> bool: + """True when ≥1 non-empty status and every one equals yzf.""" + cleaned = [(s or "").strip() for s in (statuses or [])] + if not cleaned or not any(s for s in cleaned): + return False + return all(s.lower() == "yzf" for s in cleaned) + + +def _outbound_qc_unfinished_exists_for_schedule(tenant_no: str): + """Any unfinished outbound schedule QC (dpz/ypz) for this infusion schedule.""" + return ( + exists() + .where( + and_( + BizQcWorkOrder.tenant_no == tenant_no, + BizQcWorkOrder.del_flag == 0, + BizQcWorkOrder.qc_biz_type == "outbound", + BizQcWorkOrder.trigger_source == "schedule", + BizQcWorkOrder.infusion_schedule_id == BizPilotInfusionSchedule.id, + BizQcWorkOrder.work_order_status.in_(("dpz", "ypz")), + ) + ) + .correlate(BizPilotInfusionSchedule) + ) + + +def _outbound_production_unfinished_exists_for_schedule(tenant_no: str): + return ( + exists() + .where( + and_( + BizProductionWorkOrder.tenant_no == tenant_no, + BizProductionWorkOrder.del_flag == 0, + BizProductionWorkOrder.work_order_type == "outbound", + BizProductionWorkOrder.infusion_schedule_id + == BizPilotInfusionSchedule.id, + BizProductionWorkOrder.work_order_status.in_(("dpz", "jxz")), + ) + ) + .correlate(BizPilotInfusionSchedule) + ) + + +def _outbound_qc_all_void_for_schedule(tenant_no: str): + base = [ + BizQcWorkOrder.tenant_no == tenant_no, + BizQcWorkOrder.del_flag == 0, + BizQcWorkOrder.qc_biz_type == "outbound", + BizQcWorkOrder.trigger_source == "schedule", + BizQcWorkOrder.infusion_schedule_id == BizPilotInfusionSchedule.id, + ] + any_wo = exists().where(and_(*base)).correlate(BizPilotInfusionSchedule) + any_non_void = ( + exists() + .where( + and_( + *base, + or_( + BizQcWorkOrder.work_order_status.is_(None), + BizQcWorkOrder.work_order_status != "yzf", + ), + ) + ) + .correlate(BizPilotInfusionSchedule) + ) + return and_(any_wo, ~any_non_void) + + +def _outbound_production_all_void_for_schedule(tenant_no: str): + base = [ + BizProductionWorkOrder.tenant_no == tenant_no, + BizProductionWorkOrder.del_flag == 0, + BizProductionWorkOrder.work_order_type == "outbound", + BizProductionWorkOrder.infusion_schedule_id == BizPilotInfusionSchedule.id, + ] + any_wo = exists().where(and_(*base)).correlate(BizPilotInfusionSchedule) + any_non_void = ( + exists() + .where( + and_( + *base, + or_( + BizProductionWorkOrder.work_order_status.is_(None), + BizProductionWorkOrder.work_order_status != "yzf", + ), + ) + ) + .correlate(BizPilotInfusionSchedule) + ) + return and_(any_wo, ~any_non_void) +``` + +3) 修改 `list_reviews_paginated` 签名,在 `infusion_time_end` 之后增加: + +```python + review_phase: Optional[str] = None, + ready_for_review_only=None, +``` + +4) **替换**现有「`# ── review_conclusion 过滤 ──`」整块为(保留旧 `review_conclusion=pending` 兼容,但 `review_phase` 优先): + +```python + # ── reviewPhase / review_conclusion 过滤 ── + phase = normalize_outbound_review_phase(review_phase) + rc = (review_conclusion or "").strip() + if phase == "pending" or (phase is None and rc == "pending"): + reviewed_schedule_ids = ( + db.query(BizOutboundReleaseReview.infusion_schedule_id) + .filter( + BizOutboundReleaseReview.tenant_no == tenant_no, + BizOutboundReleaseReview.del_flag == 0, + BizOutboundReleaseReview.review_conclusion.isnot(None), + BizOutboundReleaseReview.review_conclusion != "", + ) + .subquery() + ) + q0 = q0.filter(~BizPilotInfusionSchedule.id.in_(reviewed_schedule_ids)) + elif phase == "reviewed": + reviewed_schedule_ids = ( + db.query(BizOutboundReleaseReview.infusion_schedule_id) + .filter( + BizOutboundReleaseReview.tenant_no == tenant_no, + BizOutboundReleaseReview.del_flag == 0, + BizOutboundReleaseReview.review_conclusion.isnot(None), + BizOutboundReleaseReview.review_conclusion != "", + ) + .subquery() + ) + q0 = q0.filter(BizPilotInfusionSchedule.id.in_(reviewed_schedule_ids)) + if rc in ("tg", "btg"): + conclusion_ids = ( + db.query(BizOutboundReleaseReview.infusion_schedule_id) + .filter( + BizOutboundReleaseReview.tenant_no == tenant_no, + BizOutboundReleaseReview.del_flag == 0, + BizOutboundReleaseReview.review_conclusion == rc, + ) + .subquery() + ) + q0 = q0.filter(BizPilotInfusionSchedule.id.in_(conclusion_ids)) + elif rc in ("tg", "btg"): + reviewed_schedule_ids = ( + db.query(BizOutboundReleaseReview.infusion_schedule_id) + .filter( + BizOutboundReleaseReview.tenant_no == tenant_no, + BizOutboundReleaseReview.del_flag == 0, + BizOutboundReleaseReview.review_conclusion == rc, + ) + .subquery() + ) + q0 = q0.filter(BizPilotInfusionSchedule.id.in_(reviewed_schedule_ids)) + + if normalize_outbound_ready_for_review_only(ready_for_review_only) is True: + q0 = q0.filter( + ~_outbound_qc_unfinished_exists_for_schedule(tenant_no), + ~_outbound_production_unfinished_exists_for_schedule(tenant_no), + ) + + # 出库质检/生产「有工单且全部 yzf」→ 列表隐藏(两 Tab 常驻) + q0 = q0.filter( + ~or_( + _outbound_qc_all_void_for_schedule(tenant_no), + _outbound_production_all_void_for_schedule(tenant_no), + ) + ) +``` + +将上述块插在**原 review_conclusion 过滤位置**,且必须在 `if user_info:` / `total = ...` **之前**。删除旧的仅 `rc == "pending"` / `elif rc:` 重复逻辑,避免双重过滤。 + +- [ ] **Step 4: API 透传** + +在 `backend/app/api/outbound_release_review.py` 的 `list_pilot_release_reviews`: + +在 `infusionTimeEnd: Optional[str] = None,` 后增加: + +```python + reviewPhase: Optional[str] = Query(None), + readyForReviewOnly: Optional[bool] = Query(None), +``` + +在 `list_reviews_paginated(...)` 调用中增加: + +```python + review_phase=reviewPhase, + ready_for_review_only=readyForReviewOnly, +``` + +- [ ] **Step 5: 跑单测** + +```bash +cd backend && source venv/bin/activate && PYTHONPATH=. pytest tests/test_outbound_release_review_filters.py -v +``` + +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add backend/tests/test_outbound_release_review_filters.py \ + backend/app/crud/outbound_release_review.py \ + backend/app/api/outbound_release_review.py +git commit -m "$(cat <<'EOF' +feat(outbound-release): add reviewPhase, ready filter, and void hide + +List supports pending/reviewed phase, readyForReviewOnly, and always hides schedules whose outbound QC or production WOs are all voided. +EOF +)" +``` + +--- + +### Task 2: Frontend — API params + Tab + 复选框 + +**Files:** +- Modify: `web-admin/src/api/outbound-release-review.ts` +- Modify: `web-admin/src/views/pilot-service/outbound-release-review/index.vue` + +- [ ] **Step 1: API params** + +修改 `fetchOutboundReleaseReviewList` 的 params 类型与调用,增加: + +```typescript + reviewPhase?: 'pending' | 'reviewed' + readyForReviewOnly?: boolean +``` + +- [ ] **Step 2: 页面结构 — Tab + 复选框 + 去掉空「待审核」** + +在 `index.vue` 模板中,于 `panel-top` 之后、`filter-block` 之前插入(对齐入库): + +```vue +
+
+ +
+
+``` + +审核结论 `el-select`:**删除** ``,仅保留 `reviewConclusionOptions` 循环。 + +在 `filter-actions-row` 内、`filter-actions-end` 之前插入: + +```vue + +``` + +- [ ] **Step 3: script 逻辑** + +在 `defineOptions` 附近增加: + +```typescript + type ReviewTab = 'pending' | 'reviewed' + + const STATUS_TABS = [ + { name: 'pending' as const, label: '待审核' }, + { name: 'reviewed' as const, label: '已审核' } + ] + + const activeTab = ref('pending') +``` + +`searchForm` 增加 `showAllPending: false`。 + +改写 `loadData`: + +```typescript + async function loadData() { + loading.value = true + try { + const readyOnly = + activeTab.value === 'pending' && !searchForm.showAllPending ? true : undefined + const res = await fetchOutboundReleaseReviewList({ + page: pagination.page, + pageSize: pagination.pageSize, + reviewConclusion: searchForm.reviewConclusion || undefined, + keyword: searchForm.keyword || undefined, + bizType: effectiveBizType.value, + infusionTimeStart: searchForm.infusionTimeRange?.[0] || undefined, + infusionTimeEnd: searchForm.infusionTimeRange?.[1] || undefined, + reviewPhase: activeTab.value, + readyForReviewOnly: readyOnly + }) + tableData.value = res.list || [] + pagination.total = res.total ?? 0 + } finally { + loading.value = false + } + } +``` + +增加: + +```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` 增加:`searchForm.showAllPending = false`(若当前在待审核;或始终重置该字段)。 + +注意:现有 `watchDebounced(searchForm, ...)` 会在勾选复选框时自动搜索——保留即可。Tab 切换走 `handleTabChange`,不要让 `activeTab` 进 `searchForm` 以免重复请求逻辑混乱。 + +- [ ] **Step 4: 样式** + +若文件无 ` +``` + +- [ ] **Step 5: Commit** + +```bash +git add web-admin/src/api/outbound-release-review.ts \ + web-admin/src/views/pilot-service/outbound-release-review/index.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): outbound release pending/reviewed tabs and ready checkbox + +Match storage-release review UX for pilot/basic-experiment outbound release lists. +EOF +)" +``` + +--- + +### Task 3: 规格状态 + 手工验收 + +**Files:** +- Modify: `docs/superpowers/specs/2026-07-29-outbound-release-tabs-ready-void-design.md` + +- [ ] **Step 1:** 将 `> 状态:已确认` 改为 `> 状态:已实现` + +- [ ] **Step 2: 手工验收清单(实现者报告中粘贴给用户)** + +1. `http://localhost:3006/#/market/pilot-service/outbound-release-review` 与基础实验同路径入口:双 Tab 默认待审核。 +2. 待审核默认:有未完成出库质检/生产 → 不出现;双完成或双无工单 → 可出现。 +3. 勾选「展示全部待审核」→ 可见未完成行;全作废行仍不可见。 +4. 出库质检或出库生产全部 `yzf` → 两 Tab 均不出现。 +5. 已审核无复选框;作废隐藏仍生效。 +6. 详情直链仍可打开。 + +- [ ] **Step 3: Commit** + +```bash +git add docs/superpowers/specs/2026-07-29-outbound-release-tabs-ready-void-design.md +git commit -m "$(cat <<'EOF' +docs: mark outbound release tabs/ready/void design as implemented +EOF +)" +``` + +--- + +## Spec coverage (self-review) + +| Spec | Task | +|------|------| +| 双 Tab pending/reviewed | Task 2 + Task 1 phase | +| 去掉空「待审核」选项 | Task 2 | +| ready 复选框仅待审核、默认不勾选 | Task 2 | +| ready SQL 双 NOT EXISTS | Task 1 | +| 出库 WO 范围 schedule + outbound | Task 1 | +| 作废全 yzf 常驻隐藏 | Task 1 | +| 详情 API 不改 | 无 Task 改详情 | +| 中试/基础实验同页 | Task 2 单页 | +| 单测 | Task 1 | + +无 TBD。函数名统一为 `outbound_work_orders_all_void`。