协议样本、生产产出和盘点明细按行更新并保留原 id;补齐存储证明质检预填、离职人员显示名,以及样本采集通知模板字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
1345 lines
45 KiB
Python
1345 lines
45 KiB
Python
"""样本运费批次 CRUD(biz_sample_freight / biz_sample_freight_item)"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime, timedelta
|
||
from decimal import Decimal
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
from sqlalchemy import and_, exists, func, or_
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.data_scope import apply_data_scope, get_user_dept_id
|
||
from app.core.utils import generate_uuid
|
||
from app.models.models import (
|
||
BizCellSampleType,
|
||
BizCollectionProtocol,
|
||
BizProtocolPartyCustomer,
|
||
BizProtocolSample,
|
||
BizSampleFreight,
|
||
BizSampleFreightItem,
|
||
SysDictData,
|
||
SysUser,
|
||
)
|
||
from app.models.research_donation import (
|
||
BizResearchDonation,
|
||
BizResearchDonationParty,
|
||
BizResearchDonationSample,
|
||
)
|
||
from app.services.sample_transport_mode import DICT_TYPE, resolve_transport_mode_value
|
||
from app.services.sf_express_settlement import (
|
||
assert_freight_mutable,
|
||
freight_amount_label,
|
||
resolve_header_for_save,
|
||
)
|
||
|
||
SAMPLE_SOURCE_CELL = "cell_storage"
|
||
SAMPLE_SOURCE_DONATION = "research_donation"
|
||
|
||
|
||
def _now() -> datetime:
|
||
return datetime.now()
|
||
|
||
|
||
def _fmt_dt(t: Optional[datetime]) -> str:
|
||
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
|
||
|
||
|
||
def _parse_amount(amount: Any) -> Decimal:
|
||
if amount is None or amount == "":
|
||
raise ValueError("运费不能为空")
|
||
try:
|
||
val = Decimal(str(amount))
|
||
except Exception as exc:
|
||
raise ValueError("运费格式无效") from exc
|
||
if not val.is_finite():
|
||
raise ValueError("运费格式无效")
|
||
if val < 0:
|
||
raise ValueError("运费不能为负数")
|
||
return val
|
||
|
||
|
||
def _item_field(raw: Dict[str, Any], camel: str, snake: str, default: str = "") -> str:
|
||
v = raw.get(camel)
|
||
if v is None or v == "":
|
||
v = raw.get(snake)
|
||
if v is None:
|
||
return default
|
||
return str(v).strip()
|
||
|
||
|
||
def _normalize_item(raw: Any) -> Dict[str, Any]:
|
||
if not isinstance(raw, dict):
|
||
raise ValueError("关联样本项无效")
|
||
sample_source = _item_field(raw, "sampleSource", "sample_source")
|
||
sample_id = _item_field(raw, "sampleId", "sample_id")
|
||
if not sample_source or not sample_id:
|
||
raise ValueError("关联样本须包含 sampleSource 与 sampleId")
|
||
if sample_source not in (SAMPLE_SOURCE_CELL, SAMPLE_SOURCE_DONATION):
|
||
raise ValueError("sampleSource 无效")
|
||
protocol_id = _item_field(raw, "protocolId", "protocol_id")
|
||
sample_code = _item_field(raw, "sampleCode", "sample_code")
|
||
return {
|
||
"sample_source": sample_source,
|
||
"sample_id": sample_id,
|
||
"protocol_id": protocol_id or None,
|
||
"sample_code": sample_code or None,
|
||
}
|
||
|
||
|
||
def _label_map(db: Session, tenant_no: str, modes: List[str]) -> Dict[str, str]:
|
||
uniq = [m for m in {str(x).strip() for x in modes if str(x or "").strip()}]
|
||
if not uniq:
|
||
return {}
|
||
rows = (
|
||
db.query(SysDictData.dict_value, SysDictData.dict_label)
|
||
.filter(
|
||
SysDictData.dict_type == DICT_TYPE,
|
||
SysDictData.tenant_no == tenant_no,
|
||
SysDictData.dict_value.in_(uniq),
|
||
SysDictData.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
return {str(v): (lbl or v or "") for v, lbl in rows}
|
||
|
||
|
||
def _linked_count(db: Session, freight_id: str, tenant_no: str) -> int:
|
||
return (
|
||
db.query(func.count(BizSampleFreightItem.id))
|
||
.filter(
|
||
BizSampleFreightItem.freight_id == freight_id,
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.del_flag == 0,
|
||
)
|
||
.scalar()
|
||
or 0
|
||
)
|
||
|
||
|
||
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 "").strip() for u in rows}
|
||
|
||
|
||
def _generate_freight_no(
|
||
db: Session, tenant_no: str, when: Optional[datetime] = None
|
||
) -> str:
|
||
"""生成批次号:YF + YYYYMMDD + 001~999(按租户、按日)。"""
|
||
day = (when or _now()).strftime("%Y%m%d")
|
||
prefix = f"YF{day}"
|
||
existing = (
|
||
db.query(BizSampleFreight.freight_no)
|
||
.filter(
|
||
BizSampleFreight.tenant_no == tenant_no,
|
||
BizSampleFreight.freight_no.like(f"{prefix}%"),
|
||
)
|
||
.order_by(BizSampleFreight.freight_no.desc())
|
||
.first()
|
||
)
|
||
seq = 1
|
||
if existing and existing[0]:
|
||
try:
|
||
seq = int(str(existing[0])[-3:]) + 1
|
||
except ValueError:
|
||
seq = 1
|
||
if seq > 999:
|
||
raise ValueError("当日运费批次号已达上限(999)")
|
||
return f"{prefix}{seq:03d}"
|
||
|
||
|
||
def _row_to_dict(
|
||
obj: BizSampleFreight,
|
||
*,
|
||
linked_sample_count: int = 0,
|
||
transport_mode_label: str = "",
|
||
create_by_name: str = "",
|
||
) -> Dict[str, Any]:
|
||
amount = obj.freight_amount
|
||
create_by = obj.create_by or ""
|
||
return {
|
||
"id": obj.id,
|
||
"freightNo": getattr(obj, "freight_no", None) or "",
|
||
"transportMode": obj.transport_mode or "",
|
||
"transportModeLabel": transport_mode_label or (obj.transport_mode or ""),
|
||
"freightAmount": float(amount) if amount is not None else None,
|
||
"freightAmountLabel": freight_amount_label(obj.transport_mode, amount),
|
||
"waybillNo": getattr(obj, "waybill_no", None) or "",
|
||
"settleStatus": getattr(obj, "settle_status", None) or "",
|
||
"settleTime": _fmt_dt(getattr(obj, "settle_time", None))
|
||
if getattr(obj, "settle_time", None)
|
||
else "",
|
||
"linkedSampleCount": int(linked_sample_count or 0),
|
||
"tenantNo": obj.tenant_no,
|
||
"orderNum": obj.order_num if obj.order_num is not None else 0,
|
||
"remark": obj.remark or "",
|
||
"createBy": create_by,
|
||
"createByName": create_by_name or create_by,
|
||
"createTime": _fmt_dt(obj.create_time),
|
||
"updateTime": _fmt_dt(obj.update_time),
|
||
}
|
||
|
||
|
||
def _item_to_dict(item: BizSampleFreightItem) -> Dict[str, Any]:
|
||
return {
|
||
"id": item.id,
|
||
"freightId": item.freight_id,
|
||
"sampleSource": item.sample_source or "",
|
||
"sampleId": item.sample_id or "",
|
||
"sampleCode": item.sample_code or "",
|
||
"protocolId": item.protocol_id or "",
|
||
}
|
||
|
||
|
||
def _item_to_dict_enriched(
|
||
item: BizSampleFreightItem,
|
||
*,
|
||
protocol_no: str = "",
|
||
sample_type_name: str = "",
|
||
mother_name: str = "",
|
||
newborn_name: str = "",
|
||
) -> Dict[str, Any]:
|
||
data = _item_to_dict(item)
|
||
data["protocolNo"] = protocol_no or ""
|
||
data["sampleTypeName"] = sample_type_name or ""
|
||
data["motherName"] = mother_name or ""
|
||
data["newbornName"] = newborn_name or ""
|
||
return data
|
||
|
||
|
||
def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> datetime:
|
||
raw = (value or "").strip()
|
||
if not raw:
|
||
raise ValueError("时间参数无效")
|
||
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
|
||
try:
|
||
dt = datetime.strptime(raw, fmt)
|
||
if fmt == "%Y-%m-%d" and end_of_day:
|
||
dt = dt + timedelta(days=1) - timedelta(seconds=1)
|
||
return dt
|
||
except ValueError:
|
||
continue
|
||
try:
|
||
return datetime.fromisoformat(raw.replace("Z", "+00:00")).replace(tzinfo=None)
|
||
except ValueError as exc:
|
||
raise ValueError("时间参数格式无效") from exc
|
||
|
||
|
||
def _enrich_items_maps(
|
||
db: Session,
|
||
items: List[BizSampleFreightItem],
|
||
tenant_no: str,
|
||
) -> Dict[str, Dict[str, str]]:
|
||
"""批量解析协议号/样本类型/母亲子女姓名,key = sample_source|sample_id。"""
|
||
out: Dict[str, Dict[str, str]] = {}
|
||
if not items:
|
||
return out
|
||
|
||
cell_ids = [
|
||
i.sample_id
|
||
for i in items
|
||
if i.sample_source == SAMPLE_SOURCE_CELL and i.sample_id
|
||
]
|
||
donation_ids = [
|
||
i.sample_id
|
||
for i in items
|
||
if i.sample_source == SAMPLE_SOURCE_DONATION and i.sample_id
|
||
]
|
||
|
||
if cell_ids:
|
||
rows = (
|
||
db.query(
|
||
BizProtocolSample.id,
|
||
BizCollectionProtocol.protocol_no,
|
||
BizCellSampleType.type_name,
|
||
BizProtocolPartyCustomer.mother_name,
|
||
BizProtocolPartyCustomer.newborn_name,
|
||
)
|
||
.outerjoin(
|
||
BizCollectionProtocol,
|
||
and_(
|
||
BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
|
||
BizCollectionProtocol.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizCellSampleType,
|
||
and_(
|
||
BizCellSampleType.id == BizProtocolSample.sample_type_id,
|
||
BizCellSampleType.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizProtocolPartyCustomer,
|
||
and_(
|
||
BizProtocolPartyCustomer.collection_protocol_id
|
||
== BizProtocolSample.collection_protocol_id,
|
||
BizProtocolPartyCustomer.del_flag == 0,
|
||
),
|
||
)
|
||
.filter(
|
||
BizProtocolSample.id.in_(cell_ids),
|
||
BizProtocolSample.tenant_no == tenant_no,
|
||
BizProtocolSample.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
for sid, pno, tname, mname, nname in rows:
|
||
out[f"{SAMPLE_SOURCE_CELL}|{sid}"] = {
|
||
"protocolNo": pno or "",
|
||
"sampleTypeName": tname or "",
|
||
"motherName": mname or "",
|
||
"newbornName": nname or "",
|
||
}
|
||
|
||
if donation_ids:
|
||
rows = (
|
||
db.query(
|
||
BizResearchDonationSample.id,
|
||
BizResearchDonation.donation_no,
|
||
BizCellSampleType.type_name,
|
||
BizResearchDonationParty.mother_name,
|
||
BizResearchDonationParty.newborn_name,
|
||
)
|
||
.outerjoin(
|
||
BizResearchDonation,
|
||
and_(
|
||
BizResearchDonation.id
|
||
== BizResearchDonationSample.research_donation_id,
|
||
BizResearchDonation.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizCellSampleType,
|
||
and_(
|
||
BizCellSampleType.id == BizResearchDonationSample.sample_type_id,
|
||
BizCellSampleType.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizResearchDonationParty,
|
||
and_(
|
||
BizResearchDonationParty.research_donation_id
|
||
== BizResearchDonationSample.research_donation_id,
|
||
BizResearchDonationParty.del_flag == 0,
|
||
),
|
||
)
|
||
.filter(
|
||
BizResearchDonationSample.id.in_(donation_ids),
|
||
BizResearchDonationSample.tenant_no == tenant_no,
|
||
BizResearchDonationSample.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
for sid, dno, tname, mname, nname in rows:
|
||
out[f"{SAMPLE_SOURCE_DONATION}|{sid}"] = {
|
||
"protocolNo": dno or "",
|
||
"sampleTypeName": tname or "",
|
||
"motherName": mname or "",
|
||
"newbornName": nname or "",
|
||
}
|
||
|
||
return out
|
||
|
||
|
||
def get_freight(
|
||
db: Session, freight_id: str, tenant_no: str, user_info: Optional[dict] = None
|
||
) -> Optional[BizSampleFreight]:
|
||
query = db.query(BizSampleFreight).filter(
|
||
BizSampleFreight.id == freight_id,
|
||
BizSampleFreight.tenant_no == tenant_no,
|
||
BizSampleFreight.del_flag == 0,
|
||
)
|
||
return apply_data_scope(query, BizSampleFreight, user_info, db, tenant_no).first()
|
||
|
||
|
||
def _validated_items(
|
||
db: Session, items: List[Dict[str, Any]], tenant_no: str, user_info: dict
|
||
) -> List[Dict[str, Any]]:
|
||
"""写入前校验全部样本的租户和数据范围,快照只从数据库派生。"""
|
||
validated = []
|
||
for raw in items:
|
||
item = _normalize_item(raw)
|
||
if item["sample_source"] == SAMPLE_SOURCE_CELL:
|
||
sample_model, parent_model = BizProtocolSample, BizCollectionProtocol
|
||
parent_key = BizProtocolSample.collection_protocol_id
|
||
else:
|
||
sample_model, parent_model = BizResearchDonationSample, BizResearchDonation
|
||
parent_key = BizResearchDonationSample.research_donation_id
|
||
query = db.query(sample_model, parent_model).join(
|
||
parent_model, parent_model.id == parent_key
|
||
).filter(
|
||
sample_model.id == item["sample_id"],
|
||
sample_model.tenant_no == tenant_no,
|
||
sample_model.del_flag == 0,
|
||
parent_model.tenant_no == tenant_no,
|
||
parent_model.del_flag == 0,
|
||
)
|
||
row = apply_data_scope(query, parent_model, user_info, db, tenant_no).first()
|
||
if not row:
|
||
raise PermissionError("样本不存在或无权访问")
|
||
existing_links = db.query(BizSampleFreightItem).filter(
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.sample_source == item["sample_source"],
|
||
BizSampleFreightItem.sample_id == item["sample_id"],
|
||
BizSampleFreightItem.del_flag == 0,
|
||
).all()
|
||
if any(
|
||
not get_freight(db, link.freight_id, tenant_no, user_info)
|
||
for link in existing_links
|
||
):
|
||
raise PermissionError("无权修改样本当前关联的运费批次")
|
||
sample, parent = row
|
||
item["sample_code"] = sample.sample_code
|
||
item["protocol_id"] = parent.id
|
||
validated.append(item)
|
||
return validated
|
||
|
||
|
||
def list_freights(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
page: int = 1,
|
||
page_size: int = 10,
|
||
keyword: Optional[str] = None,
|
||
transport_mode: Optional[str] = None,
|
||
sample_source: Optional[str] = None,
|
||
create_time_from: Optional[str] = None,
|
||
create_time_to: Optional[str] = None,
|
||
include_items: bool = False,
|
||
user_info: Optional[dict] = None,
|
||
) -> Tuple[List[Dict[str, Any]], int]:
|
||
page = max(1, int(page or 1))
|
||
page_size = max(1, min(100, int(page_size or 10)))
|
||
q = db.query(BizSampleFreight).filter(
|
||
BizSampleFreight.tenant_no == tenant_no,
|
||
BizSampleFreight.del_flag == 0,
|
||
)
|
||
|
||
q = apply_data_scope(q, BizSampleFreight, user_info, db, tenant_no)
|
||
|
||
mode = (transport_mode or "").strip()
|
||
if mode:
|
||
q = q.filter(BizSampleFreight.transport_mode == mode)
|
||
|
||
src = (sample_source or "").strip()
|
||
if src:
|
||
if src not in (SAMPLE_SOURCE_CELL, SAMPLE_SOURCE_DONATION):
|
||
raise ValueError("sampleSource 无效")
|
||
item_exists = exists().where(
|
||
and_(
|
||
BizSampleFreightItem.freight_id == BizSampleFreight.id,
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.del_flag == 0,
|
||
BizSampleFreightItem.sample_source == src,
|
||
)
|
||
)
|
||
q = q.filter(item_exists)
|
||
|
||
if create_time_from and str(create_time_from).strip():
|
||
q = q.filter(
|
||
BizSampleFreight.create_time
|
||
>= _normalize_date_bound(str(create_time_from).strip())
|
||
)
|
||
if create_time_to and str(create_time_to).strip():
|
||
q = q.filter(
|
||
BizSampleFreight.create_time
|
||
<= _normalize_date_bound(str(create_time_to).strip(), end_of_day=True)
|
||
)
|
||
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
label_values = [
|
||
r[0]
|
||
for r in db.query(SysDictData.dict_value)
|
||
.filter(
|
||
SysDictData.dict_type == DICT_TYPE,
|
||
SysDictData.tenant_no == tenant_no,
|
||
SysDictData.del_flag == 0,
|
||
SysDictData.dict_label.like(like),
|
||
)
|
||
.all()
|
||
]
|
||
conds = [
|
||
BizSampleFreight.transport_mode.like(like),
|
||
BizSampleFreight.freight_no.like(like),
|
||
]
|
||
if label_values:
|
||
conds.append(BizSampleFreight.transport_mode.in_(label_values))
|
||
|
||
sample_code_exists = exists().where(
|
||
and_(
|
||
BizSampleFreightItem.freight_id == BizSampleFreight.id,
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.del_flag == 0,
|
||
BizSampleFreightItem.sample_code.like(like),
|
||
)
|
||
)
|
||
conds.append(sample_code_exists)
|
||
|
||
q = q.filter(or_(*conds))
|
||
|
||
total = q.count()
|
||
rows = (
|
||
q.order_by(BizSampleFreight.create_time.desc(), BizSampleFreight.id.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
labels = _label_map(db, tenant_no, [r.transport_mode for r in rows])
|
||
name_map = _user_nick_name_map(db, [r.create_by for r in rows if r.create_by])
|
||
|
||
items_by_freight: Dict[str, List[BizSampleFreightItem]] = {}
|
||
if include_items and rows:
|
||
freight_ids = [r.id for r in rows]
|
||
all_items = (
|
||
db.query(BizSampleFreightItem)
|
||
.filter(
|
||
BizSampleFreightItem.freight_id.in_(freight_ids),
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.del_flag == 0,
|
||
)
|
||
.order_by(
|
||
BizSampleFreightItem.create_time.asc(),
|
||
BizSampleFreightItem.id.asc(),
|
||
)
|
||
.all()
|
||
)
|
||
for it in all_items:
|
||
items_by_freight.setdefault(it.freight_id, []).append(it)
|
||
enrich_map = _enrich_items_maps(db, all_items, tenant_no)
|
||
else:
|
||
enrich_map = {}
|
||
|
||
out: List[Dict[str, Any]] = []
|
||
for r in rows:
|
||
linked = (
|
||
len(items_by_freight.get(r.id, []))
|
||
if include_items
|
||
else _linked_count(db, r.id, tenant_no)
|
||
)
|
||
data = _row_to_dict(
|
||
r,
|
||
linked_sample_count=linked,
|
||
transport_mode_label=labels.get(r.transport_mode or "", r.transport_mode or ""),
|
||
create_by_name=name_map.get(r.create_by or "", ""),
|
||
)
|
||
if include_items:
|
||
enriched = []
|
||
for it in items_by_freight.get(r.id, []):
|
||
key = f"{it.sample_source}|{it.sample_id}"
|
||
info = enrich_map.get(key, {})
|
||
enriched.append(
|
||
_item_to_dict_enriched(
|
||
it,
|
||
protocol_no=info.get("protocolNo", ""),
|
||
sample_type_name=info.get("sampleTypeName", ""),
|
||
mother_name=info.get("motherName", ""),
|
||
newborn_name=info.get("newbornName", ""),
|
||
)
|
||
)
|
||
data["items"] = enriched
|
||
out.append(data)
|
||
return out, total
|
||
|
||
|
||
def get_freight_detail(
|
||
db: Session, freight_id: str, tenant_no: str, user_info: Optional[dict] = None
|
||
) -> Dict[str, Any]:
|
||
obj = get_freight(db, freight_id, tenant_no, user_info)
|
||
if not obj:
|
||
raise ValueError("运费批次不存在")
|
||
items = (
|
||
db.query(BizSampleFreightItem)
|
||
.filter(
|
||
BizSampleFreightItem.freight_id == freight_id,
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.del_flag == 0,
|
||
)
|
||
.order_by(BizSampleFreightItem.create_time.asc())
|
||
.all()
|
||
)
|
||
labels = _label_map(db, tenant_no, [obj.transport_mode])
|
||
name_map = _user_nick_name_map(db, [obj.create_by] if obj.create_by else [])
|
||
data = _row_to_dict(
|
||
obj,
|
||
linked_sample_count=len(items),
|
||
transport_mode_label=labels.get(obj.transport_mode or "", obj.transport_mode or ""),
|
||
create_by_name=name_map.get(obj.create_by or "", ""),
|
||
)
|
||
enrich_map = _enrich_items_maps(db, items, tenant_no)
|
||
enriched = []
|
||
for i in items:
|
||
key = f"{i.sample_source}|{i.sample_id}"
|
||
info = enrich_map.get(key, {})
|
||
enriched.append(
|
||
_item_to_dict_enriched(
|
||
i,
|
||
protocol_no=info.get("protocolNo", ""),
|
||
sample_type_name=info.get("sampleTypeName", ""),
|
||
mother_name=info.get("motherName", ""),
|
||
newborn_name=info.get("newbornName", ""),
|
||
)
|
||
)
|
||
data["items"] = enriched
|
||
return data
|
||
|
||
|
||
def list_sample_options(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
keyword: Optional[str] = None,
|
||
sample_source: Optional[str] = None,
|
||
user_info: Optional[dict] = None,
|
||
) -> Tuple[List[Dict[str, Any]], int]:
|
||
"""可选样本列表(配置运费弹窗),支持细胞存储 / 科研捐赠。"""
|
||
page = max(1, int(page or 1))
|
||
page_size = max(1, min(100, int(page_size or 20)))
|
||
src = (sample_source or "").strip() or None
|
||
if src and src not in (SAMPLE_SOURCE_CELL, SAMPLE_SOURCE_DONATION):
|
||
raise ValueError("sampleSource 无效")
|
||
kw = (keyword or "").strip()
|
||
like = f"%{kw}%" if kw else None
|
||
|
||
rows: List[Dict[str, Any]] = []
|
||
|
||
if src in (None, SAMPLE_SOURCE_CELL):
|
||
cq = (
|
||
db.query(
|
||
BizProtocolSample.id,
|
||
BizProtocolSample.sample_code,
|
||
BizProtocolSample.collection_protocol_id,
|
||
BizProtocolSample.create_time,
|
||
BizCollectionProtocol.protocol_no,
|
||
BizCellSampleType.type_name,
|
||
BizProtocolPartyCustomer.mother_name,
|
||
BizProtocolPartyCustomer.newborn_name,
|
||
)
|
||
.outerjoin(
|
||
BizCollectionProtocol,
|
||
and_(
|
||
BizCollectionProtocol.id
|
||
== BizProtocolSample.collection_protocol_id,
|
||
BizCollectionProtocol.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizCellSampleType,
|
||
and_(
|
||
BizCellSampleType.id == BizProtocolSample.sample_type_id,
|
||
BizCellSampleType.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizProtocolPartyCustomer,
|
||
and_(
|
||
BizProtocolPartyCustomer.collection_protocol_id
|
||
== BizProtocolSample.collection_protocol_id,
|
||
BizProtocolPartyCustomer.del_flag == 0,
|
||
),
|
||
)
|
||
.filter(
|
||
BizProtocolSample.tenant_no == tenant_no,
|
||
BizProtocolSample.del_flag == 0,
|
||
)
|
||
)
|
||
cq = cq.filter(
|
||
BizCollectionProtocol.tenant_no == tenant_no,
|
||
BizCollectionProtocol.del_flag == 0,
|
||
)
|
||
cq = apply_data_scope(cq, BizCollectionProtocol, user_info, db, tenant_no)
|
||
if like:
|
||
cq = cq.filter(
|
||
or_(
|
||
BizProtocolSample.sample_code.like(like),
|
||
BizProtocolPartyCustomer.mother_name.like(like),
|
||
BizProtocolPartyCustomer.newborn_name.like(like),
|
||
)
|
||
)
|
||
for sid, scode, pid, ctime, pno, tname, mname, nname in cq.all():
|
||
rows.append(
|
||
{
|
||
"sampleSource": SAMPLE_SOURCE_CELL,
|
||
"sampleId": sid,
|
||
"sampleCode": scode or "",
|
||
"protocolId": pid or "",
|
||
"protocolNo": pno or "",
|
||
"sampleTypeName": tname or "",
|
||
"motherName": mname or "",
|
||
"newbornName": nname or "",
|
||
"createTime": ctime,
|
||
"freightId": "",
|
||
}
|
||
)
|
||
|
||
if src in (None, SAMPLE_SOURCE_DONATION):
|
||
dq = (
|
||
db.query(
|
||
BizResearchDonationSample.id,
|
||
BizResearchDonationSample.sample_code,
|
||
BizResearchDonationSample.research_donation_id,
|
||
BizResearchDonationSample.create_time,
|
||
BizResearchDonation.donation_no,
|
||
BizCellSampleType.type_name,
|
||
BizResearchDonationParty.mother_name,
|
||
BizResearchDonationParty.newborn_name,
|
||
)
|
||
.outerjoin(
|
||
BizResearchDonation,
|
||
and_(
|
||
BizResearchDonation.id
|
||
== BizResearchDonationSample.research_donation_id,
|
||
BizResearchDonation.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizCellSampleType,
|
||
and_(
|
||
BizCellSampleType.id == BizResearchDonationSample.sample_type_id,
|
||
BizCellSampleType.del_flag == 0,
|
||
),
|
||
)
|
||
.outerjoin(
|
||
BizResearchDonationParty,
|
||
and_(
|
||
BizResearchDonationParty.research_donation_id
|
||
== BizResearchDonationSample.research_donation_id,
|
||
BizResearchDonationParty.del_flag == 0,
|
||
),
|
||
)
|
||
.filter(
|
||
BizResearchDonationSample.tenant_no == tenant_no,
|
||
BizResearchDonationSample.del_flag == 0,
|
||
)
|
||
)
|
||
dq = dq.filter(
|
||
BizResearchDonation.tenant_no == tenant_no,
|
||
BizResearchDonation.del_flag == 0,
|
||
)
|
||
dq = apply_data_scope(dq, BizResearchDonation, user_info, db, tenant_no)
|
||
if like:
|
||
dq = dq.filter(
|
||
or_(
|
||
BizResearchDonationSample.sample_code.like(like),
|
||
BizResearchDonationParty.mother_name.like(like),
|
||
BizResearchDonationParty.newborn_name.like(like),
|
||
)
|
||
)
|
||
for sid, scode, pid, ctime, dno, tname, mname, nname in dq.all():
|
||
rows.append(
|
||
{
|
||
"sampleSource": SAMPLE_SOURCE_DONATION,
|
||
"sampleId": sid,
|
||
"sampleCode": scode or "",
|
||
"protocolId": pid or "",
|
||
"protocolNo": dno or "",
|
||
"sampleTypeName": tname or "",
|
||
"motherName": mname or "",
|
||
"newbornName": nname or "",
|
||
"createTime": ctime,
|
||
"freightId": "",
|
||
}
|
||
)
|
||
|
||
# 左连当前运费关联
|
||
if rows:
|
||
sample_ids = [r["sampleId"] for r in rows]
|
||
link_rows = (
|
||
db.query(
|
||
BizSampleFreightItem.sample_source,
|
||
BizSampleFreightItem.sample_id,
|
||
BizSampleFreightItem.freight_id,
|
||
)
|
||
.filter(
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.del_flag == 0,
|
||
BizSampleFreightItem.sample_id.in_(sample_ids),
|
||
)
|
||
.all()
|
||
)
|
||
freight_map = {
|
||
f"{src_}|{sid}": fid for src_, sid, fid in link_rows if src_ and sid and fid
|
||
}
|
||
for r in rows:
|
||
r["freightId"] = freight_map.get(f"{r['sampleSource']}|{r['sampleId']}", "") or ""
|
||
|
||
rows.sort(
|
||
key=lambda r: (r["createTime"] or datetime.min, r["sampleId"]),
|
||
reverse=True,
|
||
)
|
||
total = len(rows)
|
||
start = (page - 1) * page_size
|
||
page_rows = rows[start : start + page_size]
|
||
for r in page_rows:
|
||
r["createTime"] = _fmt_dt(r.get("createTime"))
|
||
return page_rows, total
|
||
|
||
|
||
def _upsert_item(
|
||
db: Session,
|
||
*,
|
||
freight_id: str,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
item: Dict[str, Any],
|
||
) -> None:
|
||
norm = _normalize_item(item)
|
||
existing = (
|
||
db.query(BizSampleFreightItem)
|
||
.filter(
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.sample_source == norm["sample_source"],
|
||
BizSampleFreightItem.sample_id == norm["sample_id"],
|
||
)
|
||
.order_by(BizSampleFreightItem.del_flag.asc(), BizSampleFreightItem.create_time.desc())
|
||
.first()
|
||
)
|
||
now = _now()
|
||
if existing:
|
||
existing.freight_id = freight_id
|
||
existing.protocol_id = norm["protocol_id"] or existing.protocol_id
|
||
existing.sample_code = norm["sample_code"] or existing.sample_code
|
||
existing.del_flag = 0
|
||
existing.update_by = user_id
|
||
existing.update_time = now
|
||
db.add(existing)
|
||
return
|
||
db.add(
|
||
BizSampleFreightItem(
|
||
id=generate_uuid(),
|
||
freight_id=freight_id,
|
||
sample_source=norm["sample_source"],
|
||
sample_id=norm["sample_id"],
|
||
sample_code=norm["sample_code"],
|
||
protocol_id=norm["protocol_id"],
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=None,
|
||
create_by=user_id,
|
||
create_dept_id="",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
)
|
||
|
||
|
||
def create_freight(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
transport_mode_raw: str,
|
||
freight_amount: Any,
|
||
items: Optional[List[Dict[str, Any]]] = None,
|
||
commit: bool = True,
|
||
user_info: Optional[dict] = None,
|
||
waybill_no: Any = None,
|
||
waybill_supplied: bool = True,
|
||
amount_supplied: bool = True,
|
||
) -> Dict[str, Any]:
|
||
if user_info is not None:
|
||
items = _validated_items(db, items or [], tenant_no, user_info)
|
||
mode = resolve_transport_mode_value(
|
||
db, tenant_no=tenant_no, raw=transport_mode_raw, user_id=user_id
|
||
)
|
||
plan = resolve_header_for_save(
|
||
transport_mode=mode,
|
||
existing_waybill=None,
|
||
existing_amount=None,
|
||
existing_settle_status=None,
|
||
waybill_supplied=waybill_supplied,
|
||
waybill_no=waybill_no,
|
||
amount_supplied=amount_supplied,
|
||
freight_amount=freight_amount,
|
||
)
|
||
now = _now()
|
||
obj = BizSampleFreight(
|
||
id=generate_uuid(),
|
||
freight_no=_generate_freight_no(db, tenant_no, when=now),
|
||
transport_mode=plan.transport_mode,
|
||
freight_amount=plan.freight_amount,
|
||
waybill_no=plan.waybill_no,
|
||
settle_status=plan.settle_status,
|
||
settle_time=None,
|
||
tenant_no=tenant_no,
|
||
order_num=0,
|
||
remark=None,
|
||
create_by=user_id,
|
||
create_dept_id=(get_user_dept_id(db, user_id) or "") if user_info is not None else "",
|
||
create_time=now,
|
||
update_by=user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(obj)
|
||
db.flush()
|
||
for it in items or []:
|
||
_upsert_item(db, freight_id=obj.id, tenant_no=tenant_no, user_id=user_id, item=it)
|
||
if commit:
|
||
db.commit()
|
||
else:
|
||
db.flush()
|
||
return get_freight_detail(db, obj.id, tenant_no)
|
||
|
||
|
||
def update_freight(
|
||
db: Session,
|
||
freight_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
transport_mode_raw: Optional[str] = None,
|
||
freight_amount: Any = None,
|
||
commit: bool = True,
|
||
user_info: Optional[dict] = None,
|
||
waybill_no: Any = None,
|
||
waybill_supplied: bool = False,
|
||
amount_supplied: bool = False,
|
||
) -> Dict[str, Any]:
|
||
obj = get_freight(db, freight_id, tenant_no, user_info)
|
||
if not obj:
|
||
raise ValueError("运费批次不存在")
|
||
mode = obj.transport_mode or ""
|
||
if transport_mode_raw is not None and str(transport_mode_raw).strip() != "":
|
||
mode = resolve_transport_mode_value(
|
||
db, tenant_no=tenant_no, raw=str(transport_mode_raw), user_id=user_id
|
||
)
|
||
plan = resolve_header_for_save(
|
||
transport_mode=mode,
|
||
existing_waybill=obj.waybill_no,
|
||
existing_amount=obj.freight_amount,
|
||
existing_settle_status=obj.settle_status,
|
||
waybill_supplied=waybill_supplied,
|
||
waybill_no=waybill_no,
|
||
amount_supplied=amount_supplied,
|
||
freight_amount=freight_amount,
|
||
)
|
||
obj.transport_mode = plan.transport_mode
|
||
obj.waybill_no = plan.waybill_no
|
||
obj.freight_amount = plan.freight_amount
|
||
obj.settle_status = plan.settle_status
|
||
if plan.clear_settle_time:
|
||
obj.settle_time = None
|
||
obj.update_by = user_id
|
||
obj.update_time = _now()
|
||
db.add(obj)
|
||
if commit:
|
||
db.commit()
|
||
else:
|
||
db.flush()
|
||
return get_freight_detail(db, freight_id, tenant_no)
|
||
|
||
|
||
def link_samples(
|
||
db: Session,
|
||
freight_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
items: List[Dict[str, Any]],
|
||
commit: bool = True,
|
||
user_info: Optional[dict] = None,
|
||
allow_settled_relink: bool = False,
|
||
) -> Dict[str, Any]:
|
||
header = get_freight(db, freight_id, tenant_no, user_info)
|
||
if not header:
|
||
raise ValueError("运费批次不存在")
|
||
if not allow_settled_relink:
|
||
assert_freight_mutable(header.settle_status, "items")
|
||
if not items:
|
||
raise ValueError("请选择要关联的样本")
|
||
if user_info is not None:
|
||
items = _validated_items(db, items, tenant_no, user_info)
|
||
for it in items:
|
||
_upsert_item(db, freight_id=freight_id, tenant_no=tenant_no, user_id=user_id, item=it)
|
||
if commit:
|
||
db.commit()
|
||
else:
|
||
db.flush()
|
||
return get_freight_detail(db, freight_id, tenant_no)
|
||
|
||
|
||
def unlink_samples(
|
||
db: Session,
|
||
freight_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
items: List[Dict[str, Any]],
|
||
commit: bool = True,
|
||
user_info: Optional[dict] = None,
|
||
) -> Dict[str, Any]:
|
||
header = get_freight(db, freight_id, tenant_no, user_info)
|
||
if not header:
|
||
raise ValueError("运费批次不存在")
|
||
assert_freight_mutable(header.settle_status, "items")
|
||
obj = header
|
||
now = _now()
|
||
for it in items or []:
|
||
norm = _normalize_item(it)
|
||
row = (
|
||
db.query(BizSampleFreightItem)
|
||
.filter(
|
||
BizSampleFreightItem.freight_id == freight_id,
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.sample_source == norm["sample_source"],
|
||
BizSampleFreightItem.sample_id == norm["sample_id"],
|
||
BizSampleFreightItem.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if not row:
|
||
continue
|
||
row.del_flag = 1
|
||
row.update_by = user_id
|
||
row.update_time = now
|
||
db.add(row)
|
||
db.flush()
|
||
remaining = _linked_count(db, freight_id, tenant_no)
|
||
if remaining == 0:
|
||
obj.del_flag = 1
|
||
obj.update_by = user_id
|
||
obj.update_time = now
|
||
db.add(obj)
|
||
if commit:
|
||
db.commit()
|
||
else:
|
||
db.flush()
|
||
if remaining == 0:
|
||
return {
|
||
"id": freight_id,
|
||
"deleted": True,
|
||
"linkedSampleCount": 0,
|
||
}
|
||
return get_freight_detail(db, freight_id, tenant_no)
|
||
|
||
|
||
def delete_freight(
|
||
db: Session,
|
||
freight_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
commit: bool = True,
|
||
user_info: Optional[dict] = None,
|
||
) -> Dict[str, Any]:
|
||
"""级联软删运费批次及其全部有效关联样本。"""
|
||
header = get_freight(db, freight_id, tenant_no, user_info)
|
||
if not header:
|
||
raise ValueError("运费批次不存在")
|
||
assert_freight_mutable(header.settle_status, "delete")
|
||
obj = (
|
||
db.query(BizSampleFreight)
|
||
.filter(
|
||
BizSampleFreight.id == freight_id,
|
||
BizSampleFreight.tenant_no == tenant_no,
|
||
BizSampleFreight.del_flag == 0,
|
||
)
|
||
.with_for_update()
|
||
.first()
|
||
)
|
||
if not obj:
|
||
raise ValueError("运费批次不存在")
|
||
now = _now()
|
||
items = (
|
||
db.query(BizSampleFreightItem)
|
||
.filter(
|
||
BizSampleFreightItem.freight_id == freight_id,
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.del_flag == 0,
|
||
)
|
||
.with_for_update()
|
||
.all()
|
||
)
|
||
for row in items:
|
||
row.del_flag = 1
|
||
row.update_by = user_id
|
||
row.update_time = now
|
||
db.add(row)
|
||
obj.del_flag = 1
|
||
obj.update_by = user_id
|
||
obj.update_time = now
|
||
db.add(obj)
|
||
if commit:
|
||
db.commit()
|
||
else:
|
||
db.flush()
|
||
return {
|
||
"id": freight_id,
|
||
"deleted": True,
|
||
"linkedSampleCount": 0,
|
||
}
|
||
|
||
|
||
def freight_amount_payload(
|
||
amount: Any, transport_mode: Optional[str], money_as_string: bool
|
||
) -> Dict[str, Any]:
|
||
if amount is None:
|
||
freight_amount: Any = None
|
||
elif money_as_string:
|
||
freight_amount = format(Decimal(str(amount)).quantize(Decimal("0.01")), "f")
|
||
else:
|
||
freight_amount = float(amount)
|
||
return {
|
||
"freightAmount": freight_amount,
|
||
"freightAmountLabel": freight_amount_label(transport_mode, amount),
|
||
}
|
||
|
||
|
||
def freight_map_for_samples(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
sample_source: str,
|
||
sample_ids: List[str],
|
||
money_as_string: bool = False,
|
||
) -> Dict[str, Dict[str, Any]]:
|
||
ids = [str(x).strip() for x in sample_ids if str(x or "").strip()]
|
||
if not ids:
|
||
return {}
|
||
rows = (
|
||
db.query(BizSampleFreightItem, BizSampleFreight)
|
||
.join(
|
||
BizSampleFreight,
|
||
and_(
|
||
BizSampleFreight.id == BizSampleFreightItem.freight_id,
|
||
BizSampleFreight.tenant_no == tenant_no,
|
||
BizSampleFreight.del_flag == 0,
|
||
),
|
||
)
|
||
.filter(
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.sample_source == sample_source,
|
||
BizSampleFreightItem.sample_id.in_(ids),
|
||
BizSampleFreightItem.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
if not rows:
|
||
return {}
|
||
freight_ids = list({f.id for _, f in rows})
|
||
count_rows = (
|
||
db.query(BizSampleFreightItem.freight_id, func.count(BizSampleFreightItem.id))
|
||
.filter(
|
||
BizSampleFreightItem.freight_id.in_(freight_ids),
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.del_flag == 0,
|
||
)
|
||
.group_by(BizSampleFreightItem.freight_id)
|
||
.all()
|
||
)
|
||
count_map = {fid: int(c) for fid, c in count_rows}
|
||
labels = _label_map(db, tenant_no, [f.transport_mode for _, f in rows])
|
||
out: Dict[str, Dict[str, Any]] = {}
|
||
for item, freight in rows:
|
||
out[item.sample_id] = {
|
||
"id": freight.id,
|
||
"transportMode": freight.transport_mode or "",
|
||
"transportModeLabel": labels.get(
|
||
freight.transport_mode or "", freight.transport_mode or ""
|
||
),
|
||
**freight_amount_payload(
|
||
freight.freight_amount, freight.transport_mode, money_as_string
|
||
),
|
||
"waybillNo": getattr(freight, "waybill_no", None) or "",
|
||
"settleStatus": getattr(freight, "settle_status", None) or "",
|
||
"linkedSampleCount": count_map.get(freight.id, 0),
|
||
}
|
||
return out
|
||
|
||
|
||
def _input_freight_id(raw: Any) -> str:
|
||
if not isinstance(raw, dict):
|
||
return ""
|
||
nested = raw.get("freight") if isinstance(raw.get("freight"), dict) else {}
|
||
return (
|
||
str(raw.get("freightId") or raw.get("freight_id") or "").strip()
|
||
or str(nested.get("id") or "").strip()
|
||
)
|
||
|
||
|
||
def apply_freight_on_protocol_save(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
user_id: Optional[str],
|
||
sample_source: str,
|
||
protocol_id: str,
|
||
saved_samples: list,
|
||
input_samples: list,
|
||
replaced_sample_ids: Optional[List[str]] = None,
|
||
) -> None:
|
||
"""协议保存挂钩:按 freightId / freightDraft 关联运费(flush,不单独 commit)。"""
|
||
old_ids = [
|
||
str(sample_id).strip()
|
||
for sample_id in (replaced_sample_ids or [])
|
||
if str(sample_id or "").strip()
|
||
]
|
||
stale_filters = [BizSampleFreightItem.protocol_id == protocol_id]
|
||
if old_ids:
|
||
stale_filters.append(BizSampleFreightItem.sample_id.in_(old_ids))
|
||
stale_items = (
|
||
db.query(BizSampleFreightItem)
|
||
.filter(
|
||
BizSampleFreightItem.tenant_no == tenant_no,
|
||
BizSampleFreightItem.sample_source == sample_source,
|
||
BizSampleFreightItem.del_flag == 0,
|
||
or_(*stale_filters),
|
||
)
|
||
.order_by(BizSampleFreightItem.id.asc())
|
||
.with_for_update()
|
||
.all()
|
||
)
|
||
now = _now()
|
||
affected_freight_ids = {item.freight_id for item in stale_items}
|
||
stale_by_freight: Dict[str, List[BizSampleFreightItem]] = {}
|
||
for item in stale_items:
|
||
stale_by_freight.setdefault(item.freight_id, []).append(item)
|
||
settled_relink_ids = set()
|
||
if stale_by_freight:
|
||
headers = (
|
||
db.query(BizSampleFreight)
|
||
.filter(
|
||
BizSampleFreight.id.in_(list(stale_by_freight.keys())),
|
||
BizSampleFreight.tenant_no == tenant_no,
|
||
BizSampleFreight.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
header_by_id = {row.id: row for row in headers}
|
||
for freight_id, grouped in stale_by_freight.items():
|
||
header = header_by_id.get(freight_id)
|
||
if header is None or (header.settle_status or "") != "yjs":
|
||
continue
|
||
stale_count = len(grouped)
|
||
keep_count = sum(
|
||
1
|
||
for raw in (input_samples or [])
|
||
if _input_freight_id(raw) == freight_id
|
||
)
|
||
if keep_count != stale_count:
|
||
raise ValueError("已结算的运费不能调整样本")
|
||
settled_relink_ids.add(freight_id)
|
||
keep_sample_ids = set()
|
||
for idx, sample_obj in enumerate(saved_samples or []):
|
||
if idx >= len(input_samples or []):
|
||
break
|
||
raw = input_samples[idx] if isinstance(input_samples[idx], dict) else {}
|
||
sample_id = str(getattr(sample_obj, "id", None) or "").strip()
|
||
if not sample_id:
|
||
continue
|
||
draft = raw.get("freightDraft")
|
||
if draft is None:
|
||
draft = raw.get("freight_draft")
|
||
if _input_freight_id(raw) or isinstance(draft, dict):
|
||
keep_sample_ids.add(sample_id)
|
||
removed_freight_item = False
|
||
for stale in stale_items:
|
||
if stale.sample_id in keep_sample_ids:
|
||
continue
|
||
stale.del_flag = 1
|
||
stale.update_by = user_id
|
||
stale.update_time = now
|
||
db.add(stale)
|
||
removed_freight_item = True
|
||
if removed_freight_item:
|
||
db.flush()
|
||
|
||
draft_groups: Dict[str, Dict[str, Any]] = {}
|
||
for idx, sample_obj in enumerate(saved_samples or []):
|
||
if idx >= len(input_samples or []):
|
||
break
|
||
raw = input_samples[idx] if isinstance(input_samples[idx], dict) else {}
|
||
sample_id = getattr(sample_obj, "id", None) or ""
|
||
sample_code = (
|
||
getattr(sample_obj, "sample_code", None)
|
||
or raw.get("sampleCode")
|
||
or raw.get("sample_code")
|
||
or ""
|
||
)
|
||
if not sample_id:
|
||
continue
|
||
item = {
|
||
"sampleSource": sample_source,
|
||
"sampleId": sample_id,
|
||
"protocolId": protocol_id,
|
||
"sampleCode": sample_code,
|
||
}
|
||
freight_id = _input_freight_id(raw)
|
||
draft = raw.get("freightDraft")
|
||
if draft is None:
|
||
draft = raw.get("freight_draft")
|
||
if freight_id:
|
||
link_samples(
|
||
db,
|
||
freight_id,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
items=[item],
|
||
commit=False,
|
||
allow_settled_relink=freight_id in settled_relink_ids,
|
||
)
|
||
continue
|
||
if isinstance(draft, dict):
|
||
client_key = str(draft.get("clientKey") or "").strip()
|
||
if not client_key:
|
||
client_key = f"__idx_{idx}"
|
||
group = draft_groups.get(client_key)
|
||
if not group:
|
||
draft_groups[client_key] = {
|
||
"transportMode": draft.get("transportMode") or draft.get("transport_mode"),
|
||
"freightAmount": draft.get("freightAmount")
|
||
if draft.get("freightAmount") is not None
|
||
else draft.get("freight_amount"),
|
||
"waybillNo": draft.get("waybillNo")
|
||
if draft.get("waybillNo") is not None
|
||
else draft.get("waybill_no"),
|
||
"items": [item],
|
||
}
|
||
else:
|
||
group["items"].append(item)
|
||
|
||
for group in draft_groups.values():
|
||
create_freight(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
user_id=user_id,
|
||
transport_mode_raw=str(group.get("transportMode") or ""),
|
||
freight_amount=group.get("freightAmount"),
|
||
waybill_no=group.get("waybillNo"),
|
||
waybill_supplied=True,
|
||
amount_supplied=True,
|
||
items=group["items"],
|
||
commit=False,
|
||
)
|
||
for freight_id in sorted(affected_freight_ids):
|
||
if _linked_count(db, freight_id, tenant_no) != 0:
|
||
continue
|
||
freight = (
|
||
db.query(BizSampleFreight)
|
||
.filter(
|
||
BizSampleFreight.id == freight_id,
|
||
BizSampleFreight.tenant_no == tenant_no,
|
||
BizSampleFreight.del_flag == 0,
|
||
)
|
||
.with_for_update()
|
||
.first()
|
||
)
|
||
if freight is not None:
|
||
assert_freight_mutable(freight.settle_status, "delete")
|
||
freight.del_flag = 1
|
||
freight.update_by = user_id
|
||
freight.update_time = now
|
||
db.add(freight)
|
||
db.flush()
|