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

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

138 lines
4.3 KiB
Python

"""批量发放把同一备注和附件写到每张待发放报销单,实际金额按单提交。"""
from datetime import date, datetime
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 BxAttachment, BxReimburseApply, BxReimburseOffset, BxReimbursePayment
from app.services.bx.reimburse import ReimburseService
TENANT = "shanxia2.0"
NOW = datetime(2026, 11, 4, 15, 0, 0)
def _session():
engine = create_engine("sqlite://")
BxReimburseApply.__table__.create(engine)
BxReimburseOffset.__table__.create(engine)
BxReimbursePayment.__table__.create(engine)
BxAttachment.__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(*, apply_id: str, amount: float, status: str = "approved", pay_status: str = None) -> BxReimburseApply:
return BxReimburseApply(
id=apply_id,
apply_code=apply_id,
title=f"报销{apply_id}",
reimburse_date=date(2026, 11, 4),
applicant_id="applicant",
applicant_name="汪璇",
expense_id="58",
expense_name="车辆用品费",
amount=amount,
status=status,
pay_status=pay_status,
tenant_no=TENANT,
create_dept_id="dept",
create_time=NOW,
update_time=NOW,
del_flag=0,
)
def _attachment(attachment_id: str = "att-1") -> BxAttachment:
return BxAttachment(
id=attachment_id,
biz_type="reimburse_pay",
biz_id="0",
file_name="发放截图.png",
file_path="bx/reimburse_pay/shot.png",
file_type="image/png",
file_size=12,
tenant_no=TENANT,
create_dept_id="dept",
create_time=NOW,
del_flag=0,
)
def test_batch_pay_writes_shared_remark_and_attachment_with_per_bill_amount():
db = _session()
db.add_all([_apply(apply_id="BX-A", amount=100), _apply(apply_id="BX-B", amount=80.5)])
db.add(_attachment())
db.commit()
rows = ReimburseService.batch_pay(
db,
[{"id": "BX-A", "actual_amount": 100}, {"id": "BX-B", "actual_amount": 70}],
_user(),
pay_remark="同一批次发放",
attachment_ids=["att-1"],
)
db.commit()
assert [row.id for row in rows] == ["BX-A", "BX-B"]
saved = {row.id: row for row in db.query(BxReimburseApply).all()}
assert saved["BX-A"].status == "processed"
assert saved["BX-A"].pay_status == "paid"
assert float(saved["BX-A"].actual_amount) == 100
assert saved["BX-A"].pay_remark == "同一批次发放"
assert float(saved["BX-B"].actual_amount) == 70
assert saved["BX-B"].pay_remark == "同一批次发放"
assert saved["BX-A"].remark is None
copies = (
db.query(BxAttachment)
.filter(BxAttachment.del_flag == 0, BxAttachment.biz_type == "reimburse_pay")
.order_by(BxAttachment.biz_id)
.all()
)
assert [(row.biz_id, row.file_path) for row in copies] == [
("BX-A", "bx/reimburse_pay/shot.png"),
("BX-B", "bx/reimburse_pay/shot.png"),
]
assert db.query(BxAttachment).filter(BxAttachment.id == "att-1").one().del_flag == 1
def test_batch_pay_rejects_non_pending_bill_without_writing_the_others():
db = _session()
db.add_all(
[
_apply(apply_id="BX-A", amount=100),
_apply(apply_id="BX-B", amount=80, status="processed", pay_status="paid"),
]
)
db.commit()
try:
ReimburseService.batch_pay(
db,
[{"id": "BX-A", "actual_amount": 100}, {"id": "BX-B", "actual_amount": 80}],
_user(),
pay_remark="不应写入",
)
except HTTPException as exc:
assert exc.status_code == 400
db.rollback()
else:
raise AssertionError("expected batch pay to reject a paid bill")
saved = {row.id: row for row in db.query(BxReimburseApply).all()}
assert saved["BX-A"].status == "approved"
assert saved["BX-A"].pay_status is None
assert saved["BX-A"].pay_remark is None
assert saved["BX-B"].pay_remark is None