feat: add presign transfer withdraw API for jfqrz.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-10 12:16:58 +08:00
1 parent d4477f357d
commit eb8a76d8d2
3 files changed
+211 -6

No files matched your search

+22
View File
@@ -46,6 +46,7 @@ class PresignOrderBody(BaseModel):
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")
collector_user_id: Optional[str] = Field(None, alias="collectorUserId")
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")
@@ -286,6 +287,27 @@ async def submit_presign_transfer_finance(
return error_response(400, str(e))
@router.post("/presign-transfers/{transfer_id}/withdraw")
async def withdraw_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.withdraw_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))
@router.get("/presign-transfers/receipt-confirm")
async def list_presign_receipt_confirm(
page: int = Query(1, ge=1),
+142 -6
View File
@@ -12,13 +12,19 @@ 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 (
BizCellSampleType,
BizCollectionProtocol,
BizPresignOrder,
BizPresignOrderSampleType,
BizPresignTransfer,
BizProtocolPartyCustomer,
SysUser,
)
from app.services.protocol_transfer_status import (
can_confirm_receipt,
can_submit_receipt,
can_withdraw_receipt,
)
from app.services.protocol_transfer_status import can_confirm_receipt, can_submit_receipt
def _now() -> datetime:
@@ -31,6 +37,17 @@ def _dec(v: Any) -> Optional[Decimal]:
return Decimal(str(v))
def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
ids = [str(x).strip() for x in user_ids if str(x or "").strip()]
if not ids:
return {}
rows = db.query(SysUser.id, SysUser.nick_name, SysUser.user_no).filter(SysUser.id.in_(ids)).all()
out: Dict[str, str] = {}
for u in rows:
out[str(u.id)] = (u.nick_name or u.user_no or "").strip()
return out
def assert_presign_convertible(*, order_status: Optional[str]) -> None:
st = (order_status or "").strip() or "draft"
if st == "converted":
@@ -39,6 +56,24 @@ def assert_presign_convertible(*, order_status: Optional[str]) -> None:
raise ValueError("已作废的预签约订单不能转正式")
def resolve_convert_staff_fields(
*,
collector_user_id: Optional[str],
sales_user_id: Optional[str],
region_manager_user_id: Optional[str],
fallback_user_id: Optional[str] = None,
) -> Dict[str, Optional[str]]:
"""转正式人员映射:采集专员→采集专员+市场专员;采集经理→地区经理。"""
_ = sales_user_id # 正式单市场专员以采集专员为准,不再沿用预签市场专员
collector = (collector_user_id or "").strip() or (fallback_user_id or "").strip() or None
region_mgr = (region_manager_user_id or "").strip() or None
return {
"collector_user_id": collector,
"sales_user_id": collector,
"region_manager_user_id": region_mgr,
}
def _get_order_or_raise(
db: Session, *, order_id: str, tenant_no: str
) -> BizPresignOrder:
@@ -117,6 +152,27 @@ def confirmed_deposit_amount(
return total
def _sample_type_names(
db: Session, *, presign_order_id: str, tenant_no: str
) -> str:
ids = list_sample_type_ids(
db, presign_order_id=presign_order_id, tenant_no=tenant_no
)
if not ids:
return ""
rows = (
db.query(BizCellSampleType.id, BizCellSampleType.type_name)
.filter(
BizCellSampleType.id.in_(ids),
BizCellSampleType.del_flag == 0,
)
.all()
)
name_map = {str(r.id): (r.type_name or "").strip() for r in rows}
names = [name_map.get(i, "") for i in ids if name_map.get(i)]
return "、".join(names)
def list_sample_type_ids(
db: Session, *, presign_order_id: str, tenant_no: str
) -> List[str]:
@@ -218,6 +274,7 @@ def order_to_dict(
else "",
"payMode": o.pay_mode or "",
"contractAmount": float(o.contract_amount) if o.contract_amount is not None else None,
"collectorUserId": o.collector_user_id or "",
"salesUserId": o.sales_user_id or "",
"regionManagerUserId": o.region_manager_user_id or "",
"orderStatus": o.order_status or "",
@@ -227,6 +284,17 @@ def order_to_dict(
"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 "",
}
name_map = _user_nick_name_map(
db,
[
data["collectorUserId"],
data["salesUserId"],
data["regionManagerUserId"],
],
)
data["collectorUserName"] = name_map.get(data["collectorUserId"], "")
data["salesUserName"] = name_map.get(data["salesUserId"], "")
data["regionManagerUserName"] = name_map.get(data["regionManagerUserId"], "")
if with_children:
data["sampleTypeIds"] = list_sample_type_ids(
db, presign_order_id=o.id, tenant_no=tenant_no
@@ -339,6 +407,11 @@ def create_presign_order(
sample_collect_time=sct,
pay_mode=pay_mode,
contract_amount=_dec(fields.get("contract_amount", fields.get("contractAmount"))),
collector_user_id=(
fields.get("collector_user_id") or fields.get("collectorUserId") or ""
).strip()
or user_id
or None,
sales_user_id=(fields.get("sales_user_id") or fields.get("salesUserId") or "").strip()
or None,
region_manager_user_id=(
@@ -411,6 +484,8 @@ def update_presign_order(
"hospitalId": "hospital_id",
"pay_mode": "pay_mode",
"payMode": "pay_mode",
"collector_user_id": "collector_user_id",
"collectorUserId": "collector_user_id",
"sales_user_id": "sales_user_id",
"salesUserId": "sales_user_id",
"region_manager_user_id": "region_manager_user_id",
@@ -430,6 +505,7 @@ def update_presign_order(
"region_id",
"hospital_id",
"pay_mode",
"collector_user_id",
"sales_user_id",
"region_manager_user_id",
"order_status",
@@ -540,7 +616,7 @@ def update_transfer_draft(
) -> BizPresignTransfer:
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
if t.pay_status != "dfj":
raise ValueError("仅待缴费状态可编辑意向金转账笔")
raise ValueError("仅已缴费状态可编辑意向金转账笔")
if "transfer_amount" in fields or "transferAmount" in fields:
t.transfer_amount = _dec(
fields.get("transfer_amount", fields.get("transferAmount"))
@@ -579,7 +655,7 @@ def submit_receipt_finance(
) -> 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("仅待缴费状态可提交财务确认")
raise ValueError("仅已缴费状态可提交财务确认")
if not (t.transfer_type or "").strip():
raise ValueError("请选择转账方式")
if t.transfer_amount is None or Decimal(str(t.transfer_amount)) <= 0:
@@ -593,6 +669,22 @@ def submit_receipt_finance(
return t
def withdraw_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_withdraw_receipt(t.pay_status):
raise ValueError("仅缴费确认中可撤回")
t.pay_status = "dfj"
# 故意不改 finance_remark / finance_confirm_attachments
t.update_by = user_id
t.update_time = _now()
db.add(t)
db.commit()
db.refresh(t)
return t
def confirm_receipt(
db: Session,
*,
@@ -690,6 +782,42 @@ def list_presign_transfers_for_finance(
item["motherName"] = (order.mother_name if order else "") or ""
item["sampleCode"] = ""
item["presignOrderId"] = order.id if order else ""
item["orderDate"] = (order.order_date if order else "") or ""
item["expectedDeliveryDate"] = (
(order.expected_delivery_date if order else "") or ""
)
item["contractAmount"] = (
float(order.contract_amount)
if order and order.contract_amount is not None
else None
)
item["contractPayMode"] = (order.pay_mode if order else "") or ""
item["orderStatus"] = (order.order_status if order else "") or ""
item["orderRemark"] = (order.remark if order else "") or ""
name_map = _user_nick_name_map(
db,
[
(order.collector_user_id if order else "") or "",
(order.sales_user_id if order else "") or "",
(order.region_manager_user_id if order else "") or "",
],
)
item["collectorUserName"] = name_map.get(
(order.collector_user_id if order else "") or "", ""
)
item["salesUserName"] = name_map.get(
(order.sales_user_id if order else "") or "", ""
)
item["regionManagerUserName"] = name_map.get(
(order.region_manager_user_id if order else "") or "", ""
)
item["sampleTypeNames"] = (
_sample_type_names(
db, presign_order_id=order.id, tenant_no=tenant_no
)
if order
else ""
)
out.append(item)
return out, int(total)
@@ -701,19 +829,26 @@ def convert_to_protocol(
tenant_no: str,
user_id: Optional[str],
) -> Dict[str, Any]:
"""转正式协议:创建协议主单 + 当事人/采集信息 + 样本类型行;不写入样本缴费金额。"""
"""转正式协议:创建协议主单 + 当事人/采集信息 + 样本类型行;意向金仅关联预签订单 ID,不拷贝。"""
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)
staff = resolve_convert_staff_fields(
collector_user_id=order.collector_user_id,
sales_user_id=order.sales_user_id,
region_manager_user_id=order.region_manager_user_id,
fallback_user_id=user_id,
)
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,
collector_user_id=staff["collector_user_id"],
source_presign_order_id=order.id,
payment_model_version=2,
protocol_total_amount=None,
@@ -731,7 +866,8 @@ def convert_to_protocol(
collection_fields: Dict[str, Any] = {
"region_id": order.region_id,
"hospital_id": order.hospital_id,
"sales_user_id": order.sales_user_id,
"sales_user_id": staff["sales_user_id"],
"region_manager_user_id": staff["region_manager_user_id"],
"sample_collect_time": order.sample_collect_time,
}
cp_svc.upsert_collection_info(
+47
View File
@@ -96,5 +96,52 @@ class TestProtocolDepositAssociation(unittest.TestCase):
)
class TestPresignWithdrawReceipt(unittest.TestCase):
def test_withdraw_sets_dfj_keeps_finance_remark(self):
from types import SimpleNamespace
from unittest.mock import MagicMock
from app.crud import presign_order as po
t = SimpleNamespace(
id="t1",
pay_status="jfqrz",
finance_remark="keep-me",
finance_confirm_attachments="http://x",
update_by=None,
update_time=None,
)
db = MagicMock()
orig = po._get_transfer_or_raise
po._get_transfer_or_raise = lambda *a, **k: t
try:
out = po.withdraw_receipt(
db, transfer_id="t1", tenant_no="zkxw", user_id="u1"
)
self.assertEqual(out.pay_status, "dfj")
self.assertEqual(out.finance_remark, "keep-me")
self.assertEqual(out.finance_confirm_attachments, "http://x")
db.commit.assert_called()
finally:
po._get_transfer_or_raise = orig
def test_withdraw_rejects_non_jfqrz(self):
from types import SimpleNamespace
from unittest.mock import MagicMock
from app.crud import presign_order as po
t = SimpleNamespace(id="t1", pay_status="yjf", finance_remark="x")
db = MagicMock()
orig = po._get_transfer_or_raise
po._get_transfer_or_raise = lambda *a, **k: t
try:
with self.assertRaises(ValueError) as ctx:
po.withdraw_receipt(
db, transfer_id="t1", tenant_no="zkxw", user_id="u1"
)
self.assertIn("撤回", str(ctx.exception))
finally:
po._get_transfer_or_raise = orig
if __name__ == "__main__":
unittest.main()