TorbatYar/backend/services/accounting/app/api/v1/fiscal.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

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