2026-07-29-storage-release-hide-voided-related.md 12 KB

入库放行审核 · 关联无效/全作废列表隐藏 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)

  • [x] Step 1: 写失败单测

创建 backend/tests/test_storage_release_review_hide_voided.py:

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: 跑单测确认失败
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 区增加(若尚未导入):

from app.core.protocol_status_display import INACTIVE_PROTOCOL_STATUSES

(若文件已有 from app.core.protocol_status_display import ...,合并进同一行。)

在 normalize_ready_for_review_only 之后增加:

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: 跑单测确认通过
cd backend && source venv/bin/activate && pytest tests/test_storage_release_review_hide_voided.py -v

Expected: PASS

  • Step 5: Commit
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

  • [x] Step 1: 实现三个 EXISTS 条件构造函数

在同一文件、_production_work_order_unfinished_exists_for_review_sample 之后增加:

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: 之前插入:

    # 协议无效 或 入库质检/生产「有工单且全部 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: 跑既有 + 新单测
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
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

  • [x] Step 1: 将规格头部状态改为已实现

把:

> 状态:已确认

改为:

> 状态:已实现
  • 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 规格状态
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。