Wire DocumentNumberSequence allocator, ledger-based subsidiary/detail reports, and posted-voucher reopen flow without mock ops forms. Co-authored-by: Cursor <cursoragent@cursor.com>
234 lines
6.9 KiB
Python
234 lines
6.9 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 | None = Field(default=None, 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
|
|
party_id: UUID | 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
|
|
party_id: UUID | None = None
|
|
amount: Decimal
|
|
status: str
|
|
description: str | None
|
|
meta_json: str | None = 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),
|
|
):
|
|
from app.services.document_number_service import DocumentNumberService, ops_document_type
|
|
|
|
data = body.model_dump()
|
|
svc = DocumentNumberService(db)
|
|
data["number"] = await svc.allocate_if_blank(
|
|
tenant_id, ops_document_type(body.module, body.doc_type), body.number
|
|
)
|
|
entity = BusinessDocument(tenant_id=tenant_id, **data)
|
|
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(
|
|
q: str | None = Query(default=None, description="Search code/name"),
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
rows = await ItemRepo(db).list_by_tenant(tenant_id, limit=200)
|
|
if q:
|
|
needle = q.strip().lower()
|
|
rows = [r for r in rows if needle in (r.code or "").lower() or needle in (r.name or "").lower()]
|
|
return rows
|
|
|
|
|
|
@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
|