feat: add presign transfer withdraw API for jfqrz.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
d4477f357d
commit
eb8a76d8d2
3 files changed
+211
-6
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user