TorbatYar/backend/services/accounting/app/api/v1/payroll.py
Mortezakoohjani 12c8615615 Ship enterprise Accounting FE/API with CRUD parity and production wiring.
Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 15:26:43 +03:30

186 lines
5.8 KiB
Python

"""Phase 5.9 — Payroll API."""
from __future__ import annotations
from datetime import date
from decimal import Decimal
from uuid import UUID
from fastapi import APIRouter, Depends, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_db, require_tenant
from app.core.security import get_current_user
from app.models.payroll import Department, Employee, PayrollPeriod
from app.models.types import EmploymentStatus, PayrollPeriodStatus
from app.repositories.base import TenantBaseRepository
from app.services.payroll_service import PayrollAccountingService
from shared.security import CurrentUser
router = APIRouter()
class DepartmentCreate(BaseModel):
code: str
name: str
class EmployeeCreate(BaseModel):
employee_code: str
first_name: str
last_name: str
department_id: UUID | None = None
base_salary: Decimal = Decimal("0")
hire_date: date | None = None
class PayrollPeriodCreate(BaseModel):
name: str
start_date: date
end_date: date
class CalculatePayrollRequest(BaseModel):
payroll_period_id: UUID
employee_id: UUID
class PostPayrollRequest(BaseModel):
profile_id: UUID | None = None
class DepartmentRepo(TenantBaseRepository[Department]):
model = Department
class EmployeeRepo(TenantBaseRepository[Employee]):
model = Employee
class PayrollPeriodRepo(TenantBaseRepository[PayrollPeriod]):
model = PayrollPeriod
@router.post("/departments", status_code=status.HTTP_201_CREATED)
async def create_department(
body: DepartmentCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
repo = DepartmentRepo(db)
entity = Department(tenant_id=tenant_id, **body.model_dump())
await repo.add(entity)
await db.commit()
return {"id": str(entity.id)}
@router.get("/departments")
async def list_departments(
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
deps = await DepartmentRepo(db).list_by_tenant(tenant_id, limit=100)
return [{"id": str(d.id), "code": d.code, "name": d.name} for d in deps]
@router.post("/employees", status_code=status.HTTP_201_CREATED)
async def create_employee(
body: EmployeeCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
repo = EmployeeRepo(db)
entity = Employee(tenant_id=tenant_id, employment_status=EmploymentStatus.ACTIVE, **body.model_dump())
await repo.add(entity)
await db.commit()
return {"id": str(entity.id), "employee_code": entity.employee_code}
@router.get("/employees")
async def list_employees(
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
repo = EmployeeRepo(db)
employees = await repo.list_by_tenant(tenant_id, limit=100)
return [{"id": str(e.id), "code": e.employee_code, "name": f"{e.first_name} {e.last_name}"} for e in employees]
@router.post("/periods", status_code=status.HTTP_201_CREATED)
async def create_payroll_period(
body: PayrollPeriodCreate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
repo = PayrollPeriodRepo(db)
entity = PayrollPeriod(tenant_id=tenant_id, status=PayrollPeriodStatus.OPEN, **body.model_dump())
await repo.add(entity)
await db.commit()
return {"id": str(entity.id)}
@router.get("/periods")
async def list_payroll_periods(
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
periods = await PayrollPeriodRepo(db).list_by_tenant(tenant_id, limit=100)
return [
{
"id": str(p.id),
"name": p.name,
"start_date": p.start_date.isoformat(),
"end_date": p.end_date.isoformat(),
"status": p.status.value,
}
for p in periods
]
@router.post("/calculate")
async def calculate_payroll(
body: CalculatePayrollRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
svc = PayrollAccountingService(db)
payroll = await svc.calculate_payroll(tenant_id, body.payroll_period_id, body.employee_id)
await db.commit()
return {
"id": str(payroll.id),
"gross_salary": str(payroll.gross_salary),
"net_salary": str(payroll.net_salary),
"status": payroll.status.value,
}
@router.post("/payrolls/{payroll_id}/post")
async def post_payroll(
payroll_id: UUID,
body: PostPayrollRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
):
from app.models.types import PayrollStatus
from app.services.payroll_service import PayrollRepo
payroll_repo = PayrollRepo(db)
payroll = await payroll_repo.get(tenant_id, payroll_id)
if payroll is None:
from shared.exceptions import NotFoundError
raise NotFoundError("حقوق یافت نشد", error_code="payroll_not_found")
payroll.status = PayrollStatus.APPROVED
svc = PayrollAccountingService(db)
result = await svc.post_payroll(tenant_id, payroll_id, actor_user_id=user.user_id, profile_id=body.profile_id)
await db.commit()
return {"id": str(result.id), "status": result.status.value, "voucher_id": str(result.voucher_id)}