TorbatYar/backend/services/accounting/app/api/v1/setup.py
Mortezakoohjani 320d9cd547 Replace opaque company settings form with a real company profile page.
Users now fill legal name, national ID, economic code, address and contacts instead of generic setting key/value ops documents.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 18:37:50 +03:30

193 lines
6.3 KiB
Python

"""Setup progress + document number sequences API."""
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, Field
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.repositories.foundation import (
AccountRepository,
ChartOfAccountsRepository,
CurrencyRepository,
FiscalPeriodRepository,
FiscalYearRepository,
)
from app.repositories.posting import VoucherRepository
from app.schemas.foundation import SetupStatusRead
from app.services.document_number_service import DocumentNumberService
from shared.security import CurrentUser
router = APIRouter()
class AllocateRequest(BaseModel):
document_type: str = Field(min_length=1, max_length=50)
class SequencePreview(BaseModel):
document_type: str
prefix: str
next_number: int
padding: int
preview: str
@router.get("/status", response_model=SetupStatusRead)
async def setup_status(
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
charts = await ChartOfAccountsRepository(db).list_by_tenant(tenant_id, limit=1)
accounts = await AccountRepository(db).list_by_tenant(tenant_id, limit=1)
currencies = await CurrencyRepository(db).list_by_tenant(tenant_id, limit=100)
years = await FiscalYearRepository(db).list_by_tenant(tenant_id, limit=1)
periods = await FiscalPeriodRepository(db).list_by_tenant(tenant_id, limit=1)
has_cash = False
has_customer = False
try:
from app.api.v1.treasury import CashBoxRepo
from app.api.v1.receivable_payable import CustomerRepo
has_cash = bool(await CashBoxRepo(db).list_by_tenant(tenant_id, limit=1))
has_customer = bool(await CustomerRepo(db).list_by_tenant(tenant_id, limit=1))
except Exception:
pass
has_voucher = bool(await VoucherRepository(db).list_by_tenant(tenant_id, limit=1))
flags = {
"has_chart": bool(charts),
"has_accounts": bool(accounts),
"has_currency": bool(currencies),
"has_base_currency": any(c.is_base for c in currencies),
"has_fiscal_year": bool(years),
"has_fiscal_period": bool(periods),
"has_cash_box": has_cash,
"has_customer": has_customer,
"has_voucher": has_voucher,
}
completed = sum(1 for v in flags.values() if v)
total = len(flags)
return SetupStatusRead(
**flags,
completed_steps=completed,
total_steps=total,
percent=round(100 * completed / total) if total else 0,
)
@router.get("/sequences/next", response_model=SequencePreview)
async def peek_next_number(
document_type: str = Query(min_length=1, max_length=50),
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
"""Preview next number without consuming it (for form autofill)."""
svc = DocumentNumberService(db)
data = await svc.peek(tenant_id, document_type)
await db.commit()
return SequencePreview(**data)
@router.post("/sequences/allocate")
async def allocate_number(
body: AllocateRequest,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
"""Consume and return the next number for a document type."""
svc = DocumentNumberService(db)
number = await svc.allocate(tenant_id, body.document_type)
await db.commit()
return {"document_type": body.document_type, "number": number}
class PostingPolicyUpdate(BaseModel):
auto_post: dict[str, bool] | None = None
require_balanced: bool | None = None
require_source_reference: bool | None = None
lock_after_post: bool | None = None
sequential_numbers: bool | None = None
idempotent_source: bool | None = None
posting_mode: str | None = None
@router.get("/posting-policy")
async def get_posting_policy(
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
from app.services.posting_policy_service import PostingPolicyService
return await PostingPolicyService(db).get_policy(tenant_id)
@router.put("/posting-policy")
async def update_posting_policy(
body: PostingPolicyUpdate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
from app.services.posting_policy_service import PostingPolicyService
policy = await PostingPolicyService(db).update_policy(
tenant_id, body.model_dump(exclude_none=True)
)
await db.commit()
return policy
class CompanyProfileUpdate(BaseModel):
legal_name: str | None = None
trade_name: str | None = None
national_id: str | None = None
economic_code: str | None = None
registration_number: str | None = None
postal_code: str | None = None
province: str | None = None
city: str | None = None
address: str | None = None
phone: str | None = None
mobile: str | None = None
email: str | None = None
website: str | None = None
ceo_name: str | None = None
accountant_name: str | None = None
@router.get("/company-profile")
async def get_company_profile(
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
from app.services.company_profile_service import CompanyProfileService
return await CompanyProfileService(db).get_profile(tenant_id)
@router.put("/company-profile")
async def update_company_profile(
body: CompanyProfileUpdate,
tenant_id: UUID = Depends(require_tenant),
db: AsyncSession = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
):
from app.services.company_profile_service import CompanyProfileService
profile = await CompanyProfileService(db).update_profile(
tenant_id, body.model_dump(exclude_none=False)
)
await db.commit()
return profile