|
|
@@ -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
|