| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- """车辆充值审批工作流:状态常量与核心流转单元测试。"""
- from __future__ import annotations
- from datetime import date, datetime
- from decimal import Decimal
- from types import SimpleNamespace
- import pytest
- from sqlalchemy import create_engine
- from sqlalchemy.orm import sessionmaker
- from app.models.bx_models import BxWorkflowDef, BxWorkflowNode
- from app.models.pl_models import (
- PlVehicle,
- PlVehicleRechargeApply,
- PlVehicleRechargeApproval,
- PlVehicleRechargeRecord,
- )
- from app.schemas.pl.vehicle_recharge import APPLY_STATUSES, NODE_ACTION_TYPES, RECHARGE_TYPES
- def test_apply_statuses_include_workflow_states():
- assert {"draft", "in_approval", "rejected", "disbursed", "cancelled"} <= APPLY_STATUSES
- assert {"approve", "disburse"} <= NODE_ACTION_TYPES
- assert {"etc", "fuel"} <= RECHARGE_TYPES
- def _make_session():
- engine = create_engine("sqlite://")
- for table in (
- PlVehicle.__table__,
- PlVehicleRechargeApply.__table__,
- PlVehicleRechargeApproval.__table__,
- PlVehicleRechargeRecord.__table__,
- BxWorkflowDef.__table__,
- BxWorkflowNode.__table__,
- ):
- table.create(engine)
- return sessionmaker(bind=engine)()
- def _user(uid: str = "u-applicant", name: str = "申请人") -> SimpleNamespace:
- return SimpleNamespace(id=uid, name=name, dept_id="d1", role="admin", tenant_no="t1")
- def _seed_vehicle(db, plate: str = "鄂A00001") -> None:
- db.add(
- PlVehicle(
- id="veh-1",
- vehicle_type="self",
- license_plate=plate,
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- def _seed_workflow(db) -> tuple[str, str, str]:
- wf_id = "wf-1"
- node_gm = "node-gm"
- node_fin = "node-fin"
- db.add(
- BxWorkflowDef(
- id=wf_id,
- biz_type="vehicle_recharge",
- wf_name="充值流程",
- status=1,
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.add(
- BxWorkflowNode(
- id=node_gm,
- wf_id=wf_id,
- node_name="总经理审批",
- node_order=1,
- approver_id="u-gm",
- approver_name="总经理",
- action_type="approve",
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.add(
- BxWorkflowNode(
- id=node_fin,
- wf_id=wf_id,
- node_name="财务发放",
- node_order=2,
- approver_id="u-fin",
- approver_name="财务",
- action_type="disburse",
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- return wf_id, node_gm, node_fin
- def _patch_scope(monkeypatch, *modules) -> None:
- for mod in modules:
- monkeypatch.setattr(mod, "apply_pl_scope", lambda query, *_a, **_k: query)
- monkeypatch.setattr(mod, "assert_pl_row_in_scope", lambda *_a, **_k: None)
- from app.services import pl_vehicle_recharge_service as svc
- monkeypatch.setattr(svc, "_resolve_user_name", lambda *_a, **_k: "测试用户")
- monkeypatch.setattr(
- svc, "_resolve_applicant_id", lambda *_a, **_k: "u-applicant"
- )
- @pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"])
- def test_submit_sets_in_approval_and_first_node(monkeypatch, recharge_type):
- from app.services import pl_vehicle_recharge_service as svc
- from app.schemas.pl.vehicle_recharge import VehicleRechargeApplyCreate
- db = _make_session()
- _seed_vehicle(db)
- wf_id, node_gm, _node_fin = _seed_workflow(db)
- db.commit()
- _patch_scope(monkeypatch, svc)
- user = _user()
- created = svc.create_apply(
- db,
- "t1",
- user,
- VehicleRechargeApplyCreate(
- licensePlate="鄂A00001",
- rechargeType=recharge_type,
- applyDate=date(2026, 9, 26),
- odometerKm=Decimal("1000"),
- attachmentUrls="https://example.com/a.jpg",
- ),
- )
- assert created.recharge_type == recharge_type
- assert created.status == "draft"
- assert created.approval_round == 0
- result = svc.submit_apply(db, "t1", user, created.id)
- assert result.status == "in_approval"
- assert result.approval_round == 1
- assert result.wf_id == wf_id
- assert result.current_node == node_gm
- def test_resubmit_increments_round_keeps_history(monkeypatch):
- from app.services import pl_vehicle_recharge_service as svc
- db = _make_session()
- _seed_vehicle(db)
- wf_id, node_gm, _node_fin = _seed_workflow(db)
- apply = PlVehicleRechargeApply(
- id="apply-1",
- apply_no="ETC2026092601",
- license_plate="鄂A00001",
- recharge_type="etc",
- apply_date=date(2026, 9, 26),
- applicant_id="u-applicant",
- status="rejected",
- wf_id=wf_id,
- current_node=None,
- approval_round=1,
- tenant_no="t1",
- create_dept_id="d1",
- )
- db.add(apply)
- db.add(
- PlVehicleRechargeApproval(
- id="appr-1",
- apply_id="apply-1",
- approval_round=1,
- node_id=node_gm,
- node_name="总经理审批",
- approver_id="u-gm",
- approver_name="总经理",
- approval_status="rejected",
- approval_opinion="驳回",
- approval_time=datetime(2026, 9, 26, 10, 0, 0),
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.commit()
- _patch_scope(monkeypatch, svc)
- result = svc.submit_apply(db, "t1", _user(), "apply-1")
- assert result.approval_round == 2
- assert result.status == "in_approval"
- assert result.current_node == node_gm
- assert len(result.approvals) == 1
- assert result.approvals[0].approval_status == "rejected"
- # 当前轮进度:全部节点均展示,尚未审批则为 pending
- assert len(result.approval_flow) == 2
- assert all(n.approval_status == "pending" for n in result.approval_flow)
- # 历史:保留打回前的审批动作
- assert len(result.approval_history) == 1
- assert result.approval_history[0].approval_status == "rejected"
- assert result.approval_history[0].approval_round == 1
- def test_approve_advances_to_next_node(monkeypatch):
- from app.services import pl_vehicle_recharge_approval_service as approval_svc
- from app.services import pl_vehicle_recharge_service as svc
- db = _make_session()
- _seed_vehicle(db)
- wf_id, node_gm, node_fin = _seed_workflow(db)
- db.add(
- PlVehicleRechargeApply(
- id="apply-1",
- apply_no="ETC2026092601",
- license_plate="鄂A00001",
- recharge_type="etc",
- apply_date=date(2026, 9, 26),
- applicant_id="u-applicant",
- status="in_approval",
- wf_id=wf_id,
- current_node=node_gm,
- approval_round=1,
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.commit()
- _patch_scope(monkeypatch, svc)
- result = approval_svc.approve_apply(
- db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="同意"
- )
- assert result.status == "in_approval"
- assert result.current_node == node_fin
- assert any(a.approval_status == "approved" for a in result.approvals)
- assert len(result.approval_flow) == 2
- assert result.approval_flow[0].approval_status == "approved"
- assert result.approval_flow[0].approver_name == "总经理"
- assert result.approval_flow[0].approval_opinion == "同意"
- assert result.approval_flow[1].approval_status == "pending"
- assert result.approval_flow[1].node_name == "财务发放"
- assert len(result.approval_history) == 1
- assert result.approval_history[0].approval_status == "approved"
- def test_reject_sets_rejected_and_appends_history(monkeypatch):
- from app.services import pl_vehicle_recharge_approval_service as approval_svc
- from app.services import pl_vehicle_recharge_service as svc
- db = _make_session()
- _seed_vehicle(db)
- wf_id, node_gm, _node_fin = _seed_workflow(db)
- db.add(
- PlVehicleRechargeApply(
- id="apply-1",
- apply_no="ETC2026092601",
- license_plate="鄂A00001",
- recharge_type="etc",
- apply_date=date(2026, 9, 26),
- applicant_id="u-applicant",
- status="in_approval",
- wf_id=wf_id,
- current_node=node_gm,
- approval_round=1,
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.commit()
- _patch_scope(monkeypatch, svc)
- result = approval_svc.reject_apply(
- db, "t1", _user("u-gm", "总经理"), "apply-1", opinion="材料不全"
- )
- assert result.status == "rejected"
- assert result.current_node is None
- assert result.approvals[-1].approval_status == "rejected"
- @pytest.mark.parametrize("recharge_type", ["etc", "fuel", "shanxia"])
- def test_disburse_writes_record_and_disbursed(monkeypatch, recharge_type):
- from app.services import pl_vehicle_recharge_approval_service as approval_svc
- from app.services import pl_vehicle_recharge_service as svc
- from app.schemas.pl.vehicle_recharge import VehicleRechargeRecordCreate
- db = _make_session()
- _seed_vehicle(db)
- wf_id, _node_gm, node_fin = _seed_workflow(db)
- db.add(
- PlVehicleRechargeApply(
- id="apply-1",
- apply_no="ETC2026092601",
- license_plate="鄂A00001",
- recharge_type=recharge_type,
- apply_date=date(2026, 9, 26),
- applicant_id="u-applicant",
- status="in_approval",
- wf_id=wf_id,
- current_node=node_fin,
- approval_round=1,
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.commit()
- _patch_scope(monkeypatch, svc)
- payload = VehicleRechargeRecordCreate(
- rechargeDate=date(2026, 9, 26),
- rechargeType=recharge_type,
- amount=Decimal("100.00"),
- applyId="apply-1",
- )
- result = approval_svc.disburse_apply(
- db, "t1", _user("u-fin", "财务"), "apply-1", payload
- )
- assert result.status == "disbursed"
- assert result.record is not None
- assert result.record.recharge_type == recharge_type
- assert result.record.amount == Decimal("100.00")
- assert any(a.approval_status == "disbursed" for a in result.approvals)
- def test_approve_forbidden_on_disburse_node(monkeypatch):
- from fastapi import HTTPException
- from app.services import pl_vehicle_recharge_approval_service as approval_svc
- from app.services import pl_vehicle_recharge_service as svc
- db = _make_session()
- _seed_vehicle(db)
- wf_id, _node_gm, node_fin = _seed_workflow(db)
- db.add(
- PlVehicleRechargeApply(
- id="apply-1",
- apply_no="ETC2026092601",
- license_plate="鄂A00001",
- recharge_type="etc",
- apply_date=date(2026, 9, 26),
- applicant_id="u-applicant",
- status="in_approval",
- wf_id=wf_id,
- current_node=node_fin,
- approval_round=1,
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.commit()
- _patch_scope(monkeypatch, svc)
- with pytest.raises(HTTPException) as exc:
- approval_svc.approve_apply(
- db, "t1", _user("u-fin", "财务"), "apply-1", opinion="ok"
- )
- assert exc.value.status_code == 400
- def test_withdraw_in_approval_to_draft_keeps_round_and_approvals(monkeypatch):
- from app.services import pl_vehicle_recharge_service as svc
- db = _make_session()
- _seed_vehicle(db)
- wf_id, node_gm, _node_fin = _seed_workflow(db)
- db.add(
- PlVehicleRechargeApply(
- id="a-wd",
- apply_no="SXSQWD001",
- license_plate="鄂A00001",
- recharge_type="etc",
- apply_date=date.today(),
- applicant_id="u-applicant",
- status="in_approval",
- wf_id=wf_id,
- current_node=node_gm,
- approval_round=1,
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.add(
- PlVehicleRechargeApproval(
- id="ap1",
- apply_id="a-wd",
- node_id=node_gm,
- node_name="总经理审批",
- approval_round=1,
- approval_status="approved",
- approver_id="u-gm",
- approver_name="总经理",
- approval_time=datetime.now(),
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.commit()
- _patch_scope(monkeypatch, svc)
- out = svc.withdraw_apply(db, "t1", _user("u-applicant"), "a-wd")
- assert out.status == "draft"
- assert out.current_node in (None, "")
- row = db.query(PlVehicleRechargeApply).filter_by(id="a-wd").one()
- assert row.status == "draft"
- assert row.current_node is None
- assert row.approval_round == 1
- assert (
- db.query(PlVehicleRechargeApproval)
- .filter_by(apply_id="a-wd", del_flag=0)
- .count()
- == 1
- )
- def test_withdraw_rejects_non_applicant(monkeypatch):
- from fastapi import HTTPException
- from app.services import pl_vehicle_recharge_service as svc
- db = _make_session()
- _seed_vehicle(db)
- db.add(
- PlVehicleRechargeApply(
- id="a-wd2",
- apply_no="SXSQWD002",
- license_plate="鄂A00001",
- recharge_type="etc",
- apply_date=date.today(),
- applicant_id="u-applicant",
- status="in_approval",
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.commit()
- _patch_scope(monkeypatch, svc)
- with pytest.raises(HTTPException) as ei:
- svc.withdraw_apply(db, "t1", _user("u-other"), "a-wd2")
- assert ei.value.status_code == 403
- def test_withdraw_rejects_non_in_approval(monkeypatch):
- from fastapi import HTTPException
- from app.services import pl_vehicle_recharge_service as svc
- db = _make_session()
- _seed_vehicle(db)
- db.add(
- PlVehicleRechargeApply(
- id="a-wd3",
- apply_no="SXSQWD003",
- license_plate="鄂A00001",
- recharge_type="etc",
- apply_date=date.today(),
- applicant_id="u-applicant",
- status="draft",
- tenant_no="t1",
- create_dept_id="d1",
- )
- )
- db.commit()
- _patch_scope(monkeypatch, svc)
- with pytest.raises(HTTPException) as ei:
- svc.withdraw_apply(db, "t1", _user("u-applicant"), "a-wd3")
- assert ei.value.status_code == 400
|