protocol_status_display.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """协议状态展示(含作废类型后缀)。"""
  2. from __future__ import annotations
  3. from typing import Dict, List, Optional, Tuple
  4. from sqlalchemy.orm import Session
  5. from app.models.models import SysDictData
  6. VOID_TYPE_DICT = "xyzflx"
  7. # 协议/捐赠单无效态(与样本接收、生产工单列表过滤口径一致)
  8. INACTIVE_PROTOCOL_STATUSES = ("yzf", "sdfq")
  9. # 科研捐赠质检列表/角标默认协议状态:待生效、生效中
  10. DEFAULT_QC_DONATION_PROTOCOL_STATUSES = ("dsx", "sxz")
  11. def resolve_research_donation_protocol_status_filter(
  12. protocol_status: Optional[str],
  13. ) -> Tuple[str, List[str]]:
  14. """解析科研捐赠质检列表的协议状态筛选语义。
  15. Returns:
  16. ("in", statuses) — 按所选状态 IN 过滤
  17. ("none", []) — 显式空选择,无匹配结果
  18. ("default", DEFAULT_QC_DONATION_PROTOCOL_STATUSES) — 未传参,与列表默认一致
  19. """
  20. if protocol_status is None:
  21. return ("default", list(DEFAULT_QC_DONATION_PROTOCOL_STATUSES))
  22. statuses = [s.strip() for s in str(protocol_status).split(",") if s.strip()]
  23. if not statuses:
  24. return ("none", [])
  25. return ("in", statuses)
  26. _PROTOCOL_STATUS_LABELS = {
  27. "dsx": "待生效",
  28. "sxz": "生效中",
  29. "ygq": "已过期",
  30. "yzf": "已作废",
  31. "sdfq": "属地废弃",
  32. "zky": "转科研",
  33. }
  34. _DONATION_STATUS_LABELS = {
  35. "dsx": "待生效",
  36. "sxz": "生效中",
  37. "ygq": "已过期",
  38. "yzf": "已作废",
  39. "sdfq": "属地废弃",
  40. "zckc": "转细胞存储",
  41. }
  42. _VOID_TYPE_FALLBACK_LABELS = {
  43. "khhc": "客户毁存",
  44. "jcbhg": "检测不合格",
  45. }
  46. # 非法来源状态视为终态,不允许再转出
  47. _PROTOCOL_STATUS_TRANSITIONS = {
  48. "dsx": ("sxz", "yzf", "sdfq"),
  49. "sxz": ("ygq", "yzf", "zky", "sdfq"),
  50. }
  51. _DONATION_STATUS_TRANSITIONS = {
  52. "dsx": ("sxz", "yzf", "sdfq"),
  53. "sxz": ("ygq", "yzf", "zckc", "sdfq"),
  54. }
  55. def resolve_void_type_label(
  56. db: Optional[Session],
  57. tenant_no: Optional[str],
  58. void_type: Optional[str],
  59. ) -> str:
  60. code = (void_type or "").strip()
  61. if not code:
  62. return ""
  63. if db is not None and tenant_no:
  64. row = (
  65. db.query(SysDictData.dict_label)
  66. .filter(
  67. SysDictData.dict_type == VOID_TYPE_DICT,
  68. SysDictData.dict_value == code,
  69. SysDictData.tenant_no == tenant_no,
  70. SysDictData.del_flag == 0,
  71. )
  72. .first()
  73. )
  74. if row and (row.dict_label or "").strip():
  75. return (row.dict_label or "").strip()
  76. return _VOID_TYPE_FALLBACK_LABELS.get(code, code)
  77. def status_transition_denied_message(current: str, target: str, *, kind: str = "protocol") -> str:
  78. """状态流转被拒绝时的提示,状态码展示为中文名称。"""
  79. current_label = format_protocol_status_label(current, kind=kind)
  80. target_label = format_protocol_status_label(target, kind=kind)
  81. return f"状态{current_label}不可转为{target_label}"
  82. def assert_protocol_status_transition(current: str, target: str, *, kind: str = "protocol") -> None:
  83. cur = (current or "").strip()
  84. nxt = (target or "").strip()
  85. if cur == nxt:
  86. return
  87. table = _DONATION_STATUS_TRANSITIONS if kind == "donation" else _PROTOCOL_STATUS_TRANSITIONS
  88. if nxt not in table.get(cur, ()):
  89. raise ValueError(status_transition_denied_message(cur, nxt, kind=kind))
  90. def format_protocol_status_label(
  91. status: str,
  92. void_type: Optional[str] = None,
  93. *,
  94. kind: str = "protocol",
  95. db: Optional[Session] = None,
  96. tenant_no: Optional[str] = None,
  97. ) -> str:
  98. st = (status or "").strip()
  99. labels = _DONATION_STATUS_LABELS if kind == "donation" else _PROTOCOL_STATUS_LABELS
  100. base = labels.get(st, st)
  101. if st != "yzf":
  102. return base
  103. vt_label = resolve_void_type_label(db, tenant_no, void_type)
  104. if vt_label:
  105. return f"{base}({vt_label})"
  106. return base
  107. def build_void_type_label_map(
  108. db: Session,
  109. tenant_no: str,
  110. codes: list[str],
  111. ) -> Dict[str, str]:
  112. normalized = list({(c or "").strip() for c in codes if (c or "").strip()})
  113. if not normalized:
  114. return {}
  115. rows = (
  116. db.query(SysDictData.dict_value, SysDictData.dict_label)
  117. .filter(
  118. SysDictData.dict_type == VOID_TYPE_DICT,
  119. SysDictData.dict_value.in_(normalized),
  120. SysDictData.tenant_no == tenant_no,
  121. SysDictData.del_flag == 0,
  122. )
  123. .all()
  124. )
  125. out = {r.dict_value: (r.dict_label or "").strip() for r in rows if (r.dict_value or "").strip()}
  126. for code in normalized:
  127. out.setdefault(code, _VOID_TYPE_FALLBACK_LABELS.get(code, code))
  128. return out