Explorar el Código

feat(bx): add advance apply withdraw API

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong hace 1 mes
padre
commit
9d06e1830e

+ 17 - 0
backend/app/api/bx/advance.py

@@ -613,6 +613,23 @@ def reject_advance(
     return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
 
 
+@router.post("/{advance_id}/withdraw", response_model=ApiResponse[AdvanceApplyOut])
+def withdraw_advance(
+    advance_id: str,
+    db: Session = Depends(get_db),
+    user: BxUserContext = Depends(get_bx_current_user),
+):
+    advance = AdvanceService.withdraw(db, advance_id, user)
+    if not advance:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="备用金申请不存在、非本人申请或当前状态不可撤回",
+        )
+    db.commit()
+    db.refresh(advance)
+    return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
+
+
 @router.delete("/{advance_id}", response_model=ApiResponse[None])
 def delete_advance(
     advance_id: str,

+ 16 - 0
backend/app/services/bx/advance.py

@@ -347,6 +347,22 @@ class AdvanceService:
         db.flush()
         return apply_obj
 
+    @staticmethod
+    def withdraw(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxAdvanceApply]:
+        tenant_no = AdvanceService._tenant_no(db, user)
+        apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
+        if not apply_obj:
+            return None
+        if apply_obj.applicant_id != user.id:
+            return None
+        if apply_obj.status != "pending":
+            return None
+        apply_obj.status = "draft"
+        apply_obj.current_node = None
+        apply_obj.update_by = user.id
+        db.flush()
+        return apply_obj
+
     @staticmethod
     def _approval_record_dict(approval: BxAdvanceApproval, apply_id: str) -> Dict[str, Any]:
         return {

+ 56 - 0
backend/tests/test_bx_advance_withdraw.py

@@ -0,0 +1,56 @@
+"""备用金撤回:仅申请人且审批中可回草稿。"""
+from types import SimpleNamespace
+from unittest.mock import MagicMock, patch
+
+from app.services.bx.advance import AdvanceService
+
+
+def _user(uid="u1"):
+    return SimpleNamespace(id=uid, name="张三", dept_id="d1")
+
+
+def _apply(**kwargs):
+    base = {
+        "id": "a1",
+        "applicant_id": "u1",
+        "status": "pending",
+        "current_node": "一级审批",
+        "update_by": None,
+    }
+    base.update(kwargs)
+    return SimpleNamespace(**base)
+
+
+@patch.object(AdvanceService, "_tenant_no", return_value="T1")
+@patch.object(AdvanceService, "get_by_id")
+def test_withdraw_pending_by_applicant(mock_get, _tenant):
+    apply_obj = _apply()
+    mock_get.return_value = apply_obj
+    db = MagicMock()
+    out = AdvanceService.withdraw(db, "a1", _user("u1"))
+    assert out is apply_obj
+    assert apply_obj.status == "draft"
+    assert apply_obj.current_node is None
+    assert apply_obj.update_by == "u1"
+    db.flush.assert_called_once()
+
+
+@patch.object(AdvanceService, "_tenant_no", return_value="T1")
+@patch.object(AdvanceService, "get_by_id")
+def test_withdraw_rejects_non_applicant(mock_get, _tenant):
+    mock_get.return_value = _apply(applicant_id="u1")
+    assert AdvanceService.withdraw(MagicMock(), "a1", _user("u2")) is None
+
+
+@patch.object(AdvanceService, "_tenant_no", return_value="T1")
+@patch.object(AdvanceService, "get_by_id")
+def test_withdraw_rejects_non_pending(mock_get, _tenant):
+    mock_get.return_value = _apply(status="draft")
+    assert AdvanceService.withdraw(MagicMock(), "a1", _user("u1")) is None
+
+
+@patch.object(AdvanceService, "_tenant_no", return_value="T1")
+@patch.object(AdvanceService, "get_by_id")
+def test_withdraw_missing_returns_none(mock_get, _tenant):
+    mock_get.return_value = None
+    assert AdvanceService.withdraw(MagicMock(), "a1", _user("u1")) is None