|
|
@@ -0,0 +1,349 @@
|
|
|
+# 入库放行审核 · 关联无效/全作废列表隐藏 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:** 入库放行审核列表在关联协议无效(`yzf`/`sdfq`),或入库质检/入库生产工单「有工单且全部 `yzf`」时,待审核与已审核均不展示该行。
|
|
|
+
|
|
|
+**Architecture:** 在 `list_reviews` 常驻叠加 SQL 过滤:`NOT (协议无效 OR 入库质检全作废 OR 入库生产全作废)`。用 `exists()` + `eq_unicode(sample_code)` 关联工单;协议按 `reception_source` + `protocol_id` 关联。另抽纯函数描述「有状态且全作废」语义供单测。前端与详情/审核/入库 API 不改。
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI + SQLAlchemy(backend)、pytest
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-07-29-storage-release-hide-voided-related-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File Map
|
|
|
+
|
|
|
+| 文件 | 职责 |
|
|
|
+|------|------|
|
|
|
+| `backend/tests/test_storage_release_review_hide_voided.py` | **新建** 纯函数语义单测 |
|
|
|
+| `backend/app/crud/storage_release_review.py` | 纯函数 + EXISTS 辅助 + `list_reviews` 常驻过滤 |
|
|
|
+| `docs/superpowers/specs/2026-07-29-storage-release-hide-voided-related-design.md` | 实现后将状态改为「已实现」 |
|
|
|
+
|
|
|
+不改:`storage_release_review` API 签名、web-admin 页面、表结构、详情/审核/入库。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 纯函数单测(TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/tests/test_storage_release_review_hide_voided.py`
|
|
|
+- Modify: `backend/app/crud/storage_release_review.py`(仅加纯函数,下一步再接 SQL)
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败单测**
|
|
|
+
|
|
|
+创建 `backend/tests/test_storage_release_review_hide_voided.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+from app.core.protocol_status_display import INACTIVE_PROTOCOL_STATUSES
|
|
|
+from app.crud.storage_release_review import (
|
|
|
+ inbound_work_orders_all_void,
|
|
|
+ is_inactive_protocol_status_for_hide,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def test_inactive_protocol_statuses_match_shared_constant():
|
|
|
+ assert is_inactive_protocol_status_for_hide("yzf") is True
|
|
|
+ assert is_inactive_protocol_status_for_hide("sdfq") is True
|
|
|
+ assert is_inactive_protocol_status_for_hide(" SDFQ ") is True
|
|
|
+ assert is_inactive_protocol_status_for_hide("sxz") is False
|
|
|
+ assert is_inactive_protocol_status_for_hide("zky") is False
|
|
|
+ assert is_inactive_protocol_status_for_hide("") is False
|
|
|
+ assert is_inactive_protocol_status_for_hide(None) is False
|
|
|
+ assert set(INACTIVE_PROTOCOL_STATUSES) == {"yzf", "sdfq"}
|
|
|
+
|
|
|
+
|
|
|
+def test_inbound_work_orders_all_void_requires_at_least_one():
|
|
|
+ assert inbound_work_orders_all_void([]) is False
|
|
|
+ assert inbound_work_orders_all_void([""]) is False
|
|
|
+ assert inbound_work_orders_all_void([" "]) is False
|
|
|
+
|
|
|
+
|
|
|
+def test_inbound_work_orders_all_void_true_when_every_status_is_yzf():
|
|
|
+ assert inbound_work_orders_all_void(["yzf"]) is True
|
|
|
+ assert inbound_work_orders_all_void(["yzf", "yzf"]) is True
|
|
|
+ assert inbound_work_orders_all_void([" YZF ", "yzf"]) is True
|
|
|
+
|
|
|
+
|
|
|
+def test_inbound_work_orders_all_void_false_when_any_non_void():
|
|
|
+ assert inbound_work_orders_all_void(["yzf", "ywc"]) is False
|
|
|
+ assert inbound_work_orders_all_void(["dpz"]) is False
|
|
|
+ assert inbound_work_orders_all_void(["ywc"]) is False
|
|
|
+ assert inbound_work_orders_all_void(["yzf", ""]) is False
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑单测确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_hide_voided.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(函数未定义或导入失败)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现纯函数**
|
|
|
+
|
|
|
+在 `backend/app/crud/storage_release_review.py` 顶部 import 区增加(若尚未导入):
|
|
|
+
|
|
|
+```python
|
|
|
+from app.core.protocol_status_display import INACTIVE_PROTOCOL_STATUSES
|
|
|
+```
|
|
|
+
|
|
|
+(若文件已有 `from app.core.protocol_status_display import ...`,合并进同一行。)
|
|
|
+
|
|
|
+在 `normalize_ready_for_review_only` **之后**增加:
|
|
|
+
|
|
|
+```python
|
|
|
+def is_inactive_protocol_status_for_hide(status) -> bool:
|
|
|
+ """True when protocol/donation status is yzf or sdfq (list-hide rule)."""
|
|
|
+ code = (status or "").strip()
|
|
|
+ return code in INACTIVE_PROTOCOL_STATUSES
|
|
|
+
|
|
|
+
|
|
|
+def inbound_work_orders_all_void(statuses) -> bool:
|
|
|
+ """True when there is ≥1 non-empty status and every one equals yzf."""
|
|
|
+ cleaned = [(s or "").strip() for s in (statuses or [])]
|
|
|
+ cleaned = [s for s in cleaned if s]
|
|
|
+ if not cleaned:
|
|
|
+ return False
|
|
|
+ return all(s.lower() == "yzf" for s in cleaned)
|
|
|
+```
|
|
|
+
|
|
|
+说明:`inbound_work_orders_all_void` 用 `.lower()` 与单测 `" YZF "` 对齐;SQL 侧状态码按库内存值比较 `== 'yzf'`(库内为小写拼音码)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑单测确认通过**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_hide_voided.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/tests/test_storage_release_review_hide_voided.py \
|
|
|
+ backend/app/crud/storage_release_review.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(storage-release): add hide-voided predicate helpers
|
|
|
+
|
|
|
+Document protocol inactive and all-inbound-WO-void semantics for list filtering.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: SQL EXISTS 辅助 + 接入 `list_reviews`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/crud/storage_release_review.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 实现三个 EXISTS 条件构造函数**
|
|
|
+
|
|
|
+在同一文件、`_production_work_order_unfinished_exists_for_review_sample` **之后**增加:
|
|
|
+
|
|
|
+```python
|
|
|
+def _inactive_protocol_exists_for_review():
|
|
|
+ """True when linked collection/donation protocol is yzf or sdfq."""
|
|
|
+ inactive_cell = and_(
|
|
|
+ or_(
|
|
|
+ BizStorageReleaseReview.reception_source == "cell_storage",
|
|
|
+ BizStorageReleaseReview.reception_source.is_(None),
|
|
|
+ BizStorageReleaseReview.reception_source == "",
|
|
|
+ ),
|
|
|
+ BizStorageReleaseReview.protocol_id.isnot(None),
|
|
|
+ BizStorageReleaseReview.protocol_id != "",
|
|
|
+ exists()
|
|
|
+ .where(
|
|
|
+ and_(
|
|
|
+ eq_unicode(
|
|
|
+ BizCollectionProtocol.id, BizStorageReleaseReview.protocol_id
|
|
|
+ ),
|
|
|
+ BizCollectionProtocol.del_flag == 0,
|
|
|
+ BizCollectionProtocol.protocol_status.in_(INACTIVE_PROTOCOL_STATUSES),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .correlate(BizStorageReleaseReview),
|
|
|
+ )
|
|
|
+ inactive_donation = and_(
|
|
|
+ BizStorageReleaseReview.reception_source == "research_donation",
|
|
|
+ BizStorageReleaseReview.protocol_id.isnot(None),
|
|
|
+ BizStorageReleaseReview.protocol_id != "",
|
|
|
+ exists()
|
|
|
+ .where(
|
|
|
+ and_(
|
|
|
+ eq_unicode(
|
|
|
+ BizResearchDonation.id, BizStorageReleaseReview.protocol_id
|
|
|
+ ),
|
|
|
+ BizResearchDonation.del_flag == 0,
|
|
|
+ BizResearchDonation.donation_status.in_(INACTIVE_PROTOCOL_STATUSES),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .correlate(BizStorageReleaseReview),
|
|
|
+ )
|
|
|
+ return or_(inactive_cell, inactive_donation)
|
|
|
+
|
|
|
+
|
|
|
+def _inbound_qc_all_void_for_review_sample(tenant_no: str):
|
|
|
+ """True when sample has ≥1 inbound QC WO and every such WO is yzf."""
|
|
|
+ base = [
|
|
|
+ BizQcWorkOrder.tenant_no == tenant_no,
|
|
|
+ BizQcWorkOrder.del_flag == 0,
|
|
|
+ BizQcWorkOrder.qc_biz_type == "inbound",
|
|
|
+ BizQcWorkOrder.sample_code.isnot(None),
|
|
|
+ BizQcWorkOrder.sample_code != "",
|
|
|
+ eq_unicode(BizQcWorkOrder.sample_code, BizStorageReleaseReview.sample_code),
|
|
|
+ BizStorageReleaseReview.sample_code.isnot(None),
|
|
|
+ BizStorageReleaseReview.sample_code != "",
|
|
|
+ ]
|
|
|
+ any_inbound = exists().where(and_(*base)).correlate(BizStorageReleaseReview)
|
|
|
+ any_non_void = (
|
|
|
+ exists()
|
|
|
+ .where(
|
|
|
+ and_(
|
|
|
+ *base,
|
|
|
+ or_(
|
|
|
+ BizQcWorkOrder.work_order_status.is_(None),
|
|
|
+ BizQcWorkOrder.work_order_status != "yzf",
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .correlate(BizStorageReleaseReview)
|
|
|
+ )
|
|
|
+ return and_(any_inbound, ~any_non_void)
|
|
|
+
|
|
|
+
|
|
|
+def _inbound_production_all_void_for_review_sample(tenant_no: str):
|
|
|
+ """True when sample has ≥1 inbound production WO and every such WO is yzf."""
|
|
|
+ base = [
|
|
|
+ BizProductionWorkOrder.tenant_no == tenant_no,
|
|
|
+ BizProductionWorkOrder.del_flag == 0,
|
|
|
+ BizProductionWorkOrder.work_order_type == "inbound",
|
|
|
+ BizProductionWorkOrder.sample_code.isnot(None),
|
|
|
+ BizProductionWorkOrder.sample_code != "",
|
|
|
+ eq_unicode(
|
|
|
+ BizProductionWorkOrder.sample_code, BizStorageReleaseReview.sample_code
|
|
|
+ ),
|
|
|
+ BizStorageReleaseReview.sample_code.isnot(None),
|
|
|
+ BizStorageReleaseReview.sample_code != "",
|
|
|
+ ]
|
|
|
+ any_inbound = exists().where(and_(*base)).correlate(BizStorageReleaseReview)
|
|
|
+ any_non_void = (
|
|
|
+ exists()
|
|
|
+ .where(
|
|
|
+ and_(
|
|
|
+ *base,
|
|
|
+ or_(
|
|
|
+ BizProductionWorkOrder.work_order_status.is_(None),
|
|
|
+ BizProductionWorkOrder.work_order_status != "yzf",
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .correlate(BizStorageReleaseReview)
|
|
|
+ )
|
|
|
+ return and_(any_inbound, ~any_non_void)
|
|
|
+```
|
|
|
+
|
|
|
+确认文件顶部已导入:`BizCollectionProtocol`、`BizResearchDonation`、`BizQcWorkOrder`、`BizProductionWorkOrder`、`eq_unicode`、`exists`、`and_`、`or_`。`BizResearchDonation` 当前从 `app.models.research_donation` 导入;若缺失则补上。
|
|
|
+
|
|
|
+- [ ] **Step 2: 在 `list_reviews` 常驻应用过滤**
|
|
|
+
|
|
|
+在 `list_reviews` 内,于 `ready_for_review_only` 过滤块**之后**、`if user_info:` **之前**插入:
|
|
|
+
|
|
|
+```python
|
|
|
+ # 协议无效 或 入库质检/生产「有工单且全部 yzf」→ 列表隐藏(两 Tab 常驻)
|
|
|
+ q0 = q0.filter(
|
|
|
+ ~or_(
|
|
|
+ _inactive_protocol_exists_for_review(),
|
|
|
+ _inbound_qc_all_void_for_review_sample(tenant_no),
|
|
|
+ _inbound_production_all_void_for_review_sample(tenant_no),
|
|
|
+ )
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+必须位于 `total = q0...count` **之前**。
|
|
|
+
|
|
|
+- [ ] **Step 3: 跑既有 + 新单测**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest \
|
|
|
+ tests/test_storage_release_review_hide_voided.py \
|
|
|
+ tests/test_storage_release_review_ready_filter.py \
|
|
|
+ tests/test_storage_release_review_qc_completed.py \
|
|
|
+ tests/test_storage_release_review_phase.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 全部 PASS
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/crud/storage_release_review.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(storage-release): hide list rows when related docs are void
|
|
|
+
|
|
|
+Exclude reviews whose protocol is inactive or whose inbound QC/production work orders are all voided.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 规格状态 + 手工验收清单
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `docs/superpowers/specs/2026-07-29-storage-release-hide-voided-related-design.md`
|
|
|
+
|
|
|
+- [ ] **Step 1: 将规格头部状态改为已实现**
|
|
|
+
|
|
|
+把:
|
|
|
+
|
|
|
+```markdown
|
|
|
+> 状态:已确认
|
|
|
+```
|
|
|
+
|
|
|
+改为:
|
|
|
+
|
|
|
+```markdown
|
|
|
+> 状态:已实现
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 手工验收(本地有数据时)**
|
|
|
+
|
|
|
+打开 `http://localhost:3006/#/market/cell-storage/storage-release-review`:
|
|
|
+
|
|
|
+1. 协议 `yzf`/`sdfq` 的样本 → 待审核/已审核均不出现。
|
|
|
+2. 入库质检全部 `yzf`、协议有效 → 不出现;混有 `ywc` → 可出现。
|
|
|
+3. 入库生产全部 `yzf` → 不出现;仅出库生产 `yzf` → 不因生产侧隐藏。
|
|
|
+4. 勾选「展示全部待审核」仍看不到全作废行(常驻规则优先于就绪开关)。
|
|
|
+5. 已知隐藏行的 id 直链详情仍可打开(若产品允许)。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit 规格状态**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/specs/2026-07-29-storage-release-hide-voided-related-design.md
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+docs: mark storage-release hide-voided design as implemented
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec coverage (self-review)
|
|
|
+
|
|
|
+| Spec 要求 | Task |
|
|
|
+|-----------|------|
|
|
|
+| 协议 `yzf`/`sdfq` 隐藏 | Task 2 `_inactive_protocol_exists_for_review` |
|
|
|
+| 入库质检全 `yzf` 隐藏 | Task 2 `_inbound_qc_all_void_for_review_sample` |
|
|
|
+| 入库生产全 `yzf` 隐藏 | Task 2 `_inbound_production_all_void_for_review_sample` |
|
|
|
+| 两 Tab 常驻 | Task 2 无 `reviewPhase` 分支条件 |
|
|
|
+| 仅入库侧工单 | `qc_biz_type` / `work_order_type` == `inbound` |
|
|
|
+| 无工单不因该侧隐藏 | `any_inbound AND ~any_non_void` |
|
|
|
+| 混有非作废不隐藏 | `any_non_void` 存在则非全作废 |
|
|
|
+| 详情/审核 API 不改 | 无 Task 改 API |
|
|
|
+| 前端不改 | File Map 已声明 |
|
|
|
+| 与 ready 过滤叠加 | 插在 ready 块之后、COUNT 之前 |
|
|
|
+| 单测 | Task 1 |
|
|
|
+
|
|
|
+无 TBD/占位符。纯函数与 SQL 语义一致:有工单且全为 `yzf`。
|