Files
shanxia2.0/backend/app/crud/collection_protocol.py
T
2026-05-17 12:31:11 +08:00

1978 lines
66 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(细胞存储 2.2.6~2.2.10)"""
from __future__ import annotations
import math
from datetime import datetime, timedelta
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
from app.crud.base import camel_to_snake
from app.models.models import (
BizAttachment,
BizCollectionProtocol,
BizProtocolAudit,
BizProtocolCollectionInfo,
BizProtocolPartyCustomer,
BizProtocolPayment,
BizProtocolPaymentDetail,
BizProtocolSample,
BizProtocolSampleTransport,
BizProtocolStorageCert,
BizCollectionRegion,
BizCollectionHospital,
SysUser,
)
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 _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))
def _today_yyyymmdd() -> str:
return datetime.now().strftime("%Y%m%d")
def _normalize_yyyymmdd_date(val: Any) -> Optional[str]:
"""将 YYYYMMDD / YYYY-MM-DD 等格式规范为 8 位日期字符串。"""
if val is None:
return None
s = str(val).strip()
if not s:
return None
compact = s.replace("-", "").replace("/", "")[:8]
if len(compact) == 8 and compact.isdigit():
return compact
return None
_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 attachments_to_urls(attachments: Any) -> Optional[str]:
"""附件列表或字符串 → 逗号分隔 URL(MinIO)。"""
if attachments is None:
return None
if isinstance(attachments, str):
return attachments.strip() or None
if isinstance(attachments, list):
urls: List[str] = []
for item in attachments:
if isinstance(item, dict):
u = (item.get("fileUrl") or item.get("file_url") or "").strip()
if u:
urls.append(u)
elif isinstance(item, str) and item.strip():
urls.append(item.strip())
return ",".join(urls) if urls else None
return None
def _normalize_attachment_url_key(url: str) -> str:
u = (url or "").strip()
if not u:
return ""
if "?" in u:
u = u.split("?", 1)[0]
return u.lstrip("/")
def _format_attachment_file_size(size_bytes: int) -> str:
if size_bytes < 1024:
return f"{size_bytes} B"
if size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
return f"{size_bytes / (1024 * 1024):.1f} MB"
def _lookup_attachment_by_url(
db: Session,
url: str,
tenant_no: Optional[str],
) -> Optional[BizAttachment]:
key = _normalize_attachment_url_key(url)
if not key:
return None
q = db.query(BizAttachment).filter(BizAttachment.del_flag == 0)
if tenant_no:
q = q.filter(BizAttachment.tenant_no == tenant_no)
row = q.filter(BizAttachment.file_url == key).first()
if row:
return row
tail = key.rsplit("/", 1)[-1]
if tail:
row = (
q.filter(BizAttachment.file_url.like(f"%/{tail}"))
.order_by(BizAttachment.create_time.desc())
.first()
)
return row
def urls_to_attachment_list(
urls_str: Optional[str],
db: Optional[Session] = None,
tenant_no: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""逗号分隔 URL → 前端附件卡片结构;有 db 时从 biz_attachment 取原始文件名。"""
if not urls_str:
return []
result: List[Dict[str, Any]] = []
for url in urls_str.split(","):
u = url.strip()
if not u:
continue
att = _lookup_attachment_by_url(db, u, tenant_no) if db else None
if att:
size = att.file_size or 0
result.append(
{
"id": att.id,
"fileUrl": u,
"fileName": att.original_name or u.rsplit("/", 1)[-1],
"fileSize": size,
"fileSizeDisplay": _format_attachment_file_size(size),
"fileType": att.file_type or "",
}
)
else:
name = u.rsplit("/", 1)[-1] if "/" in u else u
result.append({"fileUrl": u, "fileName": name, "fileSize": 0})
return result
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 _protocol_to_dict(p: BizCollectionProtocol) -> Dict[str, Any]:
return {
"id": p.id,
"protocolNoId": p.protocol_no_id or "",
"protocolNo": p.protocol_no or "",
"protocolStatus": p.protocol_status or "",
"protocolAttachments": p.protocol_attachments or "",
"registerDate": (p.register_date or "").strip() if p.register_date else "",
"effectiveTime": _fmt_dt(p.effective_time),
"effectiveDesc": p.effective_desc or "",
"expireTime": _fmt_dt(p.expire_time),
"expireDesc": p.expire_desc or "",
"voidTime": _fmt_dt(p.void_time),
"voidDesc": p.void_desc or "",
"toResearchTime": _fmt_dt(p.to_research_time),
"toResearchDesc": p.to_research_desc or "",
"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 _payment_to_dict(p: BizProtocolPayment) -> Dict[str, Any]:
return {
"id": p.id,
"collectionProtocolId": p.collection_protocol_id,
"payMode": p.pay_mode or "",
"contractAmount": _dec(p.contract_amount),
"installmentCount": p.installment_count,
"installmentIntervalMonths": p.installment_interval_months,
"lumpSumDeadline": (p.lump_sum_deadline or "").strip() if p.lump_sum_deadline else "",
"installmentStartDate": (p.installment_start_date or "").strip() if p.installment_start_date else "",
"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 _detail_to_dict(
d: BizProtocolPaymentDetail,
*,
protocol_no: str = "",
collection_protocol_id: str = "",
protocol_status: str = "",
pay_mode: str = "",
mother_name: str = "",
mother_id_card: str = "",
newborn_name: str = "",
newborn_id_card: str = "",
) -> Dict[str, Any]:
return {
"id": d.id,
"paymentId": d.payment_id,
"collectionProtocolId": collection_protocol_id,
"protocolNo": protocol_no,
"protocolStatus": protocol_status,
"payMode": pay_mode,
"motherName": mother_name,
"motherIdCard": mother_id_card,
"newbornName": newborn_name,
"newbornIdCard": newborn_id_card,
"transferType": d.transfer_type or "",
"dueAmount": _dec(d.due_amount),
"transferAmount": _dec(d.transfer_amount),
"transferTime": _fmt_dt(d.transfer_time),
"transferAttachments": d.transfer_attachments or "",
"financeConfirmAttachments": d.finance_confirm_attachments or "",
"financeRemark": d.finance_remark or "",
"refundFinanceConfirmAttachments": d.refund_finance_confirm_attachments or "",
"refundFinanceRemark": d.refund_finance_remark or "",
"payStatus": d.pay_status or "",
"installmentNo": d.installment_no,
"installmentDueDate": (d.installment_due_date or "").strip() if d.installment_due_date else "",
"refundAmount": _dec(d.refund_amount),
"refundRemark": d.refund_remark or "",
"refundAttachments": d.refund_attachments 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: BizProtocolAudit,
*,
protocol_no: str = "",
auditor_user_name: str = "",
) -> Dict[str, Any]:
return {
"id": a.id,
"collectionProtocolId": a.collection_protocol_id,
"protocolNo": protocol_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_customer_to_dict(p: BizProtocolPartyCustomer) -> Dict[str, Any]:
return {
"id": p.id,
"collectionProtocolId": p.collection_protocol_id,
"customerId": p.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 "",
"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 "",
"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_info_to_dict(c: BizProtocolCollectionInfo) -> Dict[str, Any]:
return {
"id": c.id,
"collectionProtocolId": c.collection_protocol_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: BizProtocolSample) -> Dict[str, Any]:
return {
"id": s.id,
"collectionProtocolId": s.collection_protocol_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 "",
"price": _dec(s.price),
"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 "",
"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: BizProtocolSampleTransport) -> Dict[str, Any]:
return {
"id": t.id,
"collectionProtocolId": t.collection_protocol_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),
}
def _storage_cert_to_dict(s: BizProtocolStorageCert) -> Dict[str, Any]:
return {
"id": s.id,
"collectionProtocolId": s.collection_protocol_id,
"makeStatus": s.make_status or "0",
"certImageUrls": s.cert_image_urls or "",
"makeDate": (s.make_date or "").strip() if s.make_date else "",
"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 list_collection_protocols(
db: Session,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
protocol_status: Optional[str] = None,
tenant_no: Optional[str] = None,
scope_collector_user_id: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q0 = db.query(BizCollectionProtocol).filter(BizCollectionProtocol.del_flag == 0)
if tenant_no:
q0 = q0.filter(BizCollectionProtocol.tenant_no == tenant_no)
if scope_collector_user_id:
q0 = q0.filter(
BizCollectionProtocol.collector_user_id == scope_collector_user_id
)
if protocol_status:
q0 = q0.filter(BizCollectionProtocol.protocol_status == protocol_status.strip())
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(
or_(
BizCollectionProtocol.protocol_no.like(like),
BizCollectionProtocol.remark.like(like),
)
)
return q0
total = _filtered().order_by(None).with_entities(func.count(BizCollectionProtocol.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(BizCollectionProtocol.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
# 批量加载子表数据,避免 N+1 查询
protocol_ids = [r.id for r in rows]
pc_map: Dict[str, Dict[str, Any]] = {}
ci_map: Dict[str, Dict[str, Any]] = {}
if protocol_ids:
pc_rows = (
db.query(BizProtocolPartyCustomer)
.filter(
BizProtocolPartyCustomer.collection_protocol_id.in_(protocol_ids),
BizProtocolPartyCustomer.del_flag == 0,
)
.all()
)
for pc in pc_rows:
pc_map[pc.collection_protocol_id] = _party_customer_to_dict(pc)
ci_rows = (
db.query(BizProtocolCollectionInfo)
.filter(
BizProtocolCollectionInfo.collection_protocol_id.in_(protocol_ids),
BizProtocolCollectionInfo.del_flag == 0,
)
.all()
)
for ci in ci_rows:
ci_map[ci.collection_protocol_id] = _collection_info_to_dict(ci)
# 批量解析地区/医院名称
region_ids = list({ci_map.get(pid, {}).get("regionId", "") for pid in protocol_ids} - {""})
hospital_ids = list({ci_map.get(pid, {}).get("hospitalId", "") for pid in protocol_ids} - {""})
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)).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)).all():
hospital_name_map[h.id] = h.hospital_name or ""
# 批量解析销售人员名称
sales_user_ids = list({ci_map.get(pid, {}).get("salesUserId", "") for pid in protocol_ids} - {""})
collector_user_ids = list({_protocol_to_dict(r).get("collectorUserId", "") for r in rows} - {""})
all_user_ids = list(set(sales_user_ids + collector_user_ids))
user_name_map: Dict[str, str] = {}
if all_user_ids:
for u in db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(all_user_ids)).all():
user_name_map[u.id] = u.nick_name or ""
# 合并主表 + 子表数据
result = []
for r in rows:
d = _protocol_to_dict(r)
pc = pc_map.get(r.id)
ci = ci_map.get(r.id)
d["collectorUserName"] = user_name_map.get(d.get("collectorUserId", ""), "")
if pc:
d["motherName"] = pc.get("motherName", "")
d["motherPhone"] = pc.get("motherPhone", "")
d["motherIdCard"] = pc.get("motherIdCard", "")
d["motherBirthYm"] = pc.get("motherBirthYm", "")
d["motherEthnic"] = pc.get("motherEthnic", "")
d["fatherName"] = pc.get("fatherName", "")
d["fatherPhone"] = pc.get("fatherPhone", "")
d["newbornDeliveryTime"] = pc.get("newbornDeliveryTime", "")
d["newbornSex"] = pc.get("newbornSex", "")
d["deliveryMode"] = pc.get("deliveryMode", "")
d["birthOrder"] = pc.get("birthOrder")
d["newbornWeightGram"] = pc.get("newbornWeightGram")
else:
d["motherName"] = ""
d["motherPhone"] = ""
d["motherIdCard"] = ""
d["motherBirthYm"] = ""
d["motherEthnic"] = ""
d["fatherName"] = ""
d["fatherPhone"] = ""
d["newbornDeliveryTime"] = ""
d["newbornSex"] = ""
d["deliveryMode"] = ""
d["birthOrder"] = None
d["newbornWeightGram"] = None
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"] = ""
result.append(d)
return result, int(total)
def get_collection_protocol(
db: Session,
protocol_id: str,
tenant_no: str,
*,
scope_collector_user_id: Optional[str] = None,
) -> Optional[BizCollectionProtocol]:
filters = [
BizCollectionProtocol.id == protocol_id,
BizCollectionProtocol.del_flag == 0,
BizCollectionProtocol.tenant_no == tenant_no,
]
if scope_collector_user_id:
filters.append(
BizCollectionProtocol.collector_user_id == scope_collector_user_id
)
return db.query(BizCollectionProtocol).filter(and_(*filters)).first()
def get_payment_by_protocol_id(db: Session, collection_protocol_id: str, tenant_no: str) -> Optional[BizProtocolPayment]:
return (
db.query(BizProtocolPayment)
.filter(
and_(
BizProtocolPayment.collection_protocol_id == collection_protocol_id,
BizProtocolPayment.tenant_no == tenant_no,
BizProtocolPayment.del_flag == 0,
)
)
.first()
)
def get_protocol_detail_bundle(
db: Session,
protocol_id: str,
tenant_no: str,
*,
scope_collector_user_id: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
p = get_collection_protocol(
db, protocol_id, tenant_no, scope_collector_user_id=scope_collector_user_id
)
if not p:
return None
pay = get_payment_by_protocol_id(db, protocol_id, tenant_no)
pc = get_party_customer(db, protocol_id, tenant_no)
ci = get_collection_info(db, protocol_id, tenant_no)
s_rows = list_samples(db, protocol_id, tenant_no)
sc = get_storage_cert(db, protocol_id, tenant_no)
protocol_dict = _protocol_to_dict(p)
ci_dict = _collection_info_to_dict(ci) if ci else None
user_ids: List[str] = []
if protocol_dict.get("collectorUserId"):
user_ids.append(str(protocol_dict["collectorUserId"]))
if ci_dict and ci_dict.get("salesUserId"):
user_ids.append(str(ci_dict["salesUserId"]))
user_name_map = _user_nick_name_map(db, user_ids)
protocol_dict["collectorUserName"] = user_name_map.get(
str(protocol_dict.get("collectorUserId") or ""), ""
)
if ci_dict:
ci_dict["salesUserName"] = user_name_map.get(str(ci_dict.get("salesUserId") or ""), "")
region_id = str(ci_dict.get("regionId") or "")
if region_id:
r = (
db.query(BizCollectionRegion.region_name)
.filter(BizCollectionRegion.id == region_id)
.first()
)
ci_dict["regionName"] = r.region_name if r else ""
hospital_id = str(ci_dict.get("hospitalId") or "")
if hospital_id:
h = (
db.query(BizCollectionHospital.hospital_name)
.filter(BizCollectionHospital.id == hospital_id)
.first()
)
ci_dict["hospitalName"] = h.hospital_name if h else ""
out: Dict[str, Any] = {"protocol": protocol_dict}
out["payment"] = _payment_to_dict(pay) if pay else None
out["partyCustomer"] = _party_customer_to_dict(pc) if pc else None
out["collectionInfo"] = ci_dict
transport_rows = list_sample_transports(db, protocol_id, tenant_no)
transport_by_sample = {t.sample_id: _transport_to_dict(t) for t in transport_rows}
samples_out: List[Dict[str, Any]] = []
for s in s_rows:
item = _sample_to_dict(s)
item["transport"] = transport_by_sample.get(s.id)
samples_out.append(item)
out["samples"] = samples_out
out["storageCert"] = _storage_cert_to_dict(sc) if sc else None
out["attachments"] = urls_to_attachment_list(p.protocol_attachments, db, tenant_no)
if pay:
detail_rows = list_payment_details(db, pay.id, tenant_no)
details_out: List[Dict[str, Any]] = []
for d in detail_rows:
item = _detail_to_dict(
d,
protocol_no=p.protocol_no or "",
collection_protocol_id=protocol_id,
)
item["attachments"] = urls_to_attachment_list(d.transfer_attachments, db, tenant_no)
item["financeConfirmAttachmentFiles"] = urls_to_attachment_list(
d.finance_confirm_attachments, db, tenant_no
)
item["refundFinanceConfirmAttachmentFiles"] = urls_to_attachment_list(
d.refund_finance_confirm_attachments, db, tenant_no
)
item["refundAttachmentFiles"] = urls_to_attachment_list(
d.refund_attachments, db, tenant_no
)
details_out.append(item)
out["paymentDetails"] = details_out
else:
out["paymentDetails"] = []
return out
def list_audits(
db: Session,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
audit_status: Optional[str] = None,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
q_base = (
db.query(BizProtocolAudit, BizCollectionProtocol.protocol_no)
.join(
BizCollectionProtocol,
and_(
BizProtocolAudit.collection_protocol_id == BizCollectionProtocol.id,
BizCollectionProtocol.del_flag == 0,
),
)
.filter(BizProtocolAudit.del_flag == 0)
)
if tenant_no:
q_base = q_base.filter(BizProtocolAudit.tenant_no == tenant_no)
if audit_status:
q_base = q_base.filter(BizProtocolAudit.audit_status == audit_status.strip())
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q_base = q_base.filter(
or_(BizCollectionProtocol.protocol_no.like(like), BizProtocolAudit.audit_remark.like(like))
)
total = q_base.order_by(None).with_entities(func.count(BizProtocolAudit.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q_base.order_by(BizProtocolAudit.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: Dict[str, str] = {}
if auditor_ids:
for u in db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(auditor_ids)).all():
auditor_name_map[u.id] = u.nick_name or ""
out = [
_audit_to_dict(
a,
protocol_no=pn or "",
auditor_user_name=auditor_name_map.get(a.auditor_user_id or "", ""),
)
for a, pn in rows
]
return out, int(total)
def create_audit(
db: Session,
*,
collection_protocol_id: str,
audit_status: str,
audit_remark: Optional[str],
tenant_no: str,
user_id: Optional[str],
) -> BizProtocolAudit:
p = get_collection_protocol(db, collection_protocol_id, tenant_no)
if not p:
raise ValueError("协议单不存在")
st = (audit_status or "").strip()
if st not in ("shtg", "shth"):
raise ValueError("审核状态无效")
obj = BizProtocolAudit(
id=generate_uuid(),
collection_protocol_id=collection_protocol_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=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def _detail_query_with_protocol(
db: Session,
tenant_no: str,
pay_status_filter: str,
):
return (
db.query(BizProtocolPaymentDetail, BizProtocolPayment, BizCollectionProtocol)
.join(BizProtocolPayment, BizProtocolPaymentDetail.payment_id == BizProtocolPayment.id)
.join(BizCollectionProtocol, BizProtocolPayment.collection_protocol_id == BizCollectionProtocol.id)
.filter(
BizProtocolPaymentDetail.del_flag == 0,
BizProtocolPayment.del_flag == 0,
BizCollectionProtocol.del_flag == 0,
BizProtocolPaymentDetail.tenant_no == tenant_no,
BizProtocolPaymentDetail.pay_status == pay_status_filter,
)
)
def _rows_to_payment_detail_dicts(
db: Session,
rows: List[Any],
tenant_no: str,
*,
post_process: Optional[Any] = None,
) -> List[Dict[str, Any]]:
"""缴费明细行 → 前端字典(含协议、客户、转账附件)。"""
protocol_ids = [
pay.collection_protocol_id for _, pay, _ in rows if pay and pay.collection_protocol_id
]
pc_map = _party_customer_map_by_protocol_ids(db, protocol_ids)
out: List[Dict[str, Any]] = []
for d, pay, prot in rows:
cpid = pay.collection_protocol_id if pay else ""
pc = pc_map.get(cpid)
item = _detail_to_dict(
d,
protocol_no=(prot.protocol_no if prot else "") or "",
collection_protocol_id=cpid,
protocol_status=(prot.protocol_status if prot else "") or "",
pay_mode=(pay.pay_mode if pay else "") or "",
mother_name=(pc.mother_name if pc else "") or "",
mother_id_card=(pc.mother_id_card if pc else "") or "",
newborn_name=(pc.newborn_name if pc else "") or "",
newborn_id_card=(pc.newborn_id_card if pc else "") or "",
)
item["attachments"] = urls_to_attachment_list(d.transfer_attachments, db, tenant_no)
if post_process:
post_process(item, d)
out.append(item)
return out
def list_receipt_confirm_details(
db: Session,
*,
page: int = 1,
page_size: int = 10,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
q_base = _detail_query_with_protocol(db, tenant_no, "jfqrz")
total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q_base.order_by(BizProtocolPaymentDetail.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return _rows_to_payment_detail_dicts(db, rows, tenant_no), int(total)
def list_receipt_confirmed_details(
db: Session,
*,
page: int = 1,
page_size: int = 10,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
"""财务已确认收款(yjf),按确认时间倒序。"""
if not tenant_no:
return [], 0
q_base = _detail_query_with_protocol(db, tenant_no, "yjf")
total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q_base.order_by(BizProtocolPaymentDetail.update_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
def _add_finance_files(item: Dict[str, Any], d: BizProtocolPaymentDetail) -> None:
item["financeConfirmAttachmentFiles"] = urls_to_attachment_list(
d.finance_confirm_attachments, db, tenant_no
)
return (
_rows_to_payment_detail_dicts(db, rows, tenant_no, post_process=_add_finance_files),
int(total),
)
def confirm_receipt(
db: Session,
*,
detail_id: str,
tenant_no: str,
user_id: Optional[str],
finance_confirm_attachments: Optional[str] = None,
finance_remark: Optional[str] = None,
) -> BizProtocolPaymentDetail:
d = (
db.query(BizProtocolPaymentDetail)
.filter(
and_(
BizProtocolPaymentDetail.id == detail_id,
BizProtocolPaymentDetail.tenant_no == tenant_no,
BizProtocolPaymentDetail.del_flag == 0,
)
)
.first()
)
if not d:
raise ValueError("缴费明细不存在")
if d.pay_status != "jfqrz":
if d.pay_status == "yjf":
raise ValueError("该明细已确认收款")
raise ValueError("当前状态不可确认收款")
d.pay_status = "yjf"
if finance_confirm_attachments is not None:
d.finance_confirm_attachments = finance_confirm_attachments.strip() or None
if finance_remark is not None:
d.finance_remark = finance_remark.strip() or None
d.update_by = user_id
d.update_time = datetime.now()
db.add(d)
db.commit()
db.refresh(d)
return d
def list_refund_confirm_details(
db: Session,
*,
page: int = 1,
page_size: int = 10,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
q_base = _detail_query_with_protocol(db, tenant_no, "tfqrz")
total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q_base.order_by(BizProtocolPaymentDetail.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
def _add_refund_apply_files(item: Dict[str, Any], d: BizProtocolPaymentDetail) -> None:
item["refundAttachmentFiles"] = urls_to_attachment_list(
d.refund_attachments, db, tenant_no
)
return (
_rows_to_payment_detail_dicts(db, rows, tenant_no, post_process=_add_refund_apply_files),
int(total),
)
def list_refund_confirmed_details(
db: Session,
*,
page: int = 1,
page_size: int = 10,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
"""财务已确认退费(ytf),按确认时间倒序。"""
if not tenant_no:
return [], 0
q_base = _detail_query_with_protocol(db, tenant_no, "ytf")
total = q_base.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q_base.order_by(BizProtocolPaymentDetail.update_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
def _add_refund_confirmed_files(item: Dict[str, Any], d: BizProtocolPaymentDetail) -> None:
item["refundAttachmentFiles"] = urls_to_attachment_list(
d.refund_attachments, db, tenant_no
)
item["refundFinanceConfirmAttachmentFiles"] = urls_to_attachment_list(
d.refund_finance_confirm_attachments, db, tenant_no
)
return (
_rows_to_payment_detail_dicts(db, rows, tenant_no, post_process=_add_refund_confirmed_files),
int(total),
)
def confirm_refund(
db: Session,
*,
detail_id: str,
tenant_no: str,
user_id: Optional[str],
refund_finance_confirm_attachments: Optional[str] = None,
refund_finance_remark: Optional[str] = None,
) -> BizProtocolPaymentDetail:
d = (
db.query(BizProtocolPaymentDetail)
.filter(
and_(
BizProtocolPaymentDetail.id == detail_id,
BizProtocolPaymentDetail.tenant_no == tenant_no,
BizProtocolPaymentDetail.del_flag == 0,
)
)
.first()
)
if not d:
raise ValueError("缴费明细不存在")
if d.pay_status != "tfqrz":
if d.pay_status == "ytf":
raise ValueError("该明细已确认退费")
raise ValueError("当前状态不可确认退费")
d.pay_status = "ytf"
if refund_finance_confirm_attachments is not None:
d.refund_finance_confirm_attachments = refund_finance_confirm_attachments.strip() or None
if refund_finance_remark is not None:
d.refund_finance_remark = refund_finance_remark.strip() or None
d.update_by = user_id
d.update_time = datetime.now()
db.add(d)
db.commit()
db.refresh(d)
return d
def _get_payment_detail_or_raise(
db: Session,
*,
detail_id: str,
tenant_no: str,
) -> BizProtocolPaymentDetail:
d = (
db.query(BizProtocolPaymentDetail)
.filter(
and_(
BizProtocolPaymentDetail.id == detail_id,
BizProtocolPaymentDetail.tenant_no == tenant_no,
BizProtocolPaymentDetail.del_flag == 0,
)
)
.first()
)
if not d:
raise ValueError("缴费明细不存在")
return d
def _apply_submit_receipt_finance(
d: BizProtocolPaymentDetail,
*,
user_id: Optional[str],
) -> None:
if d.pay_status != "dfj":
raise ValueError("仅待缴费状态可提交财务确认")
d.pay_status = "jfqrz"
d.update_by = user_id
d.update_time = datetime.now()
def submit_receipt_finance(
db: Session,
*,
detail_id: str,
tenant_no: str,
user_id: Optional[str],
) -> BizProtocolPaymentDetail:
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
_apply_submit_receipt_finance(d, user_id=user_id)
db.add(d)
db.commit()
db.refresh(d)
return d
def batch_submit_receipt_finance(
db: Session,
*,
detail_ids: List[str],
tenant_no: str,
user_id: Optional[str],
) -> int:
ids = [str(i).strip() for i in (detail_ids or []) if str(i).strip()]
if not ids:
raise ValueError("请选择缴费明细")
for detail_id in ids:
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
_apply_submit_receipt_finance(d, user_id=user_id)
db.add(d)
db.commit()
return len(ids)
def _apply_submit_refund_finance(
d: BizProtocolPaymentDetail,
*,
user_id: Optional[str],
refund_amount: Any,
refund_remark: Optional[str] = None,
refund_attachments: Optional[str] = None,
) -> None:
if d.pay_status != "yjf":
raise ValueError("仅已缴费状态可提交退费财务确认")
if refund_amount is None or Decimal(str(refund_amount)) <= 0:
raise ValueError("请填写有效的退费金额")
d.pay_status = "tfqrz"
d.refund_amount = Decimal(str(refund_amount))
d.refund_remark = (refund_remark or "").strip() or None
d.refund_attachments = (refund_attachments or "").strip() or None
d.update_by = user_id
d.update_time = datetime.now()
def submit_refund_finance(
db: Session,
*,
detail_id: str,
tenant_no: str,
user_id: Optional[str],
refund_amount: Any,
refund_remark: Optional[str] = None,
refund_attachments: Optional[str] = None,
) -> BizProtocolPaymentDetail:
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
_apply_submit_refund_finance(
d,
user_id=user_id,
refund_amount=refund_amount,
refund_remark=refund_remark,
refund_attachments=refund_attachments,
)
db.add(d)
db.commit()
db.refresh(d)
return d
def batch_submit_refund_finance(
db: Session,
*,
detail_ids: List[str],
tenant_no: str,
user_id: Optional[str],
refund_amount: Any,
refund_remark: Optional[str] = None,
refund_attachments: Optional[str] = None,
) -> int:
ids = [str(i).strip() for i in (detail_ids or []) if str(i).strip()]
if not ids:
raise ValueError("请选择缴费明细")
for detail_id in ids:
d = _get_payment_detail_or_raise(db, detail_id=detail_id, tenant_no=tenant_no)
_apply_submit_refund_finance(
d,
user_id=user_id,
refund_amount=refund_amount,
refund_remark=refund_remark,
refund_attachments=refund_attachments,
)
db.add(d)
db.commit()
return len(ids)
def _party_customer_map_by_protocol_ids(
db: Session,
protocol_ids: List[str],
) -> Dict[str, BizProtocolPartyCustomer]:
if not protocol_ids:
return {}
rows = (
db.query(BizProtocolPartyCustomer)
.filter(
BizProtocolPartyCustomer.collection_protocol_id.in_(protocol_ids),
BizProtocolPartyCustomer.del_flag == 0,
)
.all()
)
return {r.collection_protocol_id: r for r in rows}
def list_payment_details_page(
db: Session,
*,
page: int = 1,
page_size: int = 10,
pay_status: Optional[str] = None,
keyword: Optional[str] = None,
transfer_type: Optional[str] = None,
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
q = (
db.query(BizProtocolPaymentDetail, BizProtocolPayment, BizCollectionProtocol)
.join(BizProtocolPayment, BizProtocolPaymentDetail.payment_id == BizProtocolPayment.id)
.join(BizCollectionProtocol, BizProtocolPayment.collection_protocol_id == BizCollectionProtocol.id)
.filter(
BizProtocolPaymentDetail.del_flag == 0,
BizProtocolPayment.del_flag == 0,
BizCollectionProtocol.del_flag == 0,
BizProtocolPaymentDetail.tenant_no == tenant_no,
)
)
if pay_status:
q = q.filter(BizProtocolPaymentDetail.pay_status == pay_status)
if keyword and keyword.strip():
like = f"%{keyword.strip()}%"
q = q.filter(BizCollectionProtocol.protocol_no.like(like))
if transfer_type and transfer_type.strip():
q = q.filter(BizProtocolPaymentDetail.transfer_type == transfer_type.strip())
total = q.order_by(None).with_entities(func.count(BizProtocolPaymentDetail.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q.order_by(BizProtocolPaymentDetail.installment_due_date.asc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
protocol_ids = [
pay.collection_protocol_id for _, pay, _ in rows if pay and pay.collection_protocol_id
]
pc_map = _party_customer_map_by_protocol_ids(db, protocol_ids)
out = []
for d, pay, prot in rows:
cpid = pay.collection_protocol_id if pay else ""
pc = pc_map.get(cpid)
out.append(
_detail_to_dict(
d,
protocol_no=(prot.protocol_no if prot else "") or "",
collection_protocol_id=cpid,
protocol_status=(prot.protocol_status if prot else "") or "",
pay_mode=(pay.pay_mode if pay else "") or "",
mother_name=(pc.mother_name if pc else "") or "",
mother_id_card=(pc.mother_id_card if pc else "") or "",
newborn_name=(pc.newborn_name if pc else "") or "",
newborn_id_card=(pc.newborn_id_card if pc else "") or "",
)
)
return out, int(total)
def _classify_installment_warning(due: str, today: str) -> str:
"""overdue | warn | upcoming — due 为 YYYYMMDD"""
if not due or len(due) != 8 or not due.isdigit():
return "unknown"
if due < today:
return "overdue"
last_warn = (datetime.strptime(today, "%Y%m%d") + timedelta(days=15)).strftime("%Y%m%d")
if today <= due <= last_warn:
return "warn"
return "upcoming"
def list_installment_warning(
db: Session,
*,
page: int = 1,
page_size: int = 10,
warning_type: str = "all",
tenant_no: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
today = _today_yyyymmdd()
q0 = _detail_query_with_protocol(db, tenant_no, "dfj").filter(
BizProtocolPaymentDetail.installment_no.isnot(None),
)
# 拉取当前租户下候选(分期号非空);日期过滤在内存中完成以保证与 classify 一致
rows_all = q0.order_by(BizProtocolPaymentDetail.installment_due_date.asc()).all()
flattened: List[Dict[str, Any]] = []
for d, pay, prot in rows_all:
due_raw = (d.installment_due_date or "").strip() if d.installment_due_date else ""
wt = _classify_installment_warning(due_raw, today)
cpid = pay.collection_protocol_id if pay else ""
item = _detail_to_dict(
d,
protocol_no=(prot.protocol_no if prot else "") or "",
collection_protocol_id=cpid,
protocol_status=(prot.protocol_status if prot else "") or "",
pay_mode=(pay.pay_mode if pay else "") or "",
)
item["warningType"] = wt
wt_filter = (warning_type or "all").strip().lower()
if wt_filter == "overdue" and wt != "overdue":
continue
if wt_filter == "warn" and wt != "warn":
continue
if wt_filter == "all":
pass
elif wt_filter not in ("all", "warn", "overdue"):
pass
flattened.append(item)
total = len(flattened)
page = max(1, page)
page_size = min(max(1, page_size), 100)
start = (page - 1) * page_size
page_rows = flattened[start : start + page_size]
return page_rows, total
def create_collection_protocol(
db: Session,
*,
protocol_no_id: Optional[str] = None,
protocol_no: str = "",
protocol_status: str = "dsx",
protocol_attachments: Optional[str] = None,
register_date: Optional[str] = None,
collector_user_id: Optional[str] = None,
remark: Optional[str] = None,
tenant_no: str = "zkxw",
user_id: Optional[str] = None,
) -> BizCollectionProtocol:
pn = (protocol_no or "").strip()
if not pn:
raise ValueError("协议编号不能为空")
dup = (
db.query(BizCollectionProtocol)
.filter(
and_(
BizCollectionProtocol.protocol_no == pn,
BizCollectionProtocol.tenant_no == tenant_no,
BizCollectionProtocol.del_flag == 0,
)
)
.first()
)
if dup:
raise ValueError("协议编号已存在")
obj = BizCollectionProtocol(
id=generate_uuid(),
protocol_no_id=(protocol_no_id or "").strip() or None,
protocol_no=pn,
protocol_status=protocol_status.strip() or "dsx",
protocol_attachments=(protocol_attachments or "").strip() or None,
register_date=(register_date or "").strip() or None,
collector_user_id=(collector_user_id or "").strip() or None,
tenant_no=tenant_no,
order_num=0,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def update_collection_protocol(
db: Session,
protocol_id: str,
*,
tenant_no: str,
user_id: Optional[str] = None,
scope_collector_user_id: Optional[str] = None,
protocol_no: Optional[str] = None,
protocol_status: Optional[str] = None,
protocol_attachments: Optional[str] = None,
register_date: Optional[str] = None,
effective_time: Optional[datetime] = None,
effective_desc: Optional[str] = None,
expire_time: Optional[datetime] = None,
expire_desc: Optional[str] = None,
void_time: Optional[datetime] = None,
void_desc: Optional[str] = None,
to_research_time: Optional[datetime] = None,
to_research_desc: Optional[str] = None,
collector_user_id: Optional[str] = None,
remark: Optional[str] = None,
) -> BizCollectionProtocol:
obj = get_collection_protocol(
db, protocol_id, tenant_no, scope_collector_user_id=scope_collector_user_id
)
if not obj:
raise ValueError("协议单不存在或无权操作")
if protocol_no is not None:
pn = protocol_no.strip()
if not pn:
raise ValueError("协议编号不能为空")
if pn != obj.protocol_no:
dup = (
db.query(BizCollectionProtocol)
.filter(
and_(
BizCollectionProtocol.protocol_no == pn,
BizCollectionProtocol.tenant_no == tenant_no,
BizCollectionProtocol.del_flag == 0,
BizCollectionProtocol.id != protocol_id,
)
)
.first()
)
if dup:
raise ValueError("协议编号已存在")
obj.protocol_no = pn
if protocol_status is not None:
obj.protocol_status = protocol_status.strip()
if protocol_attachments is not None:
obj.protocol_attachments = protocol_attachments.strip() or None
if register_date is not None:
obj.register_date = register_date.strip() or None
if effective_time is not None:
obj.effective_time = effective_time
if effective_desc is not None:
obj.effective_desc = effective_desc.strip() or None
if expire_time is not None:
obj.expire_time = expire_time
if expire_desc is not None:
obj.expire_desc = expire_desc.strip() or None
if void_time is not None:
obj.void_time = void_time
if void_desc is not None:
obj.void_desc = void_desc.strip() or None
if to_research_time is not None:
obj.to_research_time = to_research_time
if to_research_desc is not None:
obj.to_research_desc = to_research_desc.strip() or None
if collector_user_id is not None:
obj.collector_user_id = collector_user_id.strip() or None
if remark is not None:
obj.remark = remark.strip() or None
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def soft_delete_collection_protocol(
db: Session,
protocol_id: str,
tenant_no: str,
user_id: Optional[str] = None,
scope_collector_user_id: Optional[str] = None,
) -> None:
obj = get_collection_protocol(
db, protocol_id, tenant_no, scope_collector_user_id=scope_collector_user_id
)
if not obj:
raise ValueError("协议单不存在或无权操作")
obj.del_flag = 1
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
# ---------------------------------------------------------------------------
# 子表:当事人客户 / 采集信息 / 样本 / 储存证书
# ---------------------------------------------------------------------------
def get_party_customer(db: Session, collection_protocol_id: str, tenant_no: str) -> Optional[BizProtocolPartyCustomer]:
return (
db.query(BizProtocolPartyCustomer)
.filter(
and_(
BizProtocolPartyCustomer.collection_protocol_id == collection_protocol_id,
BizProtocolPartyCustomer.tenant_no == tenant_no,
BizProtocolPartyCustomer.del_flag == 0,
)
)
.first()
)
def upsert_party_customer(
db: Session,
collection_protocol_id: str,
tenant_no: str,
user_id: Optional[str],
**fields,
) -> BizProtocolPartyCustomer:
fields = _normalize_fields(fields)
obj = get_party_customer(db, collection_protocol_id, tenant_no)
_STR_FIELDS = [
"customer_id", "mother_name", "mother_phone", "mother_id_card",
"mother_birth_ym", "mother_ethnic", "mother_address",
"father_name", "father_phone", "father_id_card",
"newborn_name", "newborn_id_card",
"newborn_sex", "delivery_mode", "collector_user_id",
]
_INT_FIELDS = ["birth_order", "newborn_weight_gram"]
_DEC_FIELDS = ["apgar_1min", "apgar_5min"]
_DT_FIELDS = ["newborn_delivery_time"]
if obj:
for k in _STR_FIELDS:
if k in fields:
setattr(obj, k, (fields[k] or "").strip() or None)
for k in _INT_FIELDS:
if k in fields:
setattr(obj, k, fields[k])
for k in _DEC_FIELDS:
if k in fields:
setattr(obj, k, _optional_decimal(fields[k]))
for k in _DT_FIELDS:
if k in fields:
setattr(obj, k, fields[k])
if "remark" in fields:
obj.remark = (fields["remark"] or "").strip() or None
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
obj = BizProtocolPartyCustomer(
id=generate_uuid(),
collection_protocol_id=collection_protocol_id,
tenant_no=tenant_no,
order_num=0,
remark=(fields.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
for k in _STR_FIELDS:
if k in fields:
setattr(obj, k, (fields[k] or "").strip() or None)
for k in _INT_FIELDS:
if k in fields:
setattr(obj, k, fields[k])
for k in _DEC_FIELDS:
if k in fields:
setattr(obj, k, _optional_decimal(fields[k]))
for k in _DT_FIELDS:
if k in fields:
setattr(obj, k, fields[k])
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def get_collection_info(db: Session, collection_protocol_id: str, tenant_no: str) -> Optional[BizProtocolCollectionInfo]:
return (
db.query(BizProtocolCollectionInfo)
.filter(
and_(
BizProtocolCollectionInfo.collection_protocol_id == collection_protocol_id,
BizProtocolCollectionInfo.tenant_no == tenant_no,
BizProtocolCollectionInfo.del_flag == 0,
)
)
.first()
)
def upsert_collection_info(
db: Session,
collection_protocol_id: str,
tenant_no: str,
user_id: Optional[str],
**fields,
) -> BizProtocolCollectionInfo:
fields = _normalize_fields(fields)
obj = get_collection_info(db, collection_protocol_id, tenant_no)
_STR_FIELDS = ["region_id", "hospital_id", "medical_staff", "sales_user_id"]
_DT_FIELDS = ["sample_collect_time", "maternal_blood_collect_time"]
if obj:
for k in _STR_FIELDS:
if k in fields:
setattr(obj, k, (fields[k] or "").strip() or None)
for k in _DT_FIELDS:
if k in fields:
setattr(obj, k, fields[k])
if "remark" in fields:
obj.remark = (fields["remark"] or "").strip() or None
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
obj = BizProtocolCollectionInfo(
id=generate_uuid(),
collection_protocol_id=collection_protocol_id,
tenant_no=tenant_no,
order_num=0,
remark=(fields.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
for k in _STR_FIELDS:
if k in fields:
setattr(obj, k, (fields[k] or "").strip() or None)
for k in _DT_FIELDS:
if k in fields:
setattr(obj, k, fields[k])
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def list_samples(db: Session, collection_protocol_id: str, tenant_no: str) -> List[BizProtocolSample]:
return (
db.query(BizProtocolSample)
.filter(
and_(
BizProtocolSample.collection_protocol_id == collection_protocol_id,
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.del_flag == 0,
)
)
.order_by(BizProtocolSample.create_time.asc())
.all()
)
def save_samples(
db: Session,
collection_protocol_id: str,
tenant_no: str,
user_id: Optional[str],
samples: list,
) -> List[BizProtocolSample]:
existing = list_samples(db, collection_protocol_id, tenant_no)
for s in existing:
s.del_flag = 1
s.update_by = user_id
s.update_time = datetime.now()
db.add(s)
if existing:
db.flush()
_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",
]
_DEC_FIELDS = ["collect_amount", "price"]
_INT_FIELDS = ["storage_years"]
result = []
for idx, raw in enumerate(samples):
raw = _normalize_fields(raw)
obj = BizProtocolSample(
id=generate_uuid(),
collection_protocol_id=collection_protocol_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=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
for k in _STR_FIELDS:
if k == "sample_status":
continue
if k in raw:
setattr(obj, k, (raw[k] or "").strip() or None)
# 样本状态:显式支持 null/空字符串,避免 ORM/库默认值 ycj 回填
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 _DEC_FIELDS:
if k in raw and raw[k] is not None:
setattr(obj, k, Decimal(str(raw[k])))
for k in _INT_FIELDS:
if k in raw:
setattr(obj, k, raw[k])
db.add(obj)
result.append(obj)
db.commit()
for o in result:
db.refresh(o)
save_sample_transports(db, collection_protocol_id, tenant_no, user_id, result, samples)
return result
def list_sample_transports(
db: Session,
collection_protocol_id: str,
tenant_no: str,
) -> List[BizProtocolSampleTransport]:
return (
db.query(BizProtocolSampleTransport)
.filter(
and_(
BizProtocolSampleTransport.collection_protocol_id == collection_protocol_id,
BizProtocolSampleTransport.tenant_no == tenant_no,
BizProtocolSampleTransport.del_flag == 0,
)
)
.order_by(BizProtocolSampleTransport.order_num.asc(), BizProtocolSampleTransport.create_time.asc())
.all()
)
def _soft_delete_transports_for_protocol(
db: Session,
collection_protocol_id: str,
tenant_no: str,
user_id: Optional[str],
) -> None:
rows = list_sample_transports(db, collection_protocol_id, tenant_no)
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 save_sample_transports(
db: Session,
collection_protocol_id: str,
tenant_no: str,
user_id: Optional[str],
saved_samples: List[BizProtocolSample],
input_samples: list,
) -> None:
_soft_delete_transports_for_protocol(db, collection_protocol_id, tenant_no, user_id)
for idx, sample_obj in enumerate(saved_samples):
if idx >= len(input_samples):
break
raw = _normalize_fields(input_samples[idx] if isinstance(input_samples[idx], dict) else {})
transport_raw = raw.get("transport")
if not isinstance(transport_raw, dict):
continue
tnorm = _normalize_fields(transport_raw)
transport_type = (tnorm.get("transport_type") or "").strip()
if not transport_type:
continue
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 = BizProtocolSampleTransport(
id=generate_uuid(),
collection_protocol_id=collection_protocol_id,
sample_id=sample_obj.id,
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=idx,
remark=(tnorm.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(obj)
db.commit()
def get_storage_cert(db: Session, collection_protocol_id: str, tenant_no: str) -> Optional[BizProtocolStorageCert]:
return (
db.query(BizProtocolStorageCert)
.filter(
and_(
BizProtocolStorageCert.collection_protocol_id == collection_protocol_id,
BizProtocolStorageCert.tenant_no == tenant_no,
BizProtocolStorageCert.del_flag == 0,
)
)
.first()
)
def list_payment_details(
db: Session,
payment_id: str,
tenant_no: str,
) -> List[BizProtocolPaymentDetail]:
return (
db.query(BizProtocolPaymentDetail)
.filter(
and_(
BizProtocolPaymentDetail.payment_id == payment_id,
BizProtocolPaymentDetail.tenant_no == tenant_no,
BizProtocolPaymentDetail.del_flag == 0,
)
)
.order_by(
BizProtocolPaymentDetail.order_num.asc(),
BizProtocolPaymentDetail.create_time.asc(),
)
.all()
)
def upsert_payment(
db: Session,
collection_protocol_id: str,
tenant_no: str,
user_id: Optional[str],
**fields,
) -> BizProtocolPayment:
fields = _normalize_fields(fields)
obj = get_payment_by_protocol_id(db, collection_protocol_id, tenant_no)
_STR_FIELDS = ["pay_mode"]
_STR_DATE_FIELDS = ["lump_sum_deadline", "installment_start_date"]
_DEC_FIELDS = ["contract_amount"]
_INT_FIELDS = ["installment_count", "installment_interval_months"]
def _apply(target: BizProtocolPayment) -> None:
for k in _STR_FIELDS:
if k in fields:
setattr(target, k, (fields[k] or "").strip() or None)
for k in _STR_DATE_FIELDS:
if k in fields:
setattr(target, k, _normalize_yyyymmdd_date(fields[k]))
for k in _DEC_FIELDS:
if k in fields and fields[k] is not None:
setattr(target, k, Decimal(str(fields[k])))
for k in _INT_FIELDS:
if k in fields:
setattr(target, k, fields[k])
if "remark" in fields:
target.remark = (fields["remark"] or "").strip() or None
if obj:
_apply(obj)
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
pay_mode = (fields.get("pay_mode") or "").strip() or "dj"
obj = BizProtocolPayment(
id=generate_uuid(),
collection_protocol_id=collection_protocol_id,
pay_mode=pay_mode,
tenant_no=tenant_no,
order_num=0,
remark=(fields.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
_apply(obj)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def save_payment_details(
db: Session,
payment_id: str,
tenant_no: str,
user_id: Optional[str],
details: list,
) -> List[BizProtocolPaymentDetail]:
existing = list_payment_details(db, payment_id, tenant_no)
for d in existing:
d.del_flag = 1
d.update_by = user_id
d.update_time = datetime.now()
db.add(d)
if existing:
db.flush()
_STR_FIELDS = ["transfer_type", "pay_status"]
_STR_DATE_FIELDS = ["installment_due_date"]
_DEC_FIELDS = ["due_amount", "transfer_amount"]
_INT_FIELDS = ["installment_no"]
result: List[BizProtocolPaymentDetail] = []
for idx, raw in enumerate(details or []):
raw = _normalize_fields(raw)
att_src = raw.pop("attachments", None)
if att_src is None:
att_src = raw.get("transfer_attachments")
transfer_att = attachments_to_urls(att_src)
transfer_time = raw.get("transfer_time")
if isinstance(transfer_time, str):
transfer_time = parse_datetime(transfer_time)
installment_no = raw.get("installment_no")
if installment_no is None and raw.get("installment_due_date"):
installment_no = idx + 1
obj = BizProtocolPaymentDetail(
id=generate_uuid(),
payment_id=payment_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=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
transfer_attachments=transfer_att,
transfer_time=transfer_time,
installment_no=installment_no,
)
for k in _STR_FIELDS:
if k in raw:
setattr(obj, k, (raw[k] or "").strip() or None)
for k in _STR_DATE_FIELDS:
if k in raw:
setattr(obj, k, _normalize_yyyymmdd_date(raw[k]))
for k in _DEC_FIELDS:
if k in raw and raw[k] is not None:
setattr(obj, k, Decimal(str(raw[k])))
for k in _INT_FIELDS:
if k in raw and raw[k] is not None:
setattr(obj, k, raw[k])
if "pay_status" not in raw or not (raw.get("pay_status") or "").strip():
obj.pay_status = "dfj"
db.add(obj)
result.append(obj)
db.commit()
for o in result:
db.refresh(o)
return result
def upsert_storage_cert(
db: Session,
collection_protocol_id: str,
tenant_no: str,
user_id: Optional[str],
**fields,
) -> BizProtocolStorageCert:
fields = _normalize_fields(fields)
obj = get_storage_cert(db, collection_protocol_id, tenant_no)
_STR_FIELDS = ["make_status", "cert_image_urls", "make_date"]
if obj:
for k in _STR_FIELDS:
if k in fields:
setattr(obj, k, (fields[k] or "").strip() or None)
if "remark" in fields:
obj.remark = (fields["remark"] or "").strip() or None
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
obj = BizProtocolStorageCert(
id=generate_uuid(),
collection_protocol_id=collection_protocol_id,
tenant_no=tenant_no,
order_num=0,
remark=(fields.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
for k in _STR_FIELDS:
if k in fields:
setattr(obj, k, (fields[k] or "").strip() or None)
db.add(obj)
db.commit()
db.refresh(obj)
return obj