feat(bx): 报销模块接入数据权限过滤
新增 bx_data_scope 与 BxUserContext.to_user_info,列表/我的单据 AND scope,待办审批不加 scope,审批操作 skip_scope。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
eff99edeea
commit
2905dc3d5f
14 files changed
+351
-123
No files matched your search
@@ -67,7 +67,7 @@ def list_advances(
|
||||
d0, d1 = bounds
|
||||
filters.append(BxAdvanceApply.create_time >= d0)
|
||||
filters.append(BxAdvanceApply.create_time < d1)
|
||||
rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters)
|
||||
rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user)
|
||||
out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
|
||||
_fill_offset_amounts(db, tenant_no, rows, out_rows)
|
||||
return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
|
||||
@@ -104,7 +104,7 @@ def my_advances(
|
||||
d0, d1 = bounds
|
||||
filters.append(BxAdvanceApply.create_time >= d0)
|
||||
filters.append(BxAdvanceApply.create_time < d1)
|
||||
rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters)
|
||||
rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user)
|
||||
out_rows = [AdvanceApplyOut.model_validate(r) for r in rows]
|
||||
_fill_offset_amounts(db, tenant_no, rows, out_rows)
|
||||
return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
|
||||
@@ -116,7 +116,7 @@ def my_advance_stats(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
data = AdvanceService.my_apply_stats(db, user.id, tenant_no)
|
||||
data = AdvanceService.my_apply_stats(db, user, tenant_no)
|
||||
return ApiResponse(data=AdvanceMyStatsOut.model_validate(data))
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ def advance_status_counts(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
return ApiResponse(data=AdvanceService.global_status_counts(db, tenant_no))
|
||||
return ApiResponse(data=AdvanceService.global_status_counts(db, tenant_no, user=user))
|
||||
|
||||
|
||||
@router.get("/pending-approval", response_model=ApiResponse[PagedData[AdvanceApplyOut]])
|
||||
@@ -198,7 +198,7 @@ def get_advance(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
advance = AdvanceService.get_by_id(db, advance_id, tenant_no)
|
||||
advance = AdvanceService.get_by_id(db, advance_id, tenant_no, user=user)
|
||||
if not advance:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
|
||||
out = AdvanceApplyOut.model_validate(advance)
|
||||
@@ -214,9 +214,9 @@ def get_approvals(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
if not AdvanceService.get_by_id(db, advance_id, tenant_no):
|
||||
if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
|
||||
approvals = AdvanceService.get_approvals(db, advance_id, tenant_no)
|
||||
approvals = AdvanceService.get_approvals(db, advance_id, tenant_no, user=user)
|
||||
return ApiResponse(data=[AdvanceApprovalOut.model_validate(a) for a in approvals])
|
||||
|
||||
|
||||
@@ -227,9 +227,9 @@ def get_approval_history(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
if not AdvanceService.get_by_id(db, advance_id, tenant_no):
|
||||
if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
|
||||
history = AdvanceService.get_approval_history(db, advance_id, tenant_no)
|
||||
history = AdvanceService.get_approval_history(db, advance_id, tenant_no, user=user)
|
||||
return ApiResponse(data=[AdvanceApprovalOut.model_validate(a) for a in history])
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ def create_advance(
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
eid = data.get("expense_item_id")
|
||||
if eid:
|
||||
if not ExpenseItemService.get_by_id(db, eid, tenant_no):
|
||||
if not ExpenseItemService.get_by_id(db, eid, tenant_no, user=user):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在")
|
||||
data["applicant_id"] = user.id
|
||||
data["applicant_name"] = user.name
|
||||
@@ -277,7 +277,7 @@ def update_advance(
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
if data.get("expense_item_id"):
|
||||
eid = data["expense_item_id"]
|
||||
if not ExpenseItemService.get_by_id(db, eid, tenant_no):
|
||||
if not ExpenseItemService.get_by_id(db, eid, tenant_no, user=user):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"费用项目 {eid} 不存在")
|
||||
advance = AdvanceService.update(db, advance_id, data, user=user)
|
||||
if not advance:
|
||||
@@ -338,7 +338,7 @@ def delete_advance(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
if not AdvanceService.get_by_id(db, advance_id, tenant_no):
|
||||
if not AdvanceService.get_by_id(db, advance_id, tenant_no, user=user):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备用金申请不存在")
|
||||
AdvanceService.soft_delete(db, advance_id, user=user)
|
||||
db.commit()
|
||||
|
||||
@@ -70,7 +70,7 @@ def download_file(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no)
|
||||
attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user)
|
||||
if not attachment:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
|
||||
try:
|
||||
@@ -92,7 +92,7 @@ def delete_attachment(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
if not AttachmentService.get_by_id(db, attachment_id, tenant_no):
|
||||
if not AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
|
||||
success = AttachmentService.soft_delete(db, attachment_id, user=user)
|
||||
db.commit()
|
||||
@@ -107,7 +107,7 @@ def preview_attachment(
|
||||
):
|
||||
"""返回图片/附件预览 URL(MinIO 预签名或本地公开地址,对齐门户 storage 能力)。"""
|
||||
tenant_no = _tenant(db, user)
|
||||
attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no)
|
||||
attachment = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user)
|
||||
if not attachment:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
|
||||
try:
|
||||
@@ -132,7 +132,9 @@ def list_by_biz(
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
biz_id_value = str(biz_id).strip() if biz_id else "0"
|
||||
attachments = AttachmentService.get_by_biz(db, biz_type=biz_type, biz_id=biz_id_value, tenant_no=tenant_no)
|
||||
attachments = AttachmentService.get_by_biz(
|
||||
db, biz_type=biz_type, biz_id=biz_id_value, tenant_no=tenant_no, user=user
|
||||
)
|
||||
return ApiResponse(data=[AttachmentOut.model_validate(a) for a in attachments])
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ def get_expense_item_tree(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
tree = ExpenseItemService.get_item_tree(db, tenant_no)
|
||||
tree = ExpenseItemService.get_item_tree(db, tenant_no, user=user)
|
||||
return ApiResponse(data=[ExpenseItemOut.model_validate(i) for i in tree])
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def get_expense_item(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
item = ExpenseItemService.get_by_id(db, item_id, tenant_no)
|
||||
item = ExpenseItemService.get_by_id(db, item_id, tenant_no, user=user)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="费用项目不存在")
|
||||
return ApiResponse(data=ExpenseItemOut.model_validate(item))
|
||||
@@ -80,7 +80,7 @@ def delete_expense_item(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
item = ExpenseItemService.get_by_id(db, item_id, tenant_no)
|
||||
item = ExpenseItemService.get_by_id(db, item_id, tenant_no, user=user)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="费用项目不存在")
|
||||
ExpenseItemService.soft_delete(db, item_id, user=user)
|
||||
|
||||
@@ -70,7 +70,7 @@ def list_pending_payments(
|
||||
d0, d1 = bounds
|
||||
filters.append(BxReimburseApply.create_time >= d0)
|
||||
filters.append(BxReimburseApply.create_time < d1)
|
||||
rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters)
|
||||
rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user)
|
||||
outs = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows))
|
||||
return ApiResponse(
|
||||
data=PagedData(rows=_wrap_reimburse_rows(outs), total=total, page=page, page_size=pageSize)
|
||||
@@ -83,7 +83,7 @@ def payment_finance_stats(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
data = ReimburseService.finance_payment_stats(db, tenant_no)
|
||||
data = ReimburseService.finance_payment_stats(db, tenant_no, user=user)
|
||||
return ApiResponse(data=PaymentFinanceStatsOut.model_validate(data))
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ def list_processed_payments(
|
||||
d0, d1 = bounds
|
||||
filters.append(BxReimburseApply.create_time >= d0)
|
||||
filters.append(BxReimburseApply.create_time < d1)
|
||||
rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters)
|
||||
rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user)
|
||||
outs = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows))
|
||||
return ApiResponse(
|
||||
data=PagedData(rows=_wrap_reimburse_rows(outs), total=total, page=page, page_size=pageSize)
|
||||
@@ -220,7 +220,7 @@ def list_advance_issues(
|
||||
d0, d1 = bounds
|
||||
filters.append(BxAdvanceApply.create_time >= d0)
|
||||
filters.append(BxAdvanceApply.create_time < d1)
|
||||
rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters)
|
||||
rows, total = AdvanceService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user)
|
||||
out_rows = [AdvanceIssueOut.model_validate(r) for r in rows]
|
||||
if out_rows:
|
||||
offset_map = AdvanceService.offset_amount_map(db, tenant_no, [r.id for r in rows])
|
||||
@@ -235,7 +235,7 @@ def advance_issue_stats(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
data = AdvanceService.finance_issue_stats(db, tenant_no)
|
||||
data = AdvanceService.finance_issue_stats(db, tenant_no, user=user)
|
||||
return ApiResponse(data=AdvanceFinanceIssueStatsOut.model_validate(data))
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ def export_advance_issues(
|
||||
d0, d1 = bounds
|
||||
filters.append(BxAdvanceApply.create_time >= d0)
|
||||
filters.append(BxAdvanceApply.create_time < d1)
|
||||
rows, _ = AdvanceService.get_paged(db, tenant_no, page=1, page_size=99999, filters=filters)
|
||||
rows, _ = AdvanceService.get_paged(db, tenant_no, page=1, page_size=99999, filters=filters, user=user)
|
||||
out_rows = [AdvanceIssueOut.model_validate(r) for r in rows]
|
||||
|
||||
buf = io.StringIO()
|
||||
@@ -323,7 +323,7 @@ def issue_advance(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
issue_atts = AttachmentService.get_by_biz(db, "advance_issue", advance_id, tenant_no)
|
||||
issue_atts = AttachmentService.get_by_biz(db, "advance_issue", advance_id, tenant_no, user=user)
|
||||
if not issue_atts:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请至少上传 1 个发放附件")
|
||||
advance = AdvanceService.issue(
|
||||
@@ -346,6 +346,7 @@ def list_advance_offset_records(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
AdvanceService.get_by_id(db, advance_id, tenant_no, user=user)
|
||||
rows = (
|
||||
db.query(BxReimburseOffset, BxReimburseApply)
|
||||
.join(
|
||||
@@ -385,6 +386,7 @@ def list_reimburse_offset_records(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
ReimburseService.get_by_id(db, payment_id, tenant_no, user=user)
|
||||
rows = (
|
||||
db.query(BxReimburseOffset, BxAdvanceApply)
|
||||
.join(BxAdvanceApply, BxAdvanceApply.id == BxReimburseOffset.advance_id)
|
||||
|
||||
@@ -33,7 +33,7 @@ def _tenant(db: Session, user: BxUserContext) -> str:
|
||||
def _build_details_data(db: Session, tenant_no: str, body_details) -> List[dict]:
|
||||
details_data: List[dict] = []
|
||||
for d in body_details:
|
||||
item = ExpenseItemService.get_by_id(db, d.expense_item_id, tenant_no)
|
||||
item = ExpenseItemService.get_by_id(db, d.expense_item_id, tenant_no, user=user)
|
||||
if not item:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -95,7 +95,7 @@ def list_reimburses(
|
||||
d0, d1 = bounds
|
||||
filters.append(BxReimburseApply.create_time >= d0)
|
||||
filters.append(BxReimburseApply.create_time < d1)
|
||||
rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters)
|
||||
rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user)
|
||||
out_rows = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows))
|
||||
return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
|
||||
|
||||
@@ -131,7 +131,7 @@ def my_reimburses(
|
||||
d0, d1 = bounds
|
||||
filters.append(BxReimburseApply.create_time >= d0)
|
||||
filters.append(BxReimburseApply.create_time < d1)
|
||||
rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters)
|
||||
rows, total = ReimburseService.get_paged(db, tenant_no, page=page, page_size=pageSize, filters=filters, user=user)
|
||||
out_rows = ReimburseService.out_rows_with_attachment_counts(db, tenant_no, list(rows))
|
||||
return ApiResponse(data=PagedData(rows=out_rows, total=total, page=page, page_size=pageSize))
|
||||
|
||||
@@ -142,7 +142,7 @@ def my_reimburse_stats(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
data = ReimburseService.my_apply_stats(db, user.id, tenant_no)
|
||||
data = ReimburseService.my_apply_stats(db, user, tenant_no)
|
||||
return ApiResponse(data=ReimburseMyStatsOut.model_validate(data))
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ def reimburse_status_counts(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
return ApiResponse(data=ReimburseService.global_status_counts(db, tenant_no))
|
||||
return ApiResponse(data=ReimburseService.global_status_counts(db, tenant_no, user=user))
|
||||
|
||||
|
||||
@router.get("/pending-approval", response_model=ApiResponse[PagedData[ReimburseApplyOut]])
|
||||
@@ -224,7 +224,7 @@ def get_reimburse(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
reimburse = ReimburseService.get_by_id(db, reimburse_id, tenant_no)
|
||||
reimburse = ReimburseService.get_by_id(db, reimburse_id, tenant_no, user=user)
|
||||
if not reimburse:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在")
|
||||
out = ReimburseService.build_apply_out_with_details(db, tenant_no, reimburse)
|
||||
@@ -238,9 +238,9 @@ def get_approvals(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
if not ReimburseService.get_by_id(db, reimburse_id, tenant_no):
|
||||
if not ReimburseService.get_by_id(db, reimburse_id, tenant_no, user=user):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在")
|
||||
approvals = ReimburseService.get_approvals(db, reimburse_id, tenant_no)
|
||||
approvals = ReimburseService.get_approvals(db, reimburse_id, tenant_no, user=user)
|
||||
return ApiResponse(data=[ReimburseApprovalOut.model_validate(a) for a in approvals])
|
||||
|
||||
|
||||
@@ -251,9 +251,9 @@ def get_approval_history(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
if not ReimburseService.get_by_id(db, reimburse_id, tenant_no):
|
||||
if not ReimburseService.get_by_id(db, reimburse_id, tenant_no, user=user):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在")
|
||||
history = ReimburseService.get_approval_history(db, reimburse_id, tenant_no)
|
||||
history = ReimburseService.get_approval_history(db, reimburse_id, tenant_no, user=user)
|
||||
return ApiResponse(data=[ReimburseApprovalOut.model_validate(a) for a in history])
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ def delete_reimburse(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = _tenant(db, user)
|
||||
if not ReimburseService.get_by_id(db, reimburse_id, tenant_no):
|
||||
if not ReimburseService.get_by_id(db, reimburse_id, tenant_no, user=user):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销单不存在")
|
||||
ReimburseService.soft_delete(db, reimburse_id, user=user)
|
||||
db.commit()
|
||||
|
||||
@@ -26,7 +26,7 @@ def list_workflows(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
rows, total = WorkflowService.get_paged(db, tenant_no, page=page, page_size=pageSize)
|
||||
rows, total = WorkflowService.get_paged(db, tenant_no, page=page, page_size=pageSize, user=user)
|
||||
out_rows = [WorkflowDefOut.model_validate(r) for r in rows]
|
||||
paged_data = PagedData(rows=out_rows, total=total, page=page, page_size=pageSize)
|
||||
return ApiResponse(data=paged_data)
|
||||
@@ -51,7 +51,7 @@ def get_workflow(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no)
|
||||
workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no, user=user)
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工作流不存在")
|
||||
return ApiResponse(data=WorkflowDefOut.model_validate(workflow))
|
||||
@@ -107,7 +107,7 @@ def save_workflow_nodes(
|
||||
except ValueError as exc:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no)
|
||||
workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no, user=user)
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工作流不存在")
|
||||
db.commit()
|
||||
@@ -122,7 +122,7 @@ def delete_workflow(
|
||||
user: BxUserContext = Depends(get_bx_current_user),
|
||||
):
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no)
|
||||
workflow = WorkflowService.get_by_id(db, workflow_id, tenant_no, user=user)
|
||||
if not workflow:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="工作流不存在")
|
||||
WorkflowService.soft_delete(db, workflow_id, user=user)
|
||||
|
||||
@@ -27,6 +27,16 @@ class BxUserContext:
|
||||
"tenant_no": self.tenant_no,
|
||||
}
|
||||
|
||||
def to_user_info(self) -> Dict[str, Any]:
|
||||
"""与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。"""
|
||||
return {
|
||||
"user_id": self.id,
|
||||
"role": self.role,
|
||||
"tenant_no": self.tenant_no,
|
||||
"dept_id": self.dept_id,
|
||||
"region_id": self.dept_id,
|
||||
}
|
||||
|
||||
|
||||
def get_bx_current_user(
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
|
||||
@@ -18,6 +18,7 @@ from app.models.bx_models import (
|
||||
)
|
||||
from app.services.bx.approval_cycle import filter_current_cycle_approvals
|
||||
from app.services.bx.base import BxCrudService
|
||||
from app.services.bx_data_scope import apply_bx_scope
|
||||
from app.utils.bx_codegen import generate_apply_code
|
||||
from app.utils.bx_date_range import current_month_bounds, parse_create_time_day_bounds
|
||||
|
||||
@@ -28,8 +29,17 @@ class AdvanceService:
|
||||
return get_tenant_no(db, user.to_tenant_user_dict())
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, advance_id: str, tenant_no: str) -> Optional[BxAdvanceApply]:
|
||||
return BxCrudService.get_by_id(BxAdvanceApply, db, advance_id, tenant_no)
|
||||
def get_by_id(
|
||||
db: Session,
|
||||
advance_id: str,
|
||||
tenant_no: str,
|
||||
*,
|
||||
user: Optional[BxUserContext] = None,
|
||||
skip_scope: bool = False,
|
||||
) -> Optional[BxAdvanceApply]:
|
||||
return BxCrudService.get_by_id(
|
||||
BxAdvanceApply, db, advance_id, tenant_no, user=user, skip_scope=skip_scope
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_paged(
|
||||
@@ -38,8 +48,12 @@ class AdvanceService:
|
||||
page: int,
|
||||
page_size: int,
|
||||
filters: Optional[List[Any]] = None,
|
||||
*,
|
||||
user: Optional[BxUserContext] = None,
|
||||
) -> Tuple[Sequence[BxAdvanceApply], int]:
|
||||
return BxCrudService.get_paged(BxAdvanceApply, db, tenant_no, page, page_size, filters)
|
||||
return BxCrudService.get_paged(
|
||||
BxAdvanceApply, db, tenant_no, page, page_size, filters, user=user
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
@@ -74,13 +88,14 @@ class AdvanceService:
|
||||
tenant_no,
|
||||
obj_in,
|
||||
user_id=user.id,
|
||||
user=user,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def soft_delete(db: Session, advance_id: str, *, user: BxUserContext) -> bool:
|
||||
tenant_no = AdvanceService._tenant_no(db, user)
|
||||
return BxCrudService.soft_delete(
|
||||
BxAdvanceApply, db, advance_id, tenant_no, user_id=user.id
|
||||
BxAdvanceApply, db, advance_id, tenant_no, user_id=user.id, user=user
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -178,6 +193,7 @@ class AdvanceService:
|
||||
if bounds:
|
||||
d0, d1 = bounds
|
||||
query = query.filter(BxAdvanceApply.create_time >= d0, BxAdvanceApply.create_time < d1)
|
||||
query = apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
|
||||
total = query.count()
|
||||
rows = (
|
||||
query.order_by(BxAdvanceApply.create_time.desc())
|
||||
@@ -203,7 +219,7 @@ class AdvanceService:
|
||||
@staticmethod
|
||||
def submit(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxAdvanceApply]:
|
||||
tenant_no = AdvanceService._tenant_no(db, user)
|
||||
apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no)
|
||||
apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
|
||||
if not apply_obj:
|
||||
return None
|
||||
apply_obj.status = "pending"
|
||||
@@ -222,7 +238,7 @@ class AdvanceService:
|
||||
db: Session, apply_id: str, approver: BxUserContext, opinion: str
|
||||
) -> Optional[BxAdvanceApply]:
|
||||
tenant_no = AdvanceService._tenant_no(db, approver)
|
||||
apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no)
|
||||
apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, skip_scope=True)
|
||||
if not apply_obj or not apply_obj.wf_id:
|
||||
return None
|
||||
nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
|
||||
@@ -267,7 +283,7 @@ class AdvanceService:
|
||||
db: Session, apply_id: str, approver: BxUserContext, opinion: str
|
||||
) -> Optional[BxAdvanceApply]:
|
||||
tenant_no = AdvanceService._tenant_no(db, approver)
|
||||
apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no)
|
||||
apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, skip_scope=True)
|
||||
if not apply_obj or not apply_obj.wf_id:
|
||||
return None
|
||||
nodes = AdvanceService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
|
||||
@@ -342,8 +358,10 @@ class AdvanceService:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_approvals(db: Session, apply_id: str, tenant_no: str) -> List[Dict[str, Any]]:
|
||||
apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no)
|
||||
def get_approvals(
|
||||
db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
|
||||
if not apply_obj or not apply_obj.wf_id:
|
||||
return []
|
||||
wf = BxCrudService.get_by_id(BxWorkflowDef, db, apply_obj.wf_id, tenant_no)
|
||||
@@ -376,9 +394,11 @@ class AdvanceService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_approval_history(db: Session, apply_id: str, tenant_no: str) -> List[Dict[str, Any]]:
|
||||
def get_approval_history(
|
||||
db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""返回全部审批动作,按时间正序排列,用于展示完整审批历史。"""
|
||||
if not AdvanceService.get_by_id(db, apply_id, tenant_no):
|
||||
if not AdvanceService.get_by_id(db, apply_id, tenant_no, user=user):
|
||||
return []
|
||||
approvals = (
|
||||
db.query(BxAdvanceApproval)
|
||||
@@ -393,26 +413,33 @@ class AdvanceService:
|
||||
return [AdvanceService._approval_record_dict(a, apply_id) for a in approvals]
|
||||
|
||||
@staticmethod
|
||||
def global_status_counts(db: Session, tenant_no: str) -> Dict[str, int]:
|
||||
rows = (
|
||||
db.query(BxAdvanceApply.status, func.count())
|
||||
.filter(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no)
|
||||
.group_by(BxAdvanceApply.status)
|
||||
.all()
|
||||
def global_status_counts(db: Session, tenant_no: str, *, user: BxUserContext) -> Dict[str, int]:
|
||||
query = db.query(BxAdvanceApply.status, func.count()).filter(
|
||||
BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no
|
||||
)
|
||||
query = apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
|
||||
rows = query.group_by(BxAdvanceApply.status).all()
|
||||
return {str(s or ""): int(c) for s, c in rows}
|
||||
|
||||
@staticmethod
|
||||
def my_apply_stats(db: Session, user_id: str, tenant_no: str) -> dict:
|
||||
base = and_(
|
||||
def _my_apply_base(db: Session, user: BxUserContext, tenant_no: str):
|
||||
query = db.query(BxAdvanceApply).filter(
|
||||
BxAdvanceApply.del_flag == 0,
|
||||
BxAdvanceApply.tenant_no == tenant_no,
|
||||
or_(
|
||||
BxAdvanceApply.applicant_id == user_id,
|
||||
BxAdvanceApply.create_by == user_id,
|
||||
BxAdvanceApply.applicant_id == user.id,
|
||||
BxAdvanceApply.create_by == user.id,
|
||||
),
|
||||
)
|
||||
total_count = db.query(func.count()).select_from(BxAdvanceApply).filter(base).scalar() or 0
|
||||
return apply_bx_scope(query, BxAdvanceApply, user, db, tenant_no)
|
||||
|
||||
@staticmethod
|
||||
def my_apply_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict:
|
||||
base_q = AdvanceService._my_apply_base(db, user, tenant_no).subquery()
|
||||
base = BxAdvanceApply.id.in_(db.query(base_q.c.id))
|
||||
total_count = (
|
||||
db.query(func.count()).select_from(BxAdvanceApply).filter(base).scalar() or 0
|
||||
)
|
||||
pending = (
|
||||
db.query(func.count())
|
||||
.select_from(BxAdvanceApply)
|
||||
@@ -466,9 +493,7 @@ class AdvanceService:
|
||||
db.query(func.coalesce(func.sum(BxReimburseOffset.offset_amount), 0))
|
||||
.join(BxAdvanceApply, BxReimburseOffset.advance_id == BxAdvanceApply.id)
|
||||
.filter(
|
||||
BxAdvanceApply.applicant_id == user_id,
|
||||
BxAdvanceApply.del_flag == 0,
|
||||
BxAdvanceApply.tenant_no == tenant_no,
|
||||
BxAdvanceApply.id.in_(db.query(base_q.c.id)),
|
||||
BxReimburseOffset.tenant_no == tenant_no,
|
||||
BxReimburseOffset.del_flag == 0,
|
||||
)
|
||||
@@ -547,7 +572,7 @@ class AdvanceService:
|
||||
actual_amount: Optional[float] = None,
|
||||
) -> Optional[BxAdvanceApply]:
|
||||
tenant_no = AdvanceService._tenant_no(db, user)
|
||||
apply_obj = AdvanceService.get_by_id(db, advance_id, tenant_no)
|
||||
apply_obj = AdvanceService.get_by_id(db, advance_id, tenant_no, user=user)
|
||||
if not apply_obj:
|
||||
return None
|
||||
apply_obj.status = "issued"
|
||||
@@ -564,8 +589,13 @@ class AdvanceService:
|
||||
return apply_obj
|
||||
|
||||
@staticmethod
|
||||
def finance_issue_stats(db: Session, tenant_no: str) -> dict:
|
||||
base = and_(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no)
|
||||
def finance_issue_stats(db: Session, tenant_no: str, *, user: BxUserContext) -> dict:
|
||||
base_q = (
|
||||
db.query(BxAdvanceApply)
|
||||
.filter(BxAdvanceApply.del_flag == 0, BxAdvanceApply.tenant_no == tenant_no)
|
||||
)
|
||||
base_q = apply_bx_scope(base_q, BxAdvanceApply, user, db, tenant_no).subquery()
|
||||
base = BxAdvanceApply.id.in_(db.query(base_q.c.id))
|
||||
pending_f = and_(
|
||||
BxAdvanceApply.status == "approved",
|
||||
or_(BxAdvanceApply.issue_status.is_(None), BxAdvanceApply.issue_status != "issued"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Optional
|
||||
|
||||
from sqlalchemy import case as sql_case, func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -9,6 +9,7 @@ from app.core.bx_auth import BxUserContext
|
||||
from app.core.tenant_context import get_tenant_no
|
||||
from app.models.bx_models import BxAttachment
|
||||
from app.services.bx.base import BxCrudService
|
||||
from app.services.bx_data_scope import assert_bx_attachment_biz_in_scope, assert_bx_biz_in_scope
|
||||
|
||||
|
||||
class AttachmentService:
|
||||
@@ -17,8 +18,17 @@ class AttachmentService:
|
||||
return get_tenant_no(db, user.to_tenant_user_dict())
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, attachment_id: str, tenant_no: str) -> Optional[BxAttachment]:
|
||||
return BxCrudService.get_by_id(BxAttachment, db, attachment_id, tenant_no)
|
||||
def get_by_id(
|
||||
db: Session,
|
||||
attachment_id: str,
|
||||
tenant_no: str,
|
||||
*,
|
||||
user: Optional[BxUserContext] = None,
|
||||
) -> Optional[BxAttachment]:
|
||||
row = BxCrudService.get_by_id(BxAttachment, db, attachment_id, tenant_no)
|
||||
if row and user:
|
||||
assert_bx_attachment_biz_in_scope(db, row, user, tenant_no)
|
||||
return row
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
@@ -40,12 +50,18 @@ class AttachmentService:
|
||||
@staticmethod
|
||||
def soft_delete(db: Session, attachment_id: str, *, user: BxUserContext) -> bool:
|
||||
tenant_no = AttachmentService._tenant_no(db, user)
|
||||
row = AttachmentService.get_by_id(db, attachment_id, tenant_no, user=user)
|
||||
if not row:
|
||||
return False
|
||||
return BxCrudService.soft_delete(
|
||||
BxAttachment, db, attachment_id, tenant_no, user_id=user.id
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_biz(db: Session, biz_type: str, biz_id: str, tenant_no: str) -> List[BxAttachment]:
|
||||
def get_by_biz(
|
||||
db: Session, biz_type: str, biz_id: str, tenant_no: str, *, user: BxUserContext
|
||||
) -> List[BxAttachment]:
|
||||
assert_bx_biz_in_scope(db, biz_type, biz_id, user, tenant_no)
|
||||
query = db.query(BxAttachment).filter(
|
||||
BxAttachment.biz_type == biz_type,
|
||||
BxAttachment.biz_id == biz_id,
|
||||
@@ -64,6 +80,7 @@ class AttachmentService:
|
||||
user: BxUserContext,
|
||||
) -> int:
|
||||
tenant_no = AttachmentService._tenant_no(db, user)
|
||||
assert_bx_biz_in_scope(db, biz_type, biz_id, user, tenant_no)
|
||||
if not ids:
|
||||
return 0
|
||||
count = (
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
"""报销模块 Service 基类 — 租户隔离 + UUID + 软删除。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, List, Optional, Sequence, Tuple, Type, TypeVar
|
||||
from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple, Type, TypeVar
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.utils import generate_uuid
|
||||
from app.services.bx_data_scope import apply_bx_scope, assert_bx_row_in_scope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.core.bx_auth import BxUserContext
|
||||
|
||||
M = TypeVar("M")
|
||||
|
||||
|
||||
class BxCrudService:
|
||||
@staticmethod
|
||||
def get_by_id(model: Type[M], db: Session, id: str, tenant_no: str) -> Optional[M]:
|
||||
def get_by_id(
|
||||
model: Type[M],
|
||||
db: Session,
|
||||
id: str,
|
||||
tenant_no: str,
|
||||
*,
|
||||
user: Optional["BxUserContext"] = None,
|
||||
skip_scope: bool = False,
|
||||
) -> Optional[M]:
|
||||
query = db.query(model).filter(model.id == id, model.tenant_no == tenant_no)
|
||||
if hasattr(model, "del_flag"):
|
||||
query = query.filter(model.del_flag == 0)
|
||||
return query.first()
|
||||
row = query.first()
|
||||
if row and user and not skip_scope:
|
||||
assert_bx_row_in_scope(db, row, model, user, tenant_no)
|
||||
return row
|
||||
|
||||
@staticmethod
|
||||
def get_paged(
|
||||
@@ -26,12 +41,16 @@ class BxCrudService:
|
||||
page: int,
|
||||
page_size: int,
|
||||
filters: Optional[List[Any]] = None,
|
||||
*,
|
||||
user: Optional["BxUserContext"] = None,
|
||||
) -> Tuple[Sequence[M], int]:
|
||||
query = db.query(model).filter(model.tenant_no == tenant_no)
|
||||
if hasattr(model, "del_flag"):
|
||||
query = query.filter(model.del_flag == 0)
|
||||
if filters:
|
||||
query = query.filter(*filters)
|
||||
if user:
|
||||
query = apply_bx_scope(query, model, user, db, tenant_no)
|
||||
total = query.count()
|
||||
if hasattr(model, "sort_order"):
|
||||
query = query.order_by(model.sort_order.asc(), model.create_time.desc())
|
||||
@@ -69,8 +88,12 @@ class BxCrudService:
|
||||
obj_in: dict,
|
||||
*,
|
||||
user_id: str,
|
||||
user: Optional["BxUserContext"] = None,
|
||||
skip_scope: bool = False,
|
||||
) -> Optional[M]:
|
||||
obj = BxCrudService.get_by_id(model, db, id, tenant_no)
|
||||
obj = BxCrudService.get_by_id(
|
||||
model, db, id, tenant_no, user=user, skip_scope=skip_scope
|
||||
)
|
||||
if not obj:
|
||||
return None
|
||||
for key, value in obj_in.items():
|
||||
@@ -81,8 +104,19 @@ class BxCrudService:
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def soft_delete(model: Type[M], db: Session, id: str, tenant_no: str, *, user_id: str = "") -> bool:
|
||||
obj = BxCrudService.get_by_id(model, db, id, tenant_no)
|
||||
def soft_delete(
|
||||
model: Type[M],
|
||||
db: Session,
|
||||
id: str,
|
||||
tenant_no: str,
|
||||
*,
|
||||
user_id: str = "",
|
||||
user: Optional["BxUserContext"] = None,
|
||||
skip_scope: bool = False,
|
||||
) -> bool:
|
||||
obj = BxCrudService.get_by_id(
|
||||
model, db, id, tenant_no, user=user, skip_scope=skip_scope
|
||||
)
|
||||
if not obj:
|
||||
return False
|
||||
obj.del_flag = 1
|
||||
|
||||
@@ -8,6 +8,7 @@ from app.core.bx_auth import BxUserContext
|
||||
from app.core.tenant_context import get_tenant_no
|
||||
from app.models.bx_models import BxExpenseItem
|
||||
from app.services.bx.base import BxCrudService
|
||||
from app.services.bx_data_scope import apply_bx_scope
|
||||
|
||||
|
||||
class ExpenseItemService:
|
||||
@@ -16,8 +17,17 @@ class ExpenseItemService:
|
||||
return get_tenant_no(db, user.to_tenant_user_dict())
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, item_id: str, tenant_no: str) -> Optional[BxExpenseItem]:
|
||||
return BxCrudService.get_by_id(BxExpenseItem, db, item_id, tenant_no)
|
||||
def get_by_id(
|
||||
db: Session,
|
||||
item_id: str,
|
||||
tenant_no: str,
|
||||
*,
|
||||
user: Optional[BxUserContext] = None,
|
||||
skip_scope: bool = False,
|
||||
) -> Optional[BxExpenseItem]:
|
||||
return BxCrudService.get_by_id(
|
||||
BxExpenseItem, db, item_id, tenant_no, user=user, skip_scope=skip_scope
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
@@ -31,7 +41,7 @@ class ExpenseItemService:
|
||||
parent_id = data.get("parent_id") or "0"
|
||||
data["parent_id"] = parent_id
|
||||
if parent_id and parent_id != "0":
|
||||
parent = ExpenseItemService.get_by_id(db, parent_id, tenant_no)
|
||||
parent = ExpenseItemService.get_by_id(db, parent_id, tenant_no, user=user)
|
||||
if not parent:
|
||||
raise ValueError("父级费用项目不存在")
|
||||
data.setdefault("level", (parent.level or 1) + 1)
|
||||
@@ -63,7 +73,7 @@ class ExpenseItemService:
|
||||
if parent_id != "0":
|
||||
if parent_id == item_id:
|
||||
raise ValueError("不能将项目设置为自己的父级")
|
||||
parent = ExpenseItemService.get_by_id(db, parent_id, tenant_no)
|
||||
parent = ExpenseItemService.get_by_id(db, parent_id, tenant_no, user=user)
|
||||
if not parent:
|
||||
raise ValueError("父级费用项目不存在")
|
||||
return BxCrudService.update(
|
||||
@@ -73,23 +83,25 @@ class ExpenseItemService:
|
||||
tenant_no,
|
||||
data,
|
||||
user_id=user.id,
|
||||
user=user,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def soft_delete(db: Session, item_id: str, *, user: BxUserContext) -> bool:
|
||||
tenant_no = ExpenseItemService._tenant_no(db, user)
|
||||
return BxCrudService.soft_delete(
|
||||
BxExpenseItem, db, item_id, tenant_no, user_id=user.id
|
||||
BxExpenseItem, db, item_id, tenant_no, user_id=user.id, user=user
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_item_tree(db: Session, tenant_no: str) -> List[BxExpenseItem]:
|
||||
items = (
|
||||
db.query(BxExpenseItem)
|
||||
.filter(BxExpenseItem.del_flag == 0, BxExpenseItem.tenant_no == tenant_no)
|
||||
.order_by(BxExpenseItem.sort_order.asc(), BxExpenseItem.create_time.desc())
|
||||
.all()
|
||||
def get_item_tree(db: Session, tenant_no: str, *, user: BxUserContext) -> List[BxExpenseItem]:
|
||||
query = db.query(BxExpenseItem).filter(
|
||||
BxExpenseItem.del_flag == 0, BxExpenseItem.tenant_no == tenant_no
|
||||
)
|
||||
query = apply_bx_scope(query, BxExpenseItem, user, db, tenant_no)
|
||||
items = query.order_by(
|
||||
BxExpenseItem.sort_order.asc(), BxExpenseItem.create_time.desc()
|
||||
).all()
|
||||
return ExpenseItemService._build_tree(items)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -23,6 +23,7 @@ from app.schemas.bx.reimburse import ReimburseApplyOut, ReimburseDetailOut
|
||||
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_data_scope import apply_bx_scope
|
||||
from app.utils.bx_codegen import (
|
||||
allocate_reimburse_title_seq,
|
||||
generate_apply_code,
|
||||
@@ -42,8 +43,17 @@ class ReimburseService:
|
||||
return get_tenant_no(db, user.to_tenant_user_dict())
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, reimburse_id: str, tenant_no: str) -> Optional[BxReimburseApply]:
|
||||
return BxCrudService.get_by_id(BxReimburseApply, db, reimburse_id, tenant_no)
|
||||
def get_by_id(
|
||||
db: Session,
|
||||
reimburse_id: str,
|
||||
tenant_no: str,
|
||||
*,
|
||||
user: Optional[BxUserContext] = None,
|
||||
skip_scope: bool = False,
|
||||
) -> Optional[BxReimburseApply]:
|
||||
return BxCrudService.get_by_id(
|
||||
BxReimburseApply, db, reimburse_id, tenant_no, user=user, skip_scope=skip_scope
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_paged(
|
||||
@@ -52,14 +62,18 @@ class ReimburseService:
|
||||
page: int,
|
||||
page_size: int,
|
||||
filters: Optional[List[Any]] = None,
|
||||
*,
|
||||
user: Optional[BxUserContext] = None,
|
||||
) -> Tuple[Sequence[BxReimburseApply], int]:
|
||||
return BxCrudService.get_paged(BxReimburseApply, db, tenant_no, page, page_size, filters)
|
||||
return BxCrudService.get_paged(
|
||||
BxReimburseApply, db, tenant_no, page, page_size, filters, user=user
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def soft_delete(db: Session, reimburse_id: str, *, user: BxUserContext) -> bool:
|
||||
tenant_no = ReimburseService._tenant_no(db, user)
|
||||
return BxCrudService.soft_delete(
|
||||
BxReimburseApply, db, reimburse_id, tenant_no, user_id=user.id
|
||||
BxReimburseApply, db, reimburse_id, tenant_no, user_id=user.id, user=user
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -170,6 +184,7 @@ class ReimburseService:
|
||||
if bounds:
|
||||
d0, d1 = bounds
|
||||
query = query.filter(BxReimburseApply.create_time >= d0, BxReimburseApply.create_time < d1)
|
||||
query = apply_bx_scope(query, BxReimburseApply, user, db, tenant_no)
|
||||
total = query.count()
|
||||
rows = (
|
||||
query.order_by(BxReimburseApply.create_time.desc())
|
||||
@@ -182,7 +197,7 @@ class ReimburseService:
|
||||
@staticmethod
|
||||
def submit(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)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user)
|
||||
if not apply_obj:
|
||||
return None
|
||||
apply_obj.status = "pending"
|
||||
@@ -201,7 +216,7 @@ class ReimburseService:
|
||||
db: Session, apply_id: str, approver: BxUserContext, opinion: str
|
||||
) -> Optional[BxReimburseApply]:
|
||||
tenant_no = ReimburseService._tenant_no(db, approver)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, skip_scope=True)
|
||||
if not apply_obj or not apply_obj.wf_id:
|
||||
return None
|
||||
nodes = ReimburseService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
|
||||
@@ -246,7 +261,7 @@ class ReimburseService:
|
||||
db: Session, apply_id: str, approver: BxUserContext, opinion: str
|
||||
) -> Optional[BxReimburseApply]:
|
||||
tenant_no = ReimburseService._tenant_no(db, approver)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, skip_scope=True)
|
||||
if not apply_obj or not apply_obj.wf_id:
|
||||
return None
|
||||
nodes = ReimburseService._workflow_nodes(db, apply_obj.wf_id, tenant_no)
|
||||
@@ -321,8 +336,10 @@ class ReimburseService:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_approvals(db: Session, apply_id: str, tenant_no: str) -> List[Dict[str, Any]]:
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no)
|
||||
def get_approvals(
|
||||
db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user)
|
||||
if not apply_obj or not apply_obj.wf_id:
|
||||
return []
|
||||
wf = BxCrudService.get_by_id(BxWorkflowDef, db, apply_obj.wf_id, tenant_no)
|
||||
@@ -355,9 +372,11 @@ class ReimburseService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_approval_history(db: Session, apply_id: str, tenant_no: str) -> List[Dict[str, Any]]:
|
||||
def get_approval_history(
|
||||
db: Session, apply_id: str, tenant_no: str, *, user: Optional[BxUserContext] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""返回全部审批动作,按时间正序排列,用于展示完整审批历史。"""
|
||||
if not ReimburseService.get_by_id(db, apply_id, tenant_no):
|
||||
if not ReimburseService.get_by_id(db, apply_id, tenant_no, user=user):
|
||||
return []
|
||||
approvals = (
|
||||
db.query(BxReimburseApproval)
|
||||
@@ -445,7 +464,7 @@ class ReimburseService:
|
||||
user: BxUserContext,
|
||||
) -> Optional[BxReimburseApply]:
|
||||
tenant_no = ReimburseService._tenant_no(db, user)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user)
|
||||
if not apply_obj:
|
||||
return None
|
||||
|
||||
@@ -560,26 +579,33 @@ class ReimburseService:
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def global_status_counts(db: Session, tenant_no: str) -> Dict[str, int]:
|
||||
rows = (
|
||||
db.query(BxReimburseApply.status, func.count())
|
||||
.filter(BxReimburseApply.del_flag == 0, BxReimburseApply.tenant_no == tenant_no)
|
||||
.group_by(BxReimburseApply.status)
|
||||
.all()
|
||||
def global_status_counts(db: Session, tenant_no: str, *, user: BxUserContext) -> Dict[str, int]:
|
||||
query = db.query(BxReimburseApply.status, func.count()).filter(
|
||||
BxReimburseApply.del_flag == 0, BxReimburseApply.tenant_no == tenant_no
|
||||
)
|
||||
query = apply_bx_scope(query, BxReimburseApply, user, db, tenant_no)
|
||||
rows = query.group_by(BxReimburseApply.status).all()
|
||||
return {str(s or ""): int(c) for s, c in rows}
|
||||
|
||||
@staticmethod
|
||||
def my_apply_stats(db: Session, user_id: str, tenant_no: str) -> dict:
|
||||
base = and_(
|
||||
def _my_apply_base(db: Session, user: BxUserContext, tenant_no: str):
|
||||
query = db.query(BxReimburseApply).filter(
|
||||
BxReimburseApply.del_flag == 0,
|
||||
BxReimburseApply.tenant_no == tenant_no,
|
||||
or_(
|
||||
BxReimburseApply.applicant_id == user_id,
|
||||
BxReimburseApply.create_by == user_id,
|
||||
BxReimburseApply.applicant_id == user.id,
|
||||
BxReimburseApply.create_by == user.id,
|
||||
),
|
||||
)
|
||||
total_count = db.query(func.count()).select_from(BxReimburseApply).filter(base).scalar() or 0
|
||||
return apply_bx_scope(query, BxReimburseApply, user, db, tenant_no)
|
||||
|
||||
@staticmethod
|
||||
def my_apply_stats(db: Session, user: BxUserContext, tenant_no: str) -> dict:
|
||||
base_q = ReimburseService._my_apply_base(db, user, tenant_no).subquery()
|
||||
base = BxReimburseApply.id.in_(db.query(base_q.c.id))
|
||||
total_count = (
|
||||
db.query(func.count()).select_from(BxReimburseApply).filter(base).scalar() or 0
|
||||
)
|
||||
pending = (
|
||||
db.query(func.count())
|
||||
.select_from(BxReimburseApply)
|
||||
@@ -671,7 +697,7 @@ class ReimburseService:
|
||||
actual_amount: Optional[float] = None,
|
||||
) -> Optional[BxReimburseApply]:
|
||||
tenant_no = ReimburseService._tenant_no(db, user)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user)
|
||||
if not apply_obj:
|
||||
return None
|
||||
apply_obj.status = "processed"
|
||||
@@ -695,7 +721,7 @@ class ReimburseService:
|
||||
remark: str = "",
|
||||
) -> Optional[BxReimburseApply]:
|
||||
tenant_no = ReimburseService._tenant_no(db, user)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user)
|
||||
if not apply_obj:
|
||||
return None
|
||||
apply_obj.status = "offset"
|
||||
@@ -726,7 +752,7 @@ class ReimburseService:
|
||||
remark: str = "",
|
||||
) -> Optional[BxReimburseApply]:
|
||||
tenant_no = ReimburseService._tenant_no(db, user)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no)
|
||||
apply_obj = ReimburseService.get_by_id(db, apply_id, tenant_no, user=user)
|
||||
if not apply_obj:
|
||||
return None
|
||||
|
||||
@@ -772,8 +798,13 @@ class ReimburseService:
|
||||
return apply_obj
|
||||
|
||||
@staticmethod
|
||||
def finance_payment_stats(db: Session, tenant_no: str) -> dict:
|
||||
base = and_(BxReimburseApply.del_flag == 0, BxReimburseApply.tenant_no == tenant_no)
|
||||
def finance_payment_stats(db: Session, tenant_no: str, *, user: BxUserContext) -> dict:
|
||||
base_q = (
|
||||
db.query(BxReimburseApply)
|
||||
.filter(BxReimburseApply.del_flag == 0, BxReimburseApply.tenant_no == tenant_no)
|
||||
)
|
||||
base_q = apply_bx_scope(base_q, BxReimburseApply, user, db, tenant_no).subquery()
|
||||
base = BxReimburseApply.id.in_(db.query(base_q.c.id))
|
||||
pending = (
|
||||
db.query(func.count())
|
||||
.select_from(BxReimburseApply)
|
||||
|
||||
@@ -29,8 +29,17 @@ class WorkflowService:
|
||||
return get_tenant_no(db, user.to_tenant_user_dict())
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, workflow_id: str, tenant_no: str) -> Optional[BxWorkflowDef]:
|
||||
return BxCrudService.get_by_id(BxWorkflowDef, db, workflow_id, tenant_no)
|
||||
def get_by_id(
|
||||
db: Session,
|
||||
workflow_id: str,
|
||||
tenant_no: str,
|
||||
*,
|
||||
user: Optional[BxUserContext] = None,
|
||||
skip_scope: bool = False,
|
||||
) -> Optional[BxWorkflowDef]:
|
||||
return BxCrudService.get_by_id(
|
||||
BxWorkflowDef, db, workflow_id, tenant_no, user=user, skip_scope=skip_scope
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_paged(
|
||||
@@ -38,8 +47,12 @@ class WorkflowService:
|
||||
tenant_no: str,
|
||||
page: int,
|
||||
page_size: int,
|
||||
*,
|
||||
user: Optional[BxUserContext] = None,
|
||||
) -> Tuple[Sequence[BxWorkflowDef], int]:
|
||||
return BxCrudService.get_paged(BxWorkflowDef, db, tenant_no, page, page_size)
|
||||
return BxCrudService.get_paged(
|
||||
BxWorkflowDef, db, tenant_no, page, page_size, user=user
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
@@ -74,13 +87,14 @@ class WorkflowService:
|
||||
tenant_no,
|
||||
obj_in,
|
||||
user_id=user.id,
|
||||
user=user,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def soft_delete(db: Session, workflow_id: str, *, user: BxUserContext) -> bool:
|
||||
tenant_no = WorkflowService._tenant_no(db, user)
|
||||
deleted = BxCrudService.soft_delete(
|
||||
BxWorkflowDef, db, workflow_id, tenant_no, user_id=user.id
|
||||
BxWorkflowDef, db, workflow_id, tenant_no, user_id=user.id, user=user
|
||||
)
|
||||
if deleted:
|
||||
db.query(BxWorkflowNode).filter(
|
||||
@@ -142,7 +156,7 @@ class WorkflowService:
|
||||
user: BxUserContext,
|
||||
) -> List[BxWorkflowNode]:
|
||||
tenant_no = WorkflowService._tenant_no(db, user)
|
||||
workflow = WorkflowService.get_by_id(db, wf_id, tenant_no)
|
||||
workflow = WorkflowService.get_by_id(db, wf_id, tenant_no, user=user)
|
||||
if not workflow:
|
||||
raise ValueError("工作流不存在")
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""BX 模块数据权限辅助(create_by / create_dept_id)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.data_scope import apply_data_scope, assert_row_in_data_scope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.core.bx_auth import BxUserContext
|
||||
|
||||
|
||||
def apply_bx_scope(query, model, user: "BxUserContext", db: Session, tenant_no: str):
|
||||
"""列表/统计查询应用数据权限。"""
|
||||
return apply_data_scope(query, model, user.to_user_info(), db, tenant_no)
|
||||
|
||||
|
||||
def assert_bx_row_in_scope(
|
||||
db: Session,
|
||||
row,
|
||||
model,
|
||||
user: "BxUserContext",
|
||||
tenant_no: str,
|
||||
) -> None:
|
||||
"""详情/改/删前校验行级数据权限。"""
|
||||
assert_row_in_data_scope(db, row, model, user.to_user_info(), tenant_no)
|
||||
|
||||
|
||||
def assert_bx_biz_in_scope(
|
||||
db: Session,
|
||||
biz_type: str,
|
||||
biz_id: str,
|
||||
user: "BxUserContext",
|
||||
tenant_no: str,
|
||||
) -> None:
|
||||
"""附件下载/删除/绑定前校验关联业务单在数据权限内。"""
|
||||
bid = (biz_id or "").strip()
|
||||
if not bid or bid == "0":
|
||||
return
|
||||
btype = (biz_type or "").strip().lower()
|
||||
if btype in ("advance", "advance_apply", "advance_issue"):
|
||||
from app.services.bx.advance import AdvanceService
|
||||
|
||||
row = AdvanceService.get_by_id(db, bid, tenant_no, user=user)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="关联备用金申请不存在")
|
||||
return
|
||||
if btype in ("reimburse", "reimburse_apply"):
|
||||
from app.services.bx.reimburse import ReimburseService
|
||||
|
||||
row = ReimburseService.get_by_id(db, bid, tenant_no, user=user)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="关联报销单不存在")
|
||||
return
|
||||
if btype == "workflow":
|
||||
from app.services.bx.workflow import WorkflowService
|
||||
|
||||
row = WorkflowService.get_by_id(db, bid, tenant_no, user=user)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="关联工作流不存在")
|
||||
|
||||
|
||||
def assert_bx_attachment_biz_in_scope(
|
||||
db: Session,
|
||||
attachment,
|
||||
user: "BxUserContext",
|
||||
tenant_no: str,
|
||||
) -> None:
|
||||
"""按附件关联业务单校验数据权限。"""
|
||||
assert_bx_biz_in_scope(
|
||||
db,
|
||||
getattr(attachment, "biz_type", None) or "",
|
||||
getattr(attachment, "biz_id", None) or "",
|
||||
user,
|
||||
tenant_no,
|
||||
)
|
||||
Reference in new issue
Block a user