207 lines
7.5 KiB
Python
207 lines
7.5 KiB
Python
import uuid
|
|
from typing import Literal
|
|
|
|
from fastapi import APIRouter, Depends, Query, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.db.engine import get_db
|
|
from app.routers.pm import ArrivalScope, arrivals_scope
|
|
from app.schemas.project_transfers import (
|
|
ProjectTransferCreate,
|
|
ProjectTransferOut,
|
|
ProjectTransferReceive,
|
|
ProjectTransferUpdate,
|
|
)
|
|
from app.services import project_transfer_service
|
|
from app.utils.errors import ForbiddenError
|
|
|
|
router = APIRouter(tags=["project-transfers"])
|
|
|
|
|
|
def _to_transfer_out(t) -> dict:
|
|
return {
|
|
"id": t.id,
|
|
"from_project": (
|
|
{"id": t.from_project.id, "name": t.from_project.name}
|
|
if t.from_project
|
|
else {"id": t.from_project_id, "name": ""}
|
|
),
|
|
"from_warehouse": (
|
|
{"id": t.from_warehouse.id, "name": t.from_warehouse.name}
|
|
if t.from_warehouse
|
|
else {"id": t.from_warehouse_id, "name": ""}
|
|
),
|
|
"to_project": (
|
|
{"id": t.to_project.id, "name": t.to_project.name}
|
|
if t.to_project
|
|
else {"id": t.to_project_id, "name": ""}
|
|
),
|
|
"to_warehouse": (
|
|
{"id": t.to_warehouse.id, "name": t.to_warehouse.name}
|
|
if t.to_warehouse
|
|
else None
|
|
),
|
|
"material": (
|
|
{"id": t.material.id, "name": t.material.name}
|
|
if t.material
|
|
else {"id": t.material_id, "name": ""}
|
|
),
|
|
"brand": (
|
|
{"id": t.brand.id, "name": t.brand.name}
|
|
if t.brand
|
|
else None
|
|
),
|
|
"unit": (
|
|
{"id": t.unit.id, "code": t.unit.code, "name_fa": t.unit.name_fa}
|
|
if t.unit
|
|
else {"id": t.unit_id, "code": "", "name_fa": None}
|
|
),
|
|
"quantity": t.quantity,
|
|
"specs": t.specs or {},
|
|
"specs_hash": t.specs_hash,
|
|
"note": t.note,
|
|
"status": t.status,
|
|
"transfer_date": t.transfer_date,
|
|
"transferred_by": t.transferred_by,
|
|
"received_at": t.received_at,
|
|
"received_by": t.received_by,
|
|
"receipt_allocations": t.receipt_allocations,
|
|
"created_at": t.created_at,
|
|
"updated_at": t.updated_at,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# POST /inventory/project-transfers — Create inter-project transfer
|
|
# ---------------------------------------------------------------------------
|
|
@router.post("/inventory/project-transfers", status_code=201)
|
|
async def create_transfer(
|
|
payload: ProjectTransferCreate,
|
|
ctx: ArrivalScope = Depends(arrivals_scope),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
transfer = await project_transfer_service.create_project_transfer(
|
|
db=db,
|
|
payload=payload,
|
|
caller_project_id=ctx.project_id,
|
|
caller_role=ctx.role,
|
|
transferred_by=ctx.display_name,
|
|
)
|
|
return {"data": _to_transfer_out(transfer)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /inventory/project-transfers — List transfers with direction and filters
|
|
# ---------------------------------------------------------------------------
|
|
@router.get("/inventory/project-transfers")
|
|
async def list_transfers(
|
|
project_id: uuid.UUID | None = Query(default=None),
|
|
direction: Literal["outgoing", "incoming", "all"] = Query(default="all"),
|
|
status: str | None = Query(default=None),
|
|
ctx: ArrivalScope = Depends(arrivals_scope),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
target_pid = project_id or ctx.project_id
|
|
transfers = await project_transfer_service.list_project_transfers(
|
|
db=db,
|
|
project_id=target_pid,
|
|
direction=direction,
|
|
status=status,
|
|
)
|
|
return {"data": [_to_transfer_out(t) for t in transfers]}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /inventory/project-transfers/{id} — Single transfer details
|
|
# ---------------------------------------------------------------------------
|
|
@router.get("/inventory/project-transfers/{transfer_id}")
|
|
async def get_transfer(
|
|
transfer_id: uuid.UUID,
|
|
ctx: ArrivalScope = Depends(arrivals_scope),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
transfer = await project_transfer_service.get_project_transfer_or_404(db, transfer_id)
|
|
if ctx.role == "pm" and ctx.project_id is not None:
|
|
if transfer.from_project_id != ctx.project_id and transfer.to_project_id != ctx.project_id:
|
|
raise ForbiddenError("شما به این رکورد انتقال دسترسی ندارید")
|
|
return {"data": _to_transfer_out(transfer)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PATCH /inventory/project-transfers/{id} — Edit in-transit transfer (source PM only)
|
|
# ---------------------------------------------------------------------------
|
|
@router.patch("/inventory/project-transfers/{transfer_id}")
|
|
async def update_transfer(
|
|
transfer_id: uuid.UUID,
|
|
payload: ProjectTransferUpdate,
|
|
ctx: ArrivalScope = Depends(arrivals_scope),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
transfer = await project_transfer_service.update_project_transfer(
|
|
db=db,
|
|
transfer_id=transfer_id,
|
|
payload=payload,
|
|
caller_project_id=ctx.project_id,
|
|
caller_role=ctx.role,
|
|
)
|
|
return {"data": _to_transfer_out(transfer)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DELETE /inventory/project-transfers/{id} — Cancel in-transit transfer (source PM only)
|
|
# ---------------------------------------------------------------------------
|
|
@router.delete("/inventory/project-transfers/{transfer_id}")
|
|
async def delete_transfer(
|
|
transfer_id: uuid.UUID,
|
|
ctx: ArrivalScope = Depends(arrivals_scope),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
res = await project_transfer_service.delete_project_transfer(
|
|
db=db,
|
|
transfer_id=transfer_id,
|
|
caller_project_id=ctx.project_id,
|
|
caller_role=ctx.role,
|
|
)
|
|
return {"data": res}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /pm/transfers/on-the-way — Incoming in-transit transfers for active project
|
|
# ---------------------------------------------------------------------------
|
|
@router.get("/pm/transfers/on-the-way")
|
|
async def pm_incoming_transfers(
|
|
ctx: ArrivalScope = Depends(arrivals_scope),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
if ctx.role == "pm" and ctx.project_id is None:
|
|
raise ForbiddenError("شما به هیچ پروژهای متصل نیستید")
|
|
|
|
transfers = await project_transfer_service.list_project_transfers(
|
|
db=db,
|
|
project_id=ctx.project_id,
|
|
direction="incoming",
|
|
status="in_transit",
|
|
)
|
|
return {"data": [_to_transfer_out(t) for t in transfers]}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# POST /inventory/project-transfers/{id}/receive — Destination PM confirms arrival
|
|
# ---------------------------------------------------------------------------
|
|
@router.post("/inventory/project-transfers/{transfer_id}/receive")
|
|
async def receive_transfer(
|
|
transfer_id: uuid.UUID,
|
|
payload: ProjectTransferReceive,
|
|
ctx: ArrivalScope = Depends(arrivals_scope),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
transfer = await project_transfer_service.receive_project_transfer(
|
|
db=db,
|
|
transfer_id=transfer_id,
|
|
payload=payload,
|
|
caller_project_id=ctx.project_id,
|
|
caller_role=ctx.role,
|
|
received_by=ctx.display_name,
|
|
)
|
|
return {"data": _to_transfer_out(transfer)}
|