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>
203 lines
6.8 KiB
Python
203 lines
6.8 KiB
Python
"""Posting Engine API — Phase 5.2."""
|
|
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, get_pagination, require_tenant
|
|
from app.core.security import get_current_user
|
|
from app.models.posting import Voucher, VoucherLine
|
|
from app.models.types import VoucherStatus
|
|
from app.repositories.posting import AuditLogRepository, VoucherRepository
|
|
from app.schemas.posting import (
|
|
AuditLogRead,
|
|
PostRequest,
|
|
ReverseRequest,
|
|
VoucherCreate,
|
|
VoucherRead,
|
|
VoucherUpdate,
|
|
)
|
|
from app.services.posting_engine import PostingEngine
|
|
from shared.exceptions import AppError, NotFoundError
|
|
from shared.pagination import PaginationParams
|
|
from shared.security import CurrentUser
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/vouchers", response_model=list[VoucherRead])
|
|
async def list_vouchers(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
pagination: PaginationParams = Depends(get_pagination),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
stmt = (
|
|
select(Voucher)
|
|
.options(selectinload(Voucher.lines))
|
|
.where(Voucher.tenant_id == tenant_id)
|
|
.offset(pagination.offset)
|
|
.limit(pagination.page_size)
|
|
.order_by(Voucher.created_at.desc())
|
|
)
|
|
result = await db.execute(stmt)
|
|
return list(result.scalars().all())
|
|
|
|
|
|
@router.post("/vouchers", response_model=VoucherRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_voucher(
|
|
body: VoucherCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = VoucherRepository(db)
|
|
voucher = Voucher(
|
|
tenant_id=tenant_id,
|
|
fiscal_period_id=body.fiscal_period_id,
|
|
voucher_number=body.voucher_number,
|
|
voucher_date=body.voucher_date,
|
|
status=VoucherStatus.DRAFT,
|
|
description=body.description,
|
|
reference_number=body.reference_number,
|
|
source_module=body.source_module,
|
|
created_by=user.user_id,
|
|
)
|
|
await repo.add(voucher)
|
|
for i, line in enumerate(body.lines, start=1):
|
|
db.add(VoucherLine(
|
|
tenant_id=tenant_id,
|
|
voucher_id=voucher.id,
|
|
line_number=i,
|
|
**line.model_dump(),
|
|
))
|
|
await db.commit()
|
|
result = await repo.get_with_lines(tenant_id, voucher.id)
|
|
return result
|
|
|
|
|
|
@router.get("/vouchers/{voucher_id}", response_model=VoucherRead)
|
|
async def get_voucher(
|
|
voucher_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = VoucherRepository(db)
|
|
voucher = await repo.get_with_lines(tenant_id, voucher_id)
|
|
if voucher is None:
|
|
raise NotFoundError("سند یافت نشد", error_code="voucher_not_found")
|
|
return voucher
|
|
|
|
|
|
@router.patch("/vouchers/{voucher_id}", response_model=VoucherRead)
|
|
async def update_draft_voucher(
|
|
voucher_id: UUID,
|
|
body: VoucherUpdate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = VoucherRepository(db)
|
|
voucher = await repo.get_with_lines(tenant_id, voucher_id)
|
|
if voucher is None:
|
|
raise NotFoundError("سند یافت نشد", error_code="voucher_not_found")
|
|
if voucher.status != VoucherStatus.DRAFT:
|
|
raise AppError(
|
|
"فقط اسناد پیشنویس قابل ویرایش هستند",
|
|
status_code=422,
|
|
error_code="voucher_not_draft",
|
|
)
|
|
data = body.model_dump(exclude_unset=True)
|
|
lines_data = data.pop("lines", None)
|
|
for key, value in data.items():
|
|
setattr(voucher, key, value)
|
|
if lines_data is not None:
|
|
for line in list(voucher.lines):
|
|
await db.delete(line)
|
|
await db.flush()
|
|
for i, line in enumerate(lines_data, start=1):
|
|
db.add(
|
|
VoucherLine(
|
|
tenant_id=tenant_id,
|
|
voucher_id=voucher.id,
|
|
line_number=i,
|
|
**line,
|
|
)
|
|
)
|
|
await db.commit()
|
|
return await repo.get_with_lines(tenant_id, voucher_id)
|
|
|
|
|
|
@router.post("/vouchers/{voucher_id}/validate", response_model=VoucherRead)
|
|
async def validate_voucher(
|
|
voucher_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
engine = PostingEngine(db)
|
|
voucher = await engine.validate_voucher(tenant_id, voucher_id)
|
|
await db.commit()
|
|
return await VoucherRepository(db).get_with_lines(tenant_id, voucher.id)
|
|
|
|
|
|
@router.post("/vouchers/{voucher_id}/post", response_model=VoucherRead)
|
|
async def post_voucher(
|
|
voucher_id: UUID,
|
|
body: PostRequest,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
engine = PostingEngine(db)
|
|
voucher = await engine.post_voucher(
|
|
tenant_id, voucher_id, actor_user_id=user.user_id, source_module=body.source_module
|
|
)
|
|
await db.commit()
|
|
return await VoucherRepository(db).get_with_lines(tenant_id, voucher.id)
|
|
|
|
|
|
@router.post("/vouchers/{voucher_id}/reverse", response_model=VoucherRead)
|
|
async def reverse_voucher(
|
|
voucher_id: UUID,
|
|
body: ReverseRequest,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
engine = PostingEngine(db)
|
|
voucher = await engine.reverse_voucher(
|
|
tenant_id, voucher_id, actor_user_id=user.user_id, reversal_date=body.reversal_date
|
|
)
|
|
await db.commit()
|
|
return await VoucherRepository(db).get_with_lines(tenant_id, voucher.id)
|
|
|
|
|
|
@router.post("/vouchers/{voucher_id}/cancel", response_model=VoucherRead)
|
|
async def cancel_voucher(
|
|
voucher_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
engine = PostingEngine(db)
|
|
voucher = await engine.cancel_voucher(tenant_id, voucher_id, actor_user_id=user.user_id)
|
|
await db.commit()
|
|
return voucher
|
|
|
|
|
|
@router.get("/audit-logs", response_model=list[AuditLogRead])
|
|
async def list_audit_logs(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = AuditLogRepository(db)
|
|
return await repo.list_by_tenant(tenant_id, limit=100)
|