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>
186 lines
5.8 KiB
Python
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)}
|