178 lines
6.2 KiB
Python
178 lines
6.2 KiB
Python
from fastapi import APIRouter, Depends, Query
|
|
from typing import Optional
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from auth.models import TokenPayload
|
|
from auth.dependencies import require_permission
|
|
from crm.models import OrderCreate, OrderUpdate, OrderInDB, OrderListResponse
|
|
from crm import service
|
|
from database.postgres import get_pg_session
|
|
from shared.audit import log_action
|
|
|
|
router = APIRouter(prefix="/api/crm/customers/{customer_id}/orders", tags=["crm-orders"])
|
|
|
|
|
|
@router.get("", response_model=OrderListResponse)
|
|
def list_orders(
|
|
customer_id: str,
|
|
_user: TokenPayload = Depends(require_permission("crm", "view")),
|
|
):
|
|
orders = service.list_orders(customer_id)
|
|
return OrderListResponse(orders=orders, total=len(orders))
|
|
|
|
|
|
# IMPORTANT: specific sub-paths must come before /{order_id}
|
|
@router.get("/next-order-number")
|
|
def get_next_order_number(
|
|
customer_id: str,
|
|
_user: TokenPayload = Depends(require_permission("crm", "view")),
|
|
):
|
|
"""Return the next globally unique order number (ORD-DDMMYY-NNN across all customers)."""
|
|
return {"order_number": service._generate_order_number(customer_id)}
|
|
|
|
|
|
@router.post("/init-negotiations", response_model=OrderInDB, status_code=201)
|
|
async def init_negotiations(
|
|
customer_id: str,
|
|
body: dict,
|
|
_user: TokenPayload = Depends(require_permission("crm", "edit")),
|
|
db: AsyncSession = Depends(get_pg_session),
|
|
):
|
|
order = service.init_negotiations(
|
|
customer_id=customer_id,
|
|
title=body.get("title", ""),
|
|
note=body.get("note", ""),
|
|
date=body.get("date"),
|
|
created_by=body.get("created_by", ""),
|
|
)
|
|
await log_action(db, _user.sub, _user.name or _user.email, "CREATE", "order",
|
|
order.id, order.order_number or order.id, meta={"action_detail": "negotiations_started"})
|
|
return order
|
|
|
|
|
|
@router.post("", response_model=OrderInDB, status_code=201)
|
|
async def create_order(
|
|
customer_id: str,
|
|
body: OrderCreate,
|
|
_user: TokenPayload = Depends(require_permission("crm", "edit")),
|
|
db: AsyncSession = Depends(get_pg_session),
|
|
):
|
|
order = service.create_order(customer_id, body)
|
|
await log_action(db, _user.sub, _user.name or _user.email, "CREATE", "order",
|
|
order.id, order.order_number or order.id)
|
|
return order
|
|
|
|
|
|
@router.get("/{order_id}", response_model=OrderInDB)
|
|
def get_order(
|
|
customer_id: str,
|
|
order_id: str,
|
|
_user: TokenPayload = Depends(require_permission("crm", "view")),
|
|
):
|
|
return service.get_order(customer_id, order_id)
|
|
|
|
|
|
@router.patch("/{order_id}", response_model=OrderInDB)
|
|
async def update_order(
|
|
customer_id: str,
|
|
order_id: str,
|
|
body: OrderUpdate,
|
|
_user: TokenPayload = Depends(require_permission("crm", "edit")),
|
|
db: AsyncSession = Depends(get_pg_session),
|
|
):
|
|
old = service.get_order(customer_id, order_id)
|
|
order = service.update_order(customer_id, order_id, body)
|
|
action = "STATUS_CHANGE" if body.status is not None else "UPDATE"
|
|
_SKIP = {"updated_at", "id", "customer_id", "items", "timeline", "discount", "shipping", "payment_status"}
|
|
changes = {
|
|
k: {"old": getattr(old, k, None), "new": getattr(order, k, None)}
|
|
for k in body.model_fields_set
|
|
if k not in _SKIP and getattr(old, k, None) != getattr(order, k, None)
|
|
}
|
|
await log_action(db, _user.sub, _user.name or _user.email, action, "order",
|
|
order_id, order.order_number or order_id, changes=changes or None)
|
|
return order
|
|
|
|
|
|
@router.delete("/{order_id}", status_code=204)
|
|
async def delete_order(
|
|
customer_id: str,
|
|
order_id: str,
|
|
_user: TokenPayload = Depends(require_permission("crm", "edit")),
|
|
db: AsyncSession = Depends(get_pg_session),
|
|
):
|
|
service.delete_order(customer_id, order_id)
|
|
await log_action(db, _user.sub, _user.name or _user.email, "DELETE", "order",
|
|
order_id, order_id)
|
|
|
|
|
|
@router.post("/{order_id}/timeline", response_model=OrderInDB)
|
|
def append_timeline_event(
|
|
customer_id: str,
|
|
order_id: str,
|
|
body: dict,
|
|
_user: TokenPayload = Depends(require_permission("crm", "edit")),
|
|
):
|
|
return service.append_timeline_event(customer_id, order_id, body)
|
|
|
|
|
|
@router.patch("/{order_id}/timeline/{index}", response_model=OrderInDB)
|
|
def update_timeline_event(
|
|
customer_id: str,
|
|
order_id: str,
|
|
index: int,
|
|
body: dict,
|
|
_user: TokenPayload = Depends(require_permission("crm", "edit")),
|
|
):
|
|
return service.update_timeline_event(customer_id, order_id, index, body)
|
|
|
|
|
|
@router.delete("/{order_id}/timeline/{index}", response_model=OrderInDB)
|
|
def delete_timeline_event(
|
|
customer_id: str,
|
|
order_id: str,
|
|
index: int,
|
|
_user: TokenPayload = Depends(require_permission("crm", "edit")),
|
|
):
|
|
return service.delete_timeline_event(customer_id, order_id, index)
|
|
|
|
|
|
@router.patch("/{order_id}/payment-status", response_model=OrderInDB)
|
|
def update_payment_status(
|
|
customer_id: str,
|
|
order_id: str,
|
|
body: dict,
|
|
_user: TokenPayload = Depends(require_permission("crm", "edit")),
|
|
):
|
|
return service.update_order_payment_status(customer_id, order_id, body)
|
|
|
|
|
|
# ── Global order list (collection group) ─────────────────────────────────────
|
|
# Separate router registered at /api/crm/orders for the global OrderList page
|
|
|
|
global_router = APIRouter(prefix="/api/crm/orders", tags=["crm-orders-global"])
|
|
|
|
|
|
@global_router.get("")
|
|
def list_all_orders(
|
|
status: Optional[str] = Query(None),
|
|
_user: TokenPayload = Depends(require_permission("crm", "view")),
|
|
):
|
|
orders = service.list_all_orders(status=status)
|
|
# Enrich with customer names
|
|
customer_ids = list({o.customer_id for o in orders if o.customer_id})
|
|
customer_names: dict[str, str] = {}
|
|
for cid in customer_ids:
|
|
try:
|
|
c = service.get_customer(cid)
|
|
parts = [c.name, c.organization] if c.organization else [c.name]
|
|
customer_names[cid] = " / ".join(filter(None, parts))
|
|
except Exception:
|
|
pass
|
|
enriched = []
|
|
for o in orders:
|
|
d = o.model_dump()
|
|
d["customer_name"] = customer_names.get(o.customer_id)
|
|
enriched.append(d)
|
|
return {"orders": enriched, "total": len(enriched)}
|