Files
ERP/backend/app/routers/units.py
T
MMD 0ea0322967 Revert 253c922: restore the PM/RBAC work to the integration
Reverting the production revert so the pm branch can merge cleanly on
top of the restored RBAC base. Conflicts against newer integration work
resolved by combining:
- App.tsx: v7's bot-draft wizard bypass + login returnUrl kept alongside
  the supervisor gating (no-project block screen, panel scoping)
- LoginPage: returnUrl navigation kept, supervisor routing added
- migration.sql: bot_drafts table kept, unified-projects ALTERs restored
2026-10-01 15:24:56 +03:30

136 lines
4.8 KiB
Python

"""Units CRUD. units.id is the ONLY integer PK in the system (IDENTITY)."""
from fastapi import APIRouter, Depends, Query
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import require_role
from app.db.engine import get_db
from app.models.catalog import AttributeDefinition, Material, MaterialUnit, Unit
from app.models.procurement import LineItem, LineItemSpec
from app.schemas.catalog import UnitCreate, UnitUpdate
from app.utils.errors import ConflictError, NotFoundError
from app.utils.pagination import paginate, sort_query
router = APIRouter(prefix="/units", tags=["units"])
_procurement_writer = require_role("admin", "procurement_manager")
def unit_full_to_dict(unit: Unit) -> dict:
"""Full unit shape (P0-1): includes dimension, si_factor, is_base."""
return {
"id": unit.id,
"code": unit.code,
"name_fa": unit.name_fa,
"dimension": unit.dimension,
"si_factor": float(unit.si_factor),
"is_base": unit.is_base,
}
@router.get("")
async def list_units(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
q: str | None = None,
dimension: str | None = None,
sort: str | None = None,
db: AsyncSession = Depends(get_db),
):
stmt = select(Unit)
if q:
stmt = stmt.where(or_(Unit.code.ilike(f"%{q}%"), Unit.name_fa.ilike(f"%{q}%")))
if dimension:
stmt = stmt.where(Unit.dimension == dimension)
stmt = sort_query(stmt, Unit, sort, default="id")
items, meta = await paginate(stmt, page, page_size, db)
return {"data": [unit_full_to_dict(u) for u in items], "meta": meta}
@router.post("", status_code=201)
async def create_unit(
payload: UnitCreate,
db: AsyncSession = Depends(get_db),
_auth=Depends(_procurement_writer),
):
duplicate = await db.scalar(select(Unit.id).where(Unit.code == payload.code))
if duplicate is not None:
raise ConflictError("واحدی با این کد قبلاً ثبت شده است", field="code")
unit = Unit(
code=payload.code,
name_fa=payload.name_fa,
dimension=payload.dimension,
si_factor=payload.si_factor,
is_base=payload.is_base,
)
db.add(unit)
await db.flush()
return {"data": unit_full_to_dict(unit)}
@router.get("/{unit_id}")
async def get_unit(unit_id: int, db: AsyncSession = Depends(get_db)):
unit = await db.get(Unit, unit_id)
if unit is None:
raise NotFoundError("واحد یافت نشد")
return {"data": unit_full_to_dict(unit)}
@router.patch("/{unit_id}")
async def update_unit(
unit_id: int,
payload: UnitUpdate,
db: AsyncSession = Depends(get_db),
_auth=Depends(_procurement_writer),
):
unit = await db.get(Unit, unit_id)
if unit is None:
raise NotFoundError("واحد یافت نشد")
if payload.code is not None and payload.code != unit.code:
duplicate = await db.scalar(select(Unit.id).where(Unit.code == payload.code, Unit.id != unit_id))
if duplicate is not None:
raise ConflictError("واحدی با این کد قبلاً ثبت شده است", field="code")
unit.code = payload.code
if payload.name_fa is not None:
unit.name_fa = payload.name_fa
if payload.dimension is not None:
unit.dimension = payload.dimension
if payload.si_factor is not None:
unit.si_factor = payload.si_factor
if payload.is_base is not None:
unit.is_base = payload.is_base
await db.flush()
return {"data": unit_full_to_dict(unit)}
@router.delete("/{unit_id}")
async def delete_unit(
unit_id: int,
db: AsyncSession = Depends(get_db),
_auth=Depends(_procurement_writer),
):
unit = await db.get(Unit, unit_id)
if unit is None:
raise NotFoundError("واحد یافت نشد")
refs: dict[str, int] = {
"attribute_definitions": await db.scalar(
select(func.count()).select_from(AttributeDefinition).where(AttributeDefinition.unit_id == unit_id)
),
"materials": await db.scalar(
select(func.count()).select_from(Material).where(Material.canonical_unit_id == unit_id)
),
"material_units": await db.scalar(
select(func.count()).select_from(MaterialUnit).where(MaterialUnit.unit_id == unit_id)
),
"line_items": await db.scalar(select(func.count()).select_from(LineItem).where(LineItem.unit_id == unit_id)),
"line_item_specs": await db.scalar(
select(func.count()).select_from(LineItemSpec).where(LineItemSpec.unit_id_at_time == unit_id)
),
}
total = sum(refs.values())
if total:
raise ConflictError(f"این واحد در {total} رکورد دیگر استفاده شده و قابل حذف نیست")
await db.delete(unit)
await db.flush()
return {"data": {"id": unit_id, "deleted": True}}