冲抵后的余额不再自动记成发放,发放和冲抵分开提交,未结清单据继续留在待处理。 Co-authored-by: Cursor <cursoragent@cursor.com>
146 lines
4.6 KiB
Python
146 lines
4.6 KiB
Python
"""报销支付按未结清余额分次冲抵、分次发放,两次操作互不影响。"""
|
|
from datetime import date, datetime
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from app.core.bx_auth import BxUserContext
|
|
from app.models.bx_models import BxAdvanceApply, BxReimburseApply, BxReimburseOffset, BxReimbursePayment
|
|
from app.services.bx.reimburse import ReimburseService
|
|
|
|
TENANT = "shanxia2.0"
|
|
NOW = datetime(2026, 11, 5, 10, 0, 0)
|
|
|
|
|
|
def _session():
|
|
engine = create_engine("sqlite://")
|
|
for model in (BxReimburseApply, BxReimburseOffset, BxReimbursePayment, BxAdvanceApply):
|
|
model.__table__.create(engine)
|
|
return sessionmaker(bind=engine)()
|
|
|
|
|
|
def _user() -> BxUserContext:
|
|
return BxUserContext(id="finance-1", name="财务", dept_id="dept", role="super_admin", tenant_no=TENANT)
|
|
|
|
|
|
def _apply(amount: float = 50000) -> BxReimburseApply:
|
|
return BxReimburseApply(
|
|
id="BX",
|
|
apply_code="BX",
|
|
title="报销BX",
|
|
reimburse_date=date(2026, 11, 5),
|
|
applicant_id="applicant",
|
|
applicant_name="汪璇",
|
|
expense_id="58",
|
|
expense_name="车辆用品费",
|
|
amount=amount,
|
|
status="approved",
|
|
tenant_no=TENANT,
|
|
create_dept_id="dept",
|
|
create_time=NOW,
|
|
update_time=NOW,
|
|
del_flag=0,
|
|
)
|
|
|
|
|
|
def _advance(advance_id: str, amount: float) -> BxAdvanceApply:
|
|
return BxAdvanceApply(
|
|
id=advance_id,
|
|
apply_code=advance_id,
|
|
applicant_id="applicant",
|
|
applicant_name="汪璇",
|
|
reason="备用金",
|
|
amount=amount,
|
|
status="issued",
|
|
issue_status="issued",
|
|
actual_amount=amount,
|
|
tenant_no=TENANT,
|
|
create_dept_id="dept",
|
|
create_time=NOW,
|
|
update_time=NOW,
|
|
del_flag=0,
|
|
)
|
|
|
|
|
|
def test_offset_and_pay_are_separate_and_leave_balance_open():
|
|
db = _session()
|
|
db.add(_apply())
|
|
db.add_all([_advance("ADV-A", 20000), _advance("ADV-B", 15000)])
|
|
db.commit()
|
|
|
|
ReimburseService.batch_offset(
|
|
db, "BX", [{"advance_id": "ADV-A", "offset_amount": 20000}], 0, _user()
|
|
)
|
|
row = db.query(BxReimburseApply).one()
|
|
assert row.status == "approved"
|
|
assert row.pay_status is None
|
|
assert row.pay_time is None
|
|
assert float(row.offset_amount) == 20000
|
|
assert row.actual_amount is None
|
|
|
|
ReimburseService.pay(db, "BX", _user(), actual_amount=10000)
|
|
db.refresh(row)
|
|
assert row.status == "approved"
|
|
assert row.pay_status is None
|
|
assert float(row.actual_amount) == 10000
|
|
assert float(row.offset_amount) == 20000
|
|
assert db.query(BxReimbursePayment).count() == 1
|
|
|
|
ReimburseService.batch_offset(
|
|
db, "BX", [{"advance_id": "ADV-B", "offset_amount": 10000}], 0, _user()
|
|
)
|
|
db.refresh(row)
|
|
assert row.status == "approved"
|
|
assert float(row.offset_amount) == 30000
|
|
assert db.query(BxReimburseOffset).count() == 2
|
|
|
|
ReimburseService.pay(db, "BX", _user(), actual_amount=10000)
|
|
db.refresh(row)
|
|
assert row.status == "processed"
|
|
assert row.pay_status == "paid"
|
|
assert float(row.actual_amount) == 20000
|
|
assert db.query(BxReimbursePayment).count() == 2
|
|
|
|
ReimburseService.pay(db, "BX", _user(), actual_amount=1)
|
|
assert db.query(BxReimbursePayment).count() == 2
|
|
|
|
|
|
def test_same_request_records_offset_and_pay_without_taking_the_remainder():
|
|
db = _session()
|
|
db.add(_apply())
|
|
db.add(_advance("ADV-A", 20000))
|
|
db.commit()
|
|
|
|
ReimburseService.batch_offset(
|
|
db, "BX", [{"advance_id": "ADV-A", "offset_amount": 20000}], 10000, _user()
|
|
)
|
|
row = db.query(BxReimburseApply).one()
|
|
assert row.status == "approved"
|
|
assert float(row.offset_amount) == 20000
|
|
assert float(row.actual_amount) == 10000
|
|
assert db.query(BxReimburseOffset).count() == 1
|
|
assert db.query(BxReimbursePayment).count() == 1
|
|
|
|
|
|
def test_rejects_amount_above_balance_or_advance_remaining():
|
|
db = _session()
|
|
db.add(_apply())
|
|
db.add(_advance("ADV-A", 20000))
|
|
db.commit()
|
|
|
|
with pytest.raises(HTTPException) as pay_exc:
|
|
ReimburseService.pay(db, "BX", _user(), actual_amount=50001)
|
|
assert pay_exc.value.status_code == 400
|
|
|
|
with pytest.raises(HTTPException) as offset_exc:
|
|
ReimburseService.batch_offset(
|
|
db, "BX", [{"advance_id": "ADV-A", "offset_amount": 20001}], 0, _user()
|
|
)
|
|
assert offset_exc.value.status_code == 400
|
|
row = db.query(BxReimburseApply).one()
|
|
assert row.status == "approved"
|
|
assert row.offset_amount is None
|
|
assert db.query(BxReimbursePayment).count() == 0
|