Files
zkxw/backend/app/crud/collection_protocol.py
T
2026-05-20 14:32:07 +08:00

2561 lines
86 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
import calendar
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.crud.protocol_donation_convert import (
assert_biz_no_available,
get_partner_sample_codes,
)
from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
from app.models.models import (
BizAttachment,
BizCollectionProtocol,
BizProtocolAudit,
BizProtocolCollectionInfo,
BizProtocolFullmoonTodo,
BizProtocolPartyCustomer,
BizProtocolPayment,
BizProtocolPaymentDetail,
BizProtocolSample,
BizProtocolSampleTransport,
BizProtocolStorageCert,
BizQcWorkOrder,
BizSampleReception,
BizCollectionRegion,
BizCollectionHospital,
BizCellSampleType,
SysUser,
)
_NEWBORN_SEX_LABEL = {"1": "男", "0": "女"}
_SAMPLE_STATUS_LABEL = {
"ycj": "已采集",
"jcz": "检测中",
"yrk": "已入库",
"yzf": "已作废",
}
def get_protocol_nos_used_in_protocols(
db: Session,
*,
tenant_no: str,
protocol_nos: List[str],
) -> set:
"""返回已在采集协议单或科研捐赠单中占用的协议编号集合(同租户、未删除)。"""
nos = list({(n or "").strip() for n in protocol_nos if (n or "").strip()})
if not nos:
return set()
used: set = set()
proto_rows = (
db.query(BizCollectionProtocol.protocol_no)
.filter(
BizCollectionProtocol.tenant_no == tenant_no,
BizCollectionProtocol.del_flag == 0,
BizCollectionProtocol.protocol_no.in_(nos),
)
.all()
)
used.update(r[0] for r in proto_rows if r[0])
donation_rows = (
db.query(BizResearchDonation.donation_no)
.filter(
BizResearchDonation.tenant_no == tenant_no,
BizResearchDonation.del_flag == 0,
BizResearchDonation.donation_no.in_(nos),
)
.all()
)
used.update(r[0] for r in donation_rows if r[0])
return used
def get_sample_codes_used_in_protocol_samples(
db: Session,
*,
tenant_no: str,
sample_codes: List[str],
) -> set:
"""返回已在采集协议单或科研捐赠单样本表中占用的样本编码集合(同租户、未删除)。"""
codes = list({(c or "").strip() for c in sample_codes if (c or "").strip()})
if not codes:
return set()
used: set = set()
proto_rows = (
db.query(BizProtocolSample.sample_code)
.filter(
BizProtocolSample.tenant_no == tenant_no,
BizProtocolSample.del_flag == 0,
BizProtocolSample.sample_code.in_(codes),
)
.all()
)
used.update(r[0] for r in proto_rows if r[0])
donation_rows = (
db.query(BizResearchDonationSample.sample_code)
.filter(
BizResearchDonationSample.tenant_no == tenant_no,
BizResearchDonationSample.del_flag == 0,
BizResearchDonationSample.sample_code.in_(codes),
)
.all()
)
used.update(r[0] for r in donation_rows if r[0])
return used
def validate_sample_codes_not_used(
db: Session,
*,
tenant_no: str,
samples: list,
exclude_protocol_id: Optional[str] = None,
exclude_donation_id: Optional[str] = None,
) -> None:
"""创建/更新样本行前校验:样本编码不得已被任一业务单占用(转移配对单豁免)。"""
codes: List[str] = []
for raw_item in samples or []:
raw = _normalize_fields(raw_item)
c = (raw.get("sample_code") or "").strip()
if c:
codes.append(c)
if not codes:
return
used = get_sample_codes_used_in_protocol_samples(
db, tenant_no=tenant_no, sample_codes=codes
)
exempt = get_partner_sample_codes(
db,
tenant_no,
protocol_id=exclude_protocol_id,
donation_id=exclude_donation_id,
)
used -= exempt
conflicts = set(codes) & used
if conflicts:
first = sorted(conflicts)[0]
raise ValueError(f"样本编码「{first}」已被占用")
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 _pop_attachment_urls(raw: Dict[str, Any], *keys: str) -> Optional[str]:
"""从保存载荷中取出附件字段(支持 snake/camel、URL 串或附件对象列表)。"""
for key in keys:
if key not in raw:
continue
urls = attachments_to_urls(raw.pop(key))
if urls:
return urls
return None
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 _format_newborn_info(pc: Optional[BizProtocolPartyCustomer]) -> str:
if not pc:
return ""
parts: List[str] = []
name = (pc.newborn_name or "").strip()
if name:
parts.append(name)
sex = _NEWBORN_SEX_LABEL.get((pc.newborn_sex or "").strip(), "")
if sex:
parts.append(sex)
if pc.newborn_weight_gram is not None:
parts.append(f"{pc.newborn_weight_gram}g")
if pc.newborn_delivery_time:
parts.append(_fmt_dt(pc.newborn_delivery_time))
return " / ".join(parts)
def _format_storage_cert_sample_info(
samples: List[Dict[str, Any]], type_map: Dict[str, str]
) -> str:
parts: List[str] = []
for s in samples:
type_name = type_map.get(s.get("sampleTypeId") or "", "") or "样本"
code = (s.get("sampleCode") or "").strip()
status = (s.get("sampleStatus") or "").strip()
seg = type_name
if code:
seg += f"({code})"
if status:
seg += f"[{_SAMPLE_STATUS_LABEL.get(status, status)}]"
parts.append(seg)
return ";".join(parts)
def _pick_storage_in_date(samples: List[Dict[str, Any]]) -> str:
for s in samples:
d = (s.get("storageInDate") or "").strip()
if d:
return d
return ""
def _load_sample_type_name_map(db: Session, samples: List[BizProtocolSample]) -> Dict[str, str]:
type_ids = list({s.sample_type_id for s in samples if s.sample_type_id})
if not type_ids:
return {}
type_map: Dict[str, str] = {}
for t in (
db.query(BizCellSampleType)
.filter(BizCellSampleType.id.in_(type_ids), BizCellSampleType.del_flag == 0)
.all()
):
type_map[t.id] = t.type_name or ""
return type_map
def list_storage_certs(
db: Session,
*,
tenant_no: str,
keyword: Optional[str] = None,
make_status: Optional[str] = None,
page: int = 1,
page_size: int = 10,
) -> Tuple[List[Dict[str, Any]], int]:
"""储存证管理列表:生效中协议 + 当事人/样本/储存证信息(不要求样本已入库)。"""
q = db.query(BizCollectionProtocol).filter(
BizCollectionProtocol.del_flag == 0,
BizCollectionProtocol.tenant_no == tenant_no,
BizCollectionProtocol.protocol_status == "sxz",
)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
party_ids = (
db.query(BizProtocolPartyCustomer.collection_protocol_id)
.filter(
BizProtocolPartyCustomer.del_flag == 0,
BizProtocolPartyCustomer.tenant_no == tenant_no,
BizProtocolPartyCustomer.mother_name.like(like),
)
)
q = q.filter(
or_(
BizCollectionProtocol.protocol_no.like(like),
BizCollectionProtocol.id.in_(party_ids),
)
)
rows_all = q.order_by(BizCollectionProtocol.create_time.desc()).all()
protocol_ids = [r.id for r in rows_all]
pc_map: Dict[str, BizProtocolPartyCustomer] = {}
sc_map: Dict[str, BizProtocolStorageCert] = {}
sample_map: Dict[str, List[Dict[str, Any]]] = {}
type_map: Dict[str, str] = {}
if protocol_ids:
for pc in (
db.query(BizProtocolPartyCustomer)
.filter(
BizProtocolPartyCustomer.collection_protocol_id.in_(protocol_ids),
BizProtocolPartyCustomer.del_flag == 0,
)
.all()
):
pc_map[pc.collection_protocol_id] = pc
for sc in (
db.query(BizProtocolStorageCert)
.filter(
BizProtocolStorageCert.collection_protocol_id.in_(protocol_ids),
BizProtocolStorageCert.del_flag == 0,
)
.all()
):
sc_map[sc.collection_protocol_id] = sc
s_rows = (
db.query(BizProtocolSample)
.filter(
BizProtocolSample.collection_protocol_id.in_(protocol_ids),
BizProtocolSample.del_flag == 0,
)
.all()
)
type_map = _load_sample_type_name_map(db, s_rows)
for s in s_rows:
sample_map.setdefault(s.collection_protocol_id, []).append(_sample_to_dict(s))
items: List[Dict[str, Any]] = []
ms_filter = (make_status or "").strip()
for r in rows_all:
sc = sc_map.get(r.id)
cert_ms = (sc.make_status or "0") if sc else "0"
if ms_filter and cert_ms != ms_filter:
continue
pc = pc_map.get(r.id)
samples = sample_map.get(r.id, [])
cert_dict = _storage_cert_to_dict(sc) if sc else {"makeStatus": "0", "certImageUrls": "", "makeDate": "", "remark": ""}
items.append(
{
"id": r.id,
"protocolId": r.id,
"protocolNo": r.protocol_no or "",
"protocolStatus": r.protocol_status or "",
"motherName": (pc.mother_name or "") if pc else "",
"motherPhone": (pc.mother_phone or "") if pc else "",
"motherIdCard": (pc.mother_id_card or "") if pc else "",
"newbornName": (pc.newborn_name or "") if pc else "",
"newbornIdCard": (pc.newborn_id_card or "") if pc else "",
"newbornDeliveryTime": _fmt_dt(pc.newborn_delivery_time) if pc and pc.newborn_delivery_time else "",
"newbornSex": (pc.newborn_sex or "") if pc else "",
"newbornWeightGram": pc.newborn_weight_gram if pc else None,
"newbornInfo": _format_newborn_info(pc),
"sampleInfo": _format_storage_cert_sample_info(samples, type_map),
"storageInDate": _pick_storage_in_date(samples),
"samples": samples,
"makeStatus": cert_dict.get("makeStatus", "0"),
"certImageUrls": cert_dict.get("certImageUrls", ""),
"makeDate": cert_dict.get("makeDate", ""),
"remark": cert_dict.get("remark", ""),
}
)
total = len(items)
page_safe = max(1, page)
page_size_safe = min(max(1, page_size), 100)
start = (page_safe - 1) * page_size_safe
return items[start : start + page_size_safe], total
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}
from app.crud import sample_reception as sample_reception_svc
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_samples(
db,
tenant_no,
sample_code_by_id=sample_code_by_id,
)
from app.crud import qc_work_order as qc_work_order_svc
qc_by_sample = qc_work_order_svc.get_qc_info_map_for_samples(
db,
[s.id for s in s_rows],
tenant_no,
sample_code_by_id=sample_code_by_id,
)
samples_out: List[Dict[str, Any]] = []
for s in s_rows:
item = _sample_to_dict(s)
item["transport"] = transport_by_sample.get(s.id)
item["sampleReception"] = reception_by_sample.get(s.id)
item["qcInfo"] = qc_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()
assert_biz_no_available(db, tenant_no=tenant_no, biz_no=pn)
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:
assert_biz_no_available(
db,
tenant_no=tenant_no,
biz_no=pn,
exclude_protocol_id=protocol_id,
)
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.order_num.asc(), BizProtocolSample.create_time.asc())
.all()
)
def _migrate_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:
"""协议样本行重建后,将接收/质检工单的 protocol_sample_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()
for model in (BizSampleReception, BizQcWorkOrder):
rows = (
db.query(model)
.filter(
model.protocol_sample_id.in_(old_ids),
model.tenant_no == tenant_no,
model.del_flag == 0,
)
.all()
)
for row in 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 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)
old_id_to_code = {
s.id: (s.sample_code or "").strip()
for s in existing
if (s.sample_code or "").strip()
}
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()
validate_sample_codes_not_used(
db,
tenant_no=tenant_no,
samples=samples,
exclude_protocol_id=collection_protocol_id,
)
_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)
new_code_to_id = {
(o.sample_code or "").strip(): o.id
for o in result
if (o.sample_code or "").strip()
}
_migrate_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,
)
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,
sample_code=sample_obj.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=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 _add_months_yyyymmdd(date_str: str, months: int) -> str:
raw = (date_str or "").strip().replace("-", "")
if len(raw) != 8:
return ""
y = int(raw[0:4])
m = int(raw[4:6])
d = int(raw[6:8])
month_index = m - 1 + months
ny = y + month_index // 12
nm = month_index % 12 + 1
last_day = calendar.monthrange(ny, nm)[1]
nd = min(d, last_day)
return f"{ny:04d}{nm:02d}{nd:02d}"
def _calc_installment_due_amounts(
contract_amount: Optional[Decimal],
count: int,
) -> List[Decimal]:
if contract_amount is None or count < 1:
return []
total_cents = int(round(float(contract_amount) * 100))
base_cents = total_cents // count
amounts: List[Decimal] = []
sum_cents = 0
for i in range(count):
if i == count - 1:
cents = total_cents - sum_cents
else:
cents = base_cents
sum_cents += base_cents
amounts.append(Decimal(cents) / Decimal(100))
return amounts
def align_fq_payment_details_payload(
pay: BizProtocolPayment,
details: Optional[list],
) -> list:
if pay.pay_mode != "fq" or not pay.installment_count or int(pay.installment_count) < 1:
return list(details or [])
count = int(pay.installment_count)
rows = [_normalize_fields(dict(d)) for d in (details or [])]
if len(rows) >= count:
needed = set(range(1, count + 1))
have = {
int(r["installment_no"])
for r in rows
if r.get("installment_no") is not None
}
if needed - have == set():
return rows[:count]
by_no: Dict[int, Dict[str, Any]] = {}
unnumbered: List[Dict[str, Any]] = []
for r in rows:
no = r.get("installment_no")
if no is not None:
by_no[int(no)] = r
else:
unnumbered.append(r)
amounts = _calc_installment_due_amounts(pay.contract_amount, count)
start = (pay.installment_start_date or "").strip()
interval = int(pay.installment_interval_months or 0)
result: List[Dict[str, Any]] = []
for i in range(count):
no = i + 1
if no in by_no:
result.append(by_no[no])
continue
if i < len(unnumbered):
raw = dict(unnumbered[i])
raw["installment_no"] = no
if not raw.get("installment_due_date") and start and interval:
raw["installment_due_date"] = _add_months_yyyymmdd(start, i * interval)
result.append(raw)
continue
due_date = _add_months_yyyymmdd(start, i * interval) if start and interval else ""
due = amounts[i] if i < len(amounts) else None
result.append(
{
"installment_no": no,
"installment_due_date": due_date or None,
"due_amount": float(due) if due is not None else None,
"pay_status": "dfj",
"transfer_type": "",
"transfer_amount": None,
"attachments": [],
}
)
return result
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.installment_no.asc(),
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",
"finance_remark",
"refund_finance_remark",
"refund_remark",
]
_STR_DATE_FIELDS = ["installment_due_date"]
_DEC_FIELDS = ["due_amount", "transfer_amount", "refund_amount"]
_INT_FIELDS = ["installment_no"]
result: List[BizProtocolPaymentDetail] = []
for idx, raw in enumerate(details or []):
raw = _normalize_fields(dict(raw))
transfer_att = _pop_attachment_urls(
raw, "attachments", "transfer_attachments", "transferAttachments"
)
finance_confirm_att = _pop_attachment_urls(
raw,
"finance_confirm_attachments",
"financeConfirmAttachments",
"finance_confirm_attachment_files",
"financeConfirmAttachmentFiles",
)
refund_att = _pop_attachment_urls(
raw,
"refund_attachments",
"refundAttachments",
"refund_attachment_files",
"refundAttachmentFiles",
)
refund_finance_att = _pop_attachment_urls(
raw,
"refund_finance_confirm_attachments",
"refundFinanceConfirmAttachments",
"refund_finance_confirm_attachment_files",
"refundFinanceConfirmAttachmentFiles",
)
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,
finance_confirm_attachments=finance_confirm_att,
refund_attachments=refund_att,
refund_finance_confirm_attachments=refund_finance_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
# ---------------------------------------------------------------------------
# 子表:满月待办
# ---------------------------------------------------------------------------
def _fullmoon_todo_to_dict(ft: BizProtocolFullmoonTodo) -> Dict[str, Any]:
return {
"id": ft.id,
"collectionProtocolId": ft.collection_protocol_id,
"todoStatus": ft.todo_status or "0",
"handleDate": (ft.handle_date or "").strip() if ft.handle_date else "",
"handleRemark": ft.handle_remark or "",
"insuranceAttachments": ft.insurance_attachments or "",
"insuranceCompanyId": ft.insurance_company_id or "",
"insuranceCompanyName": ft.insurance_company_name or "",
"insuranceType": ft.insurance_type or "",
"insurancePolicyNo": ft.insurance_policy_no or "",
"insuranceEffectiveDate": (ft.insurance_effective_date or "").strip() if ft.insurance_effective_date else "",
"insuranceExpiryDate": (ft.insurance_expiry_date or "").strip() if ft.insurance_expiry_date else "",
"insuranceSubscribed": ft.insurance_subscribed or "0",
"insuranceFee": _dec(ft.insurance_fee),
"tenantNo": ft.tenant_no,
"orderNum": ft.order_num if ft.order_num is not None else 0,
"remark": ft.remark or "",
"createTime": _fmt_dt(ft.create_time),
"updateTime": _fmt_dt(ft.update_time),
}
def get_fullmoon_todo(
db: Session,
collection_protocol_id: str,
tenant_no: str,
) -> Optional[BizProtocolFullmoonTodo]:
return (
db.query(BizProtocolFullmoonTodo)
.filter(
and_(
BizProtocolFullmoonTodo.collection_protocol_id == collection_protocol_id,
BizProtocolFullmoonTodo.tenant_no == tenant_no,
BizProtocolFullmoonTodo.del_flag == 0,
)
)
.first()
)
def upsert_fullmoon_todo(
db: Session,
collection_protocol_id: str,
tenant_no: str,
user_id: Optional[str],
**fields,
) -> BizProtocolFullmoonTodo:
fields = _normalize_fields(fields)
obj = get_fullmoon_todo(db, collection_protocol_id, tenant_no)
_STR_FIELDS = [
"todo_status", "handle_date", "handle_remark",
"insurance_attachments", "insurance_company_id",
"insurance_company_name", "insurance_type",
"insurance_policy_no", "insurance_effective_date",
"insurance_expiry_date", "insurance_subscribed",
]
_DEC_FIELDS = ["insurance_fee"]
if obj:
for k in _STR_FIELDS:
if k in fields:
setattr(obj, k, (fields[k] or "").strip() or None)
for k in _DEC_FIELDS:
if k in fields and fields[k] is not None:
setattr(obj, k, Decimal(str(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 = BizProtocolFullmoonTodo(
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 _DEC_FIELDS:
if k in fields and fields[k] is not None:
setattr(obj, k, Decimal(str(fields[k])))
db.add(obj)
db.commit()
db.refresh(obj)
return obj