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>
226 lines
7.5 KiB
Python
226 lines
7.5 KiB
Python
"""Fiscal year/period API — Phase 5.1 + 5.3 + PATCH / set-current / generate."""
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, status
|
|
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.foundation import FiscalPeriod, FiscalYear
|
|
from app.repositories.foundation import FiscalPeriodRepository, FiscalYearRepository
|
|
from app.schemas.foundation import (
|
|
FiscalPeriodCreate,
|
|
FiscalPeriodRead,
|
|
FiscalPeriodUpdate,
|
|
FiscalYearCreate,
|
|
FiscalYearRead,
|
|
FiscalYearUpdate,
|
|
)
|
|
from app.services.fiscal_service import FiscalManagementService
|
|
from shared.exceptions import NotFoundError
|
|
from shared.security import CurrentUser
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _apply_update(entity, data: dict) -> None:
|
|
for key, value in data.items():
|
|
setattr(entity, key, value)
|
|
|
|
|
|
@router.post("/years", response_model=FiscalYearRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_fiscal_year(
|
|
body: FiscalYearCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = FiscalYearRepository(db)
|
|
if body.is_current:
|
|
for y in await repo.list_by_tenant(tenant_id, limit=500):
|
|
y.is_current = False
|
|
entity = FiscalYear(tenant_id=tenant_id, **body.model_dump())
|
|
await repo.add(entity)
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|
|
|
|
|
|
@router.get("/years", response_model=list[FiscalYearRead])
|
|
async def list_fiscal_years(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = FiscalYearRepository(db)
|
|
return await repo.list_by_tenant(tenant_id, limit=100)
|
|
|
|
|
|
@router.patch("/years/{year_id}", response_model=FiscalYearRead)
|
|
async def update_fiscal_year(
|
|
year_id: UUID,
|
|
body: FiscalYearUpdate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = FiscalYearRepository(db)
|
|
entity = await repo.get(tenant_id, year_id)
|
|
if entity is None:
|
|
raise NotFoundError("سال مالی یافت نشد", error_code="year_not_found")
|
|
data = body.model_dump(exclude_unset=True)
|
|
if data.get("is_current") is True:
|
|
for y in await repo.list_by_tenant(tenant_id, limit=500):
|
|
y.is_current = y.id == year_id
|
|
data.pop("is_current", None)
|
|
entity.is_current = True
|
|
_apply_update(entity, data)
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|
|
|
|
|
|
@router.post("/years/{year_id}/set-current", response_model=FiscalYearRead)
|
|
async def set_current_year(
|
|
year_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
svc = FiscalManagementService(db)
|
|
year = await svc.set_current_year(tenant_id, year_id)
|
|
await db.commit()
|
|
return year
|
|
|
|
|
|
@router.post("/years/{year_id}/close", response_model=FiscalYearRead)
|
|
async def close_fiscal_year(
|
|
year_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
svc = FiscalManagementService(db)
|
|
year = await svc.close_fiscal_year(tenant_id, year_id)
|
|
await db.commit()
|
|
return year
|
|
|
|
|
|
@router.post("/years/{year_id}/generate-periods", response_model=list[FiscalPeriodRead])
|
|
async def generate_periods(
|
|
year_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
svc = FiscalManagementService(db)
|
|
periods = await svc.generate_monthly_periods(tenant_id, year_id)
|
|
await db.commit()
|
|
return periods
|
|
|
|
|
|
@router.post("/periods", response_model=FiscalPeriodRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_fiscal_period(
|
|
body: FiscalPeriodCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = FiscalPeriodRepository(db)
|
|
if body.is_current:
|
|
for p in await repo.list_by_tenant(tenant_id, limit=500):
|
|
p.is_current = False
|
|
entity = FiscalPeriod(tenant_id=tenant_id, **body.model_dump())
|
|
await repo.add(entity)
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|
|
|
|
|
|
@router.get("/periods", response_model=list[FiscalPeriodRead])
|
|
async def list_fiscal_periods(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = FiscalPeriodRepository(db)
|
|
return await repo.list_by_tenant(tenant_id, limit=100)
|
|
|
|
|
|
@router.patch("/periods/{period_id}", response_model=FiscalPeriodRead)
|
|
async def update_fiscal_period(
|
|
period_id: UUID,
|
|
body: FiscalPeriodUpdate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = FiscalPeriodRepository(db)
|
|
entity = await repo.get(tenant_id, period_id)
|
|
if entity is None:
|
|
raise NotFoundError("دوره مالی یافت نشد", error_code="period_not_found")
|
|
data = body.model_dump(exclude_unset=True)
|
|
if data.get("is_current") is True:
|
|
for p in await repo.list_by_tenant(tenant_id, limit=500):
|
|
p.is_current = p.id == period_id
|
|
data.pop("is_current", None)
|
|
entity.is_current = True
|
|
_apply_update(entity, data)
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|
|
|
|
|
|
@router.post("/periods/{period_id}/set-current", response_model=FiscalPeriodRead)
|
|
async def set_current_period(
|
|
period_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
svc = FiscalManagementService(db)
|
|
period = await svc.set_current_period(tenant_id, period_id)
|
|
await db.commit()
|
|
return period
|
|
|
|
|
|
@router.post("/periods/{period_id}/lock", response_model=FiscalPeriodRead)
|
|
async def lock_period(
|
|
period_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
svc = FiscalManagementService(db)
|
|
period = await svc.lock_period(tenant_id, period_id)
|
|
await db.commit()
|
|
return period
|
|
|
|
|
|
@router.post("/periods/{period_id}/close", response_model=FiscalPeriodRead)
|
|
async def close_period(
|
|
period_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
svc = FiscalManagementService(db)
|
|
period = await svc.close_period(tenant_id, period_id)
|
|
await db.commit()
|
|
return period
|
|
|
|
|
|
@router.post("/periods/{period_id}/reopen", response_model=FiscalPeriodRead)
|
|
async def reopen_period(
|
|
period_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
svc = FiscalManagementService(db)
|
|
period = await svc.reopen_period(tenant_id, period_id)
|
|
await db.commit()
|
|
return period
|