冲抵后的余额不再自动记成发放,发放和冲抵分开提交,未结清单据继续留在待处理。 Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
4.3 KiB
Python
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
|