Register admin sample-freight routes, enrich protocol sample details with freight, and stop writing legacy transport rows on save. Co-authored-by: Cursor <cursoragent@cursor.com>
2340 lines
77 KiB
Python
2340 lines
77 KiB
Python
"""科研捐赠管理 CRUD(捐赠单 / 审核 / 状态变更 / 转换)"""
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
from sqlalchemy import and_, func, or_
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.utils import generate_uuid, parse_datetime, format_date, optional_date
|
||
from app.crud.base import camel_to_snake
|
||
from app.crud.biz_no_generator import allocate_next_biz_no
|
||
from app.crud.collection_protocol import (
|
||
attachments_to_urls,
|
||
urls_to_attachment_list,
|
||
validate_sample_codes_not_used,
|
||
_format_sample_collect_summary,
|
||
_load_sample_type_name_map,
|
||
_load_site_name_map,
|
||
)
|
||
from app.crud.protocol_donation_convert import (
|
||
assert_biz_no_available,
|
||
create_convert_link,
|
||
get_convert_partner,
|
||
has_active_convert_link,
|
||
upsert_convert_link_for_to_storage,
|
||
)
|
||
from app.utils.sample_special import (
|
||
apply_sample_special_fields,
|
||
sample_special_to_dict,
|
||
validate_sample_special_raw,
|
||
)
|
||
from app.models.models import (
|
||
BizCollectionProtocol,
|
||
BizCollectionRegion,
|
||
BizCollectionHospital,
|
||
BizItemUnit,
|
||
BizProtocolCollectionInfo,
|
||
BizProtocolPartyCustomer,
|
||
BizProtocolSample,
|
||
BizProtocolSampleTransport,
|
||
BizQcWorkOrder,
|
||
BizSampleReception,
|
||
SysUser,
|
||
)
|
||
from app.models.research_donation import (
|
||
BizResearchDonation,
|
||
BizResearchDonationAudit,
|
||
BizResearchDonationCollection,
|
||
BizResearchDonationParty,
|
||
BizResearchDonationSample,
|
||
BizResearchDonationSampleTransport,
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def total_pages(total: int, page_size: int) -> int:
|
||
if total <= 0:
|
||
return 0
|
||
return int(math.ceil(total / float(page_size)))
|
||
|
||
|
||
_DONATION_SAMPLE_CODE_PREFIX = "KYJZ"
|
||
_DONATION_SAMPLE_CODE_SEQ_LEN = 4
|
||
|
||
|
||
def _donation_sample_code_seq(code: str, *, date_str: str) -> Optional[int]:
|
||
"""解析 KYJZ+YYYYMM+序号 中的序号;不匹配则返回 None。"""
|
||
prefix = f"{_DONATION_SAMPLE_CODE_PREFIX}{date_str}"
|
||
c = (code or "").strip()
|
||
if not c.startswith(prefix):
|
||
return None
|
||
suffix = c[len(prefix) :]
|
||
if len(suffix) != _DONATION_SAMPLE_CODE_SEQ_LEN or not suffix.isdigit():
|
||
return None
|
||
return int(suffix)
|
||
|
||
|
||
def next_donation_sample_code(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
extra_codes: Optional[List[str]] = None,
|
||
) -> str:
|
||
"""生成捐赠样本编码:KYJZ + YYYYMM + 4 位递增序号(0001 起)。"""
|
||
date_str = datetime.now().strftime("%Y%m")
|
||
prefix = f"{_DONATION_SAMPLE_CODE_PREFIX}{date_str}"
|
||
max_seq = 0
|
||
|
||
def bump(code: Optional[str]) -> None:
|
||
nonlocal max_seq
|
||
seq = _donation_sample_code_seq(code or "", date_str=date_str)
|
||
if seq is not None:
|
||
max_seq = max(max_seq, seq)
|
||
|
||
donation_rows = (
|
||
db.query(BizResearchDonationSample.sample_code)
|
||
.filter(
|
||
BizResearchDonationSample.tenant_no == tenant_no,
|
||
BizResearchDonationSample.del_flag == 0,
|
||
BizResearchDonationSample.sample_code.like(f"{prefix}%"),
|
||
)
|
||
.all()
|
||
)
|
||
for row in donation_rows:
|
||
bump(row[0])
|
||
|
||
protocol_rows = (
|
||
db.query(BizProtocolSample.sample_code)
|
||
.filter(
|
||
BizProtocolSample.tenant_no == tenant_no,
|
||
BizProtocolSample.del_flag == 0,
|
||
BizProtocolSample.sample_code.like(f"{prefix}%"),
|
||
)
|
||
.all()
|
||
)
|
||
for row in protocol_rows:
|
||
bump(row[0])
|
||
|
||
from app.models.models import BizSampleCode
|
||
|
||
pool_rows = (
|
||
db.query(BizSampleCode.sample_code)
|
||
.filter(
|
||
BizSampleCode.tenant_no == tenant_no,
|
||
BizSampleCode.del_flag == 0,
|
||
BizSampleCode.sample_code.like(f"{prefix}%"),
|
||
)
|
||
.all()
|
||
)
|
||
for row in pool_rows:
|
||
bump(row[0])
|
||
|
||
for code in extra_codes or []:
|
||
bump(code)
|
||
|
||
return f"{prefix}{str(max_seq + 1).zfill(_DONATION_SAMPLE_CODE_SEQ_LEN)}"
|
||
|
||
|
||
def _fmt_dt(t: Optional[datetime]) -> str:
|
||
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
|
||
|
||
|
||
def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
|
||
val = (value or "").strip()
|
||
if len(val) == 10:
|
||
return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
|
||
return val
|
||
|
||
|
||
def _dec(v: Any) -> Optional[float]:
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, Decimal):
|
||
return float(v)
|
||
return float(v)
|
||
|
||
|
||
def _optional_decimal(v: Any) -> Optional[Decimal]:
|
||
if v is None or v == "":
|
||
return None
|
||
return Decimal(str(v))
|
||
|
||
|
||
_SNAKE_ALIASES = {
|
||
"apgar1min": "apgar_1min",
|
||
"apgar5min": "apgar_5min",
|
||
}
|
||
|
||
|
||
def _normalize_fields(fields: Optional[dict]) -> dict:
|
||
"""将前端 camelCase 字段名转为数据库 snake_case。"""
|
||
if not fields:
|
||
return {}
|
||
out: Dict[str, Any] = {}
|
||
for k, v in fields.items():
|
||
sk = _SNAKE_ALIASES.get(k) or camel_to_snake(k)
|
||
out[sk] = v
|
||
return out
|
||
|
||
|
||
def _merge_comma_separated_urls(*parts: Optional[str]) -> Optional[str]:
|
||
"""合并逗号分隔的附件 URL,去重并保持顺序。"""
|
||
seen: set[str] = set()
|
||
urls: List[str] = []
|
||
for part in parts:
|
||
if not part:
|
||
continue
|
||
for url in str(part).split(","):
|
||
u = url.strip()
|
||
if not u or u in seen:
|
||
continue
|
||
seen.add(u)
|
||
urls.append(u)
|
||
return ",".join(urls) if urls else None
|
||
|
||
|
||
def _resolve_donation_payload(raw: dict) -> None:
|
||
"""解析保存载荷:附件列表 → URL 字段;party/collection 别名。"""
|
||
if raw.get("consent_attachments") is not None:
|
||
raw["consent_form_urls"] = attachments_to_urls(raw["consent_attachments"]) or None
|
||
elif (raw.get("consent_form_urls") or "").strip():
|
||
raw["consent_form_urls"] = (raw["consent_form_urls"] or "").strip()
|
||
|
||
if raw.get("other_attachments") is not None:
|
||
raw["attachment_urls"] = attachments_to_urls(raw["other_attachments"]) or None
|
||
elif (raw.get("attachment_urls") or "").strip():
|
||
raw["attachment_urls"] = (raw["attachment_urls"] or "").strip()
|
||
|
||
if raw.get("infectious_disease_report_attachments") is not None:
|
||
raw["infectious_disease_report_urls"] = (
|
||
attachments_to_urls(raw["infectious_disease_report_attachments"]) or None
|
||
)
|
||
elif (raw.get("infectious_disease_report_urls") or "").strip():
|
||
raw["infectious_disease_report_urls"] = (
|
||
(raw["infectious_disease_report_urls"] or "").strip()
|
||
)
|
||
|
||
if not raw.get("party") and raw.get("party_customer"):
|
||
raw["party"] = raw["party_customer"]
|
||
if not raw.get("collection") and raw.get("collection_info"):
|
||
raw["collection"] = raw["collection_info"]
|
||
|
||
|
||
def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
|
||
ids = list({x for x in user_ids if x})
|
||
if not ids:
|
||
return {}
|
||
rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
|
||
return {u.id: u.nick_name or "" for u in rows}
|
||
|
||
|
||
def _load_unit_name_map(db: Session, samples: List[BizResearchDonationSample]) -> Dict[str, str]:
|
||
unit_ids = list({s.unit_id for s in samples if s.unit_id})
|
||
if not unit_ids:
|
||
return {}
|
||
unit_map: Dict[str, str] = {}
|
||
for u in (
|
||
db.query(BizItemUnit)
|
||
.filter(BizItemUnit.id.in_(unit_ids), BizItemUnit.del_flag == 0)
|
||
.all()
|
||
):
|
||
unit_map[u.id] = u.unit_name or ""
|
||
return unit_map
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# ORM → dict serializers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _donation_to_dict(
|
||
d: BizResearchDonation,
|
||
db: Optional[Session] = None,
|
||
tenant_no: Optional[str] = None,
|
||
) -> Dict[str, Any]:
|
||
tn = tenant_no or d.tenant_no
|
||
return {
|
||
"id": d.id,
|
||
"donationNo": d.donation_no or "",
|
||
"donationStatus": d.donation_status or "",
|
||
"donationDate": (d.donation_date or "").strip() if d.donation_date else "",
|
||
"sourceType": d.source_type or "direct",
|
||
"sourceProtocolId": d.source_protocol_id or "",
|
||
"consentFormUrls": d.consent_form_urls or "",
|
||
"attachmentUrls": d.attachment_urls or "",
|
||
"infectiousDiseaseReportUrls": d.infectious_disease_report_urls or "",
|
||
"consentAttachmentFiles": urls_to_attachment_list(d.consent_form_urls, db, tn) if db else [],
|
||
"attachmentFiles": urls_to_attachment_list(d.attachment_urls, db, tn) if db else [],
|
||
"infectiousDiseaseReportAttachmentFiles": (
|
||
urls_to_attachment_list(d.infectious_disease_report_urls, db, tn) if db else []
|
||
),
|
||
"collectorUserId": d.collector_user_id or "",
|
||
"effectiveTime": _fmt_dt(d.effective_time),
|
||
"effectiveDesc": d.effective_desc or "",
|
||
"expireTime": _fmt_dt(d.expire_time),
|
||
"expireDesc": d.expire_desc or "",
|
||
"voidTime": _fmt_dt(d.void_time),
|
||
"voidDesc": d.void_desc or "",
|
||
"voidType": d.void_type or "",
|
||
"toStorageTime": _fmt_dt(d.to_storage_time),
|
||
"toStorageDesc": d.to_storage_desc or "",
|
||
"tenantNo": d.tenant_no,
|
||
"orderNum": d.order_num if d.order_num is not None else 0,
|
||
"remark": d.remark or "",
|
||
"createTime": _fmt_dt(d.create_time),
|
||
"updateTime": _fmt_dt(d.update_time),
|
||
}
|
||
|
||
|
||
def _audit_to_dict(
|
||
a: BizResearchDonationAudit,
|
||
*,
|
||
donation_no: str = "",
|
||
auditor_user_name: str = "",
|
||
) -> Dict[str, Any]:
|
||
return {
|
||
"id": a.id,
|
||
"researchDonationId": a.research_donation_id,
|
||
"donationNo": donation_no,
|
||
"auditorUserId": a.auditor_user_id or "",
|
||
"auditorUserName": auditor_user_name or "",
|
||
"auditRemark": a.audit_remark or "",
|
||
"auditStatus": a.audit_status or "",
|
||
"tenantNo": a.tenant_no,
|
||
"orderNum": a.order_num if a.order_num is not None else 0,
|
||
"remark": a.remark or "",
|
||
"createTime": _fmt_dt(a.create_time),
|
||
"updateTime": _fmt_dt(a.update_time),
|
||
}
|
||
|
||
|
||
def _party_to_dict(p: BizResearchDonationParty) -> Dict[str, Any]:
|
||
return {
|
||
"id": p.id,
|
||
"researchDonationId": p.research_donation_id,
|
||
"customerId": p.customer_id or "",
|
||
"fatherCustomerId": p.father_customer_id or "",
|
||
"newbornCustomerId": p.newborn_customer_id or "",
|
||
"motherName": p.mother_name or "",
|
||
"motherPhone": p.mother_phone or "",
|
||
"motherIdCard": p.mother_id_card or "",
|
||
"motherBirthYm": (p.mother_birth_ym or "").strip() if p.mother_birth_ym else "",
|
||
"motherEthnic": p.mother_ethnic or "",
|
||
"motherEmail": p.mother_email or "",
|
||
"motherAddress": p.mother_address or "",
|
||
"fatherName": p.father_name or "",
|
||
"fatherPhone": p.father_phone or "",
|
||
"fatherIdCard": p.father_id_card or "",
|
||
"newbornName": p.newborn_name or "",
|
||
"newbornIdCard": p.newborn_id_card or "",
|
||
"expectedDeliveryDate": format_date(p.expected_delivery_date),
|
||
"newbornDeliveryTime": _fmt_dt(p.newborn_delivery_time),
|
||
"newbornSex": p.newborn_sex or "",
|
||
"deliveryMode": p.delivery_mode or "",
|
||
"birthOrder": p.birth_order,
|
||
"newbornWeightGram": p.newborn_weight_gram,
|
||
"apgar1min": _dec(p.apgar_1min),
|
||
"apgar5min": _dec(p.apgar_5min),
|
||
"collectorUserId": p.collector_user_id or "",
|
||
"tenantNo": p.tenant_no,
|
||
"orderNum": p.order_num if p.order_num is not None else 0,
|
||
"remark": p.remark or "",
|
||
"createTime": _fmt_dt(p.create_time),
|
||
"updateTime": _fmt_dt(p.update_time),
|
||
}
|
||
|
||
|
||
def _collection_to_dict(c: BizResearchDonationCollection) -> Dict[str, Any]:
|
||
return {
|
||
"id": c.id,
|
||
"researchDonationId": c.research_donation_id,
|
||
"regionId": c.region_id or "",
|
||
"hospitalId": c.hospital_id or "",
|
||
"medicalStaff": c.medical_staff or "",
|
||
"salesUserId": c.sales_user_id or "",
|
||
"sampleCollectTime": _fmt_dt(c.sample_collect_time),
|
||
"maternalBloodCollectTime": _fmt_dt(c.maternal_blood_collect_time),
|
||
"tenantNo": c.tenant_no,
|
||
"orderNum": c.order_num if c.order_num is not None else 0,
|
||
"remark": c.remark or "",
|
||
"createTime": _fmt_dt(c.create_time),
|
||
"updateTime": _fmt_dt(c.update_time),
|
||
}
|
||
|
||
|
||
def _sample_to_dict(s: BizResearchDonationSample) -> Dict[str, Any]:
|
||
return {
|
||
"id": s.id,
|
||
"researchDonationId": s.research_donation_id,
|
||
"sampleTypeId": s.sample_type_id or "",
|
||
"sampleCodeId": s.sample_code_id or "",
|
||
"sampleCode": s.sample_code or "",
|
||
"collectAmount": _dec(s.collect_amount),
|
||
"unitId": s.unit_id or "",
|
||
"storageYears": s.storage_years,
|
||
"sampleStatus": (s.sample_status or "").strip() if (s.sample_status or "").strip() else "",
|
||
"storageInDate": (s.storage_in_date or "").strip() if s.storage_in_date else "",
|
||
"detectStartDate": (s.detect_start_date or "").strip() if s.detect_start_date else "",
|
||
"voidDate": (s.void_date or "").strip() if s.void_date else "",
|
||
"storageExpireDate": (s.storage_expire_date or "").strip() if s.storage_expire_date else "",
|
||
**sample_special_to_dict(s),
|
||
"tenantNo": s.tenant_no,
|
||
"orderNum": s.order_num if s.order_num is not None else 0,
|
||
"remark": s.remark or "",
|
||
"createTime": _fmt_dt(s.create_time),
|
||
"updateTime": _fmt_dt(s.update_time),
|
||
}
|
||
|
||
|
||
def _transport_to_dict(t: BizResearchDonationSampleTransport) -> Dict[str, Any]:
|
||
return {
|
||
"id": t.id,
|
||
"researchDonationId": t.research_donation_id,
|
||
"sampleId": t.sample_id,
|
||
"transportType": t.transport_type or "",
|
||
"expressCompanyId": t.express_company_id or "",
|
||
"waybillNo": t.waybill_no or "",
|
||
"destSiteId": t.dest_site_id or "",
|
||
"expressFee": _dec(t.express_fee),
|
||
"etaTime": _fmt_dt(t.eta_time),
|
||
"carrierName": t.carrier_name or "",
|
||
"carrierPhone": t.carrier_phone or "",
|
||
"remark": t.remark or "",
|
||
"orderNum": t.order_num if t.order_num is not None else 0,
|
||
"createTime": _fmt_dt(t.create_time),
|
||
"updateTime": _fmt_dt(t.update_time),
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Sub-resource queries
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def get_party(
|
||
db: Session, research_donation_id: str, tenant_no: str
|
||
) -> Optional[BizResearchDonationParty]:
|
||
return (
|
||
db.query(BizResearchDonationParty)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonationParty.research_donation_id == research_donation_id,
|
||
BizResearchDonationParty.tenant_no == tenant_no,
|
||
BizResearchDonationParty.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
|
||
|
||
def get_collection(
|
||
db: Session, research_donation_id: str, tenant_no: str
|
||
) -> Optional[BizResearchDonationCollection]:
|
||
return (
|
||
db.query(BizResearchDonationCollection)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonationCollection.research_donation_id == research_donation_id,
|
||
BizResearchDonationCollection.tenant_no == tenant_no,
|
||
BizResearchDonationCollection.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
|
||
|
||
def list_samples(
|
||
db: Session, research_donation_id: str, tenant_no: str
|
||
) -> List[BizResearchDonationSample]:
|
||
return (
|
||
db.query(BizResearchDonationSample)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonationSample.research_donation_id == research_donation_id,
|
||
BizResearchDonationSample.tenant_no == tenant_no,
|
||
BizResearchDonationSample.del_flag == 0,
|
||
)
|
||
)
|
||
.order_by(
|
||
BizResearchDonationSample.order_num.asc(),
|
||
BizResearchDonationSample.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
|
||
|
||
def list_sample_transports(
|
||
db: Session, research_donation_id: str, tenant_no: str
|
||
) -> List[BizResearchDonationSampleTransport]:
|
||
return (
|
||
db.query(BizResearchDonationSampleTransport)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonationSampleTransport.research_donation_id == research_donation_id,
|
||
BizResearchDonationSampleTransport.tenant_no == tenant_no,
|
||
BizResearchDonationSampleTransport.del_flag == 0,
|
||
)
|
||
)
|
||
.order_by(
|
||
BizResearchDonationSampleTransport.order_num.asc(),
|
||
BizResearchDonationSampleTransport.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. list_donations
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def list_donations(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
page: int = 1,
|
||
page_size: int = 10,
|
||
keyword: Optional[str] = None,
|
||
donation_status: Optional[str] = None,
|
||
create_time_start: Optional[str] = None,
|
||
create_time_end: Optional[str] = None,
|
||
sample_collect_time_start: Optional[str] = None,
|
||
sample_collect_time_end: Optional[str] = None,
|
||
user_info=None,
|
||
) -> Dict[str, Any]:
|
||
def _base():
|
||
q = (
|
||
db.query(BizResearchDonation)
|
||
.filter(BizResearchDonation.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q = q.filter(BizResearchDonation.tenant_no == tenant_no)
|
||
if donation_status:
|
||
q = q.filter(BizResearchDonation.donation_status == donation_status.strip())
|
||
if create_time_start:
|
||
q = q.filter(
|
||
BizResearchDonation.create_time >= _normalize_date_bound(create_time_start)
|
||
)
|
||
if create_time_end:
|
||
q = q.filter(
|
||
BizResearchDonation.create_time
|
||
<= _normalize_date_bound(create_time_end, end_of_day=True)
|
||
)
|
||
if sample_collect_time_start or sample_collect_time_end:
|
||
coll_q = db.query(BizResearchDonationCollection.research_donation_id).filter(
|
||
BizResearchDonationCollection.del_flag == 0,
|
||
)
|
||
if sample_collect_time_start:
|
||
coll_q = coll_q.filter(
|
||
BizResearchDonationCollection.sample_collect_time
|
||
>= _normalize_date_bound(sample_collect_time_start)
|
||
)
|
||
if sample_collect_time_end:
|
||
coll_q = coll_q.filter(
|
||
BizResearchDonationCollection.sample_collect_time
|
||
<= _normalize_date_bound(sample_collect_time_end, end_of_day=True)
|
||
)
|
||
q = q.filter(BizResearchDonation.id.in_(coll_q))
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
party_match = (
|
||
db.query(BizResearchDonationParty.research_donation_id)
|
||
.filter(
|
||
BizResearchDonationParty.del_flag == 0,
|
||
BizResearchDonationParty.mother_name.like(like),
|
||
)
|
||
.subquery()
|
||
)
|
||
kw_conds = [
|
||
BizResearchDonation.id.in_(party_match),
|
||
]
|
||
if tenant_no:
|
||
from app.crud import sample_reception as reception_svc
|
||
|
||
donation_ids = reception_svc.donation_ids_matching_lab_storage_keyword(
|
||
db, tenant_no, kw
|
||
)
|
||
if donation_ids:
|
||
kw_conds.append(BizResearchDonation.id.in_(donation_ids))
|
||
q = q.filter(or_(*kw_conds))
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q = apply_data_scope(q, BizResearchDonation, user_info, db, tenant_no)
|
||
return q
|
||
|
||
total = (
|
||
_base()
|
||
.order_by(None)
|
||
.with_entities(func.count(BizResearchDonation.id))
|
||
.scalar()
|
||
) or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = (
|
||
_base()
|
||
.order_by(BizResearchDonation.create_time.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
|
||
# 批量加载 party / collection 子表,避免 N+1
|
||
donation_ids = [r.id for r in rows]
|
||
party_map: Dict[str, Dict[str, Any]] = {}
|
||
collection_map: Dict[str, Dict[str, Any]] = {}
|
||
if donation_ids:
|
||
party_rows = (
|
||
db.query(BizResearchDonationParty)
|
||
.filter(
|
||
BizResearchDonationParty.research_donation_id.in_(donation_ids),
|
||
BizResearchDonationParty.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
for p in party_rows:
|
||
party_map[p.research_donation_id] = _party_to_dict(p)
|
||
|
||
collection_rows = (
|
||
db.query(BizResearchDonationCollection)
|
||
.filter(
|
||
BizResearchDonationCollection.research_donation_id.in_(donation_ids),
|
||
BizResearchDonationCollection.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
for c in collection_rows:
|
||
collection_map[c.research_donation_id] = _collection_to_dict(c)
|
||
|
||
# 批量解析地区 / 医院名称
|
||
region_ids = list(
|
||
{collection_map.get(did, {}).get("regionId", "") for did in donation_ids} - {""}
|
||
)
|
||
hospital_ids = list(
|
||
{collection_map.get(did, {}).get("hospitalId", "") for did in donation_ids} - {""}
|
||
)
|
||
region_name_map: Dict[str, str] = {}
|
||
hospital_name_map: Dict[str, str] = {}
|
||
if region_ids:
|
||
for reg in (
|
||
db.query(BizCollectionRegion.id, BizCollectionRegion.region_name)
|
||
.filter(BizCollectionRegion.id.in_(region_ids))
|
||
.all()
|
||
):
|
||
region_name_map[reg.id] = reg.region_name or ""
|
||
if hospital_ids:
|
||
for hosp in (
|
||
db.query(BizCollectionHospital.id, BizCollectionHospital.hospital_name)
|
||
.filter(BizCollectionHospital.id.in_(hospital_ids))
|
||
.all()
|
||
):
|
||
hospital_name_map[hosp.id] = hosp.hospital_name or ""
|
||
|
||
# 批量解析采集专员 / 销售人员名称
|
||
sales_user_ids = list(
|
||
{collection_map.get(did, {}).get("salesUserId", "") for did in donation_ids} - {""}
|
||
)
|
||
collector_ids = list({r.collector_user_id for r in rows if r.collector_user_id})
|
||
user_name_map = _user_nick_name_map(db, list(set(collector_ids + sales_user_ids)))
|
||
|
||
# 批量加载捐赠样本(列表展示样本类型、样本编码、采集情况)
|
||
sample_map: Dict[str, List[BizResearchDonationSample]] = {}
|
||
sample_type_name_map: Dict[str, str] = {}
|
||
if donation_ids:
|
||
s_rows = (
|
||
db.query(BizResearchDonationSample)
|
||
.filter(
|
||
BizResearchDonationSample.research_donation_id.in_(donation_ids),
|
||
BizResearchDonationSample.del_flag == 0,
|
||
)
|
||
.order_by(
|
||
BizResearchDonationSample.order_num.asc(),
|
||
BizResearchDonationSample.create_time.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
sample_type_name_map = _load_sample_type_name_map(db, s_rows)
|
||
for s in s_rows:
|
||
sample_map.setdefault(s.research_donation_id, []).append(s)
|
||
|
||
all_sample_codes: List[str] = []
|
||
for samples in sample_map.values():
|
||
for s in samples:
|
||
c = (s.sample_code or "").strip()
|
||
if c:
|
||
all_sample_codes.append(c)
|
||
lab_code_map: Dict[str, str] = {}
|
||
reception_summary_map: Dict[str, Dict[str, str]] = {}
|
||
qc_status_map: Dict[str, Dict[str, str]] = {}
|
||
from app.services.protocol_sample_list_summary import (
|
||
build_sample_item_reception_qc_fields,
|
||
get_list_qc_status_map_by_sample_codes,
|
||
get_list_reception_summary_map_by_sample_codes,
|
||
)
|
||
|
||
if tenant_no and all_sample_codes:
|
||
from app.crud import sample_reception as reception_svc
|
||
|
||
lab_code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
|
||
db, tenant_no, all_sample_codes, reception_source="research_donation"
|
||
)
|
||
reception_summary_map = get_list_reception_summary_map_by_sample_codes(
|
||
db, tenant_no, all_sample_codes, reception_source="research_donation"
|
||
)
|
||
qc_status_map = get_list_qc_status_map_by_sample_codes(
|
||
db, tenant_no, all_sample_codes, reception_source="research_donation"
|
||
)
|
||
|
||
result = []
|
||
for r in rows:
|
||
d = _donation_to_dict(r, db, tenant_no)
|
||
d["collectorUserName"] = user_name_map.get(r.collector_user_id or "", "")
|
||
pc = party_map.get(r.id)
|
||
if pc:
|
||
d["motherName"] = pc.get("motherName", "")
|
||
d["motherPhone"] = pc.get("motherPhone", "")
|
||
d["motherIdCard"] = pc.get("motherIdCard", "")
|
||
d["fatherName"] = pc.get("fatherName", "")
|
||
d["newbornName"] = pc.get("newbornName", "")
|
||
d["expectedDeliveryDate"] = pc.get("expectedDeliveryDate", "")
|
||
d["newbornDeliveryTime"] = pc.get("newbornDeliveryTime", "")
|
||
d["newbornSex"] = pc.get("newbornSex", "")
|
||
d["deliveryMode"] = pc.get("deliveryMode", "")
|
||
d["newbornWeightGram"] = pc.get("newbornWeightGram")
|
||
else:
|
||
d["motherName"] = ""
|
||
d["motherPhone"] = ""
|
||
d["motherIdCard"] = ""
|
||
d["fatherName"] = ""
|
||
d["newbornName"] = ""
|
||
d["expectedDeliveryDate"] = ""
|
||
d["newbornDeliveryTime"] = ""
|
||
d["newbornSex"] = ""
|
||
d["deliveryMode"] = ""
|
||
d["newbornWeightGram"] = None
|
||
ci = collection_map.get(r.id)
|
||
if ci:
|
||
d["regionName"] = region_name_map.get(ci.get("regionId", ""), "")
|
||
d["hospitalName"] = hospital_name_map.get(ci.get("hospitalId", ""), "")
|
||
d["salesUserName"] = user_name_map.get(ci.get("salesUserId", ""), "")
|
||
d["sampleCollectTime"] = ci.get("sampleCollectTime", "")
|
||
else:
|
||
d["regionName"] = ""
|
||
d["hospitalName"] = ""
|
||
d["salesUserName"] = ""
|
||
d["sampleCollectTime"] = ""
|
||
donation_samples = sample_map.get(r.id, [])
|
||
type_names: List[str] = []
|
||
codes: List[str] = []
|
||
sample_items: List[Dict[str, Any]] = []
|
||
for s in donation_samples:
|
||
type_name = sample_type_name_map.get(s.sample_type_id or "", "")
|
||
if type_name:
|
||
type_names.append(type_name)
|
||
code = (s.sample_code or "").strip()
|
||
if code:
|
||
codes.append(code)
|
||
sample_items.append(
|
||
{
|
||
"sampleTypeName": type_name,
|
||
"sampleCode": code,
|
||
"sampleStatus": (s.sample_status or "").strip(),
|
||
"labStorageCode": lab_code_map.get(code, "") if code else "",
|
||
**sample_special_to_dict(s),
|
||
**build_sample_item_reception_qc_fields(
|
||
reception_summary_map.get(code) if code else None,
|
||
qc_status_map.get(code) if code else None,
|
||
),
|
||
}
|
||
)
|
||
d["sampleItems"] = sample_items
|
||
d["sampleTypeNames"] = ",".join(type_names)
|
||
d["sampleCodes"] = ",".join(codes)
|
||
d["labStorageCodes"] = ",".join((lab_code_map.get(c) or "") for c in codes)
|
||
d["sampleCollectSummary"] = _format_sample_collect_summary(donation_samples)
|
||
result.append(d)
|
||
|
||
return {
|
||
"list": result,
|
||
"total": int(total),
|
||
"page": page,
|
||
"pageSize": page_size,
|
||
"totalPages": total_pages(int(total), page_size),
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. get_donation_detail
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def get_donation_detail(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
) -> Optional[Dict[str, Any]]:
|
||
"""查询单条捐赠详情,含全部子资源嵌套。"""
|
||
main = (
|
||
db.query(BizResearchDonation)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonation.id == donation_id,
|
||
BizResearchDonation.tenant_no == tenant_no,
|
||
BizResearchDonation.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if not main:
|
||
return None
|
||
|
||
out: Dict[str, Any] = {"donation": _donation_to_dict(main, db, tenant_no)}
|
||
|
||
# 采集专员名称
|
||
user_ids: List[str] = []
|
||
if main.collector_user_id:
|
||
user_ids.append(main.collector_user_id)
|
||
|
||
# party
|
||
party = get_party(db, donation_id, tenant_no)
|
||
out["party"] = _party_to_dict(party) if party else None
|
||
if party and party.collector_user_id:
|
||
user_ids.append(party.collector_user_id)
|
||
|
||
# collection
|
||
coll = get_collection(db, donation_id, tenant_no)
|
||
coll_dict = _collection_to_dict(coll) if coll else None
|
||
if coll_dict and coll_dict.get("salesUserId"):
|
||
user_ids.append(coll_dict["salesUserId"])
|
||
if coll_dict:
|
||
region_id = str(coll_dict.get("regionId") or "")
|
||
if region_id:
|
||
r = db.query(BizCollectionRegion.region_name).filter(
|
||
BizCollectionRegion.id == region_id
|
||
).first()
|
||
coll_dict["regionName"] = r.region_name if r else ""
|
||
hospital_id = str(coll_dict.get("hospitalId") or "")
|
||
if hospital_id:
|
||
h = db.query(BizCollectionHospital.hospital_name).filter(
|
||
BizCollectionHospital.id == hospital_id
|
||
).first()
|
||
coll_dict["hospitalName"] = h.hospital_name if h else ""
|
||
out["collection"] = coll_dict
|
||
|
||
# samples + transport
|
||
s_rows = list_samples(db, donation_id, tenant_no)
|
||
transport_rows = list_sample_transports(db, donation_id, tenant_no)
|
||
site_name_map = _load_site_name_map(
|
||
db, [t.dest_site_id for t in transport_rows if t.dest_site_id]
|
||
)
|
||
transport_by_sample: Dict[str, Dict[str, Any]] = {}
|
||
for t in transport_rows:
|
||
td = _transport_to_dict(t)
|
||
td["destSiteName"] = site_name_map.get(t.dest_site_id or "", "")
|
||
transport_by_sample[t.sample_id] = td
|
||
from app.crud import sample_reception as sample_reception_svc
|
||
from app.crud import qc_work_order as qc_work_order_svc
|
||
from app.crud import production_work_order as production_work_order_svc
|
||
from app.crud import storage_release_review as storage_release_review_svc
|
||
|
||
sample_ids = [s.id for s in s_rows]
|
||
sample_code_by_id = {s.id: s.sample_code or "" for s in s_rows}
|
||
reception_by_sample = sample_reception_svc.get_receptions_map_for_donation_samples(
|
||
db, sample_ids, tenant_no
|
||
)
|
||
qc_by_sample = qc_work_order_svc.get_qc_info_map_for_samples(
|
||
db,
|
||
sample_ids,
|
||
tenant_no,
|
||
)
|
||
process_qc_by_sample = qc_work_order_svc.get_process_qc_info_map_for_samples(
|
||
db,
|
||
sample_ids,
|
||
tenant_no,
|
||
sample_code_by_id=sample_code_by_id,
|
||
)
|
||
production_by_sample = production_work_order_svc.get_production_info_map_for_samples(
|
||
db, sample_ids, tenant_no
|
||
)
|
||
storage_release_by_sample = storage_release_review_svc.get_storage_release_map_for_samples(
|
||
db,
|
||
sample_ids,
|
||
tenant_no,
|
||
sample_code_by_id=sample_code_by_id,
|
||
)
|
||
samples_out: List[Dict[str, Any]] = []
|
||
unit_name_map = _load_unit_name_map(db, s_rows)
|
||
from app.crud import sample_freight as sample_freight_svc
|
||
|
||
freight_by_sample = sample_freight_svc.freight_map_for_samples(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
sample_source=sample_freight_svc.SAMPLE_SOURCE_DONATION,
|
||
sample_ids=sample_ids,
|
||
)
|
||
for s in s_rows:
|
||
item = _sample_to_dict(s)
|
||
item["unitName"] = unit_name_map.get(s.unit_id or "", "")
|
||
item["transport"] = transport_by_sample.get(s.id)
|
||
item["freight"] = freight_by_sample.get(s.id)
|
||
item["sampleReception"] = reception_by_sample.get(s.id)
|
||
item["productionReception"] = (reception_by_sample.get(s.id) or {}).get(
|
||
"productionReception"
|
||
)
|
||
item["qcInfo"] = qc_by_sample.get(s.id)
|
||
item["processQcInfoList"] = process_qc_by_sample.get(s.id, [])
|
||
item["productionInfo"] = production_by_sample.get(s.id)
|
||
item["storageReleaseInfo"] = storage_release_by_sample.get(s.id)
|
||
samples_out.append(item)
|
||
out["samples"] = samples_out
|
||
|
||
# user name map
|
||
user_name_map = _user_nick_name_map(db, user_ids)
|
||
out["donation"]["collectorUserName"] = user_name_map.get(
|
||
main.collector_user_id or "", ""
|
||
)
|
||
if coll_dict and coll_dict.get("salesUserId"):
|
||
coll_dict["salesUserName"] = user_name_map.get(
|
||
coll_dict["salesUserId"], ""
|
||
)
|
||
|
||
return out
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. create_donation
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def create_donation(
|
||
db: Session,
|
||
*,
|
||
data: dict,
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
) -> BizResearchDonation:
|
||
"""创建科研捐赠单(含 party / collection / samples + transports)。"""
|
||
raw = _normalize_fields(data)
|
||
_resolve_donation_payload(raw)
|
||
|
||
consent = (raw.get("consent_form_urls") or "").strip()
|
||
|
||
donation_no = (raw.get("donation_no") or "").strip()
|
||
if not donation_no:
|
||
donation_no = allocate_next_biz_no(
|
||
db, tenant_no=tenant_no, kind="donation", user_id=user_id
|
||
)
|
||
assert_biz_no_available(
|
||
db, tenant_no=tenant_no, biz_no=donation_no, for_donation=True
|
||
)
|
||
|
||
now = datetime.now()
|
||
obj = BizResearchDonation(
|
||
id=generate_uuid(),
|
||
donation_no=donation_no,
|
||
donation_status=(raw.get("donation_status") or "dsx").strip(),
|
||
donation_date=(raw.get("donation_date") or "").strip() or None,
|
||
source_type=(raw.get("source_type") or "direct").strip(),
|
||
source_protocol_id=(raw.get("source_protocol_id") or "").strip() or None,
|
||
consent_form_urls=consent or None,
|
||
attachment_urls=(raw.get("attachment_urls") or "").strip() or None,
|
||
infectious_disease_report_urls=(
|
||
(raw.get("infectious_disease_report_urls") or "").strip() or None
|
||
),
|
||
collector_user_id=(raw.get("collector_user_id") or "").strip() or None,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=(raw.get("remark") or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.flush()
|
||
|
||
donation_id = obj.id
|
||
|
||
# party
|
||
from app.services.party_customer_sync import sync_party_customers
|
||
|
||
party_raw = _normalize_fields(raw.get("party") or {})
|
||
party_raw = sync_party_customers(db, party_raw, tenant_no=tenant_no, user_id=user_id or "")
|
||
_create_party(db, donation_id, tenant_no, user_id, party_raw, now)
|
||
|
||
# collection
|
||
coll_raw = _normalize_fields(raw.get("collection") or {})
|
||
_create_collection(db, donation_id, tenant_no, user_id, coll_raw, now)
|
||
|
||
# samples(运费批次在 commit 后挂钩;停写旧运输依赖)
|
||
samples_raw = raw.get("samples") or []
|
||
new_samples = _create_samples(db, donation_id, tenant_no, user_id, samples_raw, now)
|
||
|
||
db.commit()
|
||
db.refresh(obj)
|
||
from app.crud import sample_freight as sample_freight_svc
|
||
|
||
sample_freight_svc.apply_freight_on_protocol_save(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
sample_source=sample_freight_svc.SAMPLE_SOURCE_DONATION,
|
||
protocol_id=donation_id,
|
||
saved_samples=new_samples,
|
||
input_samples=samples_raw,
|
||
)
|
||
db.commit()
|
||
return obj
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. update_donation
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def update_donation(
|
||
db: Session,
|
||
donation_id: str,
|
||
*,
|
||
data: dict,
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
) -> BizResearchDonation:
|
||
"""更新科研捐赠单:主表字段 + 子资源 upsert(软删旧 + 创建新)。"""
|
||
obj = (
|
||
db.query(BizResearchDonation)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonation.id == donation_id,
|
||
BizResearchDonation.tenant_no == tenant_no,
|
||
BizResearchDonation.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if not obj:
|
||
raise ValueError("捐赠单不存在或无权操作")
|
||
|
||
raw = _normalize_fields(data)
|
||
_resolve_donation_payload(raw)
|
||
now = datetime.now()
|
||
|
||
# 主表字段
|
||
_MAIN_STR_FIELDS = [
|
||
"donation_status", "donation_date", "source_type",
|
||
"source_protocol_id", "consent_form_urls", "attachment_urls",
|
||
"infectious_disease_report_urls",
|
||
"collector_user_id", "effective_desc", "expire_desc",
|
||
"void_desc", "to_storage_desc", "remark", "donation_no",
|
||
]
|
||
_MAIN_DT_FIELDS = [
|
||
"effective_time", "expire_time", "void_time", "to_storage_time",
|
||
]
|
||
for k in _MAIN_STR_FIELDS:
|
||
if k in raw:
|
||
if k == "donation_no":
|
||
pn = (raw[k] or "").strip()
|
||
if pn and pn != (obj.donation_no or ""):
|
||
raise ValueError("捐赠协议号不可修改")
|
||
else:
|
||
val = raw[k]
|
||
if val is None:
|
||
continue
|
||
setattr(obj, k, (val or "").strip() or None)
|
||
for k in _MAIN_DT_FIELDS:
|
||
if k in raw:
|
||
val = raw[k]
|
||
if isinstance(val, str):
|
||
val = parse_datetime(val)
|
||
setattr(obj, k, val)
|
||
|
||
obj.update_by = user_id
|
||
obj.update_time = now
|
||
db.add(obj)
|
||
db.flush()
|
||
|
||
# party upsert: 软删旧 → 创建新
|
||
if "party" in raw:
|
||
from app.services.party_customer_sync import sync_party_customers
|
||
|
||
_soft_delete_party(db, donation_id, tenant_no, user_id)
|
||
party_raw = _normalize_fields(raw["party"] or {})
|
||
party_raw = sync_party_customers(db, party_raw, tenant_no=tenant_no, user_id=user_id or "")
|
||
_create_party(db, donation_id, tenant_no, user_id, party_raw, now)
|
||
|
||
# collection upsert
|
||
if "collection" in raw:
|
||
_soft_delete_collection(db, donation_id, tenant_no, user_id)
|
||
coll_raw = _normalize_fields(raw["collection"] or {})
|
||
_create_collection(db, donation_id, tenant_no, user_id, coll_raw, now)
|
||
|
||
# samples upsert
|
||
if "samples" in raw:
|
||
existing_samples = list_samples(db, donation_id, tenant_no)
|
||
old_id_to_code = {
|
||
s.id: (s.sample_code or "").strip()
|
||
for s in existing_samples
|
||
if (s.sample_code or "").strip()
|
||
}
|
||
_soft_delete_samples(db, donation_id, tenant_no, user_id)
|
||
_soft_delete_transports(db, donation_id, tenant_no, user_id)
|
||
samples_raw = raw["samples"] or []
|
||
new_samples = _create_samples(db, donation_id, tenant_no, user_id, samples_raw, now)
|
||
new_code_to_id = {
|
||
(s.sample_code or "").strip(): s.id
|
||
for s in new_samples
|
||
if (s.sample_code or "").strip()
|
||
}
|
||
_migrate_donation_sample_child_refs_on_replace(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
old_id_to_code=old_id_to_code,
|
||
new_code_to_id=new_code_to_id,
|
||
user_id=user_id,
|
||
)
|
||
else:
|
||
samples_raw = None
|
||
new_samples = None
|
||
|
||
db.commit()
|
||
db.refresh(obj)
|
||
if new_samples is not None and samples_raw is not None:
|
||
from app.crud import sample_freight as sample_freight_svc
|
||
|
||
sample_freight_svc.apply_freight_on_protocol_save(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
sample_source=sample_freight_svc.SAMPLE_SOURCE_DONATION,
|
||
protocol_id=donation_id,
|
||
saved_samples=new_samples,
|
||
input_samples=samples_raw,
|
||
)
|
||
db.commit()
|
||
return obj
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 5. delete_donation / batch_delete_donations
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def delete_donation(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
) -> None:
|
||
"""软删除捐赠单 + 所有子资源。"""
|
||
obj = (
|
||
db.query(BizResearchDonation)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonation.id == donation_id,
|
||
BizResearchDonation.tenant_no == tenant_no,
|
||
BizResearchDonation.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if not obj:
|
||
raise ValueError("捐赠单不存在或无权操作")
|
||
_soft_delete_all(db, donation_id, tenant_no, user_id)
|
||
db.commit()
|
||
|
||
|
||
def batch_delete_donations(
|
||
db: Session,
|
||
ids: List[str],
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
) -> int:
|
||
"""批量软删除捐赠单。"""
|
||
count = 0
|
||
for did in (ids or []):
|
||
did = str(did).strip()
|
||
if not did:
|
||
continue
|
||
obj = (
|
||
db.query(BizResearchDonation)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonation.id == did,
|
||
BizResearchDonation.tenant_no == tenant_no,
|
||
BizResearchDonation.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if not obj:
|
||
continue
|
||
_soft_delete_all(db, did, tenant_no, user_id)
|
||
count += 1
|
||
db.commit()
|
||
return count
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 6. list_audits
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def list_audits(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
page: int = 1,
|
||
page_size: int = 10,
|
||
keyword: Optional[str] = None,
|
||
audit_status: Optional[str] = None,
|
||
research_donation_id: Optional[str] = None,
|
||
user_info=None,
|
||
) -> Dict[str, Any]:
|
||
q_base = (
|
||
db.query(BizResearchDonationAudit, BizResearchDonation.donation_no)
|
||
.join(
|
||
BizResearchDonation,
|
||
and_(
|
||
BizResearchDonationAudit.research_donation_id == BizResearchDonation.id,
|
||
BizResearchDonation.del_flag == 0,
|
||
),
|
||
)
|
||
.filter(BizResearchDonationAudit.del_flag == 0)
|
||
)
|
||
if tenant_no:
|
||
q_base = q_base.filter(BizResearchDonationAudit.tenant_no == tenant_no)
|
||
if audit_status:
|
||
q_base = q_base.filter(BizResearchDonationAudit.audit_status == audit_status.strip())
|
||
did = (research_donation_id or "").strip()
|
||
if did:
|
||
q_base = q_base.filter(BizResearchDonationAudit.research_donation_id == did)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q_base = q_base.filter(
|
||
or_(
|
||
BizResearchDonation.donation_no.like(like),
|
||
BizResearchDonationAudit.audit_remark.like(like),
|
||
)
|
||
)
|
||
if user_info:
|
||
from app.core.data_scope import apply_data_scope
|
||
q_base = apply_data_scope(q_base, BizResearchDonationAudit, user_info, db, tenant_no)
|
||
|
||
total = (
|
||
q_base.order_by(None)
|
||
.with_entities(func.count(BizResearchDonationAudit.id))
|
||
.scalar()
|
||
) or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = (
|
||
q_base.order_by(BizResearchDonationAudit.create_time.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
|
||
auditor_ids = list({a.auditor_user_id for a, _ in rows if a.auditor_user_id})
|
||
auditor_name_map = _user_nick_name_map(db, auditor_ids)
|
||
|
||
result = [
|
||
_audit_to_dict(
|
||
a,
|
||
donation_no=pn or "",
|
||
auditor_user_name=auditor_name_map.get(a.auditor_user_id or "", ""),
|
||
)
|
||
for a, pn in rows
|
||
]
|
||
|
||
return {
|
||
"list": result,
|
||
"total": int(total),
|
||
"page": page,
|
||
"pageSize": page_size,
|
||
"totalPages": total_pages(int(total), page_size),
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 7. submit_audit
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def submit_audit(
|
||
db: Session,
|
||
donation_id: str,
|
||
*,
|
||
audit_status: str,
|
||
audit_remark: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
tenant_no: str = "",
|
||
) -> BizResearchDonationAudit:
|
||
"""提交审核。shtg → 捐赠状态变为 sxz;shth → 状态不变。"""
|
||
# 校验捐赠单存在
|
||
main = (
|
||
db.query(BizResearchDonation)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonation.id == donation_id,
|
||
BizResearchDonation.tenant_no == tenant_no,
|
||
BizResearchDonation.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if not main:
|
||
raise ValueError("捐赠单不存在")
|
||
|
||
st = (audit_status or "").strip()
|
||
if st not in ("shtg", "shth"):
|
||
raise ValueError("审核状态无效")
|
||
|
||
now = datetime.now()
|
||
obj = BizResearchDonationAudit(
|
||
id=generate_uuid(),
|
||
research_donation_id=donation_id,
|
||
auditor_user_id=user_id,
|
||
audit_remark=(audit_remark or "").strip() or None,
|
||
audit_status=st,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
|
||
# 审核通过 → 变更状态为 sxz
|
||
if st == "shtg":
|
||
main.donation_status = "sxz"
|
||
main.effective_time = now
|
||
main.update_by = user_id
|
||
main.update_time = now
|
||
db.add(main)
|
||
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 8. change_status
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# 合法状态转换: {当前状态: {可转状态列表}}
|
||
_VALID_TRANSITIONS: Dict[str, List[str]] = {
|
||
"dsx": ["sxz", "yzf", "sdfq"],
|
||
"sxz": ["ygq", "yzf", "zckc", "sdfq"],
|
||
}
|
||
|
||
|
||
def change_status(
|
||
db: Session,
|
||
donation_id: str,
|
||
*,
|
||
new_status: str,
|
||
desc: Optional[str] = None,
|
||
time_val: Optional[Any] = None,
|
||
void_type: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
tenant_no: str = "",
|
||
) -> BizResearchDonation:
|
||
"""变更捐赠单状态(含合法性校验)。"""
|
||
obj = (
|
||
db.query(BizResearchDonation)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonation.id == donation_id,
|
||
BizResearchDonation.tenant_no == tenant_no,
|
||
BizResearchDonation.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if not obj:
|
||
raise ValueError("捐赠单不存在")
|
||
|
||
current = obj.donation_status or ""
|
||
ns = (new_status or "").strip()
|
||
if current != ns:
|
||
allowed = _VALID_TRANSITIONS.get(current, [])
|
||
if ns not in allowed:
|
||
raise ValueError(f"状态 {current} 不可转为 {ns}")
|
||
|
||
now = datetime.now()
|
||
if isinstance(time_val, str):
|
||
time_val = parse_datetime(time_val)
|
||
effective_time = time_val or now
|
||
|
||
if current != ns:
|
||
obj.donation_status = ns
|
||
obj.update_by = user_id
|
||
obj.update_time = now
|
||
|
||
# 更新对应的时间/描述字段
|
||
if ns == "sxz":
|
||
obj.effective_time = effective_time
|
||
obj.effective_desc = (desc or "").strip() or None
|
||
elif ns == "yzf":
|
||
obj.void_time = effective_time
|
||
obj.void_desc = (desc or "").strip() or None
|
||
from app.crud.dict_data import normalize_void_type
|
||
|
||
obj.void_type = normalize_void_type(db, value=void_type or "", tenant_no=tenant_no)
|
||
elif ns == "ygq":
|
||
obj.expire_time = effective_time
|
||
obj.expire_desc = (desc or "").strip() or None
|
||
elif ns == "zckc":
|
||
obj.to_storage_time = effective_time
|
||
obj.to_storage_desc = (desc or "").strip() or None
|
||
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return obj
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 9. convert_from_storage
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def convert_from_storage(
|
||
db: Session,
|
||
protocol_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
) -> BizResearchDonation:
|
||
"""从细胞存储协议单转换创建科研捐赠单。"""
|
||
# 读取源协议
|
||
proto = (
|
||
db.query(BizCollectionProtocol)
|
||
.filter(
|
||
and_(
|
||
BizCollectionProtocol.id == protocol_id,
|
||
BizCollectionProtocol.tenant_no == tenant_no,
|
||
BizCollectionProtocol.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if not proto:
|
||
raise ValueError("源协议单不存在")
|
||
if proto.protocol_status != "sxz":
|
||
raise ValueError("仅生效中的协议可转科研")
|
||
if has_active_convert_link(db, tenant_no, protocol_id=protocol_id):
|
||
raise ValueError("该协议已存在转移关联,无法重复转科研")
|
||
|
||
pn_src = (proto.protocol_no or "").strip()
|
||
if not pn_src:
|
||
raise ValueError("源协议单协议号为空,无法转科研")
|
||
|
||
# 读取源子资源
|
||
src_party = (
|
||
db.query(BizProtocolPartyCustomer)
|
||
.filter(
|
||
and_(
|
||
BizProtocolPartyCustomer.collection_protocol_id == protocol_id,
|
||
BizProtocolPartyCustomer.tenant_no == tenant_no,
|
||
BizProtocolPartyCustomer.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
src_coll = (
|
||
db.query(BizProtocolCollectionInfo)
|
||
.filter(
|
||
and_(
|
||
BizProtocolCollectionInfo.collection_protocol_id == protocol_id,
|
||
BizProtocolCollectionInfo.tenant_no == tenant_no,
|
||
BizProtocolCollectionInfo.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
src_samples = (
|
||
db.query(BizProtocolSample)
|
||
.filter(
|
||
and_(
|
||
BizProtocolSample.collection_protocol_id == protocol_id,
|
||
BizProtocolSample.tenant_no == tenant_no,
|
||
BizProtocolSample.del_flag == 0,
|
||
)
|
||
)
|
||
.all()
|
||
)
|
||
src_transports = (
|
||
db.query(BizProtocolSampleTransport)
|
||
.filter(
|
||
and_(
|
||
BizProtocolSampleTransport.collection_protocol_id == protocol_id,
|
||
BizProtocolSampleTransport.tenant_no == tenant_no,
|
||
BizProtocolSampleTransport.del_flag == 0,
|
||
)
|
||
)
|
||
.all()
|
||
)
|
||
transport_by_sample = {t.sample_id: t for t in src_transports}
|
||
|
||
now = datetime.now()
|
||
donation_no = pn_src
|
||
|
||
# 创建主记录
|
||
donation = BizResearchDonation(
|
||
id=generate_uuid(),
|
||
donation_no=donation_no,
|
||
donation_status="dsx",
|
||
donation_date=now.strftime("%Y%m%d"),
|
||
source_type="from_storage",
|
||
source_protocol_id=protocol_id,
|
||
attachment_urls=(proto.protocol_attachments or "").strip() or None,
|
||
collector_user_id=proto.collector_user_id,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=f"由细胞存储协议单 {proto.protocol_no} 转入",
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(donation)
|
||
db.flush()
|
||
|
||
did = donation.id
|
||
|
||
# 创建 party
|
||
if src_party:
|
||
party = BizResearchDonationParty(
|
||
id=generate_uuid(),
|
||
research_donation_id=did,
|
||
customer_id=src_party.customer_id,
|
||
father_customer_id=getattr(src_party, "father_customer_id", None),
|
||
newborn_customer_id=getattr(src_party, "newborn_customer_id", None),
|
||
mother_name=src_party.mother_name,
|
||
mother_phone=src_party.mother_phone,
|
||
mother_id_card=src_party.mother_id_card,
|
||
mother_birth_ym=src_party.mother_birth_ym,
|
||
mother_ethnic=src_party.mother_ethnic,
|
||
mother_email=src_party.mother_email,
|
||
mother_address=src_party.mother_address,
|
||
father_name=src_party.father_name,
|
||
father_phone=src_party.father_phone,
|
||
father_id_card=src_party.father_id_card,
|
||
newborn_name=src_party.newborn_name,
|
||
newborn_id_card=src_party.newborn_id_card,
|
||
expected_delivery_date=src_party.expected_delivery_date,
|
||
newborn_delivery_time=src_party.newborn_delivery_time,
|
||
newborn_sex=src_party.newborn_sex,
|
||
delivery_mode=src_party.delivery_mode,
|
||
birth_order=src_party.birth_order,
|
||
newborn_weight_gram=src_party.newborn_weight_gram,
|
||
apgar_1min=src_party.apgar_1min,
|
||
apgar_5min=src_party.apgar_5min,
|
||
collector_user_id=src_party.collector_user_id,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(party)
|
||
|
||
# 创建 collection
|
||
if src_coll:
|
||
coll = BizResearchDonationCollection(
|
||
id=generate_uuid(),
|
||
research_donation_id=did,
|
||
region_id=src_coll.region_id,
|
||
hospital_id=src_coll.hospital_id,
|
||
medical_staff=src_coll.medical_staff,
|
||
sales_user_id=src_coll.sales_user_id,
|
||
sample_collect_time=src_coll.sample_collect_time,
|
||
maternal_blood_collect_time=src_coll.maternal_blood_collect_time,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(coll)
|
||
|
||
# 创建 samples + transports
|
||
for idx, ss in enumerate(src_samples):
|
||
new_sample = BizResearchDonationSample(
|
||
id=generate_uuid(),
|
||
research_donation_id=did,
|
||
sample_type_id=ss.sample_type_id,
|
||
sample_code_id=ss.sample_code_id,
|
||
sample_code=ss.sample_code,
|
||
collect_amount=ss.collect_amount,
|
||
unit_id=ss.unit_id,
|
||
storage_years=ss.storage_years,
|
||
sample_status=ss.sample_status,
|
||
storage_in_date=ss.storage_in_date,
|
||
detect_start_date=ss.detect_start_date,
|
||
void_date=ss.void_date,
|
||
storage_expire_date=ss.storage_expire_date,
|
||
is_special=getattr(ss, "is_special", 0),
|
||
special_remark=getattr(ss, "special_remark", None),
|
||
tenant_no=tenant_no,
|
||
order_num=idx,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(new_sample)
|
||
db.flush()
|
||
|
||
# 对应运输记录
|
||
src_t = transport_by_sample.get(ss.id)
|
||
if src_t:
|
||
new_t = BizResearchDonationSampleTransport(
|
||
id=generate_uuid(),
|
||
research_donation_id=did,
|
||
sample_id=new_sample.id,
|
||
sample_code=ss.sample_code or None,
|
||
transport_type=src_t.transport_type,
|
||
express_company_id=src_t.express_company_id,
|
||
waybill_no=src_t.waybill_no,
|
||
dest_site_id=src_t.dest_site_id,
|
||
express_fee=src_t.express_fee,
|
||
eta_time=src_t.eta_time,
|
||
carrier_name=src_t.carrier_name,
|
||
carrier_phone=src_t.carrier_phone,
|
||
tenant_no=tenant_no,
|
||
order_num=idx,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(new_t)
|
||
|
||
# 更新源协议状态为 zky(转科研)
|
||
proto.protocol_status = "zky"
|
||
proto.to_research_time = now
|
||
proto.to_research_desc = f"已转入科研捐赠 {donation_no}"
|
||
proto.update_by = user_id
|
||
proto.update_time = now
|
||
db.add(proto)
|
||
|
||
create_convert_link(
|
||
db,
|
||
convert_type="storage_to_research",
|
||
protocol_id=protocol_id,
|
||
research_donation_id=did,
|
||
biz_no=donation_no,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
now=now,
|
||
)
|
||
|
||
db.commit()
|
||
db.refresh(donation)
|
||
return donation
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 10. convert_to_storage
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def convert_to_storage(
|
||
db: Session,
|
||
donation_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
user_id: Optional[str] = None,
|
||
) -> BizResearchDonation:
|
||
"""从科研捐赠单转换创建细胞存储协议单,返回更新后的捐赠单。"""
|
||
# 读取源捐赠
|
||
donation = (
|
||
db.query(BizResearchDonation)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonation.id == donation_id,
|
||
BizResearchDonation.tenant_no == tenant_no,
|
||
BizResearchDonation.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if not donation:
|
||
raise ValueError("捐赠单不存在")
|
||
if donation.donation_status != "sxz":
|
||
raise ValueError("仅生效中的捐赠单可转细胞存储")
|
||
partner = get_convert_partner(db, tenant_no, donation_id=donation_id)
|
||
if partner:
|
||
linked_proto = (
|
||
db.query(BizCollectionProtocol)
|
||
.filter(
|
||
and_(
|
||
BizCollectionProtocol.id == partner["protocol_id"],
|
||
BizCollectionProtocol.tenant_no == tenant_no,
|
||
BizCollectionProtocol.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if linked_proto and linked_proto.protocol_status != "zky":
|
||
raise ValueError("该捐赠单已转细胞存储,无法重复操作")
|
||
|
||
dn_src = (donation.donation_no or "").strip()
|
||
if not dn_src:
|
||
raise ValueError("源捐赠单协议号为空,无法转细胞存储")
|
||
|
||
src_party = get_party(db, donation_id, tenant_no)
|
||
src_coll = get_collection(db, donation_id, tenant_no)
|
||
src_samples = list_samples(db, donation_id, tenant_no)
|
||
src_transports = list_sample_transports(db, donation_id, tenant_no)
|
||
transport_by_sample = {t.sample_id: t for t in src_transports}
|
||
|
||
now = datetime.now()
|
||
protocol_no = dn_src
|
||
protocol_no_id = None
|
||
if (donation.source_protocol_id or "").strip():
|
||
src_proto = (
|
||
db.query(BizCollectionProtocol)
|
||
.filter(
|
||
and_(
|
||
BizCollectionProtocol.id == donation.source_protocol_id.strip(),
|
||
BizCollectionProtocol.tenant_no == tenant_no,
|
||
BizCollectionProtocol.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if src_proto and src_proto.protocol_no_id:
|
||
protocol_no_id = src_proto.protocol_no_id
|
||
|
||
protocol_attachment_urls = _merge_comma_separated_urls(
|
||
donation.consent_form_urls,
|
||
donation.infectious_disease_report_urls,
|
||
donation.attachment_urls,
|
||
)
|
||
|
||
# 创建协议单
|
||
proto = BizCollectionProtocol(
|
||
id=generate_uuid(),
|
||
protocol_no_id=protocol_no_id,
|
||
protocol_no=protocol_no,
|
||
protocol_status="dsx",
|
||
register_date=now.strftime("%Y%m%d"),
|
||
protocol_attachments=protocol_attachment_urls,
|
||
collector_user_id=donation.collector_user_id,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=f"由科研捐赠单 {donation.donation_no} 转入",
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(proto)
|
||
db.flush()
|
||
|
||
pid = proto.id
|
||
|
||
# 创建 party
|
||
if src_party:
|
||
party = BizProtocolPartyCustomer(
|
||
id=generate_uuid(),
|
||
collection_protocol_id=pid,
|
||
customer_id=src_party.customer_id,
|
||
father_customer_id=getattr(src_party, "father_customer_id", None),
|
||
newborn_customer_id=getattr(src_party, "newborn_customer_id", None),
|
||
mother_name=src_party.mother_name,
|
||
mother_phone=src_party.mother_phone,
|
||
mother_id_card=src_party.mother_id_card,
|
||
mother_birth_ym=src_party.mother_birth_ym,
|
||
mother_ethnic=src_party.mother_ethnic,
|
||
mother_email=src_party.mother_email,
|
||
mother_address=src_party.mother_address,
|
||
father_name=src_party.father_name,
|
||
father_phone=src_party.father_phone,
|
||
father_id_card=src_party.father_id_card,
|
||
newborn_name=src_party.newborn_name,
|
||
newborn_id_card=src_party.newborn_id_card,
|
||
expected_delivery_date=src_party.expected_delivery_date,
|
||
newborn_delivery_time=src_party.newborn_delivery_time,
|
||
newborn_sex=src_party.newborn_sex,
|
||
delivery_mode=src_party.delivery_mode,
|
||
birth_order=src_party.birth_order,
|
||
newborn_weight_gram=src_party.newborn_weight_gram,
|
||
apgar_1min=src_party.apgar_1min,
|
||
apgar_5min=src_party.apgar_5min,
|
||
collector_user_id=src_party.collector_user_id,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(party)
|
||
|
||
# 创建 collection
|
||
if src_coll:
|
||
coll = BizProtocolCollectionInfo(
|
||
id=generate_uuid(),
|
||
collection_protocol_id=pid,
|
||
region_id=src_coll.region_id,
|
||
hospital_id=src_coll.hospital_id,
|
||
medical_staff=src_coll.medical_staff,
|
||
sales_user_id=src_coll.sales_user_id,
|
||
sample_collect_time=src_coll.sample_collect_time,
|
||
maternal_blood_collect_time=src_coll.maternal_blood_collect_time,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(coll)
|
||
|
||
# 创建 samples + transports(无 price 字段 → null)
|
||
for idx, ss in enumerate(src_samples):
|
||
new_sample = BizProtocolSample(
|
||
id=generate_uuid(),
|
||
collection_protocol_id=pid,
|
||
sample_type_id=ss.sample_type_id,
|
||
sample_code_id=ss.sample_code_id,
|
||
sample_code=ss.sample_code,
|
||
collect_amount=ss.collect_amount,
|
||
unit_id=ss.unit_id,
|
||
price=None,
|
||
storage_years=ss.storage_years,
|
||
sample_status=ss.sample_status,
|
||
storage_in_date=ss.storage_in_date,
|
||
detect_start_date=ss.detect_start_date,
|
||
void_date=ss.void_date,
|
||
storage_expire_date=ss.storage_expire_date,
|
||
is_special=ss.is_special,
|
||
special_remark=ss.special_remark,
|
||
tenant_no=tenant_no,
|
||
order_num=idx,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(new_sample)
|
||
db.flush()
|
||
|
||
src_t = transport_by_sample.get(ss.id)
|
||
if src_t:
|
||
new_t = BizProtocolSampleTransport(
|
||
id=generate_uuid(),
|
||
collection_protocol_id=pid,
|
||
sample_id=new_sample.id,
|
||
sample_code=ss.sample_code or None,
|
||
transport_type=src_t.transport_type,
|
||
express_company_id=src_t.express_company_id,
|
||
waybill_no=src_t.waybill_no,
|
||
dest_site_id=src_t.dest_site_id,
|
||
express_fee=src_t.express_fee,
|
||
eta_time=src_t.eta_time,
|
||
carrier_name=src_t.carrier_name,
|
||
carrier_phone=src_t.carrier_phone,
|
||
tenant_no=tenant_no,
|
||
order_num=idx,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(new_t)
|
||
|
||
# 更新捐赠单状态为 zckc
|
||
donation.donation_status = "zckc"
|
||
donation.to_storage_time = now
|
||
donation.to_storage_desc = f"已转细胞存储 {protocol_no}"
|
||
donation.update_by = user_id
|
||
donation.update_time = now
|
||
db.add(donation)
|
||
|
||
upsert_convert_link_for_to_storage(
|
||
db,
|
||
protocol_id=pid,
|
||
research_donation_id=donation_id,
|
||
biz_no=protocol_no,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
now=now,
|
||
)
|
||
|
||
db.commit()
|
||
db.refresh(donation)
|
||
return donation
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Internal: sub-resource create helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
_PARTY_STR_FIELDS = [
|
||
"customer_id", "father_customer_id", "newborn_customer_id",
|
||
"mother_name", "mother_phone", "mother_id_card",
|
||
"mother_birth_ym", "mother_ethnic", "mother_email", "mother_address",
|
||
"father_name", "father_phone", "father_id_card",
|
||
"newborn_name", "newborn_id_card", "newborn_sex",
|
||
"delivery_mode", "collector_user_id",
|
||
]
|
||
_PARTY_INT_FIELDS = ["birth_order", "newborn_weight_gram"]
|
||
_PARTY_DEC_FIELDS = ["apgar_1min", "apgar_5min"]
|
||
_PARTY_DATE_FIELDS = ["expected_delivery_date"]
|
||
_PARTY_DT_FIELDS = ["newborn_delivery_time"]
|
||
|
||
|
||
def _create_party(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
raw: dict,
|
||
now: datetime,
|
||
) -> BizResearchDonationParty:
|
||
obj = BizResearchDonationParty(
|
||
id=generate_uuid(),
|
||
research_donation_id=donation_id,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=(raw.get("remark") or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
for k in _PARTY_STR_FIELDS:
|
||
if k in raw:
|
||
setattr(obj, k, (raw[k] or "").strip() or None)
|
||
for k in _PARTY_INT_FIELDS:
|
||
if k in raw:
|
||
setattr(obj, k, raw[k])
|
||
for k in _PARTY_DEC_FIELDS:
|
||
if k in raw:
|
||
setattr(obj, k, _optional_decimal(raw[k]))
|
||
for k in _PARTY_DATE_FIELDS:
|
||
if k in raw:
|
||
setattr(obj, k, optional_date(raw[k]))
|
||
for k in _PARTY_DT_FIELDS:
|
||
if k in raw:
|
||
val = raw[k]
|
||
if isinstance(val, str):
|
||
val = parse_datetime(val)
|
||
setattr(obj, k, val)
|
||
db.add(obj)
|
||
db.flush()
|
||
return obj
|
||
|
||
|
||
_COLL_STR_FIELDS = ["region_id", "hospital_id", "medical_staff", "sales_user_id"]
|
||
_COLL_DT_FIELDS = ["sample_collect_time", "maternal_blood_collect_time"]
|
||
|
||
|
||
def _create_collection(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
raw: dict,
|
||
now: datetime,
|
||
) -> BizResearchDonationCollection:
|
||
obj = BizResearchDonationCollection(
|
||
id=generate_uuid(),
|
||
research_donation_id=donation_id,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=(raw.get("remark") or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
for k in _COLL_STR_FIELDS:
|
||
if k in raw:
|
||
setattr(obj, k, (raw[k] or "").strip() or None)
|
||
for k in _COLL_DT_FIELDS:
|
||
if k in raw:
|
||
val = raw[k]
|
||
if isinstance(val, str):
|
||
val = parse_datetime(val)
|
||
setattr(obj, k, val)
|
||
db.add(obj)
|
||
db.flush()
|
||
return obj
|
||
|
||
|
||
_SAMPLE_STR_FIELDS = [
|
||
"sample_type_id", "sample_code_id", "sample_code", "unit_id",
|
||
"sample_status", "storage_in_date", "detect_start_date",
|
||
"void_date", "storage_expire_date",
|
||
]
|
||
_SAMPLE_DEC_FIELDS = ["collect_amount"]
|
||
_SAMPLE_INT_FIELDS = ["storage_years"]
|
||
|
||
|
||
def _migrate_donation_sample_child_refs_on_replace(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
old_id_to_code: Dict[str, str],
|
||
new_code_to_id: Dict[str, str],
|
||
user_id: Optional[str],
|
||
) -> None:
|
||
"""捐赠样本行重建后,将接收/质检工单的捐赠样本 ID 迁移到新样本 ID。"""
|
||
if not old_id_to_code or not new_code_to_id:
|
||
return
|
||
old_ids = list(old_id_to_code.keys())
|
||
now = datetime.now()
|
||
rec_rows = (
|
||
db.query(BizSampleReception)
|
||
.filter(
|
||
BizSampleReception.donation_sample_id.in_(old_ids),
|
||
BizSampleReception.tenant_no == tenant_no,
|
||
BizSampleReception.del_flag == 0,
|
||
BizSampleReception.reception_source == "research_donation",
|
||
)
|
||
.all()
|
||
)
|
||
for row in rec_rows:
|
||
old_sid = (row.donation_sample_id or "").strip()
|
||
code = old_id_to_code.get(old_sid) or (row.sample_code or "").strip()
|
||
new_sid = new_code_to_id.get(code) if code else None
|
||
if not new_sid:
|
||
continue
|
||
row.donation_sample_id = new_sid
|
||
row.update_by = user_id
|
||
row.update_time = now
|
||
db.add(row)
|
||
|
||
wo_rows = (
|
||
db.query(BizQcWorkOrder)
|
||
.filter(
|
||
BizQcWorkOrder.protocol_sample_id.in_(old_ids),
|
||
BizQcWorkOrder.tenant_no == tenant_no,
|
||
BizQcWorkOrder.del_flag == 0,
|
||
BizQcWorkOrder.reception_source == "research_donation",
|
||
)
|
||
.all()
|
||
)
|
||
for row in wo_rows:
|
||
old_sid = (row.protocol_sample_id or "").strip()
|
||
code = old_id_to_code.get(old_sid) or (row.sample_code or "").strip()
|
||
new_sid = new_code_to_id.get(code) if code else None
|
||
if not new_sid:
|
||
continue
|
||
row.protocol_sample_id = new_sid
|
||
row.update_by = user_id
|
||
row.update_time = now
|
||
db.add(row)
|
||
|
||
|
||
def _create_samples(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
samples: list,
|
||
now: datetime,
|
||
) -> List[BizResearchDonationSample]:
|
||
validate_sample_codes_not_used(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
samples=samples,
|
||
exclude_donation_id=donation_id,
|
||
)
|
||
result = []
|
||
assigned_codes: List[str] = []
|
||
for idx, raw_item in enumerate(samples or []):
|
||
raw = _normalize_fields(raw_item)
|
||
validate_sample_special_raw(raw, index=idx)
|
||
code = (raw.get("sample_code") or "").strip()
|
||
if not code and (raw.get("sample_type_id") or "").strip():
|
||
code = next_donation_sample_code(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
extra_codes=assigned_codes,
|
||
)
|
||
raw["sample_code"] = code
|
||
if code:
|
||
assigned_codes.append(code)
|
||
obj = BizResearchDonationSample(
|
||
id=generate_uuid(),
|
||
research_donation_id=donation_id,
|
||
tenant_no=tenant_no,
|
||
order_num=idx,
|
||
remark=(raw.get("remark") or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
for k in _SAMPLE_STR_FIELDS:
|
||
if k == "sample_status":
|
||
continue
|
||
if k in raw:
|
||
setattr(obj, k, (raw[k] or "").strip() or None)
|
||
if "sample_status" in raw:
|
||
status_val = raw.get("sample_status")
|
||
obj.sample_status = (
|
||
str(status_val).strip()
|
||
if status_val is not None and str(status_val).strip()
|
||
else None
|
||
)
|
||
for k in _SAMPLE_DEC_FIELDS:
|
||
if k in raw and raw[k] is not None:
|
||
setattr(obj, k, Decimal(str(raw[k])))
|
||
for k in _SAMPLE_INT_FIELDS:
|
||
if k in raw:
|
||
setattr(obj, k, raw[k])
|
||
apply_sample_special_fields(obj, raw)
|
||
db.add(obj)
|
||
db.flush()
|
||
result.append(obj)
|
||
|
||
# 本阶段不再写入旧运输明细;运费由 apply_freight_on_protocol_save 挂钩
|
||
|
||
return result
|
||
|
||
|
||
def _create_transport(
|
||
db: Session,
|
||
donation_id: str,
|
||
sample_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
raw: dict,
|
||
order_num: int,
|
||
now: datetime,
|
||
sample_code: Optional[str] = None,
|
||
) -> Optional[BizResearchDonationSampleTransport]:
|
||
tnorm = _normalize_fields(raw)
|
||
transport_type = (tnorm.get("transport_type") or "").strip()
|
||
if not transport_type:
|
||
return None
|
||
|
||
eta_time = tnorm.get("eta_time")
|
||
if isinstance(eta_time, str):
|
||
eta_time = parse_datetime(eta_time)
|
||
|
||
express_fee = tnorm.get("express_fee")
|
||
if express_fee is not None and express_fee != "":
|
||
express_fee = Decimal(str(express_fee))
|
||
else:
|
||
express_fee = None
|
||
|
||
obj = BizResearchDonationSampleTransport(
|
||
id=generate_uuid(),
|
||
research_donation_id=donation_id,
|
||
sample_id=sample_id,
|
||
sample_code=sample_code or None,
|
||
transport_type=transport_type,
|
||
express_company_id=(tnorm.get("express_company_id") or "").strip() or None,
|
||
waybill_no=(tnorm.get("waybill_no") or "").strip() or None,
|
||
dest_site_id=(tnorm.get("dest_site_id") or "").strip() or None,
|
||
express_fee=express_fee,
|
||
eta_time=eta_time,
|
||
carrier_name=(tnorm.get("carrier_name") or "").strip() or None,
|
||
carrier_phone=(tnorm.get("carrier_phone") or "").strip() or None,
|
||
tenant_no=tenant_no,
|
||
order_num=order_num,
|
||
remark=(tnorm.get("remark") or "").strip() or None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.flush()
|
||
return obj
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Internal: soft delete helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _soft_delete_party(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
) -> None:
|
||
rows = (
|
||
db.query(BizResearchDonationParty)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonationParty.research_donation_id == donation_id,
|
||
BizResearchDonationParty.tenant_no == tenant_no,
|
||
BizResearchDonationParty.del_flag == 0,
|
||
)
|
||
)
|
||
.all()
|
||
)
|
||
for r in rows:
|
||
r.del_flag = 1
|
||
r.update_by = user_id
|
||
r.update_time = datetime.now()
|
||
db.add(r)
|
||
if rows:
|
||
db.flush()
|
||
|
||
|
||
def _soft_delete_collection(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
) -> None:
|
||
rows = (
|
||
db.query(BizResearchDonationCollection)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonationCollection.research_donation_id == donation_id,
|
||
BizResearchDonationCollection.tenant_no == tenant_no,
|
||
BizResearchDonationCollection.del_flag == 0,
|
||
)
|
||
)
|
||
.all()
|
||
)
|
||
for r in rows:
|
||
r.del_flag = 1
|
||
r.update_by = user_id
|
||
r.update_time = datetime.now()
|
||
db.add(r)
|
||
if rows:
|
||
db.flush()
|
||
|
||
|
||
def _soft_delete_samples(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
) -> None:
|
||
rows = (
|
||
db.query(BizResearchDonationSample)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonationSample.research_donation_id == donation_id,
|
||
BizResearchDonationSample.tenant_no == tenant_no,
|
||
BizResearchDonationSample.del_flag == 0,
|
||
)
|
||
)
|
||
.all()
|
||
)
|
||
for r in rows:
|
||
r.del_flag = 1
|
||
r.update_by = user_id
|
||
r.update_time = datetime.now()
|
||
db.add(r)
|
||
if rows:
|
||
db.flush()
|
||
|
||
|
||
def _soft_delete_transports(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
) -> None:
|
||
rows = (
|
||
db.query(BizResearchDonationSampleTransport)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonationSampleTransport.research_donation_id == donation_id,
|
||
BizResearchDonationSampleTransport.tenant_no == tenant_no,
|
||
BizResearchDonationSampleTransport.del_flag == 0,
|
||
)
|
||
)
|
||
.all()
|
||
)
|
||
for r in rows:
|
||
r.del_flag = 1
|
||
r.update_by = user_id
|
||
r.update_time = datetime.now()
|
||
db.add(r)
|
||
if rows:
|
||
db.flush()
|
||
|
||
|
||
def _soft_delete_all(
|
||
db: Session,
|
||
donation_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
) -> None:
|
||
"""软删除主记录 + 所有子资源。"""
|
||
now = datetime.now()
|
||
# 主记录
|
||
main = (
|
||
db.query(BizResearchDonation)
|
||
.filter(
|
||
and_(
|
||
BizResearchDonation.id == donation_id,
|
||
BizResearchDonation.tenant_no == tenant_no,
|
||
BizResearchDonation.del_flag == 0,
|
||
)
|
||
)
|
||
.first()
|
||
)
|
||
if main:
|
||
main.del_flag = 1
|
||
main.update_by = user_id
|
||
main.update_time = now
|
||
db.add(main)
|
||
|
||
_soft_delete_party(db, donation_id, tenant_no, user_id)
|
||
_soft_delete_collection(db, donation_id, tenant_no, user_id)
|
||
_soft_delete_samples(db, donation_id, tenant_no, user_id)
|
||
_soft_delete_transports(db, donation_id, tenant_no, user_id)
|