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>
This commit is contained in:
zlwuandCursor committed 2026-07-29 13:34:29 +08:00
1 parent d3cc76d560
commit 12df6d6836
3 files changed
+193 -9

No files matched your search

@@ -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
View File
@@ -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 != "",
)
elif rc:
# 有具体结论
.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(
@@ -273,8 +407,20 @@ 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),
)
)
# ── 数据权限 ──
@@ -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