Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/api/v1/payroll.py

872 lines
30 KiB
Python

from __future__ import annotations
import datetime
from typing import Any, Dict, Optional
from fastapi import APIRouter, Body, Depends, File, Query, Request, UploadFile
from fastapi.responses import Response
from sqlalchemy.orm import Session
from adapters.db.session import get_db
from app.core.auth_dependency import AuthContext, get_current_user
from app.core.i18n import locale_dependency, negotiate_locale
from app.core.permissions import require_business_access_dep, require_business_permission_dep
from app.core.responses import ApiError, format_datetime_fields, success_response
from app.services import payroll_service as svc
from app.services import payroll_excel as excel_svc
from app.services import payroll_reports as reports_svc
router = APIRouter(prefix="/payroll", tags=["payroll"])
def _handle_value_error(e: ValueError) -> None:
raise ApiError("VALIDATION_ERROR", str(e), http_status=400)
def _payroll_response(data: Any, request: Request, **kwargs) -> dict:
return success_response(format_datetime_fields(data, request), request, **kwargs)
# ─── تنظیمات ───────────────────────────────────────────────────────────────
@router.get("/business/{business_id}/settings")
def get_settings_endpoint(
request: Request,
business_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = svc.get_settings(db, business_id)
return _payroll_response(data, request)
@router.put("/business/{business_id}/settings")
def update_settings_endpoint(
request: Request,
business_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.update_settings(db, business_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
# ─── داشبورد ───────────────────────────────────────────────────────────────
@router.get("/business/{business_id}/dashboard")
def dashboard_endpoint(
request: Request,
business_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = svc.get_dashboard_summary(db, business_id)
return _payroll_response(data, request)
# ─── دسته‌بندی آیتم‌ها ─────────────────────────────────────────────────────
@router.get("/business/{business_id}/item-categories")
def list_item_categories_endpoint(
request: Request,
business_id: int,
include_inactive: bool = Query(False),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
items = svc.list_item_categories(db, business_id, include_inactive=include_inactive)
return _payroll_response({"items": items}, request)
@router.post("/business/{business_id}/item-categories")
def create_item_category_endpoint(
request: Request,
business_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.create_item_category(db, business_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
@router.put("/business/{business_id}/item-categories/{category_id}")
def update_item_category_endpoint(
request: Request,
business_id: int,
category_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.update_item_category(db, business_id, category_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
@router.delete("/business/{business_id}/item-categories/{category_id}")
def delete_item_category_endpoint(
request: Request,
business_id: int,
category_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
svc.delete_item_category(db, business_id, category_id, user_id=ctx.get_user_id())
return _payroll_response({"deleted": True}, request)
# ─── آیتم‌های حقوق ─────────────────────────────────────────────────────────
@router.get("/business/{business_id}/items")
def list_items_endpoint(
request: Request,
business_id: int,
item_kind: Optional[str] = Query(None),
include_inactive: bool = Query(False),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
items = svc.list_item_definitions(
db, business_id, item_kind=item_kind, include_inactive=include_inactive
)
return _payroll_response({"items": items}, request)
@router.post("/business/{business_id}/items")
def create_item_endpoint(
request: Request,
business_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.create_item_definition(db, business_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
@router.put("/business/{business_id}/items/{item_id}")
def update_item_endpoint(
request: Request,
business_id: int,
item_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.update_item_definition(db, business_id, item_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
@router.delete("/business/{business_id}/items/{item_id}")
def delete_item_endpoint(
request: Request,
business_id: int,
item_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
svc.delete_item_definition(db, business_id, item_id, user_id=ctx.get_user_id())
return _payroll_response({"deleted": True}, request)
# ─── بخش‌ها ────────────────────────────────────────────────────────────────
@router.get("/business/{business_id}/departments")
def list_departments_endpoint(
request: Request,
business_id: int,
include_inactive: bool = Query(False),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
items = svc.list_departments(db, business_id, include_inactive=include_inactive)
return _payroll_response({"items": items}, request)
@router.post("/business/{business_id}/departments")
def create_department_endpoint(
request: Request,
business_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.create_department(db, business_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
# ─── پرسنل ──────────────────────────────────────────────────────────────────
@router.get("/business/{business_id}/employees")
def list_employees_endpoint(
request: Request,
business_id: int,
include_inactive: bool = Query(False),
department_id: Optional[int] = Query(None),
limit: int = Query(100, ge=1, le=500),
skip: int = Query(0, ge=0),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
items, total = svc.list_employees(
db,
business_id,
include_inactive=include_inactive,
department_id=department_id,
limit=limit,
skip=skip,
)
return _payroll_response({"items": items, "total": total}, request)
@router.post("/business/{business_id}/employees")
def create_employee_endpoint(
request: Request,
business_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.create_employee(db, business_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
# ─── دوره‌ها ─────────────────────────────────────────────────────────────────
@router.get("/business/{business_id}/periods")
def list_periods_endpoint(
request: Request,
business_id: int,
limit: int = Query(24, ge=1, le=120),
skip: int = Query(0, ge=0),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
items, total = svc.list_periods(db, business_id, limit=limit, skip=skip)
return _payroll_response({"items": items, "total": total}, request)
@router.post("/business/{business_id}/periods")
def create_period_endpoint(
request: Request,
business_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.create_period(db, business_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
@router.post("/business/{business_id}/periods/{period_id}/close")
def close_period_endpoint(
request: Request,
business_id: int,
period_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = svc.close_period(db, business_id, period_id, user_id=ctx.get_user_id())
return _payroll_response(data, request)
# ─── اجرای حقوق ─────────────────────────────────────────────────────────────
@router.get("/business/{business_id}/runs")
def list_runs_endpoint(
request: Request,
business_id: int,
status: Optional[str] = Query(None),
period_id: Optional[int] = Query(None),
limit: int = Query(50, ge=1, le=200),
skip: int = Query(0, ge=0),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
items, total = svc.list_runs(
db, business_id, status=status, period_id=period_id, limit=limit, skip=skip
)
return _payroll_response({"items": items, "total": total}, request)
@router.get("/business/{business_id}/runs/{run_id}")
def get_run_endpoint(
request: Request,
business_id: int,
run_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = svc.get_run(db, business_id, run_id)
return _payroll_response(data, request)
@router.post("/business/{business_id}/runs")
def create_run_endpoint(
request: Request,
business_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.create_run(db, business_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
@router.put("/business/{business_id}/runs/{run_id}")
def update_run_endpoint(
request: Request,
business_id: int,
run_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.update_run(db, business_id, run_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
@router.delete("/business/{business_id}/runs/{run_id}")
def delete_run_endpoint(
request: Request,
business_id: int,
run_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
svc.delete_run(db, business_id, run_id, user_id=ctx.get_user_id())
return _payroll_response({"deleted": True}, request)
@router.post("/business/{business_id}/runs/{run_id}/finalize")
def finalize_run_endpoint(
request: Request,
business_id: int,
run_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = svc.finalize_run(db, business_id, run_id, user_id=ctx.get_user_id())
return _payroll_response(data, request)
@router.post("/business/{business_id}/runs/{run_id}/cancel")
def cancel_run_endpoint(
request: Request,
business_id: int,
run_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = svc.cancel_run(db, business_id, run_id, user_id=ctx.get_user_id())
return _payroll_response(data, request)
@router.post("/business/{business_id}/runs/{run_id}/copy")
def copy_run_endpoint(
request: Request,
business_id: int,
run_id: int,
payload: Dict[str, Any] = Body(default={}),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.copy_run(db, business_id, run_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
@router.get("/business/{business_id}/runs/{run_id}/payslip/pdf")
async def payslip_pdf_endpoint(
request: Request,
business_id: int,
run_id: int,
line_id: Optional[int] = Query(None),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
):
from weasyprint import HTML
from weasyprint.text.fonts import FontConfiguration
from app.services.pdf.template_renderer import load_farsi_font_data_uris, render_template
locale = negotiate_locale(request.headers.get("Accept-Language"))
is_fa = locale == "fa"
calendar_type = getattr(request.state, "calendar_type", "jalali") or "jalali"
ctx_data = svc.build_payslip_render_context(
db, business_id, run_id, line_id=line_id, calendar_type=calendar_type
)
fa_reg, fa_bold = load_farsi_font_data_uris()
html_content = render_template(
"pdf/payroll/payslip.html",
{
**ctx_data,
"is_fa": is_fa,
"title_text": "فیش حقوق" if is_fa else "Payslip",
"fa_font_url_regular": fa_reg,
"fa_font_url_bold": fa_bold,
"paper_size": "A4",
"orientation": "portrait",
"footer_text": ctx_data.get("run", {}).get("code") or "",
},
)
pdf_bytes = HTML(string=html_content).write_pdf(font_config=FontConfiguration())
suffix = f"_line_{line_id}" if line_id else ""
filename = f"payslip_{run_id}{suffix}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Length": str(len(pdf_bytes)),
"Access-Control-Expose-Headers": "Content-Disposition",
},
)
@router.get("/business/{business_id}/reports/department-summary")
def department_summary_endpoint(
request: Request,
business_id: int,
period_id: Optional[int] = Query(None),
run_id: Optional[int] = Query(None),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = svc.get_run_department_summary(db, business_id, period_id=period_id, run_id=run_id)
return _payroll_response(data, request)
@router.get("/business/{business_id}/reports/item-summary")
def item_summary_report_endpoint(
request: Request,
business_id: int,
period_id: Optional[int] = Query(None),
run_id: Optional[int] = Query(None),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = reports_svc.get_item_summary_report(db, business_id, period_id=period_id, run_id=run_id)
return _payroll_response(data, request)
@router.get("/business/{business_id}/reports/employee-summary")
def employee_summary_report_endpoint(
request: Request,
business_id: int,
period_id: Optional[int] = Query(None),
run_id: Optional[int] = Query(None),
limit: int = Query(500, ge=1, le=2000),
skip: int = Query(0, ge=0),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = reports_svc.get_employee_summary_report(
db, business_id, period_id=period_id, run_id=run_id, limit=limit, skip=skip
)
return _payroll_response(data, request)
@router.get("/business/{business_id}/reports/statutory-summary")
def statutory_summary_report_endpoint(
request: Request,
business_id: int,
period_id: Optional[int] = Query(None),
run_id: Optional[int] = Query(None),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = reports_svc.get_statutory_summary_report(db, business_id, period_id=period_id, run_id=run_id)
return _payroll_response(data, request)
@router.get("/business/{business_id}/reports/period-overview")
def period_overview_report_endpoint(
request: Request,
business_id: int,
year: Optional[int] = Query(None),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = reports_svc.get_period_overview_report(db, business_id, year=year)
return _payroll_response(data, request)
@router.get("/business/{business_id}/employees/import/template")
def employees_import_template_endpoint(
request: Request,
business_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
):
_ = db # گیت لایسنس در سرویس import
content, filename = excel_svc.build_employees_import_template(db, business_id)
return Response(
content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Access-Control-Expose-Headers": "Content-Disposition",
},
)
@router.get("/business/{business_id}/employees/export/excel")
def export_employees_excel_endpoint(
request: Request,
business_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "view")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
):
content, filename = excel_svc.export_employees_excel(
db,
business_id,
calendar_type=getattr(request.state, "calendar_type", "jalali") or "jalali",
)
return Response(
content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Access-Control-Expose-Headers": "Content-Disposition",
},
)
@router.post("/business/{business_id}/employees/import/excel")
async def import_employees_excel_endpoint(
request: Request,
business_id: int,
file: UploadFile = File(...),
dry_run: bool = Query(True),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
if not file.filename or not str(file.filename).lower().endswith(".xlsx"):
raise ApiError("INVALID_FILE", "فقط فایل‌های .xlsx پشتیبانی می‌شوند", http_status=400)
content = await file.read()
data = excel_svc.import_employees_from_excel(
db, business_id, content, dry_run=dry_run, user_id=ctx.get_user_id()
)
return _payroll_response(data, request)
@router.get("/business/{business_id}/runs/{run_id}/import/template")
def run_lines_import_template_endpoint(
request: Request,
business_id: int,
run_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
):
content, filename = excel_svc.build_run_lines_import_template(db, business_id, run_id)
return Response(
content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Access-Control-Expose-Headers": "Content-Disposition",
},
)
@router.post("/business/{business_id}/runs/{run_id}/import/excel")
async def import_run_lines_excel_endpoint(
request: Request,
business_id: int,
run_id: int,
file: UploadFile = File(...),
dry_run: bool = Query(True),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "operate")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
if not file.filename or not str(file.filename).lower().endswith(".xlsx"):
raise ApiError("INVALID_FILE", "فقط فایل‌های .xlsx پشتیبانی می‌شوند", http_status=400)
content = await file.read()
data = excel_svc.import_run_lines_from_excel(
db, business_id, run_id, content, dry_run=dry_run, user_id=ctx.get_user_id()
)
return _payroll_response(data, request)
@router.post("/business/{business_id}/runs/{run_id}/approve")
def approve_run_endpoint(
request: Request,
business_id: int,
run_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "approve")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = svc.approve_run(db, business_id, run_id, user_id=ctx.get_user_id())
return _payroll_response(data, request)
@router.post("/business/{business_id}/runs/{run_id}/reject")
def reject_run_endpoint(
request: Request,
business_id: int,
run_id: int,
payload: Dict[str, Any] = Body(default={}),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "approve")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
reason = payload.get("reason") if isinstance(payload, dict) else None
data = svc.reject_run(db, business_id, run_id, user_id=ctx.get_user_id(), reason=reason)
return _payroll_response(data, request)
@router.post("/business/{business_id}/runs/{run_id}/post")
def post_run_endpoint(
request: Request,
business_id: int,
run_id: int,
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "post")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
data = svc.post_run_to_accounting(db, business_id, run_id, user_id=ctx.get_user_id())
return _payroll_response(data, request)
@router.post("/business/{business_id}/runs/{run_id}/post-payment")
def post_payment_endpoint(
request: Request,
business_id: int,
run_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "post")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
payment_account_id = payload.get("payment_account_id")
if payment_account_id is None:
raise ApiError("PAYMENT_ACCOUNT_REQUIRED", "payment_account_id الزامی است.", http_status=400)
data = svc.post_run_payment(
db, business_id, run_id, ctx.get_user_id(), int(payment_account_id)
)
return _payroll_response(data, request)
@router.put("/business/{business_id}/employees/{employee_id}")
def update_employee_endpoint(
request: Request,
business_id: int,
employee_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.update_employee(db, business_id, employee_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)
@router.put("/business/{business_id}/departments/{department_id}")
def update_department_endpoint(
request: Request,
business_id: int,
department_id: int,
payload: Dict[str, Any] = Body(...),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("payroll", "manage")),
db: Session = Depends(get_db),
ctx: AuthContext = Depends(get_current_user),
) -> dict:
try:
data = svc.update_department(db, business_id, department_id, payload, user_id=ctx.get_user_id())
except ApiError:
raise
except ValueError as e:
_handle_value_error(e)
return _payroll_response(data, request)