feat: add presign order CRUD, deposit transfers, and convert API

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-03 11:18:32 +08:00
1 parent c404c168fe
commit fdd2c1fbc3
5 files changed
+1183 -1

No files matched your search

+2
View File
@@ -7,6 +7,7 @@ from app.api.basic_info import router as basic_info_router
from app.api.collection_protocol import router as collection_protocol_router
from app.api.protocol_sample_payment import router as protocol_sample_payment_router
from app.api.protocol_refund_record import router as protocol_refund_record_router
from app.api.presign_order import router as presign_order_router
from app.api.leave import router as leave_router
from app.api.operations import router as operations_router
from app.api.ethnic import router as ethnic_router
@@ -48,6 +49,7 @@ __all__ = [
"collection_protocol_router",
"protocol_sample_payment_router",
"protocol_refund_record_router",
"presign_order_router",
"leave_router",
"operations_router",
"ethnic_router",
+386
View File
@@ -0,0 +1,386 @@
"""预签约订单 / 意向金转账 / 转正式 API"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin
from app.core.response import error_response, success_response
from app.crud import presign_order as po_svc
from app.models.models import SysUser
router = APIRouter(prefix="/web/admin", tags=["细胞存储-预签约订单"])
def _get_tenant_no(db: Session, user: dict) -> str:
user_id = user.get("user_id")
if not user_id:
raise PermissionError("用户身份信息缺失,无法确定租户")
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
if not sys_user or not sys_user.tenant_no:
raise PermissionError("用户租户信息缺失")
return sys_user.tenant_no
def _get_current_user_id(user: dict) -> str:
user_id = user.get("user_id")
if not user_id:
raise PermissionError("用户身份信息缺失")
return str(user_id)
class PresignOrderBody(BaseModel):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
order_date: Optional[str] = Field(None, alias="orderDate")
protocol_no: Optional[str] = Field(None, alias="protocolNo")
mother_name: Optional[str] = Field(None, alias="motherName")
expected_delivery_date: Optional[str] = Field(None, alias="expectedDeliveryDate")
region_id: Optional[str] = Field(None, alias="regionId")
hospital_id: Optional[str] = Field(None, alias="hospitalId")
sample_collect_time: Optional[str] = Field(None, alias="sampleCollectTime")
pay_mode: Optional[str] = Field(None, alias="payMode")
contract_amount: Optional[float] = Field(None, alias="contractAmount")
sales_user_id: Optional[str] = Field(None, alias="salesUserId")
region_manager_user_id: Optional[str] = Field(None, alias="regionManagerUserId")
order_status: Optional[str] = Field(None, alias="orderStatus")
remark: Optional[str] = None
sample_type_ids: Optional[List[str]] = Field(None, alias="sampleTypeIds")
transfers: Optional[List[Dict[str, Any]]] = None
class PresignTransferBody(BaseModel):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
transfer_amount: Optional[float] = Field(None, alias="transferAmount")
transfer_time: Optional[str] = Field(None, alias="transferTime")
transfer_type: Optional[str] = Field(None, alias="transferType")
transfer_attachments: Optional[str] = Field(None, alias="transferAttachments")
remark: Optional[str] = None
class FinanceConfirmBody(BaseModel):
model_config = ConfigDict(extra="ignore", populate_by_name=True)
finance_confirm_attachments: Optional[str] = Field(
None, alias="financeConfirmAttachments"
)
finance_remark: Optional[str] = Field(None, alias="financeRemark")
@router.get("/presign-orders")
async def list_presign_orders(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
orderStatus: Optional[str] = Query(None, alias="orderStatus"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
items, total = po_svc.list_presign_orders(
db,
tenant_no=tenant_no,
page=page,
page_size=pageSize,
keyword=keyword,
order_status=orderStatus,
user_info=user,
)
return success_response(
{"list": items, "total": total, "page": page, "pageSize": pageSize}
)
except PermissionError as e:
return error_response(403, str(e))
@router.get("/presign-orders/{order_id}")
async def get_presign_order(
order_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = po_svc.get_presign_order(db, order_id=order_id, tenant_no=tenant_no)
if not data:
return error_response(404, "预签约订单不存在")
return success_response(data)
except PermissionError as e:
return error_response(403, str(e))
@router.post("/presign-orders")
async def create_presign_order(
body: PresignOrderBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
dumped = body.model_dump(by_alias=False)
obj = po_svc.create_presign_order(
db,
tenant_no=tenant_no,
user_id=user_id,
fields=dumped,
sample_type_ids=body.sample_type_ids,
transfers=body.transfers,
)
return success_response(
po_svc.order_to_dict(db, obj, tenant_no, with_children=True),
message="创建成功",
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.put("/presign-orders/{order_id}")
async def update_presign_order(
order_id: str,
body: PresignOrderBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
dumped = body.model_dump(by_alias=False, exclude_unset=True)
sample_type_ids = dumped.pop("sample_type_ids", None)
dumped.pop("transfers", None)
obj = po_svc.update_presign_order(
db,
order_id=order_id,
tenant_no=tenant_no,
user_id=user_id,
fields=dumped,
sample_type_ids=sample_type_ids,
)
return success_response(
po_svc.order_to_dict(db, obj, tenant_no, with_children=True),
message="保存成功",
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.delete("/presign-orders/{order_id}")
async def delete_presign_order(
order_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
po_svc.soft_delete_presign_order(
db, order_id=order_id, tenant_no=tenant_no, user_id=user_id
)
return success_response(None, message="删除成功")
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.post("/presign-orders/{order_id}/convert")
async def convert_presign_order(
order_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
data = po_svc.convert_to_protocol(
db, order_id=order_id, tenant_no=tenant_no, user_id=user_id
)
return success_response(data, message="已转正式协议")
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.post("/presign-orders/{order_id}/transfers")
async def create_presign_transfer(
order_id: str,
body: PresignTransferBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = po_svc.add_transfer(
db,
presign_order_id=order_id,
tenant_no=tenant_no,
user_id=user_id,
fields=body.model_dump(by_alias=False),
)
return success_response(
po_svc._transfer_to_dict(db, obj, tenant_no), message="添加成功"
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.put("/presign-transfers/{transfer_id}")
async def update_presign_transfer(
transfer_id: str,
body: PresignTransferBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = po_svc.update_transfer_draft(
db,
transfer_id=transfer_id,
tenant_no=tenant_no,
user_id=user_id,
fields=body.model_dump(by_alias=False, exclude_unset=True),
)
return success_response(
po_svc._transfer_to_dict(db, obj, tenant_no), message="保存成功"
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.post("/presign-transfers/{transfer_id}/submit-receipt-finance")
async def submit_presign_transfer_finance(
transfer_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = po_svc.submit_receipt_finance(
db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
)
return success_response(
po_svc._transfer_to_dict(db, obj, tenant_no), message="已提交财务确认"
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.get("/presign-transfers/receipt-confirm")
async def list_presign_receipt_confirm(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
items, total = po_svc.list_presign_transfers_for_finance(
db,
pay_status="jfqrz",
tenant_no=tenant_no,
keyword=keyword,
page=page,
page_size=pageSize,
user_info=user,
)
return success_response(
{"list": items, "total": total, "page": page, "pageSize": pageSize}
)
except PermissionError as e:
return error_response(403, str(e))
@router.get("/presign-transfers/receipt-confirmed")
async def list_presign_receipt_confirmed(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = None,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
items, total = po_svc.list_presign_transfers_for_finance(
db,
pay_status="yjf",
tenant_no=tenant_no,
keyword=keyword,
page=page,
page_size=pageSize,
user_info=user,
)
return success_response(
{"list": items, "total": total, "page": page, "pageSize": pageSize}
)
except PermissionError as e:
return error_response(403, str(e))
@router.post("/presign-transfers/{transfer_id}/receipt-confirm")
async def confirm_presign_transfer(
transfer_id: str,
body: Optional[FinanceConfirmBody] = None,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = po_svc.confirm_receipt(
db,
transfer_id=transfer_id,
tenant_no=tenant_no,
user_id=user_id,
finance_confirm_attachments=body.finance_confirm_attachments if body else None,
finance_remark=body.finance_remark if body else None,
)
return success_response(
po_svc._transfer_to_dict(db, obj, tenant_no), message="确认收款成功"
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
@router.post("/presign-transfers/{transfer_id}/receipt-reject")
async def reject_presign_transfer(
transfer_id: str,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = _get_current_user_id(user)
obj = po_svc.reject_receipt(
db, transfer_id=transfer_id, tenant_no=tenant_no, user_id=user_id
)
return success_response(
po_svc._transfer_to_dict(db, obj, tenant_no), message="已退回"
)
except PermissionError as e:
return error_response(403, str(e))
except ValueError as e:
return error_response(400, str(e))
+763
View File
@@ -0,0 +1,763 @@
"""预签约订单 / 意向金转账 / 转正式协议 CRUD"""
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid, parse_datetime
from app.crud.collection_protocol import urls_to_attachment_list
from app.models.models import (
BizCollectionProtocol,
BizPresignOrder,
BizPresignOrderSampleType,
BizPresignTransfer,
BizProtocolPartyCustomer,
)
from app.services.protocol_transfer_status import can_confirm_receipt, can_submit_receipt
def _now() -> datetime:
return datetime.now()
def _dec(v: Any) -> Optional[Decimal]:
if v is None or v == "":
return None
return Decimal(str(v))
def assert_presign_convertible(*, order_status: Optional[str]) -> None:
st = (order_status or "").strip() or "draft"
if st == "converted":
raise ValueError("该预签约订单已转正式协议")
if st == "void":
raise ValueError("已作废的预签约订单不能转正式")
def _get_order_or_raise(
db: Session, *, order_id: str, tenant_no: str
) -> BizPresignOrder:
obj = (
db.query(BizPresignOrder)
.filter(
BizPresignOrder.id == order_id,
BizPresignOrder.tenant_no == tenant_no,
BizPresignOrder.del_flag == 0,
)
.first()
)
if not obj:
raise ValueError("预签约订单不存在")
return obj
def _get_transfer_or_raise(
db: Session, *, transfer_id: str, tenant_no: str
) -> BizPresignTransfer:
obj = (
db.query(BizPresignTransfer)
.filter(
BizPresignTransfer.id == transfer_id,
BizPresignTransfer.tenant_no == tenant_no,
BizPresignTransfer.del_flag == 0,
)
.first()
)
if not obj:
raise ValueError("意向金转账笔不存在")
return obj
def _transfer_to_dict(
db: Session, t: BizPresignTransfer, tenant_no: str
) -> Dict[str, Any]:
return {
"id": t.id,
"presignOrderId": t.presign_order_id,
"transferAmount": float(t.transfer_amount) if t.transfer_amount is not None else None,
"transferTime": t.transfer_time.strftime("%Y-%m-%d %H:%M:%S") if t.transfer_time else "",
"transferType": t.transfer_type or "",
"transferAttachments": t.transfer_attachments or "",
"attachments": urls_to_attachment_list(t.transfer_attachments, db, tenant_no),
"payStatus": t.pay_status or "",
"financeConfirmAttachments": t.finance_confirm_attachments or "",
"financeConfirmAttachmentFiles": urls_to_attachment_list(
t.finance_confirm_attachments, db, tenant_no
),
"financeRemark": t.finance_remark or "",
"remark": t.remark or "",
"source": "presign",
"createTime": t.create_time.strftime("%Y-%m-%d %H:%M:%S") if t.create_time else "",
"updateTime": t.update_time.strftime("%Y-%m-%d %H:%M:%S") if t.update_time else "",
}
def confirmed_deposit_amount(
db: Session, *, presign_order_id: str, tenant_no: str
) -> Decimal:
rows = (
db.query(BizPresignTransfer.transfer_amount)
.filter(
BizPresignTransfer.presign_order_id == presign_order_id,
BizPresignTransfer.tenant_no == tenant_no,
BizPresignTransfer.del_flag == 0,
BizPresignTransfer.pay_status == "yjf",
)
.all()
)
total = Decimal("0")
for (amt,) in rows:
if amt is not None:
total += Decimal(str(amt))
return total
def list_sample_type_ids(
db: Session, *, presign_order_id: str, tenant_no: str
) -> List[str]:
rows = (
db.query(BizPresignOrderSampleType)
.filter(
BizPresignOrderSampleType.presign_order_id == presign_order_id,
BizPresignOrderSampleType.tenant_no == tenant_no,
BizPresignOrderSampleType.del_flag == 0,
)
.order_by(
BizPresignOrderSampleType.order_num.asc(),
BizPresignOrderSampleType.create_time.asc(),
)
.all()
)
return [r.sample_type_id for r in rows if r.sample_type_id]
def replace_sample_types(
db: Session,
*,
presign_order_id: str,
tenant_no: str,
user_id: Optional[str],
sample_type_ids: List[str],
) -> None:
now = _now()
existing = (
db.query(BizPresignOrderSampleType)
.filter(
BizPresignOrderSampleType.presign_order_id == presign_order_id,
BizPresignOrderSampleType.tenant_no == tenant_no,
BizPresignOrderSampleType.del_flag == 0,
)
.all()
)
for r in existing:
r.del_flag = 1
r.update_by = user_id
r.update_time = now
db.add(r)
seen = set()
for idx, tid in enumerate(sample_type_ids or []):
sid = (tid or "").strip()
if not sid or sid in seen:
continue
seen.add(sid)
db.add(
BizPresignOrderSampleType(
id=generate_uuid(),
presign_order_id=presign_order_id,
sample_type_id=sid,
tenant_no=tenant_no,
order_num=idx,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
)
db.flush()
def list_transfers(
db: Session, *, presign_order_id: str, tenant_no: str
) -> List[Dict[str, Any]]:
rows = (
db.query(BizPresignTransfer)
.filter(
BizPresignTransfer.presign_order_id == presign_order_id,
BizPresignTransfer.tenant_no == tenant_no,
BizPresignTransfer.del_flag == 0,
)
.order_by(BizPresignTransfer.create_time.asc())
.all()
)
return [_transfer_to_dict(db, t, tenant_no) for t in rows]
def order_to_dict(
db: Session, o: BizPresignOrder, tenant_no: str, *, with_children: bool = True
) -> Dict[str, Any]:
deposit = confirmed_deposit_amount(
db, presign_order_id=o.id, tenant_no=tenant_no
)
data: Dict[str, Any] = {
"id": o.id,
"orderDate": o.order_date or "",
"protocolNo": o.protocol_no or "",
"motherName": o.mother_name or "",
"expectedDeliveryDate": o.expected_delivery_date or "",
"regionId": o.region_id or "",
"hospitalId": o.hospital_id or "",
"sampleCollectTime": o.sample_collect_time.strftime("%Y-%m-%d %H:%M:%S")
if o.sample_collect_time
else "",
"payMode": o.pay_mode or "",
"contractAmount": float(o.contract_amount) if o.contract_amount is not None else None,
"salesUserId": o.sales_user_id or "",
"regionManagerUserId": o.region_manager_user_id or "",
"orderStatus": o.order_status or "",
"convertedProtocolId": o.converted_protocol_id or "",
"confirmedDepositAmount": float(deposit),
"remark": o.remark or "",
"createTime": o.create_time.strftime("%Y-%m-%d %H:%M:%S") if o.create_time else "",
"updateTime": o.update_time.strftime("%Y-%m-%d %H:%M:%S") if o.update_time else "",
}
if with_children:
data["sampleTypeIds"] = list_sample_type_ids(
db, presign_order_id=o.id, tenant_no=tenant_no
)
data["transfers"] = list_transfers(
db, presign_order_id=o.id, tenant_no=tenant_no
)
return data
def list_presign_orders(
db: Session,
*,
tenant_no: str,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
order_status: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
q = db.query(BizPresignOrder).filter(
BizPresignOrder.tenant_no == tenant_no,
BizPresignOrder.del_flag == 0,
)
st = (order_status or "").strip()
if st:
q = q.filter(BizPresignOrder.order_status == st)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q = q.filter(
or_(
BizPresignOrder.protocol_no.like(like),
BizPresignOrder.mother_name.like(like),
)
)
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(q, BizPresignOrder, user_info, db, tenant_no)
total = q.order_by(None).with_entities(func.count(BizPresignOrder.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q.order_by(BizPresignOrder.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [
order_to_dict(db, o, tenant_no, with_children=False) for o in rows
], int(total)
def get_presign_order(
db: Session, *, order_id: str, tenant_no: str
) -> Optional[Dict[str, Any]]:
obj = (
db.query(BizPresignOrder)
.filter(
BizPresignOrder.id == order_id,
BizPresignOrder.tenant_no == tenant_no,
BizPresignOrder.del_flag == 0,
)
.first()
)
if not obj:
return None
return order_to_dict(db, obj, tenant_no, with_children=True)
def create_presign_order(
db: Session,
*,
tenant_no: str,
user_id: Optional[str],
fields: Dict[str, Any],
sample_type_ids: Optional[List[str]] = None,
transfers: Optional[List[Dict[str, Any]]] = None,
) -> BizPresignOrder:
now = _now()
sample_collect = fields.get("sample_collect_time") or fields.get("sampleCollectTime")
sct = None
if sample_collect not in (None, ""):
sct = (
sample_collect
if isinstance(sample_collect, datetime)
else parse_datetime(str(sample_collect))
)
pay_mode = (fields.get("pay_mode") or fields.get("payMode") or "").strip() or None
if pay_mode and pay_mode not in ("dj", "fq"):
raise ValueError("缴费方式无效")
obj = BizPresignOrder(
id=generate_uuid(),
order_date=(fields.get("order_date") or fields.get("orderDate") or "").strip()
or None,
protocol_no=(fields.get("protocol_no") or fields.get("protocolNo") or "").strip()
or None,
mother_name=(fields.get("mother_name") or fields.get("motherName") or "").strip()
or None,
expected_delivery_date=(
fields.get("expected_delivery_date")
or fields.get("expectedDeliveryDate")
or ""
).strip()
or None,
region_id=(fields.get("region_id") or fields.get("regionId") or "").strip() or None,
hospital_id=(fields.get("hospital_id") or fields.get("hospitalId") or "").strip()
or None,
sample_collect_time=sct,
pay_mode=pay_mode,
contract_amount=_dec(fields.get("contract_amount", fields.get("contractAmount"))),
sales_user_id=(fields.get("sales_user_id") or fields.get("salesUserId") or "").strip()
or None,
region_manager_user_id=(
fields.get("region_manager_user_id")
or fields.get("regionManagerUserId")
or ""
).strip()
or None,
order_status=(fields.get("order_status") or fields.get("orderStatus") or "active")
.strip()
or "active",
tenant_no=tenant_no,
order_num=0,
remark=(fields.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(obj)
db.flush()
replace_sample_types(
db,
presign_order_id=obj.id,
tenant_no=tenant_no,
user_id=user_id,
sample_type_ids=sample_type_ids or [],
)
for raw in transfers or []:
if isinstance(raw, dict):
add_transfer(
db,
presign_order_id=obj.id,
tenant_no=tenant_no,
user_id=user_id,
fields=raw,
commit=False,
)
db.commit()
db.refresh(obj)
return obj
def update_presign_order(
db: Session,
*,
order_id: str,
tenant_no: str,
user_id: Optional[str],
fields: Dict[str, Any],
sample_type_ids: Optional[List[str]] = None,
) -> BizPresignOrder:
obj = _get_order_or_raise(db, order_id=order_id, tenant_no=tenant_no)
if (obj.order_status or "") == "converted":
raise ValueError("已转正式的预签约订单不可修改")
mapping = {
"order_date": "order_date",
"orderDate": "order_date",
"protocol_no": "protocol_no",
"protocolNo": "protocol_no",
"mother_name": "mother_name",
"motherName": "mother_name",
"expected_delivery_date": "expected_delivery_date",
"expectedDeliveryDate": "expected_delivery_date",
"region_id": "region_id",
"regionId": "region_id",
"hospital_id": "hospital_id",
"hospitalId": "hospital_id",
"pay_mode": "pay_mode",
"payMode": "pay_mode",
"sales_user_id": "sales_user_id",
"salesUserId": "sales_user_id",
"region_manager_user_id": "region_manager_user_id",
"regionManagerUserId": "region_manager_user_id",
"order_status": "order_status",
"orderStatus": "order_status",
"remark": "remark",
}
for k, attr in mapping.items():
if k in fields:
val = fields[k]
if attr in (
"order_date",
"protocol_no",
"mother_name",
"expected_delivery_date",
"region_id",
"hospital_id",
"pay_mode",
"sales_user_id",
"region_manager_user_id",
"order_status",
"remark",
):
setattr(obj, attr, (str(val).strip() if val is not None else "") or None)
if "contract_amount" in fields or "contractAmount" in fields:
obj.contract_amount = _dec(
fields.get("contract_amount", fields.get("contractAmount"))
)
if "sample_collect_time" in fields or "sampleCollectTime" in fields:
v = fields.get("sample_collect_time", fields.get("sampleCollectTime"))
if v in (None, ""):
obj.sample_collect_time = None
elif isinstance(v, datetime):
obj.sample_collect_time = v
else:
obj.sample_collect_time = parse_datetime(str(v))
if obj.pay_mode and obj.pay_mode not in ("dj", "fq"):
raise ValueError("缴费方式无效")
obj.update_by = user_id
obj.update_time = _now()
db.add(obj)
if sample_type_ids is not None:
replace_sample_types(
db,
presign_order_id=obj.id,
tenant_no=tenant_no,
user_id=user_id,
sample_type_ids=sample_type_ids,
)
db.commit()
db.refresh(obj)
return obj
def soft_delete_presign_order(
db: Session, *, order_id: str, tenant_no: str, user_id: Optional[str]
) -> None:
obj = _get_order_or_raise(db, order_id=order_id, tenant_no=tenant_no)
if (obj.order_status or "") == "converted":
raise ValueError("已转正式的预签约订单不可删除")
now = _now()
obj.del_flag = 1
obj.update_by = user_id
obj.update_time = now
db.add(obj)
db.commit()
def add_transfer(
db: Session,
*,
presign_order_id: str,
tenant_no: str,
user_id: Optional[str],
fields: Dict[str, Any],
commit: bool = True,
) -> BizPresignTransfer:
_get_order_or_raise(db, order_id=presign_order_id, tenant_no=tenant_no)
now = _now()
tt = fields.get("transfer_time", fields.get("transferTime"))
transfer_time = None
if tt not in (None, ""):
transfer_time = tt if isinstance(tt, datetime) else parse_datetime(str(tt))
obj = BizPresignTransfer(
id=generate_uuid(),
presign_order_id=presign_order_id,
transfer_amount=_dec(fields.get("transfer_amount", fields.get("transferAmount"))),
transfer_time=transfer_time,
transfer_type=(
fields.get("transfer_type") or fields.get("transferType") or ""
).strip()
or None,
transfer_attachments=(
fields.get("transfer_attachments")
or fields.get("transferAttachments")
or ""
).strip()
or None,
pay_status="dfj",
tenant_no=tenant_no,
order_num=0,
remark=(fields.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(obj)
if commit:
db.commit()
db.refresh(obj)
else:
db.flush()
return obj
def update_transfer_draft(
db: Session,
*,
transfer_id: str,
tenant_no: str,
user_id: Optional[str],
fields: Dict[str, Any],
) -> BizPresignTransfer:
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
if t.pay_status != "dfj":
raise ValueError("仅待缴费状态可编辑意向金转账笔")
if "transfer_amount" in fields or "transferAmount" in fields:
t.transfer_amount = _dec(
fields.get("transfer_amount", fields.get("transferAmount"))
)
if "transfer_time" in fields or "transferTime" in fields:
v = fields.get("transfer_time", fields.get("transferTime"))
if v in (None, ""):
t.transfer_time = None
elif isinstance(v, datetime):
t.transfer_time = v
else:
t.transfer_time = parse_datetime(str(v))
if "transfer_type" in fields or "transferType" in fields:
t.transfer_type = (
str(fields.get("transfer_type", fields.get("transferType")) or "")
).strip() or None
if "transfer_attachments" in fields or "transferAttachments" in fields:
t.transfer_attachments = (
str(
fields.get("transfer_attachments", fields.get("transferAttachments"))
or ""
)
).strip() or None
if "remark" in fields:
t.remark = (fields.get("remark") or "").strip() or None
t.update_by = user_id
t.update_time = _now()
db.add(t)
db.commit()
db.refresh(t)
return t
def submit_receipt_finance(
db: Session, *, transfer_id: str, tenant_no: str, user_id: Optional[str]
) -> BizPresignTransfer:
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
if not can_submit_receipt(t.pay_status):
raise ValueError("仅待缴费状态可提交财务确认")
if not (t.transfer_type or "").strip():
raise ValueError("请选择转账方式")
if t.transfer_amount is None or Decimal(str(t.transfer_amount)) <= 0:
raise ValueError("请填写转账金额")
t.pay_status = "jfqrz"
t.update_by = user_id
t.update_time = _now()
db.add(t)
db.commit()
db.refresh(t)
return t
def confirm_receipt(
db: Session,
*,
transfer_id: str,
tenant_no: str,
user_id: Optional[str],
finance_confirm_attachments: Optional[str] = None,
finance_remark: Optional[str] = None,
) -> BizPresignTransfer:
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
if not can_confirm_receipt(t.pay_status):
raise ValueError("仅缴费确认中状态可确认收款")
t.pay_status = "yjf"
if finance_confirm_attachments is not None:
t.finance_confirm_attachments = (finance_confirm_attachments or "").strip() or None
if finance_remark is not None:
t.finance_remark = (finance_remark or "").strip() or None
t.update_by = user_id
t.update_time = _now()
db.add(t)
db.commit()
db.refresh(t)
return t
def reject_receipt(
db: Session, *, transfer_id: str, tenant_no: str, user_id: Optional[str]
) -> BizPresignTransfer:
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
if not can_confirm_receipt(t.pay_status):
raise ValueError("仅缴费确认中状态可退回")
t.pay_status = "dfj"
t.update_by = user_id
t.update_time = _now()
db.add(t)
db.commit()
db.refresh(t)
return t
def list_presign_transfers_for_finance(
db: Session,
*,
pay_status: str,
tenant_no: str,
keyword: Optional[str] = None,
page: int = 1,
page_size: int = 10,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
if not tenant_no:
return [], 0
st = (pay_status or "").strip()
q = (
db.query(BizPresignTransfer, BizPresignOrder)
.join(
BizPresignOrder,
and_(
BizPresignOrder.id == BizPresignTransfer.presign_order_id,
BizPresignOrder.del_flag == 0,
),
)
.filter(
BizPresignTransfer.tenant_no == tenant_no,
BizPresignTransfer.del_flag == 0,
BizPresignTransfer.pay_status == st,
)
)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q = q.filter(
or_(
BizPresignOrder.protocol_no.like(like),
BizPresignOrder.mother_name.like(like),
)
)
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(q, BizPresignTransfer, user_info, db, tenant_no)
total = q.order_by(None).with_entities(func.count(BizPresignTransfer.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
q.order_by(BizPresignTransfer.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
out: List[Dict[str, Any]] = []
for t, order in rows:
item = _transfer_to_dict(db, t, tenant_no)
item["protocolNo"] = (order.protocol_no if order else "") or ""
item["motherName"] = (order.mother_name if order else "") or ""
item["sampleCode"] = ""
item["presignOrderId"] = order.id if order else ""
out.append(item)
return out, int(total)
def convert_to_protocol(
db: Session,
*,
order_id: str,
tenant_no: str,
user_id: Optional[str],
) -> Dict[str, Any]:
"""转正式协议:创建协议主单 + 当事人/采集信息 + 样本类型行;不写入样本缴费金额。"""
from app.crud import collection_protocol as cp_svc
order = _get_order_or_raise(db, order_id=order_id, tenant_no=tenant_no)
assert_presign_convertible(order_status=order.order_status)
type_ids = list_sample_type_ids(db, presign_order_id=order.id, tenant_no=tenant_no)
protocol = cp_svc.create_collection_protocol(
db,
protocol_no=order.protocol_no or "",
protocol_status="dsx",
register_date=order.order_date,
collector_user_id=user_id,
source_presign_order_id=order.id,
payment_model_version=2,
protocol_total_amount=None,
remark=order.remark,
tenant_no=tenant_no,
user_id=user_id,
)
party_fields: Dict[str, Any] = {
"mother_name": order.mother_name,
"expected_delivery_date": order.expected_delivery_date,
}
cp_svc.upsert_party_customer(
db, protocol.id, tenant_no, user_id, **party_fields
)
collection_fields: Dict[str, Any] = {
"region_id": order.region_id,
"hospital_id": order.hospital_id,
"sales_user_id": order.sales_user_id,
"sample_collect_time": order.sample_collect_time,
}
cp_svc.upsert_collection_info(
db, protocol.id, tenant_no, user_id, **collection_fields
)
if type_ids:
samples = [
{
"sampleTypeId": tid,
"sampleStatus": None,
}
for tid in type_ids
]
cp_svc.save_samples(db, protocol.id, tenant_no, user_id, samples)
order.order_status = "converted"
order.converted_protocol_id = protocol.id
if not (order.protocol_no or "").strip():
order.protocol_no = protocol.protocol_no
order.update_by = user_id
order.update_time = _now()
db.add(order)
db.commit()
db.refresh(order)
return {
"presignOrder": order_to_dict(db, order, tenant_no, with_children=True),
"protocolId": protocol.id,
"protocolNo": protocol.protocol_no,
}
+2 -1
View File
@@ -13,7 +13,7 @@ from app.core.database import SessionLocal
from app.core.api_request_log_middleware import ApiRequestLogMiddleware
from app.core.api_crypto_middleware import ApiCryptoMiddleware
from app.models import models # noqa: F401
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, presign_order_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router
@asynccontextmanager
@@ -119,6 +119,7 @@ app.include_router(basic_info_router, prefix=settings.API_V1_PREFIX)
app.include_router(collection_protocol_router, prefix=settings.API_V1_PREFIX)
app.include_router(protocol_sample_payment_router, prefix=settings.API_V1_PREFIX)
app.include_router(protocol_refund_record_router, prefix=settings.API_V1_PREFIX)
app.include_router(presign_order_router, prefix=settings.API_V1_PREFIX)
app.include_router(leave_router, prefix=settings.API_V1_PREFIX)
app.include_router(operations_router, prefix=settings.API_V1_PREFIX)
app.include_router(ethnic_router, prefix=settings.API_V1_PREFIX)
+30
View File
@@ -0,0 +1,30 @@
"""预签约订单:意向金状态机与转正约束单测"""
import unittest
from app.services.protocol_transfer_status import can_confirm_receipt, can_submit_receipt
class TestPresignTransferStatus(unittest.TestCase):
def test_presign_transfer_same_status_machine(self):
self.assertTrue(can_submit_receipt("dfj"))
self.assertFalse(can_submit_receipt("jfqrz"))
self.assertFalse(can_submit_receipt("yjf"))
self.assertTrue(can_confirm_receipt("jfqrz"))
self.assertFalse(can_confirm_receipt("dfj"))
class TestConvertRules(unittest.TestCase):
def test_convert_requires_active_status(self):
from app.crud.presign_order import assert_presign_convertible
with self.assertRaises(ValueError):
assert_presign_convertible(order_status="converted")
with self.assertRaises(ValueError):
assert_presign_convertible(order_status="void")
assert_presign_convertible(order_status="active")
assert_presign_convertible(order_status="draft")
if __name__ == "__main__":
unittest.main()