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>
207 lines
5.9 KiB
Python
207 lines
5.9 KiB
Python
"""Treasury API — Phase 5.4."""
|
|
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.treasury import Bank, BankAccount, CashBox
|
|
from app.repositories.base import TenantBaseRepository
|
|
from app.services.treasury_service import TreasuryService
|
|
from shared.security import CurrentUser
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class CashBoxCreate(BaseModel):
|
|
code: str
|
|
name: str
|
|
account_id: UUID | None = None
|
|
opening_balance: Decimal = Decimal("0")
|
|
|
|
|
|
class CashBoxRead(BaseModel):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
code: str
|
|
name: str
|
|
current_balance: Decimal
|
|
is_active: bool
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class CashReceiptRequest(BaseModel):
|
|
cash_box_id: UUID
|
|
amount: Decimal
|
|
debit_account_id: UUID
|
|
credit_account_id: UUID
|
|
description: str | None = None
|
|
transaction_date: date | None = None
|
|
|
|
|
|
class BankCreate(BaseModel):
|
|
code: str
|
|
name: str
|
|
|
|
|
|
class BankAccountCreate(BaseModel):
|
|
bank_id: UUID
|
|
account_number: str
|
|
iban: str | None = None
|
|
|
|
|
|
class CashBoxRepo(TenantBaseRepository[CashBox]):
|
|
model = CashBox
|
|
|
|
|
|
class BankRepo(TenantBaseRepository[Bank]):
|
|
model = Bank
|
|
|
|
|
|
class BankAccountRepo(TenantBaseRepository[BankAccount]):
|
|
model = BankAccount
|
|
|
|
|
|
@router.post("/cash-boxes", response_model=CashBoxRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_cash_box(
|
|
body: CashBoxCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = CashBoxRepo(db)
|
|
entity = CashBox(
|
|
tenant_id=tenant_id,
|
|
code=body.code,
|
|
name=body.name,
|
|
account_id=body.account_id,
|
|
opening_balance=body.opening_balance,
|
|
current_balance=body.opening_balance,
|
|
)
|
|
await repo.add(entity)
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|
|
|
|
|
|
@router.get("/cash-boxes", response_model=list[CashBoxRead])
|
|
async def list_cash_boxes(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = CashBoxRepo(db)
|
|
return await repo.list_by_tenant(tenant_id, limit=100)
|
|
|
|
|
|
@router.post("/cash-receipts")
|
|
async def record_cash_receipt(
|
|
body: CashReceiptRequest,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
svc = TreasuryService(db)
|
|
tx = await svc.record_cash_receipt(
|
|
tenant_id, body.cash_box_id, body.amount,
|
|
debit_account_id=body.debit_account_id,
|
|
credit_account_id=body.credit_account_id,
|
|
actor_user_id=user.user_id,
|
|
description=body.description,
|
|
transaction_date=body.transaction_date,
|
|
)
|
|
await db.commit()
|
|
return {"id": str(tx.id), "voucher_id": str(tx.voucher_id)}
|
|
|
|
|
|
@router.post("/banks", status_code=status.HTTP_201_CREATED)
|
|
async def create_bank(
|
|
body: BankCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = BankRepo(db)
|
|
entity = Bank(tenant_id=tenant_id, code=body.code, name=body.name)
|
|
await repo.add(entity)
|
|
await db.commit()
|
|
return {"id": str(entity.id)}
|
|
|
|
|
|
@router.post("/bank-accounts", status_code=status.HTTP_201_CREATED)
|
|
async def create_bank_account(
|
|
body: BankAccountCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = BankAccountRepo(db)
|
|
entity = BankAccount(
|
|
tenant_id=tenant_id,
|
|
bank_id=body.bank_id,
|
|
account_number=body.account_number,
|
|
iban=body.iban,
|
|
)
|
|
await repo.add(entity)
|
|
await db.commit()
|
|
return {"id": str(entity.id)}
|
|
|
|
|
|
@router.get("/banks")
|
|
async def list_banks(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
banks = await BankRepo(db).list_by_tenant(tenant_id, limit=100)
|
|
return [{"id": str(b.id), "code": b.code, "name": b.name, "is_active": b.is_active} for b in banks]
|
|
|
|
|
|
@router.get("/bank-accounts")
|
|
async def list_bank_accounts(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
accounts = await BankAccountRepo(db).list_by_tenant(tenant_id, limit=100)
|
|
return [
|
|
{
|
|
"id": str(a.id),
|
|
"bank_id": str(a.bank_id),
|
|
"account_number": a.account_number,
|
|
"iban": a.iban,
|
|
"is_active": a.is_active,
|
|
}
|
|
for a in accounts
|
|
]
|
|
|
|
|
|
@router.patch("/cash-boxes/{cash_box_id}")
|
|
async def update_cash_box(
|
|
cash_box_id: UUID,
|
|
body: dict,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
from shared.exceptions import NotFoundError
|
|
|
|
repo = CashBoxRepo(db)
|
|
entity = await repo.get(tenant_id, cash_box_id)
|
|
if entity is None:
|
|
raise NotFoundError("صندوق یافت نشد", error_code="cash_box_not_found")
|
|
if "name" in body and body["name"] is not None:
|
|
entity.name = body["name"]
|
|
if "is_active" in body and body["is_active"] is not None:
|
|
entity.is_active = bool(body["is_active"])
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return CashBoxRead.model_validate(entity)
|