|
|
@@ -4,6 +4,7 @@ from datetime import datetime
|
|
|
from decimal import Decimal
|
|
|
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
|
|
|
|
+from fastapi import HTTPException, status
|
|
|
from sqlalchemy import and_, func, or_
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
from sqlalchemy.orm import Session
|
|
|
@@ -12,17 +13,21 @@ from app.core.bx_auth import BxUserContext
|
|
|
from app.core.tenant_context import get_tenant_no
|
|
|
from app.core.utils import generate_uuid
|
|
|
from app.models.bx_models import (
|
|
|
+ BxInvoice,
|
|
|
+ BxInvoiceLedger,
|
|
|
BxReimburseApply,
|
|
|
BxReimburseApproval,
|
|
|
BxReimburseDetail,
|
|
|
+ BxReimburseInvoice,
|
|
|
BxReimburseOffset,
|
|
|
BxWorkflowDef,
|
|
|
BxWorkflowNode,
|
|
|
)
|
|
|
-from app.schemas.bx.reimburse import ReimburseApplyOut, ReimburseDetailOut
|
|
|
+from app.schemas.bx.reimburse import ReimburseApplyOut, ReimburseDetailOut, ReimburseInvoiceBriefOut
|
|
|
from app.services.bx.approval_cycle import filter_current_cycle_approvals
|
|
|
from app.services.bx.attachment import AttachmentService
|
|
|
from app.services.bx.base import BxCrudService
|
|
|
+from app.services.bx.invoice_ledger import ENTRY_DEBIT_REIMBURSE, InvoiceLedgerService
|
|
|
from app.services.bx_data_scope import apply_bx_scope
|
|
|
from app.utils.bx_codegen import (
|
|
|
allocate_reimburse_title_seq,
|
|
|
@@ -217,11 +222,180 @@ class ReimburseService:
|
|
|
return rows, total
|
|
|
|
|
|
@staticmethod
|
|
|
- def submit(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxReimburseApply]:
|
|
|
+ def _has_debit_reimburse(db: Session, tenant_no: str, reimburse_id: str) -> bool:
|
|
|
+ return (
|
|
|
+ db.query(BxInvoiceLedger.id)
|
|
|
+ .filter(
|
|
|
+ BxInvoiceLedger.tenant_no == tenant_no,
|
|
|
+ BxInvoiceLedger.reimburse_id == reimburse_id,
|
|
|
+ BxInvoiceLedger.entry_type == ENTRY_DEBIT_REIMBURSE,
|
|
|
+ BxInvoiceLedger.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ is not None
|
|
|
+ )
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def sync_reimburse_invoices(
|
|
|
+ db: Session,
|
|
|
+ reimburse_id: str,
|
|
|
+ invoice_ids: Optional[List[str]],
|
|
|
+ tenant_no: str,
|
|
|
+ user: BxUserContext,
|
|
|
+ *,
|
|
|
+ applicant_id: str,
|
|
|
+ ) -> None:
|
|
|
+ if invoice_ids is None:
|
|
|
+ return
|
|
|
+ normalized_ids: List[str] = []
|
|
|
+ seen: set[str] = set()
|
|
|
+ for raw_id in invoice_ids:
|
|
|
+ invoice_id = str(raw_id or "").strip()
|
|
|
+ if not invoice_id or invoice_id in seen:
|
|
|
+ continue
|
|
|
+ seen.add(invoice_id)
|
|
|
+ normalized_ids.append(invoice_id)
|
|
|
+
|
|
|
+ if normalized_ids:
|
|
|
+ invoices = (
|
|
|
+ db.query(BxInvoice)
|
|
|
+ .filter(
|
|
|
+ BxInvoice.tenant_no == tenant_no,
|
|
|
+ BxInvoice.id.in_(normalized_ids),
|
|
|
+ BxInvoice.del_flag == 0,
|
|
|
+ )
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ invoice_map = {inv.id: inv for inv in invoices}
|
|
|
+ for invoice_id in normalized_ids:
|
|
|
+ invoice = invoice_map.get(invoice_id)
|
|
|
+ if not invoice:
|
|
|
+ raise HTTPException(
|
|
|
+ status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
+ detail=f"发票 {invoice_id} 不存在",
|
|
|
+ )
|
|
|
+ if invoice.owner_user_id != applicant_id:
|
|
|
+ raise HTTPException(
|
|
|
+ status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
+ detail=f"发票 {invoice_id} 不属于申请人",
|
|
|
+ )
|
|
|
+
|
|
|
+ db.query(BxReimburseInvoice).filter(
|
|
|
+ BxReimburseInvoice.reimburse_id == reimburse_id,
|
|
|
+ BxReimburseInvoice.tenant_no == tenant_no,
|
|
|
+ BxReimburseInvoice.del_flag == 0,
|
|
|
+ ).update({"del_flag": 1, "update_by": user.id}, synchronize_session=False)
|
|
|
+
|
|
|
+ for invoice_id in normalized_ids:
|
|
|
+ db.add(
|
|
|
+ BxReimburseInvoice(
|
|
|
+ id=generate_uuid(),
|
|
|
+ reimburse_id=reimburse_id,
|
|
|
+ invoice_id=invoice_id,
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ create_by=user.id,
|
|
|
+ create_dept_id=user.dept_id or "",
|
|
|
+ )
|
|
|
+ )
|
|
|
+ db.flush()
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def get_linked_invoice_ids(db: Session, reimburse_id: str, tenant_no: str) -> List[str]:
|
|
|
+ rows = (
|
|
|
+ db.query(BxReimburseInvoice.invoice_id)
|
|
|
+ .filter(
|
|
|
+ BxReimburseInvoice.reimburse_id == reimburse_id,
|
|
|
+ BxReimburseInvoice.tenant_no == tenant_no,
|
|
|
+ BxReimburseInvoice.del_flag == 0,
|
|
|
+ )
|
|
|
+ .order_by(BxReimburseInvoice.create_time.asc())
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ return [row[0] for row in rows]
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def get_linked_invoices_brief(
|
|
|
+ db: Session, reimburse_id: str, tenant_no: str
|
|
|
+ ) -> List[ReimburseInvoiceBriefOut]:
|
|
|
+ rows = (
|
|
|
+ db.query(BxReimburseInvoice, BxInvoice)
|
|
|
+ .join(
|
|
|
+ BxInvoice,
|
|
|
+ and_(
|
|
|
+ BxInvoice.id == BxReimburseInvoice.invoice_id,
|
|
|
+ BxInvoice.tenant_no == tenant_no,
|
|
|
+ BxInvoice.del_flag == 0,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .filter(
|
|
|
+ BxReimburseInvoice.reimburse_id == reimburse_id,
|
|
|
+ BxReimburseInvoice.tenant_no == tenant_no,
|
|
|
+ BxReimburseInvoice.del_flag == 0,
|
|
|
+ )
|
|
|
+ .order_by(BxReimburseInvoice.create_time.asc())
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ return [
|
|
|
+ ReimburseInvoiceBriefOut(
|
|
|
+ id=invoice.id,
|
|
|
+ amount=float(invoice.amount),
|
|
|
+ owner_user_name=invoice.owner_user_name,
|
|
|
+ )
|
|
|
+ for _, invoice in rows
|
|
|
+ ]
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def get_linked_invoice_ids_map(
|
|
|
+ db: Session, reimburse_ids: List[str], tenant_no: str
|
|
|
+ ) -> Dict[str, List[str]]:
|
|
|
+ if not reimburse_ids:
|
|
|
+ return {}
|
|
|
+ rows = (
|
|
|
+ db.query(BxReimburseInvoice.reimburse_id, BxReimburseInvoice.invoice_id)
|
|
|
+ .filter(
|
|
|
+ BxReimburseInvoice.reimburse_id.in_(reimburse_ids),
|
|
|
+ BxReimburseInvoice.tenant_no == tenant_no,
|
|
|
+ BxReimburseInvoice.del_flag == 0,
|
|
|
+ )
|
|
|
+ .order_by(BxReimburseInvoice.create_time.asc())
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ grouped: Dict[str, List[str]] = {}
|
|
|
+ for reimburse_id, invoice_id in rows:
|
|
|
+ grouped.setdefault(reimburse_id, []).append(invoice_id)
|
|
|
+ return grouped
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def submit(
|
|
|
+ db: Session,
|
|
|
+ apply_id: str,
|
|
|
+ user: BxUserContext,
|
|
|
+ *,
|
|
|
+ invoice_ids: Optional[List[str]] = None,
|
|
|
+ ) -> Optional[BxReimburseApply]:
|
|
|
tenant_no = ReimburseService._tenant_no(db, user)
|
|
|
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user)
|
|
|
if not apply_obj:
|
|
|
return None
|
|
|
+ if invoice_ids is not None:
|
|
|
+ ReimburseService.sync_reimburse_invoices(
|
|
|
+ db,
|
|
|
+ apply_id,
|
|
|
+ invoice_ids,
|
|
|
+ tenant_no,
|
|
|
+ user,
|
|
|
+ applicant_id=apply_obj.applicant_id,
|
|
|
+ )
|
|
|
+ if not ReimburseService._has_debit_reimburse(db, tenant_no, apply_id):
|
|
|
+ amount = Decimal(str(apply_obj.amount or 0))
|
|
|
+ InvoiceLedgerService.debit_reimburse(
|
|
|
+ db,
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ owner_user_id=apply_obj.applicant_id,
|
|
|
+ amount=amount,
|
|
|
+ reimburse_id=apply_id,
|
|
|
+ user=user,
|
|
|
+ )
|
|
|
apply_obj.status = "pending"
|
|
|
if not apply_obj.apply_code:
|
|
|
apply_obj.apply_code = generate_apply_code(db, "BX", tenant_no)
|
|
|
@@ -312,6 +486,28 @@ class ReimburseService:
|
|
|
apply_obj.status = "rejected"
|
|
|
apply_obj.current_node = None
|
|
|
apply_obj.update_by = approver.id
|
|
|
+ InvoiceLedgerService.release_reimburse(
|
|
|
+ db, tenant_no=tenant_no, reimburse_id=apply_id, user=approver
|
|
|
+ )
|
|
|
+ db.flush()
|
|
|
+ return apply_obj
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def withdraw(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxReimburseApply]:
|
|
|
+ tenant_no = ReimburseService._tenant_no(db, user)
|
|
|
+ apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user)
|
|
|
+ if not apply_obj:
|
|
|
+ return None
|
|
|
+ if apply_obj.applicant_id != user.id:
|
|
|
+ return None
|
|
|
+ if apply_obj.status != "pending":
|
|
|
+ return None
|
|
|
+ apply_obj.status = "draft"
|
|
|
+ apply_obj.current_node = None
|
|
|
+ apply_obj.update_by = user.id
|
|
|
+ InvoiceLedgerService.release_reimburse(
|
|
|
+ db, tenant_no=tenant_no, reimburse_id=apply_id, user=user
|
|
|
+ )
|
|
|
db.flush()
|
|
|
return apply_obj
|
|
|
|
|
|
@@ -620,17 +816,20 @@ class ReimburseService:
|
|
|
db, "reimburse_apply", ids, tenant_no
|
|
|
)
|
|
|
details_map = ReimburseService.get_details_map_by_reimburse_ids(db, ids, tenant_no)
|
|
|
+ invoice_ids_map = ReimburseService.get_linked_invoice_ids_map(db, ids, tenant_no)
|
|
|
out_list: List[ReimburseApplyOut] = []
|
|
|
for row in rows:
|
|
|
out = ReimburseApplyOut.model_validate(row)
|
|
|
inv, att = counts.get(row.id, (0, 0))
|
|
|
detail_rows = details_map.get(row.id, [])
|
|
|
+ linked_ids = invoice_ids_map.get(row.id, [])
|
|
|
out_list.append(
|
|
|
out.model_copy(
|
|
|
update={
|
|
|
"invoice_count": inv,
|
|
|
"attach_count": att,
|
|
|
"details": [ReimburseDetailOut.model_validate(d) for d in detail_rows],
|
|
|
+ "invoice_ids": linked_ids,
|
|
|
}
|
|
|
)
|
|
|
)
|
|
|
@@ -649,6 +848,8 @@ class ReimburseService:
|
|
|
out = ReimburseService.out_one_with_attachment_counts(db, tenant_no, reimburse)
|
|
|
detail_rows = ReimburseService.get_details(db, reimburse.id, tenant_no)
|
|
|
out.details = [ReimburseDetailOut.model_validate(d) for d in detail_rows]
|
|
|
+ out.invoice_ids = ReimburseService.get_linked_invoice_ids(db, reimburse.id, tenant_no)
|
|
|
+ out.invoices = ReimburseService.get_linked_invoices_brief(db, reimburse.id, tenant_no)
|
|
|
return out
|
|
|
|
|
|
@staticmethod
|