ソースを参照

feat: add presign transfer withdraw API for jfqrz.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 週間 前
親
コミット
eb8a76d8d2

+ 22 - 0
backend/app/api/presign_order.py

@@ -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
backend/app/crud/presign_order.py

@@ -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 - 0
backend/tests/test_presign_order.py

@@ -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()