Files
shanxia2.0/backend/tests/test_bx_reimburse_partial_settlement.py
T
wuzilongandCursor e7f942b399 feat(bx): 报销支付按未结清余额分次冲抵和发放。
冲抵后的余额不再自动记成发放,发放和冲抵分开提交,未结清单据继续留在待处理。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-05 21:14:09 +08:00

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