Merge feature/show-material-suppliers into the all-branches integration
Conflict in supplier_materials.py resolved by keeping HEAD's auth (_procurement_writer) and list route while adopting the branch's UUID parameter annotations.
This commit is contained in:
@@ -2,8 +2,10 @@
|
||||
|
||||
GET/POST /suppliers/{id}/materials — manage a supplier's catalog.
|
||||
DELETE /suppliers/{id}/materials/{material_id} — unlink.
|
||||
GET /materials/{id}/suppliers — price-comparison data source.
|
||||
GET /materials/{id}/suppliers — suppliers linked to a material.
|
||||
"""
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -22,29 +24,47 @@ router = APIRouter(tags=["supplier-materials"])
|
||||
_procurement_writer = require_procurement_writer()
|
||||
|
||||
|
||||
def link_to_dict(link: SupplierMaterial, material: Material | None = None,
|
||||
category: MaterialCategory | None = None,
|
||||
supplier: Supplier | None = None) -> dict:
|
||||
def link_to_dict(
|
||||
link: SupplierMaterial,
|
||||
material: Material | None = None,
|
||||
category: MaterialCategory | None = None,
|
||||
supplier: Supplier | None = None,
|
||||
) -> dict:
|
||||
"""Serialize a SupplierMaterial ORM row to a plain dict for the API envelope."""
|
||||
return {
|
||||
"id": str(link.id),
|
||||
"supplier_id": str(link.supplier_id),
|
||||
"material_id": str(link.material_id),
|
||||
"is_active": link.is_active,
|
||||
"last_quoted_price": float(link.last_quoted_price) if link.last_quoted_price is not None else None,
|
||||
"last_quoted_price": (
|
||||
float(link.last_quoted_price) if link.last_quoted_price is not None else None
|
||||
),
|
||||
"last_quoted_at": to_iso(link.last_quoted_at) if link.last_quoted_at else None,
|
||||
"created_at": to_iso(link.created_at),
|
||||
"material": {
|
||||
"id": str(material.id),
|
||||
"name": material.name,
|
||||
"category": {"id": str(category.id), "name": category.name} if category else None,
|
||||
} if material else None,
|
||||
"supplier": {"id": str(supplier.id), "name": supplier.name} if supplier else None,
|
||||
"material": (
|
||||
{
|
||||
"id": str(material.id),
|
||||
"name": material.name,
|
||||
"category": (
|
||||
{"id": str(category.id), "name": category.name} if category else None
|
||||
),
|
||||
}
|
||||
if material
|
||||
else None
|
||||
),
|
||||
"supplier": (
|
||||
{"id": str(supplier.id), "name": supplier.name} if supplier else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /suppliers/{supplier_id}/materials
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/suppliers/{supplier_id}/materials")
|
||||
async def list_supplier_materials(
|
||||
supplier_id,
|
||||
supplier_id: UUID,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -58,14 +78,24 @@ async def list_supplier_materials(
|
||||
.order_by(SupplierMaterial.created_at)
|
||||
)
|
||||
items, meta = await paginate(stmt, page, page_size, db)
|
||||
mat_ids = {l.material_id for l in items}
|
||||
materials = {m.id: m for m in (await db.execute(select(Material).where(Material.id.in_(mat_ids)))).scalars()}
|
||||
return {"data": [link_to_dict(l, materials.get(l.material_id), None) for l in items], "meta": meta}
|
||||
mat_ids = [lnk.material_id for lnk in items]
|
||||
materials: dict = {}
|
||||
if mat_ids:
|
||||
result = await db.execute(select(Material).where(Material.id.in_(mat_ids)))
|
||||
materials = {m.id: m for m in result.scalars()}
|
||||
return {
|
||||
"data": [link_to_dict(lnk, materials.get(lnk.material_id)) for lnk in items],
|
||||
"meta": meta,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /suppliers/{supplier_id}/materials
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.post("/suppliers/{supplier_id}/materials", status_code=201)
|
||||
async def create_supplier_material(
|
||||
supplier_id,
|
||||
supplier_id: UUID,
|
||||
payload: SupplierMaterialCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_auth=Depends(_procurement_writer),
|
||||
@@ -73,13 +103,17 @@ async def create_supplier_material(
|
||||
async with db.begin():
|
||||
link = await link_supplier_material(db, supplier_id, payload.material_id)
|
||||
material = await db.get(Material, payload.material_id)
|
||||
return {"data": link_to_dict(link, material, None)}
|
||||
return {"data": link_to_dict(link, material)}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DELETE /suppliers/{supplier_id}/materials/{material_id}
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.delete("/suppliers/{supplier_id}/materials/{material_id}")
|
||||
async def delete_supplier_material(
|
||||
supplier_id,
|
||||
material_id,
|
||||
supplier_id: UUID,
|
||||
material_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_auth=Depends(_procurement_writer),
|
||||
):
|
||||
@@ -93,17 +127,80 @@ async def list_material_suppliers(
|
||||
material_id,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
async with db.begin():
|
||||
await unlink_supplier_material(db, supplier_id, material_id)
|
||||
return {
|
||||
"data": {
|
||||
"supplier_id": str(supplier_id),
|
||||
"material_id": str(material_id),
|
||||
"deleted": True,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /materials/{material_id}/suppliers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get(
|
||||
"/materials/{material_id}/suppliers",
|
||||
summary="List all suppliers linked to a material",
|
||||
)
|
||||
async def list_material_suppliers(
|
||||
material_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Return the suppliers linked to a material, straight from PostgreSQL.
|
||||
|
||||
The path parameter is typed as UUID so FastAPI validates and coerces it
|
||||
before the handler runs; a malformed id is rejected with 422, an unknown
|
||||
material with 404.
|
||||
|
||||
Atomic JOIN — a supplier is returned only through its supplier_materials
|
||||
link, so the response can never contain a supplier that is not actually
|
||||
linked to this material:
|
||||
|
||||
select(Supplier)
|
||||
.join(SupplierMaterial, Supplier.id == SupplierMaterial.supplier_id)
|
||||
.where(SupplierMaterial.material_id == material_id)
|
||||
|
||||
Response envelope (frontend MaterialSupplier binds these fields directly):
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "<supplier_uuid>",
|
||||
"name": "فولاد ناب آریا",
|
||||
"phone": "021-..." | null,
|
||||
"contact_person": "علی محمدی" | null,
|
||||
"location": "تهران" | null
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
material = await db.get(Material, material_id)
|
||||
if material is None:
|
||||
raise NotFoundError("متریال یافت نشد")
|
||||
|
||||
stmt = (
|
||||
select(SupplierMaterial)
|
||||
select(Supplier)
|
||||
.join(SupplierMaterial, Supplier.id == SupplierMaterial.supplier_id)
|
||||
.where(SupplierMaterial.material_id == material_id)
|
||||
.order_by(SupplierMaterial.last_quoted_at.desc().nulls_last())
|
||||
.order_by(Supplier.name)
|
||||
)
|
||||
items, meta = await paginate(stmt, page, page_size, db)
|
||||
sup_ids = {l.supplier_id for l in items}
|
||||
suppliers = {s.id: s for s in (await db.execute(select(Supplier).where(Supplier.id.in_(sup_ids)))).scalars()}
|
||||
return {"data": [link_to_dict(l, supplier=suppliers.get(l.supplier_id)) for l in items], "meta": meta}
|
||||
suppliers = (await db.execute(stmt)).scalars().all()
|
||||
|
||||
return {
|
||||
"data": [
|
||||
{
|
||||
"id": str(supplier.id),
|
||||
"name": supplier.name,
|
||||
"phone": supplier.phone,
|
||||
"contact_person": supplier.contact_person,
|
||||
"location": supplier.location,
|
||||
}
|
||||
for supplier in suppliers
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user