Wire nested nav pages for phases 5.1-5.11 to real list/create endpoints, fix COA import connectivity via same-origin proxy, and add operational migration. Co-authored-by: Cursor <cursoragent@cursor.com>
219 lines
6.3 KiB
Python
219 lines
6.3 KiB
Python
"""Operational documents + inventory masters API."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, Query, status
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import select
|
|
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.operational import BusinessDocument, InventoryItem, Warehouse
|
|
from app.repositories.base import TenantBaseRepository
|
|
from shared.exceptions import NotFoundError
|
|
from shared.security import CurrentUser
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class BizDocCreate(BaseModel):
|
|
module: str = Field(max_length=40)
|
|
doc_type: str = Field(max_length=50)
|
|
number: str = Field(max_length=50)
|
|
doc_date: date
|
|
party_name: str | None = None
|
|
party_id: UUID | None = None
|
|
amount: Decimal = Decimal("0")
|
|
status: str = "draft"
|
|
description: str | None = None
|
|
meta_json: str | None = None
|
|
|
|
|
|
class BizDocUpdate(BaseModel):
|
|
party_name: str | None = None
|
|
amount: Decimal | None = None
|
|
status: str | None = None
|
|
description: str | None = None
|
|
doc_date: date | None = None
|
|
meta_json: str | None = None
|
|
|
|
|
|
class BizDocRead(BaseModel):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
module: str
|
|
doc_type: str
|
|
number: str
|
|
doc_date: date
|
|
party_name: str | None
|
|
amount: Decimal
|
|
status: str
|
|
description: str | None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class ItemCreate(BaseModel):
|
|
code: str
|
|
name: str
|
|
item_type: str = "goods"
|
|
unit: str = "عدد"
|
|
quantity_on_hand: Decimal = Decimal("0")
|
|
unit_cost: Decimal = Decimal("0")
|
|
|
|
|
|
class ItemRead(BaseModel):
|
|
id: UUID
|
|
code: str
|
|
name: str
|
|
item_type: str
|
|
unit: str
|
|
quantity_on_hand: Decimal
|
|
unit_cost: Decimal
|
|
is_active: bool
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class WarehouseCreate(BaseModel):
|
|
code: str
|
|
name: str
|
|
|
|
|
|
class WarehouseRead(BaseModel):
|
|
id: UUID
|
|
code: str
|
|
name: str
|
|
is_active: bool
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class BizDocRepo(TenantBaseRepository[BusinessDocument]):
|
|
model = BusinessDocument
|
|
|
|
|
|
class ItemRepo(TenantBaseRepository[InventoryItem]):
|
|
model = InventoryItem
|
|
|
|
|
|
class WarehouseRepo(TenantBaseRepository[Warehouse]):
|
|
model = Warehouse
|
|
|
|
|
|
@router.get("/documents", response_model=list[BizDocRead])
|
|
async def list_documents(
|
|
module: str | None = Query(default=None),
|
|
doc_type: str | None = Query(default=None),
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
stmt = select(BusinessDocument).where(BusinessDocument.tenant_id == tenant_id)
|
|
if module:
|
|
stmt = stmt.where(BusinessDocument.module == module)
|
|
if doc_type:
|
|
stmt = stmt.where(BusinessDocument.doc_type == doc_type)
|
|
stmt = stmt.order_by(BusinessDocument.doc_date.desc()).limit(200)
|
|
result = await db.execute(stmt)
|
|
return list(result.scalars().all())
|
|
|
|
|
|
@router.post("/documents", response_model=BizDocRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_document(
|
|
body: BizDocCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
entity = BusinessDocument(tenant_id=tenant_id, **body.model_dump())
|
|
await BizDocRepo(db).add(entity)
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|
|
|
|
|
|
@router.patch("/documents/{doc_id}", response_model=BizDocRead)
|
|
async def update_document(
|
|
doc_id: UUID,
|
|
body: BizDocUpdate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = BizDocRepo(db)
|
|
entity = await repo.get(tenant_id, doc_id)
|
|
if entity is None:
|
|
raise NotFoundError("سند یافت نشد", error_code="document_not_found")
|
|
for k, v in body.model_dump(exclude_unset=True).items():
|
|
setattr(entity, k, v)
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|
|
|
|
|
|
@router.post("/documents/{doc_id}/confirm", response_model=BizDocRead)
|
|
async def confirm_document(
|
|
doc_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
entity = await BizDocRepo(db).get(tenant_id, doc_id)
|
|
if entity is None:
|
|
raise NotFoundError("سند یافت نشد", error_code="document_not_found")
|
|
entity.status = "confirmed"
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|
|
|
|
|
|
@router.get("/items", response_model=list[ItemRead])
|
|
async def list_items(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
return await ItemRepo(db).list_by_tenant(tenant_id, limit=200)
|
|
|
|
|
|
@router.post("/items", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_item(
|
|
body: ItemCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
entity = InventoryItem(tenant_id=tenant_id, **body.model_dump())
|
|
await ItemRepo(db).add(entity)
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|
|
|
|
|
|
@router.get("/warehouses", response_model=list[WarehouseRead])
|
|
async def list_warehouses(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
return await WarehouseRepo(db).list_by_tenant(tenant_id, limit=100)
|
|
|
|
|
|
@router.post("/warehouses", response_model=WarehouseRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_warehouse(
|
|
body: WarehouseCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
entity = Warehouse(tenant_id=tenant_id, **body.model_dump())
|
|
await WarehouseRepo(db).add(entity)
|
|
await db.commit()
|
|
await db.refresh(entity)
|
|
return entity
|