feat(dashboard): supporting schema, seed, catalog links, funnel/PM labels and API contract (merge resolution)

This commit is contained in:
fatemeh sane
2026-10-08 11:18:57 +03:30
parent b82c039265
commit 7bb432e715
13 changed files with 817 additions and 18 deletions
+35 -3
View File
@@ -38,13 +38,14 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import UserContext, get_optional_user
from app.config import settings
from app.db.engine import get_db
from app.models.catalog import BrandMaterial, Material, Unit
from app.models.catalog import AttributeDefinition, Brand, BrandMaterial, Material, Unit
from app.models.inventory import InventoryItem, InventoryTransaction, Warehouse
from app.models.procurement import (
ItemDecision,
ItemReceipt,
ItemReceiptAllocation,
LineItem,
LineItemSpec,
Project,
Quote,
Supplier,
@@ -182,16 +183,24 @@ def arrival_state(ordered: Decimal, received: Decimal | None) -> str:
async def pm_on_the_way(
ctx: ArrivalScope = Depends(arrivals_scope),
db: AsyncSession = Depends(get_db),
material_id: uuid.UUID | None = Query(default=None, description="فیلتر متریال (داشبورد بازرگانی)"),
brand_id: uuid.UUID | None = Query(default=None, description="فیلتر برند (داشبورد بازرگانی)"),
):
stmt = (
select(ItemDecision, LineItem, Quote, Material, Supplier, Project)
select(ItemDecision, LineItem, Quote, Material, Supplier, Project, BrandMaterial, Brand)
.join(LineItem, LineItem.id == ItemDecision.line_item_id)
.join(Quote, Quote.id == LineItem.quote_id)
.join(Material, Material.id == LineItem.material_id)
.join(Supplier, Supplier.id == Quote.supplier_id)
.outerjoin(Project, Project.id == ItemDecision.project_id)
.outerjoin(BrandMaterial, BrandMaterial.id == LineItem.brand_material_id)
.outerjoin(Brand, Brand.id == BrandMaterial.brand_id)
.where(ItemDecision.status == "chosen")
)
if material_id is not None:
stmt = stmt.where(Material.id == material_id)
if brand_id is not None:
stmt = stmt.where(BrandMaterial.brand_id == brand_id)
if ctx.role == "pm":
allowed = ctx.project_ids or ([ctx.project_id] if ctx.project_id else [])
if not allowed:
@@ -211,8 +220,29 @@ async def pm_on_the_way(
# unit / category lookups for display
unit_ids = {d.LineItem.unit_id for d in decisions}
units = {u.id: u for u in (await db.execute(select(Unit).where(Unit.id.in_(unit_ids)))).scalars()}
# frozen spec snapshot per line item (key = attribute label)
li_ids = [d.LineItem.id for d in decisions]
specs_by_line: dict = {}
if li_ids:
spec_rows = (
await db.execute(
select(LineItemSpec, AttributeDefinition)
.join(AttributeDefinition, AttributeDefinition.id == LineItemSpec.attribute_definition_id)
.where(LineItemSpec.line_item_id.in_(li_ids))
)
).all()
for spec, ad in spec_rows:
value = spec.value_string
if value is None and spec.value_number is not None:
value = str(spec.value_number)
if value is None and spec.value_boolean is not None:
value = str(spec.value_boolean)
if value is None and spec.value_date is not None:
value = spec.value_date.isoformat()
if value is not None:
specs_by_line.setdefault(spec.line_item_id, {})[ad.label or ad.attribute_key] = value
out = []
for d, li, q, m, sup, proj in decisions:
for d, li, q, m, sup, proj, bm, br in decisions:
received = received_map.get(d.id)
out.append({
"item_decision_id": str(d.id),
@@ -224,6 +254,8 @@ async def pm_on_the_way(
"project": {"id": str(proj.id), "name": proj.name} if proj else None,
"decided_at": to_iso(d.decided_at),
"material": {"id": str(m.id), "name": m.name},
"brand": {"id": str(br.id), "name": br.name} if br else None,
"specs": specs_by_line.get(li.id, {}),
"unit": {"id": units[li.unit_id].id, "code": units[li.unit_id].code,
"name_fa": units[li.unit_id].name_fa} if li.unit_id in units else None,
"ordered_qty": str(li.quantity),