Files
zkxw/backend/app/crud/protocol_sample_collection.py
T
zlwuandCursor db31990ebe feat: 细胞存储缴费催缴、统一样本信息与协议支付能力增强
汇总近期业务改动:催缴菜单与后端、样本信息快捷登记与进度、运费关联、客户档案扩展、协议优惠/期次与押金划转,并同步 SQL 迁移与管理端页面收敛。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 21:20:37 +08:00

419 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""采集协议样本采集信息列表 CRUD(样本信息管理 · 样本采集信息 Tab)"""
from __future__ import annotations
import math
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, or_
from sqlalchemy.orm import Session
from app.core.utils import format_date
from app.utils.sample_special import sample_special_to_dict
from app.models.models import (
BizCellSampleType,
BizCollectionHospital,
BizCollectionProtocol,
BizCollectionRegion,
BizProtocolCollectionInfo,
BizProtocolPartyCustomer,
BizProtocolSample,
)
def total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
return int(math.ceil(total / float(page_size)))
def _fmt_dt(t: Optional[datetime]) -> str:
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
def _sample_status_label(status: str) -> str:
return {
"ycj": "已采集",
"jcz": "检测中",
"yrk": "已入库",
"yzf": "已作废",
}.get(status, "未采集" if not status else status)
def _row_to_dict(
*,
protocol_sample_id: str,
collection_protocol_id: str,
protocol_no: str,
protocol_status: str,
expected_delivery_date: str,
mother_name: str,
newborn_name: str,
sample_code: str,
sample_type_name: str,
sample_status: str,
sample_status_label: str,
region_name: str,
hospital_name: str,
medical_staff: str,
sample_collect_time: str,
maternal_blood_collect_time: str,
) -> Dict[str, Any]:
return {
"protocolSampleId": protocol_sample_id,
"collectionProtocolId": collection_protocol_id,
"protocolNo": protocol_no,
"protocolStatus": protocol_status,
"expectedDeliveryDate": expected_delivery_date,
"motherName": mother_name,
"newbornName": newborn_name,
"sampleCode": sample_code,
"sampleTypeName": sample_type_name,
"sampleStatus": sample_status,
"sampleStatusLabel": sample_status_label,
"regionName": region_name,
"hospitalName": hospital_name,
"medicalStaff": medical_staff,
"sampleCollectTime": sample_collect_time,
"maternalBloodCollectTime": maternal_blood_collect_time,
}
def _base_query(db: Session, tenant_no: str):
return (
db.query(
BizProtocolSample,
BizCollectionProtocol,
BizProtocolPartyCustomer,
BizProtocolCollectionInfo,
)
.join(
BizCollectionProtocol,
and_(
BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
BizCollectionProtocol.del_flag == 0,
),
)
.outerjoin(
BizProtocolPartyCustomer,
and_(
BizProtocolPartyCustomer.collection_protocol_id == BizCollectionProtocol.id,
BizProtocolPartyCustomer.del_flag == 0,
),
)
.outerjoin(
BizProtocolCollectionInfo,
and_(
BizProtocolCollectionInfo.collection_protocol_id == BizCollectionProtocol.id,
BizProtocolCollectionInfo.del_flag == 0,
),
)
.filter(
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.del_flag == 0,
)
)
def _apply_sample_status_filter(q, sample_status: Optional[str]):
st = (sample_status or "").strip()
if not st or st == "all":
return q
if st == "uncollected":
return q.filter(
or_(
BizProtocolSample.sample_status.is_(None),
BizProtocolSample.sample_status == "",
)
)
if st == "collected":
return q.filter(BizProtocolSample.sample_status.in_(["ycj", "jcz", "yrk"]))
if st == "voided":
return q.filter(BizProtocolSample.sample_status == "yzf")
return q.filter(BizProtocolSample.sample_status == st)
def _apply_reception_flag_filter(q, flag: Optional[str], *, sample_code_col, tenant_no: str, source: str):
from sqlalchemy import exists
from app.models.models import BizSampleReception
f = (flag or "").strip()
if not f:
return q
base = and_(
BizSampleReception.tenant_no == tenant_no,
BizSampleReception.del_flag == 0,
BizSampleReception.reception_source == source,
BizSampleReception.sample_code == sample_code_col,
)
if f == "js":
return q.filter(exists().where(and_(base, BizSampleReception.accept_conclusion == "js")))
if f == "bj":
return q.filter(exists().where(and_(base, BizSampleReception.accept_conclusion == "bj")))
if f == "none":
return q.filter(
~exists().where(and_(base, BizSampleReception.accept_conclusion.in_(["js", "bj"])))
)
return q
def _apply_qc_flag_filter(q, flag: Optional[str], *, sample_code_col, tenant_no: str, source: str):
from sqlalchemy import exists
from app.models.models import BizQcWorkOrder
f = (flag or "").strip()
if not f:
return q
base = and_(
BizQcWorkOrder.tenant_no == tenant_no,
BizQcWorkOrder.del_flag == 0,
BizQcWorkOrder.reception_source == source,
BizQcWorkOrder.trigger_source == "reception",
BizQcWorkOrder.sample_code == sample_code_col,
)
if f == "complete":
return q.filter(exists().where(and_(base, BizQcWorkOrder.conclusion.in_(["tg", "btg"]))))
if f == "voided":
return q.filter(exists().where(and_(base, BizQcWorkOrder.work_order_status == "yzf")))
if f == "incomplete":
return q.filter(
~exists().where(
and_(
base,
or_(
BizQcWorkOrder.conclusion.in_(["tg", "btg"]),
BizQcWorkOrder.work_order_status == "yzf",
),
)
)
)
return q
def _apply_storage_flag_filter(q, flag: Optional[str], *, sample_code_col, tenant_no: str, source: str):
from sqlalchemy import exists
from app.models.models import BizStorageReleaseReview
f = (flag or "").strip()
if not f:
return q
base = and_(
BizStorageReleaseReview.tenant_no == tenant_no,
BizStorageReleaseReview.del_flag == 0,
BizStorageReleaseReview.reception_source == source,
BizStorageReleaseReview.sample_code == sample_code_col,
)
if f == "yrk":
return q.filter(exists().where(and_(base, BizStorageReleaseReview.storage_status == "yrk")))
if f == "rejected":
return q.filter(
exists().where(
and_(
base,
BizStorageReleaseReview.storage_status != "yrk",
BizStorageReleaseReview.review_conclusion == "btg",
)
)
)
if f == "none":
return q.filter(
~exists().where(
and_(
base,
or_(
BizStorageReleaseReview.storage_status == "yrk",
BizStorageReleaseReview.review_conclusion == "btg",
),
)
)
)
return q
def list_protocol_sample_collections(
db: Session,
*,
page: int = 1,
page_size: int = 10,
tenant_no: str,
keyword: Optional[str] = None,
sample_status: Optional[str] = None,
collection_flag: Optional[str] = None,
reception_flag: Optional[str] = None,
qc_flag: Optional[str] = None,
storage_flag: Optional[str] = None,
storage_cert_handled: Optional[str] = None,
insurance_handled: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
q = _base_query(db, tenant_no)
q = _apply_sample_status_filter(q, collection_flag or sample_status)
q = _apply_reception_flag_filter(
q, reception_flag, sample_code_col=BizProtocolSample.sample_code, tenant_no=tenant_no, source="cell_storage"
)
q = _apply_qc_flag_filter(
q, qc_flag, sample_code_col=BizProtocolSample.sample_code, tenant_no=tenant_no, source="cell_storage"
)
q = _apply_storage_flag_filter(
q, storage_flag, sample_code_col=BizProtocolSample.sample_code, tenant_no=tenant_no, source="cell_storage"
)
from app.models.models import BizProtocolFullmoonTodo, BizProtocolStorageCert
sch = (storage_cert_handled or "").strip()
if sch in ("1", "0", "true", "false"):
want = sch in ("1", "true")
handled_pred = or_(
BizProtocolStorageCert.handle_time.isnot(None),
BizProtocolStorageCert.make_status == "1",
and_(BizProtocolStorageCert.make_date.isnot(None), BizProtocolStorageCert.make_date != ""),
)
q = q.outerjoin(
BizProtocolStorageCert,
and_(
BizProtocolStorageCert.collection_protocol_id == BizCollectionProtocol.id,
BizProtocolStorageCert.tenant_no == tenant_no,
BizProtocolStorageCert.del_flag == 0,
),
)
q = q.filter(handled_pred) if want else q.filter(~handled_pred)
ih = (insurance_handled or "").strip()
if ih in ("1", "0", "true", "false"):
want = ih in ("1", "true")
handled_pred = or_(
BizProtocolFullmoonTodo.handle_time.isnot(None),
BizProtocolFullmoonTodo.todo_status == "1",
and_(BizProtocolFullmoonTodo.handle_date.isnot(None), BizProtocolFullmoonTodo.handle_date != ""),
)
q = q.outerjoin(
BizProtocolFullmoonTodo,
and_(
BizProtocolFullmoonTodo.collection_protocol_id == BizCollectionProtocol.id,
BizProtocolFullmoonTodo.tenant_no == tenant_no,
BizProtocolFullmoonTodo.del_flag == 0,
),
)
q = q.filter(handled_pred) if want else q.filter(~handled_pred)
if keyword and keyword.strip():
kw = f"%{keyword.strip()}%"
kw_conds = [
BizProtocolPartyCustomer.mother_name.like(kw),
BizProtocolSample.sample_code.like(kw),
]
from app.crud import sample_reception as reception_svc
lab_codes = reception_svc.sample_codes_matching_lab_storage_keyword(
db, tenant_no, keyword, reception_source="cell_storage"
)
if lab_codes:
kw_conds.append(BizProtocolSample.sample_code.in_(lab_codes))
q = q.filter(or_(*kw_conds))
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(q, BizProtocolSample, user_info, db, tenant_no)
total = q.count()
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q.order_by(
BizProtocolPartyCustomer.expected_delivery_date.is_(None),
BizProtocolPartyCustomer.expected_delivery_date.desc(),
BizCollectionProtocol.create_time.desc(),
BizProtocolSample.id.desc(),
)
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
sample_type_ids = list({
ps.sample_type_id for ps, _, _, _ in rows if ps and ps.sample_type_id
})
type_map: Dict[str, str] = {}
if sample_type_ids:
for t in (
db.query(BizCellSampleType)
.filter(BizCellSampleType.id.in_(sample_type_ids), BizCellSampleType.del_flag == 0)
.all()
):
type_map[t.id] = t.type_name or ""
region_ids = list({
(ci.region_id or "").strip()
for _, _, _, ci in rows
if ci and (ci.region_id or "").strip()
})
hospital_ids = list({
(ci.hospital_id or "").strip()
for _, _, _, ci in rows
if ci and (ci.hospital_id or "").strip()
})
region_name_map: Dict[str, str] = {}
hospital_name_map: Dict[str, str] = {}
if region_ids:
for r in (
db.query(BizCollectionRegion.id, BizCollectionRegion.region_name)
.filter(BizCollectionRegion.id.in_(region_ids), BizCollectionRegion.del_flag == 0)
.all()
):
region_name_map[r.id] = r.region_name or ""
if hospital_ids:
for h in (
db.query(BizCollectionHospital.id, BizCollectionHospital.hospital_name)
.filter(BizCollectionHospital.id.in_(hospital_ids), BizCollectionHospital.del_flag == 0)
.all()
):
hospital_name_map[h.id] = h.hospital_name or ""
items: List[Dict[str, Any]] = []
for psample, proto, party, ci in rows:
status = (psample.sample_status or "").strip() if psample else ""
region_id = (ci.region_id or "").strip() if ci else ""
hospital_id = (ci.hospital_id or "").strip() if ci else ""
item = _row_to_dict(
protocol_sample_id=psample.id,
collection_protocol_id=proto.id if proto else "",
protocol_no=(proto.protocol_no or "") if proto else "",
protocol_status=(proto.protocol_status or "") if proto else "",
expected_delivery_date=format_date(party.expected_delivery_date) if party else "",
mother_name=(party.mother_name or "") if party else "",
newborn_name=(party.newborn_name or "") if party else "",
sample_code=psample.sample_code or "",
sample_type_name=type_map.get(psample.sample_type_id or "", ""),
sample_status=status,
sample_status_label=_sample_status_label(status),
region_name=region_name_map.get(region_id, ""),
hospital_name=hospital_name_map.get(hospital_id, ""),
medical_staff=(ci.medical_staff or "") if ci else "",
sample_collect_time=_fmt_dt(ci.sample_collect_time) if ci else "",
maternal_blood_collect_time=_fmt_dt(ci.maternal_blood_collect_time) if ci else "",
)
item.update(sample_special_to_dict(psample))
items.append(item)
if items and tenant_no:
from app.crud import sample_reception as reception_svc
codes = [i["sampleCode"] for i in items if i.get("sampleCode")]
code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
db, tenant_no, codes, reception_source="cell_storage"
)
for item in items:
sc = (item.get("sampleCode") or "").strip()
item["labStorageCode"] = code_map.get(sc, "") if sc else ""
from app.services.sample_info_list_enrich import enrich_sample_info_items
enrich_sample_info_items(
db, tenant_no, items, reception_source="cell_storage", include_handle_flags=True
)
return items, total