"""批量发放把同一备注和附件写到每张待发放报销单,实际金额按单提交。""" 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