浏览代码

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.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 月之前
父节点
当前提交
12df6d6836

+ 4 - 0
backend/app/api/outbound_release_review.py

@@ -47,6 +47,8 @@ async def list_pilot_release_reviews(
     bizType: Optional[str] = None,
     infusionTimeStart: Optional[str] = None,
     infusionTimeEnd: Optional[str] = None,
+    reviewPhase: Optional[str] = Query(None),
+    readyForReviewOnly: Optional[bool] = Query(None),
     db: Session = Depends(get_db),
     user: dict = Depends(require_menu_code("biz:outboundReleaseReview:list")),
 ):
@@ -62,6 +64,8 @@ async def list_pilot_release_reviews(
             biz_type=bizType,
             infusion_time_start=infusionTimeStart,
             infusion_time_end=infusionTimeEnd,
+            review_phase=reviewPhase,
+            ready_for_review_only=readyForReviewOnly,
             page=page,
             page_size=pageSize,
             user_info=user,

+ 155 - 9
backend/app/crud/outbound_release_review.py

@@ -6,7 +6,7 @@ from collections import defaultdict
 from datetime import datetime
 from typing import Any, Dict, List, Optional
 
-from sqlalchemy import and_, func, or_
+from sqlalchemy import and_, exists, func, or_
 from sqlalchemy.orm import Session
 
 from app.core.utils import generate_uuid
@@ -68,6 +68,117 @@ def _total_pages(total: int, page_size: int) -> int:
     return int(math.ceil(total / float(page_size)))
 
 
+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)
+
+
 def _get_user_name(db: Session, user_id: str) -> str:
     u = db.query(SysUser).filter(SysUser.id == user_id).first()
     return (u.nick_name or "") if u else ""
@@ -132,6 +243,8 @@ def list_reviews_paginated(
     biz_type: Optional[str] = None,
     infusion_time_start: Optional[str] = None,
     infusion_time_end: Optional[str] = None,
+    review_phase: Optional[str] = None,
+    ready_for_review_only=None,
     page: int = 1,
     page_size: int = 10,
     user_info=None,
@@ -246,24 +359,45 @@ def list_reviews_paginated(
     if time_bounds:
         q0 = q0.filter(or_(and_(*time_bounds), BizPilotInfusionSchedule.infusion_time.is_(None)))
 
-    # ── review_conclusion 过滤 ──
+    # ── reviewPhase / review_conclusion 过滤 ──
+    phase = normalize_outbound_review_phase(review_phase)
     rc = (review_conclusion or "").strip()
-    if rc == "pending":
-        # 待审核:不存在审核记录
+    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)
+        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()
         )
-    elif rc:
-        # 有具体结论
+        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(
@@ -273,10 +407,22 @@ def list_reviews_paginated(
             )
             .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(
-            BizPilotInfusionSchedule.id.in_(reviewed_schedule_ids)
+            ~_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),
+        )
+    )
+
     # ── 数据权限 ──
     if user_info:
         from app.core.data_scope import apply_data_scope

+ 34 - 0
backend/tests/test_outbound_release_review_filters.py

@@ -0,0 +1,34 @@
+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