| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- from typing import Optional
- from fastapi import APIRouter, Depends, HTTPException, status
- from fastapi.responses import StreamingResponse
- from sqlalchemy import func, or_
- from sqlalchemy.orm import Session
- from app.database import get_db
- from app.core.deps import require_roles
- from app.models.user import User
- from app.models.advance import AdvanceApply
- from app.models.reimburse import ReimburseApply, ReimburseOffset
- from app.schemas.common import ApiResponse, PagedData
- from app.schemas.reimburse import (
- ReimburseApplyOut,
- OffsetRequest,
- PayRequest,
- BatchOffsetRequest,
- ReimburseOffsetRecordOut,
- PaymentFinanceStatsOut,
- )
- from app.schemas.advance import AdvanceIssueOut, AdvanceIssueRequest, AdvanceOffsetRecordOut, AdvanceFinanceIssueStatsOut
- from app.services.advance import AdvanceService
- from app.services.attachment import AttachmentService
- from app.services.reimburse import ReimburseService
- from app.utils.date_range import parse_create_time_day_bounds
- router = APIRouter(prefix="/api/payments", tags=["支付处理"])
- @router.get("/pending", response_model=ApiResponse[PagedData])
- def list_pending_payments(
- page: int = 1,
- pageSize: int = 10,
- keyword: str = "",
- applicantId: Optional[int] = None,
- applyDate: Optional[str] = None,
- db: Session = Depends(get_db),
- _: User = Depends(require_roles("FINANCE")),
- ):
- filters = [ReimburseApply.del_flag == 0, ReimburseApply.status == "approved"]
- if keyword:
- filters.append(or_(ReimburseApply.apply_code.contains(keyword), ReimburseApply.applicant_name.contains(keyword)))
- if applicantId is not None:
- filters.append(ReimburseApply.applicant_id == applicantId)
- bounds = parse_create_time_day_bounds(applyDate)
- if bounds:
- d0, d1 = bounds
- filters.append(ReimburseApply.create_time >= d0)
- filters.append(ReimburseApply.create_time < d1)
- rows_r, total_r = ReimburseService.get_paged(db, page=page, page_size=pageSize, filters=filters)
- outs = ReimburseService.out_rows_with_attachment_counts(db, rows_r)
- all_rows = [{"type": "reimburse", "data": o} for o in outs]
- paged_data = PagedData(rows=all_rows, total=total_r, page=page, page_size=pageSize)
- return ApiResponse(data=paged_data)
- @router.get("/stats", response_model=ApiResponse[PaymentFinanceStatsOut])
- def payment_finance_stats(
- db: Session = Depends(get_db),
- _: User = Depends(require_roles("FINANCE")),
- ):
- data = ReimburseService.finance_payment_stats(db)
- return ApiResponse(data=PaymentFinanceStatsOut.model_validate(data))
- @router.get("/processed", response_model=ApiResponse[PagedData])
- def list_processed_payments(
- page: int = 1,
- pageSize: int = 10,
- keyword: str = "",
- applicantId: Optional[int] = None,
- applyDate: Optional[str] = None,
- db: Session = Depends(get_db),
- _: User = Depends(require_roles("FINANCE")),
- ):
- filters = [ReimburseApply.del_flag == 0, ReimburseApply.status.in_(["processed", "paid", "offset"])]
- if keyword:
- filters.append(or_(ReimburseApply.apply_code.contains(keyword), ReimburseApply.applicant_name.contains(keyword)))
- if applicantId is not None:
- filters.append(ReimburseApply.applicant_id == applicantId)
- bounds = parse_create_time_day_bounds(applyDate)
- if bounds:
- d0, d1 = bounds
- filters.append(ReimburseApply.create_time >= d0)
- filters.append(ReimburseApply.create_time < d1)
- rows_r, total_r = ReimburseService.get_paged(db, page=page, page_size=pageSize, filters=filters)
- outs = ReimburseService.out_rows_with_attachment_counts(db, rows_r)
- all_rows = [{"type": "reimburse", "data": o} for o in outs]
- paged_data = PagedData(rows=all_rows, total=total_r, page=page, page_size=pageSize)
- return ApiResponse(data=paged_data)
- @router.post("/{id}/pay", response_model=ApiResponse[ReimburseApplyOut])
- def pay(
- id: int,
- body: PayRequest | None = None,
- db: Session = Depends(get_db),
- current_user: User = Depends(require_roles("FINANCE")),
- ):
- pay_remark = body.pay_remark if body else ""
- actual_amount = body.actual_amount if body else None
- reimburse = ReimburseService.pay(db, id, current_user, pay_remark=pay_remark, actual_amount=actual_amount)
- if not reimburse:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="支付记录不存在")
- return ApiResponse(data=ReimburseService.out_one_with_attachment_counts(db, reimburse))
- @router.post("/{id}/offset", response_model=ApiResponse[ReimburseApplyOut])
- def offset(
- id: int,
- body: OffsetRequest,
- db: Session = Depends(get_db),
- current_user: User = Depends(require_roles("FINANCE")),
- ):
- reimburse = ReimburseService.offset(db, id, body.advance_id, body.offset_amount, current_user, remark=body.remark or "")
- if not reimburse:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="支付记录不存在")
- return ApiResponse(data=ReimburseService.out_one_with_attachment_counts(db, reimburse))
- @router.get("/advance-issues", response_model=ApiResponse[PagedData[AdvanceIssueOut]])
- def list_advance_issues(
- page: int = 1,
- pageSize: int = 10,
- issue_status: str | None = None,
- keyword: str = "",
- applicantId: Optional[int] = None,
- applyDate: Optional[str] = None,
- db: Session = Depends(get_db),
- _: User = Depends(require_roles("FINANCE")),
- ):
- filters = [AdvanceApply.del_flag == 0]
- if issue_status == "pending":
- filters.append(AdvanceApply.status == "approved")
- filters.append(or_(AdvanceApply.issue_status == None, AdvanceApply.issue_status != "issued"))
- elif issue_status == "issued":
- filters.append(AdvanceApply.status == "issued")
- else:
- filters.append(or_(AdvanceApply.status == "approved", AdvanceApply.status == "issued"))
- if keyword:
- filters.append(or_(AdvanceApply.apply_code.contains(keyword), AdvanceApply.applicant_name.contains(keyword)))
- if applicantId is not None:
- filters.append(AdvanceApply.applicant_id == applicantId)
- bounds = parse_create_time_day_bounds(applyDate)
- if bounds:
- d0, d1 = bounds
- filters.append(AdvanceApply.create_time >= d0)
- filters.append(AdvanceApply.create_time < d1)
- rows, total = AdvanceService.get_paged(db, page=page, page_size=pageSize, filters=filters)
- out_rows = [AdvanceIssueOut.model_validate(r) for r in rows]
- # Compute total offset_amount per advance from bx_reimburse_offset
- if out_rows:
- advance_ids = [r.id for r in rows]
- offset_map = dict(
- db.query(ReimburseOffset.advance_id, func.coalesce(func.sum(ReimburseOffset.offset_amount), 0))
- .filter(ReimburseOffset.advance_id.in_(advance_ids))
- .group_by(ReimburseOffset.advance_id)
- .all()
- )
- for out_row in out_rows:
- out_row.offset_amount = float(offset_map.get(out_row.id, 0))
- paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)
- return ApiResponse(data=paged_data)
- @router.get("/advance-issues/stats", response_model=ApiResponse[AdvanceFinanceIssueStatsOut])
- def advance_issue_stats(
- db: Session = Depends(get_db),
- _: User = Depends(require_roles("FINANCE")),
- ):
- data = AdvanceService.finance_issue_stats(db)
- return ApiResponse(data=AdvanceFinanceIssueStatsOut.model_validate(data))
- @router.get("/advance-issues/export")
- def export_advance_issues(
- issue_status: str | None = None,
- keyword: str = "",
- applicantId: Optional[int] = None,
- applyDate: Optional[str] = None,
- db: Session = Depends(get_db),
- _: User = Depends(require_roles("FINANCE")),
- ):
- import csv
- import io
- from datetime import datetime
- filters = [AdvanceApply.del_flag == 0]
- if issue_status == "pending":
- filters.append(AdvanceApply.status == "approved")
- filters.append(or_(AdvanceApply.issue_status == None, AdvanceApply.issue_status != "issued"))
- elif issue_status == "issued":
- filters.append(AdvanceApply.status == "issued")
- else:
- filters.append(or_(AdvanceApply.status == "approved", AdvanceApply.status == "issued"))
- if keyword:
- filters.append(or_(AdvanceApply.apply_code.contains(keyword), AdvanceApply.applicant_name.contains(keyword)))
- if applicantId is not None:
- filters.append(AdvanceApply.applicant_id == applicantId)
- bounds = parse_create_time_day_bounds(applyDate)
- if bounds:
- d0, d1 = bounds
- filters.append(AdvanceApply.create_time >= d0)
- filters.append(AdvanceApply.create_time < d1)
- rows, _ = AdvanceService.get_paged(db, page=1, page_size=99999, filters=filters)
- out_rows = [AdvanceIssueOut.model_validate(r) for r in rows]
- buf = io.StringIO()
- writer = csv.writer(buf)
- writer.writerow(["申请单号", "申请人", "申请原因", "申请金额", "审批通过时间", "发放状态", "发放时间", "发放操作人", "实际发放金额", "发放备注"])
- for r in out_rows:
- writer.writerow([
- r.apply_code,
- r.applicant_name,
- r.reason,
- r.amount,
- r.approve_time or "",
- "已发放" if r.issue_status == "issued" else "待发放",
- r.issue_time or "",
- r.issue_user_name or "",
- r.actual_amount or "",
- r.issue_remark or "",
- ])
- buf.seek(0)
- filename = f"备用金发放列表_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
- return StreamingResponse(
- buf,
- media_type="text/csv",
- headers={"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"},
- )
- @router.post("/advance/{advance_id}/issue", response_model=ApiResponse[AdvanceIssueOut])
- def issue_advance(
- advance_id: int,
- body: AdvanceIssueRequest,
- db: Session = Depends(get_db),
- current_user: User = Depends(require_roles("FINANCE")),
- ):
- issue_atts = AttachmentService.get_by_biz(db, "advance_issue", advance_id)
- if not issue_atts:
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail="请至少上传 1 个发放附件",
- )
- advance = AdvanceService.issue(db, advance_id, current_user, body.issue_remark or "", body.actual_amount)
- if not advance:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
- return ApiResponse(data=AdvanceIssueOut.model_validate(advance))
- @router.post("/{id}/batch-offset", response_model=ApiResponse[ReimburseApplyOut])
- def batch_offset(
- id: int,
- body: BatchOffsetRequest,
- db: Session = Depends(get_db),
- current_user: User = Depends(require_roles("FINANCE")),
- ):
- reimburse = ReimburseService.batch_offset(
- db, id,
- [item.model_dump() for item in body.items],
- body.pay_amount,
- current_user,
- remark=body.remark or "",
- )
- if not reimburse:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="支付记录不存在")
- return ApiResponse(data=ReimburseService.out_one_with_attachment_counts(db, reimburse))
- @router.get("/advance/{advance_id}/offset-records", response_model=ApiResponse[list[AdvanceOffsetRecordOut]])
- def list_advance_offset_records(
- advance_id: int,
- db: Session = Depends(get_db),
- _: User = Depends(require_roles("FINANCE")),
- ):
- rows = (
- db.query(ReimburseOffset, ReimburseApply)
- .join(ReimburseApply, ReimburseApply.id == ReimburseOffset.reimburse_id)
- .filter(
- ReimburseOffset.advance_id == advance_id,
- ReimburseApply.del_flag == 0,
- )
- .order_by(ReimburseOffset.create_time.desc(), ReimburseOffset.id.desc())
- .all()
- )
- records: list[AdvanceOffsetRecordOut] = []
- for offset, reimburse in rows:
- records.append(
- AdvanceOffsetRecordOut(
- reimburse_id=reimburse.id,
- reimburse_apply_code=reimburse.apply_code,
- reimburse_title=reimburse.title,
- reimburse_applicant_name=reimburse.applicant_name,
- offset_amount=float(offset.offset_amount or 0),
- remark=offset.remark,
- operator=offset.create_by,
- create_time=offset.create_time,
- )
- )
- return ApiResponse(data=records)
- @router.get("/{id}/offset-records", response_model=ApiResponse[list[ReimburseOffsetRecordOut]])
- def list_reimburse_offset_records(
- id: int,
- db: Session = Depends(get_db),
- _: User = Depends(require_roles("FINANCE")),
- ):
- rows = (
- db.query(ReimburseOffset, AdvanceApply)
- .join(AdvanceApply, AdvanceApply.id == ReimburseOffset.advance_id)
- .filter(
- ReimburseOffset.reimburse_id == id,
- AdvanceApply.del_flag == 0,
- )
- .order_by(ReimburseOffset.create_time.desc(), ReimburseOffset.id.desc())
- .all()
- )
- records: list[ReimburseOffsetRecordOut] = []
- for offset, advance in rows:
- records.append(
- ReimburseOffsetRecordOut(
- advance_id=advance.id,
- advance_apply_code=advance.apply_code,
- advance_applicant_name=advance.applicant_name,
- advance_reason=advance.reason,
- offset_amount=float(offset.offset_amount or 0),
- remark=offset.remark,
- operator=offset.create_by,
- create_time=offset.create_time,
- )
- )
- return ApiResponse(data=records)
|