新模块读取旧台账补录并映射字段;旧台账保存时同步到新补充表与生产传染四项,新模块保存时回写旧补录。 Co-authored-by: Cursor <cursoragent@cursor.com>
1207 lines
41 KiB
Python
1207 lines
41 KiB
Python
"""实验室样本处理数据 — 聚合市场/生产/质检已发生业务数据与部门字段回写。
|
|
|
|
Excel 仅作为字段目录示例;列表以协议样本为主表,关联样本接收与生产接收,
|
|
补充表只承载源系统没有的列,且不得覆盖源表字段。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import uuid
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from sqlalchemy import or_, text
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.models import (
|
|
BizCellSampleType,
|
|
BizCollectionHospital,
|
|
BizCollectionProtocol,
|
|
BizProtocolCollectionInfo,
|
|
BizProtocolPartyCustomer,
|
|
BizProtocolSample,
|
|
BizSampleProductionReception,
|
|
BizSampleReception,
|
|
BizSampleReceptionCheckResult,
|
|
)
|
|
from app.services.lab_sample_processing_fields import (
|
|
DATASET_HSC,
|
|
DATASET_MSC,
|
|
DATASET_UBMC,
|
|
DEPT_EDIT_PERMS,
|
|
PERM_LIST,
|
|
PERM_MANAGER,
|
|
SOURCE_EXTRA,
|
|
SOURCE_PRODUCTION,
|
|
SOURCE_PROTOCOL,
|
|
SOURCE_RECEPTION,
|
|
editable_depts_for_user,
|
|
field_map,
|
|
fields_for,
|
|
resolve_dataset_by_type_name,
|
|
)
|
|
|
|
|
|
def _new_id() -> str:
|
|
return uuid.uuid4().hex.upper()
|
|
|
|
|
|
def _fmt_dt(v: Any) -> Optional[str]:
|
|
if v is None:
|
|
return None
|
|
if isinstance(v, datetime):
|
|
return v.strftime("%Y-%m-%d %H:%M:%S")
|
|
return str(v)
|
|
|
|
|
|
def _fmt_num(v: Any) -> Any:
|
|
if v is None:
|
|
return None
|
|
if isinstance(v, Decimal):
|
|
return float(v)
|
|
return v
|
|
|
|
|
|
def _parse_extra(raw: Optional[str]) -> Dict[str, Any]:
|
|
if not raw:
|
|
return {}
|
|
try:
|
|
data = json.loads(raw)
|
|
except Exception:
|
|
return {}
|
|
return data if isinstance(data, dict) else {}
|
|
|
|
|
|
def user_acl(db: Session, user: dict) -> Dict[str, Any]:
|
|
from app.core.menu_guard import user_has_menu_perm
|
|
|
|
is_manager = user_has_menu_perm(db, user, PERM_MANAGER)
|
|
dept_flags = {
|
|
dept: user_has_menu_perm(db, user, code) for dept, code in DEPT_EDIT_PERMS.items()
|
|
}
|
|
editable = editable_depts_for_user(is_manager=is_manager, dept_flags=dept_flags)
|
|
return {
|
|
"canList": user_has_menu_perm(db, user, PERM_LIST) or is_manager or any(dept_flags.values()),
|
|
"isManager": is_manager,
|
|
"editableDepts": sorted(editable),
|
|
"deptFlags": dept_flags,
|
|
}
|
|
|
|
|
|
def _type_name_filters(dataset: str):
|
|
if dataset == DATASET_HSC:
|
|
return "%造血%"
|
|
if dataset == DATASET_MSC:
|
|
return "%间充质%"
|
|
if dataset == DATASET_UBMC:
|
|
return "%单个核%"
|
|
return None
|
|
|
|
|
|
def _code_prefix_filters(dataset: str) -> List[str]:
|
|
if dataset == DATASET_HSC:
|
|
return ["B%"]
|
|
if dataset == DATASET_MSC:
|
|
return ["BC%", "UC%"]
|
|
if dataset == DATASET_UBMC:
|
|
return ["UBMC%"]
|
|
return []
|
|
|
|
|
|
def _match_keyword(row: Dict[str, Any], kw: str) -> bool:
|
|
if not kw:
|
|
return True
|
|
text = (
|
|
f"{row.get('sampleCode') or ''} "
|
|
f"{row.get('protocolSampleCode') or ''} "
|
|
f"{row.get('motherName') or ''}"
|
|
)
|
|
return kw.lower() in text.lower()
|
|
|
|
|
|
def _infectious_label(code: Optional[str]) -> Optional[str]:
|
|
c = ("" if code is None else str(code)).strip()
|
|
if not c:
|
|
return None
|
|
if c == "zc":
|
|
return "正常"
|
|
if c == "bzc":
|
|
return "不正常"
|
|
return c
|
|
|
|
|
|
def _infectious_code(label: Optional[Any]) -> Optional[str]:
|
|
if label is None:
|
|
return None
|
|
s = str(label).strip()
|
|
if not s:
|
|
return None
|
|
if s == "正常":
|
|
return "zc"
|
|
if s == "不正常":
|
|
return "bzc"
|
|
return s
|
|
|
|
|
|
def _yes_no_from_bool(flag: Optional[bool]) -> Optional[str]:
|
|
if flag is None:
|
|
return None
|
|
return "是" if flag else "否"
|
|
|
|
|
|
def _derive_receive_check(flags: Dict[str, bool]) -> Optional[str]:
|
|
if not flags:
|
|
return None
|
|
skip = {"知情同意书", "健康调查表", "样本信息正确,标识清晰"}
|
|
preferred = [
|
|
"针路密闭",
|
|
"容器无破损",
|
|
"外观正常",
|
|
"样本无破损",
|
|
"针卡关紧",
|
|
"两端系紧",
|
|
"温度4-25",
|
|
"采集至到库时间≤24小时",
|
|
]
|
|
parts = [name for name in preferred if flags.get(name)]
|
|
if not parts:
|
|
parts = [name for name, checked in flags.items() if checked and name not in skip]
|
|
return "、".join(parts) if parts else None
|
|
|
|
|
|
def _check_flag_map(
|
|
db: Session, tenant_no: str, reception_ids: List[str]
|
|
) -> Dict[str, Dict[str, bool]]:
|
|
ids = [i for i in reception_ids if i]
|
|
if not ids:
|
|
return {}
|
|
rows = (
|
|
db.query(BizSampleReceptionCheckResult)
|
|
.filter(
|
|
BizSampleReceptionCheckResult.tenant_no == tenant_no,
|
|
BizSampleReceptionCheckResult.del_flag == 0,
|
|
BizSampleReceptionCheckResult.reception_id.in_(ids),
|
|
)
|
|
.all()
|
|
)
|
|
out: Dict[str, Dict[str, bool]] = {}
|
|
for r in rows:
|
|
bucket = out.setdefault(r.reception_id, {})
|
|
name = (r.item_name or "").strip()
|
|
if name:
|
|
bucket[name] = bool(r.checked)
|
|
return out
|
|
|
|
|
|
def _pick_extra(
|
|
extras: Dict[str, Any], *codes: Optional[str]
|
|
) -> Dict[str, Any]:
|
|
for code in codes:
|
|
key = (code or "").strip()
|
|
if key and key in extras:
|
|
return _parse_extra(extras[key].payload_json)
|
|
return {}
|
|
|
|
|
|
# 旧台账补录字段名 → 新模块字段名(同名键保持不变)
|
|
_LEGACY_SUPPLEMENT_TO_NEW: Dict[str, str] = {
|
|
"receiveCheckText": "receiveCheck",
|
|
"infectiousFourTestLabel": "infectiousFourCard",
|
|
"preOpNucleatedCount": "preOpTnc",
|
|
"preEnrichNucleatedCount": "preEnrichTnc",
|
|
"postEnrichNucleatedCount": "postEnrichTnc",
|
|
"cd34Count1e4": "cd34Count",
|
|
"transportPackingList": "hasPackingList",
|
|
"cordBloodReceiveConfirm": "hasReceiveConfirm",
|
|
"remarkProcess": "processRemark",
|
|
"motherHbsag": "hbsag",
|
|
"motherAntiHcv": "antiHcv",
|
|
"motherAntiTp": "antiTp",
|
|
"motherAntiHiv": "antiHiv",
|
|
"motherCmvIgm": "cmvIgm",
|
|
}
|
|
|
|
_NEW_TO_LEGACY_SUPPLEMENT: Dict[str, str] = {
|
|
v: k for k, v in _LEGACY_SUPPLEMENT_TO_NEW.items()
|
|
}
|
|
# MSC 等仍使用 mother* 键名,双向都保留
|
|
for _same in (
|
|
"labMaterialCode",
|
|
"storageStatus",
|
|
"discardReason",
|
|
"hasMaternalBlood",
|
|
"remeasureMaternalBlood",
|
|
"infectiousFourExternal",
|
|
"endotoxinTest",
|
|
"bacteriaTest",
|
|
"fungusTest",
|
|
"mycoplasmaTest",
|
|
"motherHbsag",
|
|
"motherHbvDna",
|
|
"motherAntiHcv",
|
|
"motherHcvRna",
|
|
"motherAntiTp",
|
|
"motherCmvIgm",
|
|
"motherAntiHiv",
|
|
"motherHiv1Rna",
|
|
"storageDateP2",
|
|
"storageTotalTubes",
|
|
"availableCellCount1e7",
|
|
):
|
|
_LEGACY_SUPPLEMENT_TO_NEW.setdefault(_same, _same)
|
|
_NEW_TO_LEGACY_SUPPLEMENT.setdefault(_same, _same)
|
|
|
|
|
|
def _is_blank(val: Any) -> bool:
|
|
return val is None or (isinstance(val, str) and not val.strip())
|
|
|
|
|
|
def _map_legacy_supplement(payload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
out: Dict[str, Any] = {}
|
|
for key, value in (payload or {}).items():
|
|
if _is_blank(value):
|
|
continue
|
|
new_key = _LEGACY_SUPPLEMENT_TO_NEW.get(str(key), str(key))
|
|
out[new_key] = value
|
|
# MSC 场景保留 mother* 原键,避免被 UBMC 别名覆盖后丢失
|
|
if str(key).startswith("mother") and new_key != str(key):
|
|
out[str(key)] = value
|
|
return out
|
|
|
|
|
|
def _map_new_to_legacy_supplement(payload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
out: Dict[str, Any] = {}
|
|
for key, value in (payload or {}).items():
|
|
if _is_blank(value):
|
|
continue
|
|
legacy_key = _NEW_TO_LEGACY_SUPPLEMENT.get(str(key), str(key))
|
|
out[legacy_key] = value
|
|
return out
|
|
|
|
|
|
def _merge_extra_with_legacy(
|
|
extra: Optional[Dict[str, Any]], legacy: Optional[Dict[str, Any]]
|
|
) -> Dict[str, Any]:
|
|
"""新补充表优先;旧台账补录填空(含字段名映射)。"""
|
|
mapped = _map_legacy_supplement(legacy)
|
|
merged = dict(mapped)
|
|
for key, value in (extra or {}).items():
|
|
if not _is_blank(value):
|
|
merged[key] = value
|
|
return merged
|
|
|
|
|
|
def _load_legacy_supplements(
|
|
db: Session, tenant_no: str, codes: List[str]
|
|
) -> Dict[str, Dict[str, Any]]:
|
|
from app.models.models import BizLabSampleProcessSupplement
|
|
from app.services import lab_sample_process as legacy_ledger
|
|
|
|
uniq = [c for c in {(x or "").strip() for x in codes} if c]
|
|
if not uniq:
|
|
return {}
|
|
rows = (
|
|
db.query(BizLabSampleProcessSupplement)
|
|
.filter(
|
|
BizLabSampleProcessSupplement.tenant_no == tenant_no,
|
|
BizLabSampleProcessSupplement.del_flag == 0,
|
|
BizLabSampleProcessSupplement.lab_storage_code.in_(uniq),
|
|
)
|
|
.all()
|
|
)
|
|
out: Dict[str, Dict[str, Any]] = {}
|
|
for row in rows:
|
|
out[row.lab_storage_code] = legacy_ledger.parse_payload(row.payload)
|
|
return out
|
|
|
|
|
|
def sync_legacy_supplement_to_processing(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
lab_storage_code: str,
|
|
sample_type_name: Optional[str] = None,
|
|
payload: Optional[Dict[str, Any]] = None,
|
|
operator_id: str = "legacy-sync",
|
|
) -> None:
|
|
"""旧台账补录保存后,同步到新模块补充表,并把传染四项写回生产接收。"""
|
|
code = (lab_storage_code or "").strip()
|
|
if not code:
|
|
return
|
|
mapped = _map_legacy_supplement(payload)
|
|
if not mapped:
|
|
return
|
|
dataset = resolve_dataset_by_type_name(sample_type_name or "", code)
|
|
if not dataset:
|
|
if code.upper().startswith("UBMC"):
|
|
dataset = DATASET_UBMC
|
|
elif code.upper().startswith(("BC", "UC")):
|
|
dataset = DATASET_MSC
|
|
else:
|
|
dataset = DATASET_HSC
|
|
|
|
upsert_extra_payload(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
dataset=dataset,
|
|
sample_code=code,
|
|
payload=mapped,
|
|
operator_id=operator_id,
|
|
replace=False,
|
|
)
|
|
|
|
infectious = mapped.get("infectiousFourCard")
|
|
if infectious not in (None, ""):
|
|
reception = (
|
|
db.query(BizSampleReception)
|
|
.filter(
|
|
BizSampleReception.tenant_no == tenant_no,
|
|
BizSampleReception.del_flag == 0,
|
|
or_(
|
|
BizSampleReception.lab_storage_code == code,
|
|
BizSampleReception.sample_code == code,
|
|
),
|
|
)
|
|
.order_by(BizSampleReception.reception_time.desc())
|
|
.first()
|
|
)
|
|
if reception:
|
|
production = (
|
|
db.query(BizSampleProductionReception)
|
|
.filter(
|
|
BizSampleProductionReception.tenant_no == tenant_no,
|
|
BizSampleProductionReception.reception_id == reception.id,
|
|
BizSampleProductionReception.del_flag == 0,
|
|
)
|
|
.order_by(BizSampleProductionReception.process_time.desc())
|
|
.first()
|
|
)
|
|
if production is not None:
|
|
production.infectious_four_test_result = _infectious_code(infectious)
|
|
production.update_by = operator_id
|
|
production.update_time = datetime.now()
|
|
|
|
|
|
def sync_processing_to_legacy_supplement(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
lab_storage_code: str,
|
|
sample_type_name: Optional[str] = None,
|
|
payload: Optional[Dict[str, Any]] = None,
|
|
operator_id: str = "processing-sync",
|
|
) -> None:
|
|
"""新模块保存后,回写旧台账补录,保持两边可见字段一致。"""
|
|
from app.models.models import BizLabSampleProcessSupplement
|
|
from app.services import lab_sample_process as legacy_ledger
|
|
|
|
code = (lab_storage_code or "").strip()
|
|
mapped = _map_new_to_legacy_supplement(payload)
|
|
if not code or not mapped:
|
|
return
|
|
now = datetime.now()
|
|
row = (
|
|
db.query(BizLabSampleProcessSupplement)
|
|
.filter(
|
|
BizLabSampleProcessSupplement.tenant_no == tenant_no,
|
|
BizLabSampleProcessSupplement.lab_storage_code == code,
|
|
BizLabSampleProcessSupplement.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
current = legacy_ledger.parse_payload(row.payload) if row else {}
|
|
current.update(mapped)
|
|
src = "research_donation" if code.upper().startswith("UBMC") else "cell_storage"
|
|
if row is None:
|
|
db.add(
|
|
BizLabSampleProcessSupplement(
|
|
id=_new_id(),
|
|
lab_storage_code=code,
|
|
reception_source=src,
|
|
sample_type_name=(sample_type_name or None),
|
|
payload=legacy_ledger.dumps_payload(current),
|
|
tenant_no=tenant_no,
|
|
create_by=operator_id,
|
|
create_dept_id="",
|
|
create_time=now,
|
|
update_by=operator_id,
|
|
update_time=now,
|
|
del_flag=0,
|
|
)
|
|
)
|
|
else:
|
|
if sample_type_name:
|
|
row.sample_type_name = sample_type_name
|
|
row.payload = legacy_ledger.dumps_payload(current)
|
|
row.update_by = operator_id
|
|
row.update_time = now
|
|
|
|
|
|
def _match_date_range(row: Dict[str, Any], date_from: Optional[str], date_to: Optional[str]) -> bool:
|
|
if not date_from and not date_to:
|
|
return True
|
|
for key in ("receptionTime", "collectTime", "cryoTime", "storageDateP2"):
|
|
val = row.get(key)
|
|
if not val:
|
|
continue
|
|
day = str(val)[:10].replace(".", "-").replace("/", "-")
|
|
# normalize compact dates like 2025-9-8
|
|
parts = day.split("-")
|
|
if len(parts) == 3:
|
|
try:
|
|
day = f"{int(parts[0]):04d}-{int(parts[1]):02d}-{int(parts[2]):02d}"
|
|
except Exception:
|
|
pass
|
|
if date_from and day < date_from:
|
|
continue
|
|
if date_to and day > date_to:
|
|
continue
|
|
return True
|
|
return False
|
|
|
|
|
|
def list_rows(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
dataset: str,
|
|
keyword: Optional[str] = None,
|
|
date_from: Optional[str] = None,
|
|
date_to: Optional[str] = None,
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
) -> Tuple[List[Dict[str, Any]], int]:
|
|
from app.models.models import BizLabSampleProcessingExtra
|
|
|
|
type_like = _type_name_filters(dataset)
|
|
code_likes = _code_prefix_filters(dataset)
|
|
|
|
q = (
|
|
db.query(
|
|
BizProtocolSample,
|
|
BizCellSampleType,
|
|
BizProtocolPartyCustomer,
|
|
BizProtocolCollectionInfo,
|
|
BizCollectionHospital,
|
|
)
|
|
.outerjoin(
|
|
BizCellSampleType,
|
|
(BizCellSampleType.id == BizProtocolSample.sample_type_id)
|
|
& (BizCellSampleType.del_flag == 0),
|
|
)
|
|
.outerjoin(
|
|
BizProtocolPartyCustomer,
|
|
(BizProtocolPartyCustomer.collection_protocol_id == BizProtocolSample.collection_protocol_id)
|
|
& (BizProtocolPartyCustomer.tenant_no == tenant_no)
|
|
& (BizProtocolPartyCustomer.del_flag == 0),
|
|
)
|
|
.outerjoin(
|
|
BizProtocolCollectionInfo,
|
|
(BizProtocolCollectionInfo.collection_protocol_id == BizProtocolSample.collection_protocol_id)
|
|
& (BizProtocolCollectionInfo.tenant_no == tenant_no)
|
|
& (BizProtocolCollectionInfo.del_flag == 0),
|
|
)
|
|
.outerjoin(
|
|
BizCollectionHospital,
|
|
(BizCollectionHospital.id == BizProtocolCollectionInfo.hospital_id)
|
|
& (BizCollectionHospital.del_flag == 0),
|
|
)
|
|
.filter(
|
|
BizProtocolSample.tenant_no == tenant_no,
|
|
BizProtocolSample.del_flag == 0,
|
|
BizProtocolSample.sample_code.isnot(None),
|
|
BizProtocolSample.sample_code != "",
|
|
)
|
|
)
|
|
|
|
type_or_code = []
|
|
if type_like:
|
|
type_or_code.append(BizCellSampleType.type_name.like(type_like))
|
|
for pref in code_likes:
|
|
type_or_code.append(BizProtocolSample.sample_code.like(pref))
|
|
if type_or_code:
|
|
q = q.filter(or_(*type_or_code))
|
|
|
|
rows = q.order_by(BizProtocolSample.create_time.desc()).all()
|
|
filtered = []
|
|
for sample, stype, party, coll, hospital in rows:
|
|
type_name = (stype.type_name if stype else "") or ""
|
|
code = sample.sample_code or ""
|
|
if resolve_dataset_by_type_name(type_name, code) != dataset:
|
|
continue
|
|
filtered.append((sample, stype, party, coll, hospital))
|
|
|
|
extras = {
|
|
e.sample_code: e
|
|
for e in db.query(BizLabSampleProcessingExtra)
|
|
.filter(
|
|
BizLabSampleProcessingExtra.tenant_no == tenant_no,
|
|
BizLabSampleProcessingExtra.del_flag == 0,
|
|
BizLabSampleProcessingExtra.dataset == dataset,
|
|
)
|
|
.all()
|
|
}
|
|
|
|
# 仅聚合真实协议样本;补充表只挂到已有样本上,不再单独展示 Excel 演示行
|
|
codes = [s.sample_code for s, *_ in filtered if s.sample_code]
|
|
sample_ids = [s.id for s, *_ in filtered if s.id]
|
|
receptions_by_sample_id: Dict[str, BizSampleReception] = {}
|
|
receptions_by_code: Dict[str, BizSampleReception] = {}
|
|
productions: Dict[str, BizSampleProductionReception] = {}
|
|
if sample_ids or codes:
|
|
recv_filters = []
|
|
if sample_ids:
|
|
recv_filters.append(BizSampleReception.protocol_sample_id.in_(sample_ids))
|
|
if codes:
|
|
recv_filters.append(BizSampleReception.sample_code.in_(codes))
|
|
for r in (
|
|
db.query(BizSampleReception)
|
|
.filter(
|
|
BizSampleReception.tenant_no == tenant_no,
|
|
BizSampleReception.del_flag == 0,
|
|
or_(*recv_filters) if recv_filters else False,
|
|
)
|
|
.order_by(BizSampleReception.reception_time.desc())
|
|
.all()
|
|
):
|
|
if r.protocol_sample_id:
|
|
receptions_by_sample_id.setdefault(r.protocol_sample_id, r)
|
|
if r.sample_code:
|
|
receptions_by_code.setdefault(r.sample_code, r)
|
|
if receptions_by_sample_id or receptions_by_code:
|
|
rids = list(
|
|
{
|
|
*(r.id for r in receptions_by_sample_id.values()),
|
|
*(r.id for r in receptions_by_code.values()),
|
|
}
|
|
)
|
|
for p in (
|
|
db.query(BizSampleProductionReception)
|
|
.filter(
|
|
BizSampleProductionReception.tenant_no == tenant_no,
|
|
BizSampleProductionReception.del_flag == 0,
|
|
BizSampleProductionReception.reception_id.in_(rids),
|
|
)
|
|
.order_by(BizSampleProductionReception.process_time.desc())
|
|
.all()
|
|
):
|
|
productions.setdefault(p.reception_id, p)
|
|
|
|
protocol_ids = sorted(
|
|
{
|
|
s.collection_protocol_id
|
|
for s, *_ in filtered
|
|
if getattr(s, "collection_protocol_id", None)
|
|
}
|
|
)
|
|
protocols: Dict[str, BizCollectionProtocol] = {}
|
|
if protocol_ids:
|
|
for p in (
|
|
db.query(BizCollectionProtocol)
|
|
.filter(
|
|
BizCollectionProtocol.tenant_no == tenant_no,
|
|
BizCollectionProtocol.del_flag == 0,
|
|
BizCollectionProtocol.id.in_(protocol_ids),
|
|
)
|
|
.all()
|
|
):
|
|
protocols[p.id] = p
|
|
|
|
reception_ids_for_flags = list(
|
|
{
|
|
*(r.id for r in receptions_by_sample_id.values() if r and r.id),
|
|
*(r.id for r in receptions_by_code.values() if r and r.id),
|
|
}
|
|
)
|
|
check_flags_by_reception = _check_flag_map(db, tenant_no, reception_ids_for_flags)
|
|
|
|
legacy_codes: List[str] = []
|
|
for sample, *_ in filtered:
|
|
legacy_codes.append(sample.sample_code or "")
|
|
reception = receptions_by_sample_id.get(sample.id) or receptions_by_code.get(sample.sample_code or "")
|
|
if reception and reception.lab_storage_code:
|
|
legacy_codes.append(reception.lab_storage_code)
|
|
legacy_supplements = _load_legacy_supplements(db, tenant_no, legacy_codes)
|
|
|
|
built_map: Dict[str, Dict[str, Any]] = {}
|
|
for sample, stype, party, coll, hospital in filtered:
|
|
code = sample.sample_code
|
|
reception = receptions_by_sample_id.get(sample.id) or receptions_by_code.get(code)
|
|
production = productions.get(reception.id) if reception else None
|
|
lab_code = (reception.lab_storage_code if reception else None) or ""
|
|
extra = _merge_extra_with_legacy(
|
|
_pick_extra(extras, lab_code, code),
|
|
legacy_supplements.get(lab_code) or legacy_supplements.get(code or ""),
|
|
)
|
|
protocol = protocols.get(sample.collection_protocol_id) if sample.collection_protocol_id else None
|
|
check_flags = check_flags_by_reception.get(reception.id, {}) if reception else {}
|
|
row = _build_row(
|
|
dataset=dataset,
|
|
sample=sample,
|
|
type_name=(stype.type_name if stype else None),
|
|
party=party,
|
|
coll=coll,
|
|
hospital=hospital,
|
|
reception=reception,
|
|
production=production,
|
|
extra=extra,
|
|
protocol=protocol,
|
|
check_flags=check_flags,
|
|
)
|
|
built_map[row.get("sampleCode") or code] = row
|
|
|
|
# 单个核:业务已发生数据主要在科研捐献接收(lab_storage_code=UBMC*),无细胞储存协议样本时仍需展示
|
|
if dataset == DATASET_UBMC:
|
|
ubmc_recs = (
|
|
db.query(BizSampleReception)
|
|
.filter(
|
|
BizSampleReception.tenant_no == tenant_no,
|
|
BizSampleReception.del_flag == 0,
|
|
BizSampleReception.lab_storage_code.like("UBMC%"),
|
|
)
|
|
.order_by(BizSampleReception.reception_time.desc())
|
|
.all()
|
|
)
|
|
mother_by_donation: Dict[str, str] = {}
|
|
donation_ids = sorted(
|
|
{r.research_donation_id for r in ubmc_recs if r.research_donation_id}
|
|
)
|
|
if donation_ids:
|
|
rows = db.execute(
|
|
text(
|
|
"SELECT research_donation_id, mother_name FROM biz_research_donation_party "
|
|
"WHERE del_flag=0 AND tenant_no=:tenant_no AND research_donation_id IN ("
|
|
+ ",".join(f":d{i}" for i in range(len(donation_ids)))
|
|
+ ")"
|
|
),
|
|
{
|
|
"tenant_no": tenant_no,
|
|
**{f"d{i}": did for i, did in enumerate(donation_ids)},
|
|
},
|
|
).fetchall()
|
|
for did, mother in rows:
|
|
if did and mother and did not in mother_by_donation:
|
|
mother_by_donation[str(did)] = mother
|
|
|
|
missing_rids = [
|
|
r.id
|
|
for r in ubmc_recs
|
|
if r.id and r.id not in productions
|
|
]
|
|
if missing_rids:
|
|
for p in (
|
|
db.query(BizSampleProductionReception)
|
|
.filter(
|
|
BizSampleProductionReception.tenant_no == tenant_no,
|
|
BizSampleProductionReception.del_flag == 0,
|
|
BizSampleProductionReception.reception_id.in_(missing_rids),
|
|
)
|
|
.order_by(BizSampleProductionReception.process_time.desc())
|
|
.all()
|
|
):
|
|
productions.setdefault(p.reception_id, p)
|
|
|
|
ubmc_flags = _check_flag_map(
|
|
db,
|
|
tenant_no,
|
|
[r.id for r in ubmc_recs if r.id and r.id not in check_flags_by_reception],
|
|
)
|
|
check_flags_by_reception.update(ubmc_flags)
|
|
|
|
ubmc_legacy = _load_legacy_supplements(
|
|
db,
|
|
tenant_no,
|
|
[
|
|
(r.lab_storage_code or r.sample_code or "")
|
|
for r in ubmc_recs
|
|
],
|
|
)
|
|
legacy_supplements.update(ubmc_legacy)
|
|
|
|
for reception in ubmc_recs:
|
|
code = (reception.lab_storage_code or reception.sample_code or "").strip()
|
|
if not code or code in built_map:
|
|
continue
|
|
production = productions.get(reception.id)
|
|
extra = _merge_extra_with_legacy(
|
|
_pick_extra(extras, code, reception.sample_code),
|
|
legacy_supplements.get(code)
|
|
or legacy_supplements.get((reception.sample_code or "").strip()),
|
|
)
|
|
mother = mother_by_donation.get(str(reception.research_donation_id or ""))
|
|
fake_sample = SimpleNamespace(
|
|
id=reception.donation_sample_id or reception.id,
|
|
sample_code=reception.sample_code or code,
|
|
collection_protocol_id=None,
|
|
)
|
|
fake_party = SimpleNamespace(mother_name=mother) if mother else None
|
|
built_map[code] = _build_row(
|
|
dataset=dataset,
|
|
sample=fake_sample,
|
|
type_name="脐血单个核细胞",
|
|
party=fake_party,
|
|
coll=None,
|
|
hospital=None,
|
|
reception=reception,
|
|
production=production,
|
|
extra=extra,
|
|
protocol=None,
|
|
check_flags=check_flags_by_reception.get(reception.id, {}),
|
|
)
|
|
|
|
kw = (keyword or "").strip()
|
|
built = []
|
|
for row in built_map.values():
|
|
if not _match_keyword(row, kw):
|
|
continue
|
|
if not _match_date_range(row, date_from, date_to):
|
|
continue
|
|
built.append(row)
|
|
|
|
# 稳定排序:优先按样本编码
|
|
built.sort(key=lambda r: str(r.get("sampleCode") or ""))
|
|
|
|
total = len(built)
|
|
page = max(1, int(page or 1))
|
|
page_size = max(1, min(5000, int(page_size or 20)))
|
|
start = (page - 1) * page_size
|
|
return built[start : start + page_size], total
|
|
|
|
|
|
def _build_row(
|
|
*,
|
|
dataset: str,
|
|
sample: Optional[BizProtocolSample],
|
|
type_name: Optional[str],
|
|
party: Optional[BizProtocolPartyCustomer],
|
|
coll: Optional[BizProtocolCollectionInfo],
|
|
hospital: Optional[BizCollectionHospital],
|
|
reception: Optional[BizSampleReception],
|
|
production: Optional[BizSampleProductionReception],
|
|
extra: Dict[str, Any],
|
|
protocol: Optional[BizCollectionProtocol] = None,
|
|
check_flags: Optional[Dict[str, bool]] = None,
|
|
) -> Dict[str, Any]:
|
|
flags = check_flags or {}
|
|
protocol_code = (
|
|
(sample.sample_code if sample else None)
|
|
or extra.get("protocolSampleCode")
|
|
or ""
|
|
)
|
|
lab_code = ((reception.lab_storage_code if reception else None) or "").strip()
|
|
# 台账「细胞编码」展示实验室入库编码(B000…),协议样本号保留检索
|
|
display_code = lab_code if lab_code and len(lab_code) > 1 else (
|
|
protocol_code or extra.get("sampleCode") or ""
|
|
)
|
|
|
|
protocol_vals = {
|
|
"sample_code": display_code,
|
|
"mother_name": party.mother_name if party else None,
|
|
"sample_collect_time": coll.sample_collect_time if coll else None,
|
|
"hospital_name": hospital.hospital_name if hospital else None,
|
|
}
|
|
reception_vals = {}
|
|
if reception:
|
|
reception_vals = {
|
|
"reception_time": reception.reception_time,
|
|
"receive_temperature": reception.receive_temperature,
|
|
"handler_name": reception.handler_name,
|
|
"remark": reception.remark,
|
|
"lab_storage_code": reception.lab_storage_code,
|
|
}
|
|
production_vals = {}
|
|
if production:
|
|
production_vals = {
|
|
"original_blood_weight_g": production.original_blood_weight_g,
|
|
"hes_volume_ml": production.hes_volume_ml,
|
|
"blood_type": production.blood_type,
|
|
"infectious_four_test_result": production.infectious_four_test_result,
|
|
"enriched_blood_weight_g": production.enriched_blood_weight_g,
|
|
"cryopreservation_volume_ml": production.cryopreservation_volume_ml,
|
|
"handler_name": production.handler_name,
|
|
"remark": production.remark,
|
|
"original_blood_volume_ml": production.original_blood_volume_ml,
|
|
"nucleated_cell_count_1e8": production.nucleated_cell_count_1e8,
|
|
"inoculation_amount": production.inoculation_amount,
|
|
"cryopreservation_time": production.cryopreservation_time,
|
|
"storage_total_tubes": production.storage_total_tubes,
|
|
"available_cell_count_1e7": production.available_cell_count_1e7,
|
|
}
|
|
|
|
data: Dict[str, Any] = {
|
|
"id": (sample.id if sample else f"extra:{dataset}:{display_code}"),
|
|
"dataset": dataset,
|
|
"sampleCode": display_code,
|
|
"protocolSampleCode": protocol_code or None,
|
|
"sampleTypeName": type_name,
|
|
"protocolSampleId": sample.id if sample else None,
|
|
"collectionProtocolId": sample.collection_protocol_id if sample else None,
|
|
"receptionId": reception.id if reception else None,
|
|
"productionReceptionId": production.id if production else None,
|
|
"fromExtraOnly": sample is None,
|
|
}
|
|
for f in fields_for(dataset):
|
|
val = None
|
|
if f.source == SOURCE_PROTOCOL:
|
|
val = protocol_vals.get(f.attr)
|
|
elif f.source == SOURCE_RECEPTION:
|
|
val = reception_vals.get(f.attr)
|
|
elif f.source == SOURCE_PRODUCTION:
|
|
val = production_vals.get(f.attr)
|
|
elif f.source == SOURCE_EXTRA:
|
|
# 仅源系统没有的列走补充表;业务源表字段不被补充表覆盖
|
|
val = extra.get(f.key)
|
|
|
|
# 源表为空时,允许用新/旧补充表填空(旧台账联动)
|
|
if f.source != SOURCE_EXTRA and _is_blank(val):
|
|
fallback = extra.get(f.key)
|
|
if not _is_blank(fallback):
|
|
val = fallback
|
|
|
|
if isinstance(val, datetime):
|
|
val = _fmt_dt(val)
|
|
elif isinstance(val, Decimal):
|
|
val = _fmt_num(val)
|
|
data[f.key] = val
|
|
|
|
data["sampleCode"] = display_code
|
|
|
|
# 从协议采集信息 / 附件 / 收样检查项推导补充列(补充表有值时不覆盖)
|
|
if not data.get("hasMaternalBlood"):
|
|
if coll is not None:
|
|
data["hasMaternalBlood"] = "是" if coll.maternal_blood_collect_time else "否"
|
|
|
|
if not data.get("hasConsent"):
|
|
if protocol and (protocol.consent_form_urls or "").strip():
|
|
data["hasConsent"] = "是"
|
|
else:
|
|
flagged = _yes_no_from_bool(flags.get("知情同意书"))
|
|
if flagged is not None:
|
|
data["hasConsent"] = flagged
|
|
elif protocol is not None:
|
|
data["hasConsent"] = "否"
|
|
|
|
if not data.get("hasHealthSurvey"):
|
|
if protocol and (protocol.health_survey_form_urls or "").strip():
|
|
data["hasHealthSurvey"] = "是"
|
|
else:
|
|
flagged = _yes_no_from_bool(flags.get("健康调查表"))
|
|
if flagged is not None:
|
|
data["hasHealthSurvey"] = flagged
|
|
elif protocol is not None:
|
|
data["hasHealthSurvey"] = "否"
|
|
|
|
if not data.get("labelClear"):
|
|
data["labelClear"] = _yes_no_from_bool(flags.get("样本信息正确,标识清晰"))
|
|
|
|
if not data.get("receiveCheck"):
|
|
data["receiveCheck"] = _derive_receive_check(flags)
|
|
|
|
if data.get("infectiousFourCard") is not None:
|
|
data["infectiousFourCard"] = _infectious_label(data.get("infectiousFourCard"))
|
|
|
|
return data
|
|
|
|
|
|
def upsert_extra_payload(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
dataset: str,
|
|
sample_code: str,
|
|
payload: Dict[str, Any],
|
|
operator_id: str = "import",
|
|
replace: bool = False,
|
|
) -> None:
|
|
from app.models.models import BizLabSampleProcessingExtra
|
|
|
|
code = (sample_code or "").strip()
|
|
if not code:
|
|
raise ValueError("样本编码不能为空")
|
|
now = datetime.now()
|
|
row = (
|
|
db.query(BizLabSampleProcessingExtra)
|
|
.filter(
|
|
BizLabSampleProcessingExtra.tenant_no == tenant_no,
|
|
BizLabSampleProcessingExtra.dataset == dataset,
|
|
BizLabSampleProcessingExtra.sample_code == code,
|
|
BizLabSampleProcessingExtra.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
current = {} if replace else _parse_extra(row.payload_json if row else None)
|
|
for k, v in (payload or {}).items():
|
|
if v in (None, ""):
|
|
continue
|
|
current[k] = v
|
|
current["sampleCode"] = code
|
|
if row is None:
|
|
db.add(
|
|
BizLabSampleProcessingExtra(
|
|
id=_new_id(),
|
|
dataset=dataset,
|
|
sample_code=code,
|
|
payload_json=json.dumps(current, ensure_ascii=False),
|
|
tenant_no=tenant_no,
|
|
create_by=operator_id,
|
|
create_dept_id="",
|
|
create_time=now,
|
|
update_by=operator_id,
|
|
update_time=now,
|
|
del_flag=0,
|
|
)
|
|
)
|
|
else:
|
|
row.payload_json = json.dumps(current, ensure_ascii=False)
|
|
row.update_by = operator_id
|
|
row.update_time = now
|
|
|
|
|
|
def update_row(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
dataset: str,
|
|
sample_code: str,
|
|
payload: Dict[str, Any],
|
|
editable_depts: set,
|
|
operator_id: str,
|
|
) -> Dict[str, Any]:
|
|
code = (sample_code or "").strip()
|
|
if not code:
|
|
raise ValueError("样本编码不能为空")
|
|
fmap = field_map(dataset)
|
|
if not fmap:
|
|
raise ValueError("未知数据集")
|
|
|
|
denied = []
|
|
accepted: Dict[str, Any] = {}
|
|
for key, value in (payload or {}).items():
|
|
f = fmap.get(key)
|
|
if not f:
|
|
continue
|
|
if f.dept not in editable_depts:
|
|
denied.append(key)
|
|
continue
|
|
accepted[key] = value
|
|
if denied:
|
|
raise PermissionError(f"无权编辑字段: {', '.join(denied)}")
|
|
if not accepted:
|
|
raise ValueError("没有可保存的字段")
|
|
|
|
reception = (
|
|
db.query(BizSampleReception)
|
|
.filter(
|
|
BizSampleReception.tenant_no == tenant_no,
|
|
BizSampleReception.del_flag == 0,
|
|
or_(
|
|
BizSampleReception.lab_storage_code == code,
|
|
BizSampleReception.sample_code == code,
|
|
),
|
|
)
|
|
.order_by(BizSampleReception.reception_time.desc())
|
|
.first()
|
|
)
|
|
sample = (
|
|
db.query(BizProtocolSample)
|
|
.filter(
|
|
BizProtocolSample.tenant_no == tenant_no,
|
|
BizProtocolSample.sample_code == code,
|
|
BizProtocolSample.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if sample is None and reception is not None and reception.protocol_sample_id:
|
|
sample = (
|
|
db.query(BizProtocolSample)
|
|
.filter(
|
|
BizProtocolSample.tenant_no == tenant_no,
|
|
BizProtocolSample.id == reception.protocol_sample_id,
|
|
BizProtocolSample.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if reception is None and sample is not None:
|
|
reception = (
|
|
db.query(BizSampleReception)
|
|
.filter(
|
|
BizSampleReception.tenant_no == tenant_no,
|
|
BizSampleReception.del_flag == 0,
|
|
or_(
|
|
BizSampleReception.protocol_sample_id == sample.id,
|
|
BizSampleReception.sample_code == sample.sample_code,
|
|
),
|
|
)
|
|
.order_by(BizSampleReception.reception_time.desc())
|
|
.first()
|
|
)
|
|
production = None
|
|
if reception:
|
|
production = (
|
|
db.query(BizSampleProductionReception)
|
|
.filter(
|
|
BizSampleProductionReception.tenant_no == tenant_no,
|
|
BizSampleProductionReception.reception_id == reception.id,
|
|
BizSampleProductionReception.del_flag == 0,
|
|
)
|
|
.order_by(BizSampleProductionReception.process_time.desc())
|
|
.first()
|
|
)
|
|
|
|
party = None
|
|
coll = None
|
|
if sample:
|
|
party = (
|
|
db.query(BizProtocolPartyCustomer)
|
|
.filter(
|
|
BizProtocolPartyCustomer.collection_protocol_id == sample.collection_protocol_id,
|
|
BizProtocolPartyCustomer.tenant_no == tenant_no,
|
|
BizProtocolPartyCustomer.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
coll = (
|
|
db.query(BizProtocolCollectionInfo)
|
|
.filter(
|
|
BizProtocolCollectionInfo.collection_protocol_id == sample.collection_protocol_id,
|
|
BizProtocolCollectionInfo.tenant_no == tenant_no,
|
|
BizProtocolCollectionInfo.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
now = datetime.now()
|
|
extra_payload: Dict[str, Any] = {}
|
|
|
|
def _set_dt(obj, attr, value):
|
|
if value in (None, ""):
|
|
setattr(obj, attr, None)
|
|
return
|
|
if isinstance(value, datetime):
|
|
setattr(obj, attr, value)
|
|
return
|
|
text = str(value).strip().replace("/", "-").replace(".", "-")
|
|
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
|
|
try:
|
|
setattr(obj, attr, datetime.strptime(text, fmt))
|
|
return
|
|
except ValueError:
|
|
continue
|
|
setattr(obj, attr, value)
|
|
|
|
for key, value in accepted.items():
|
|
f = fmap[key]
|
|
wrote = False
|
|
if f.source == SOURCE_PROTOCOL and sample is not None:
|
|
if f.attr == "mother_name" and party is not None:
|
|
party.mother_name = None if value in (None, "") else str(value)
|
|
party.update_by = operator_id
|
|
party.update_time = now
|
|
wrote = True
|
|
elif f.attr == "sample_collect_time" and coll is not None:
|
|
_set_dt(coll, "sample_collect_time", value)
|
|
coll.update_by = operator_id
|
|
coll.update_time = now
|
|
wrote = True
|
|
elif f.source == SOURCE_RECEPTION and reception is not None:
|
|
if f.attr == "reception_time":
|
|
_set_dt(reception, f.attr, value)
|
|
elif f.attr in {"receive_temperature", "handler_name", "remark", "lab_storage_code"}:
|
|
setattr(reception, f.attr, None if value in (None, "") else value)
|
|
reception.update_by = operator_id
|
|
reception.update_time = now
|
|
wrote = True
|
|
elif f.source == SOURCE_PRODUCTION and production is not None:
|
|
numeric_attrs = {
|
|
"original_blood_weight_g",
|
|
"hes_volume_ml",
|
|
"enriched_blood_weight_g",
|
|
"cryopreservation_volume_ml",
|
|
"original_blood_volume_ml",
|
|
"nucleated_cell_count_1e8",
|
|
"available_cell_count_1e7",
|
|
}
|
|
if f.attr == "cryopreservation_time":
|
|
_set_dt(production, f.attr, value)
|
|
elif f.attr == "storage_total_tubes":
|
|
production.storage_total_tubes = None if value in (None, "") else int(value)
|
|
elif f.attr in numeric_attrs:
|
|
production_val = None if value in (None, "") else Decimal(str(value))
|
|
setattr(production, f.attr, production_val)
|
|
elif f.attr == "infectious_four_test_result":
|
|
production.infectious_four_test_result = (
|
|
None if value in (None, "") else _infectious_code(value)
|
|
)
|
|
elif f.attr in {
|
|
"blood_type",
|
|
"handler_name",
|
|
"remark",
|
|
"inoculation_amount",
|
|
}:
|
|
setattr(production, f.attr, None if value in (None, "") else value)
|
|
production.update_by = operator_id
|
|
production.update_time = now
|
|
wrote = True
|
|
|
|
# 补充表始终保留一份,保证导入台账与无权源表场景可编辑
|
|
extra_payload[key] = value
|
|
_ = wrote
|
|
|
|
upsert_extra_payload(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
dataset=dataset,
|
|
sample_code=code,
|
|
payload=extra_payload,
|
|
operator_id=operator_id,
|
|
replace=False,
|
|
)
|
|
legacy_code = (
|
|
(reception.lab_storage_code if reception else None)
|
|
or (sample.sample_code if sample else None)
|
|
or code
|
|
)
|
|
sync_processing_to_legacy_supplement(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
lab_storage_code=str(legacy_code),
|
|
sample_type_name=None,
|
|
payload=extra_payload,
|
|
operator_id=operator_id,
|
|
)
|
|
db.commit()
|
|
|
|
rows, _ = list_rows(db, tenant_no=tenant_no, dataset=dataset, keyword=code, page=1, page_size=20)
|
|
for row in rows:
|
|
if row.get("sampleCode") == code or row.get("protocolSampleCode") == code:
|
|
return row
|
|
raise LookupError("保存成功但读取失败")
|
|
|
|
|
|
def export_rows(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
dataset: str,
|
|
keyword: Optional[str] = None,
|
|
date_from: Optional[str] = None,
|
|
date_to: Optional[str] = None,
|
|
) -> Tuple[List[str], List[List[Any]]]:
|
|
rows, _ = list_rows(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
dataset=dataset,
|
|
keyword=keyword,
|
|
date_from=date_from,
|
|
date_to=date_to,
|
|
page=1,
|
|
page_size=5000,
|
|
)
|
|
headers = [f.label for f in fields_for(dataset)]
|
|
keys = [f.key for f in fields_for(dataset)]
|
|
body = [[row.get(k) for k in keys] for row in rows]
|
|
return headers, body
|