New alert service
This commit is contained in:
@@ -18,13 +18,11 @@ from shared.routing import RouteBuilder
|
||||
from app.core.database import get_db
|
||||
from app.services.performance_service import PerformanceTrackingService, AlertService
|
||||
from app.services.dashboard_service import DashboardService
|
||||
from app.services.delivery_service import DeliveryService
|
||||
from app.schemas.performance import (
|
||||
PerformanceMetric, Alert, PerformanceDashboardSummary,
|
||||
SupplierPerformanceInsights, PerformanceAnalytics, BusinessModelInsights,
|
||||
AlertSummary, PerformanceReportRequest, ExportDataResponse
|
||||
)
|
||||
from app.schemas.suppliers import DeliveryPerformanceStats, DeliverySummaryStats
|
||||
from app.models.performance import PerformancePeriod, PerformanceMetricType, AlertType, AlertSeverity
|
||||
|
||||
logger = structlog.get_logger()
|
||||
@@ -50,52 +48,6 @@ async def get_dashboard_service() -> DashboardService:
|
||||
return DashboardService()
|
||||
|
||||
|
||||
# ===== Delivery Analytics =====
|
||||
|
||||
@router.get(
|
||||
route_builder.build_analytics_route("deliveries/performance-stats"),
|
||||
response_model=DeliveryPerformanceStats
|
||||
)
|
||||
async def get_delivery_performance_stats(
|
||||
tenant_id: UUID = Path(...),
|
||||
days_back: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
|
||||
supplier_id: Optional[UUID] = Query(None, description="Filter by supplier ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get delivery performance statistics"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
stats = await service.get_delivery_performance_stats(
|
||||
tenant_id=current_user["tenant_id"],
|
||||
days_back=days_back,
|
||||
supplier_id=supplier_id
|
||||
)
|
||||
return DeliveryPerformanceStats(**stats)
|
||||
except Exception as e:
|
||||
logger.error("Error getting delivery performance stats", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve delivery performance statistics")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_analytics_route("deliveries/summary-stats"),
|
||||
response_model=DeliverySummaryStats
|
||||
)
|
||||
async def get_delivery_summary_stats(
|
||||
tenant_id: UUID = Path(...),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get delivery summary statistics for dashboard"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
stats = await service.get_upcoming_deliveries_summary(current_user["tenant_id"])
|
||||
return DeliverySummaryStats(**stats)
|
||||
except Exception as e:
|
||||
logger.error("Error getting delivery summary stats", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve delivery summary statistics")
|
||||
|
||||
|
||||
# ===== Performance Metrics =====
|
||||
|
||||
@router.post(
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
# services/suppliers/app/api/deliveries.py
|
||||
"""
|
||||
Delivery CRUD API endpoints (ATOMIC)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
import structlog
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.services.delivery_service import DeliveryService
|
||||
from app.schemas.suppliers import (
|
||||
DeliveryCreate, DeliveryUpdate, DeliveryResponse, DeliverySummary,
|
||||
DeliverySearchParams
|
||||
)
|
||||
from app.models.suppliers import DeliveryStatus
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing import RouteBuilder
|
||||
from shared.auth.access_control import require_user_role
|
||||
|
||||
# Create route builder for consistent URL structure
|
||||
route_builder = RouteBuilder('suppliers')
|
||||
|
||||
|
||||
router = APIRouter(tags=["deliveries"])
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("deliveries"), response_model=DeliveryResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def create_delivery(
|
||||
delivery_data: DeliveryCreate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new delivery"""
|
||||
# require_permissions(current_user, ["deliveries:create"])
|
||||
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
delivery = await service.create_delivery(
|
||||
tenant_id=current_user["tenant_id"],
|
||||
delivery_data=delivery_data,
|
||||
created_by=current_user["user_id"]
|
||||
)
|
||||
return DeliveryResponse.from_orm(delivery)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error creating delivery", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to create delivery")
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("deliveries"), response_model=List[DeliverySummary])
|
||||
async def list_deliveries(
|
||||
supplier_id: Optional[UUID] = Query(None, description="Filter by supplier ID"),
|
||||
status: Optional[str] = Query(None, description="Filter by status"),
|
||||
date_from: Optional[str] = Query(None, description="Filter from date (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Filter to date (YYYY-MM-DD)"),
|
||||
search_term: Optional[str] = Query(None, description="Search term"),
|
||||
limit: int = Query(50, ge=1, le=1000, description="Number of results to return"),
|
||||
offset: int = Query(0, ge=0, description="Number of results to skip"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""List deliveries with optional filters"""
|
||||
# require_permissions(current_user, ["deliveries:read"])
|
||||
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
# Parse date filters
|
||||
date_from_parsed = None
|
||||
date_to_parsed = None
|
||||
if date_from:
|
||||
try:
|
||||
date_from_parsed = datetime.fromisoformat(date_from)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date_from format")
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
date_to_parsed = datetime.fromisoformat(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date_to format")
|
||||
|
||||
# Validate status
|
||||
status_enum = None
|
||||
if status:
|
||||
try:
|
||||
status_enum = DeliveryStatus(status.upper())
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid status")
|
||||
|
||||
service = DeliveryService(db)
|
||||
search_params = DeliverySearchParams(
|
||||
supplier_id=supplier_id,
|
||||
status=status_enum,
|
||||
date_from=date_from_parsed,
|
||||
date_to=date_to_parsed,
|
||||
search_term=search_term,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
deliveries = await service.search_deliveries(
|
||||
tenant_id=current_user["tenant_id"],
|
||||
search_params=search_params
|
||||
)
|
||||
|
||||
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error listing deliveries", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve deliveries")
|
||||
|
||||
|
||||
@router.get(route_builder.build_resource_detail_route("deliveries", "delivery_id"), response_model=DeliveryResponse)
|
||||
async def get_delivery(
|
||||
delivery_id: UUID = Path(..., description="Delivery ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get delivery by ID with items"""
|
||||
# require_permissions(current_user, ["deliveries:read"])
|
||||
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
delivery = await service.get_delivery(delivery_id)
|
||||
|
||||
if not delivery:
|
||||
raise HTTPException(status_code=404, detail="Delivery not found")
|
||||
|
||||
# Check tenant access
|
||||
if delivery.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
return DeliveryResponse.from_orm(delivery)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error getting delivery", delivery_id=str(delivery_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve delivery")
|
||||
|
||||
|
||||
@router.put(route_builder.build_resource_detail_route("deliveries", "delivery_id"), response_model=DeliveryResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def update_delivery(
|
||||
delivery_data: DeliveryUpdate,
|
||||
delivery_id: UUID = Path(..., description="Delivery ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update delivery information"""
|
||||
# require_permissions(current_user, ["deliveries:update"])
|
||||
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
|
||||
# Check delivery exists and belongs to tenant
|
||||
existing_delivery = await service.get_delivery(delivery_id)
|
||||
if not existing_delivery:
|
||||
raise HTTPException(status_code=404, detail="Delivery not found")
|
||||
if existing_delivery.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
delivery = await service.update_delivery(
|
||||
delivery_id=delivery_id,
|
||||
delivery_data=delivery_data,
|
||||
updated_by=current_user["user_id"]
|
||||
)
|
||||
|
||||
if not delivery:
|
||||
raise HTTPException(status_code=404, detail="Delivery not found")
|
||||
|
||||
return DeliveryResponse.from_orm(delivery)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error updating delivery", delivery_id=str(delivery_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to update delivery")
|
||||
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
# services/suppliers/app/api/purchase_orders.py
|
||||
"""
|
||||
Purchase Order CRUD API endpoints (ATOMIC)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
import structlog
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.services.purchase_order_service import PurchaseOrderService
|
||||
from app.schemas.suppliers import (
|
||||
PurchaseOrderCreate, PurchaseOrderUpdate, PurchaseOrderResponse, PurchaseOrderSummary,
|
||||
PurchaseOrderSearchParams
|
||||
)
|
||||
from app.models.suppliers import PurchaseOrderStatus
|
||||
from app.models import AuditLog
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing import RouteBuilder
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||
|
||||
# Create route builder for consistent URL structure
|
||||
route_builder = RouteBuilder('suppliers')
|
||||
|
||||
|
||||
router = APIRouter(tags=["purchase-orders"])
|
||||
logger = structlog.get_logger()
|
||||
audit_logger = create_audit_logger("suppliers-service", AuditLog)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("purchase-orders"), response_model=PurchaseOrderResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def create_purchase_order(
|
||||
po_data: PurchaseOrderCreate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new purchase order"""
|
||||
# require_permissions(current_user, ["purchase_orders:create"])
|
||||
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
purchase_order = await service.create_purchase_order(
|
||||
tenant_id=current_user["tenant_id"],
|
||||
po_data=po_data,
|
||||
created_by=current_user["user_id"]
|
||||
)
|
||||
return PurchaseOrderResponse.from_orm(purchase_order)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error creating purchase order", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to create purchase order")
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("purchase-orders"), response_model=List[PurchaseOrderSummary])
|
||||
async def list_purchase_orders(
|
||||
supplier_id: Optional[UUID] = Query(None, description="Filter by supplier ID"),
|
||||
status: Optional[str] = Query(None, description="Filter by status"),
|
||||
priority: Optional[str] = Query(None, description="Filter by priority"),
|
||||
date_from: Optional[str] = Query(None, description="Filter from date (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Filter to date (YYYY-MM-DD)"),
|
||||
search_term: Optional[str] = Query(None, description="Search term"),
|
||||
limit: int = Query(50, ge=1, le=1000, description="Number of results to return"),
|
||||
offset: int = Query(0, ge=0, description="Number of results to skip"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""List purchase orders with optional filters"""
|
||||
# require_permissions(current_user, ["purchase_orders:read"])
|
||||
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
# Parse date filters
|
||||
date_from_parsed = None
|
||||
date_to_parsed = None
|
||||
if date_from:
|
||||
try:
|
||||
date_from_parsed = datetime.fromisoformat(date_from)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date_from format")
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
date_to_parsed = datetime.fromisoformat(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date_to format")
|
||||
|
||||
# Validate status
|
||||
status_enum = None
|
||||
if status:
|
||||
try:
|
||||
# Convert from PENDING_APPROVAL to pending_approval format
|
||||
status_value = status.lower()
|
||||
status_enum = PurchaseOrderStatus(status_value)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid status")
|
||||
|
||||
service = PurchaseOrderService(db)
|
||||
search_params = PurchaseOrderSearchParams(
|
||||
supplier_id=supplier_id,
|
||||
status=status_enum,
|
||||
priority=priority,
|
||||
date_from=date_from_parsed,
|
||||
date_to=date_to_parsed,
|
||||
search_term=search_term,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
orders = await service.search_purchase_orders(
|
||||
tenant_id=current_user["tenant_id"],
|
||||
search_params=search_params
|
||||
)
|
||||
|
||||
# Convert to response with supplier names
|
||||
response = []
|
||||
for order in orders:
|
||||
order_dict = {
|
||||
"id": order.id,
|
||||
"po_number": order.po_number,
|
||||
"supplier_id": order.supplier_id,
|
||||
"supplier_name": order.supplier.name if order.supplier else None,
|
||||
"status": order.status,
|
||||
"priority": order.priority,
|
||||
"order_date": order.order_date,
|
||||
"required_delivery_date": order.required_delivery_date,
|
||||
"total_amount": order.total_amount,
|
||||
"currency": order.currency,
|
||||
"created_at": order.created_at
|
||||
}
|
||||
response.append(PurchaseOrderSummary(**order_dict))
|
||||
|
||||
return response
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error listing purchase orders", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve purchase orders")
|
||||
|
||||
|
||||
@router.get(route_builder.build_resource_detail_route("purchase-orders", "po_id"), response_model=PurchaseOrderResponse)
|
||||
async def get_purchase_order(
|
||||
po_id: UUID = Path(..., description="Purchase order ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get purchase order by ID with items"""
|
||||
# require_permissions(current_user, ["purchase_orders:read"])
|
||||
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
purchase_order = await service.get_purchase_order(po_id)
|
||||
|
||||
if not purchase_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
|
||||
# Tenant access control is handled by the gateway
|
||||
return PurchaseOrderResponse.from_orm(purchase_order)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error getting purchase order", po_id=str(po_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve purchase order")
|
||||
|
||||
|
||||
@router.put(route_builder.build_resource_detail_route("purchase-orders", "po_id"), response_model=PurchaseOrderResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def update_purchase_order(
|
||||
po_data: PurchaseOrderUpdate,
|
||||
po_id: UUID = Path(..., description="Purchase order ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update purchase order information"""
|
||||
# require_permissions(current_user, ["purchase_orders:update"])
|
||||
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
|
||||
# Check order exists and belongs to tenant
|
||||
existing_order = await service.get_purchase_order(po_id)
|
||||
if not existing_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
if existing_order.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
purchase_order = await service.update_purchase_order(
|
||||
po_id=po_id,
|
||||
po_data=po_data,
|
||||
updated_by=current_user["user_id"]
|
||||
)
|
||||
|
||||
if not purchase_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
|
||||
return PurchaseOrderResponse.from_orm(purchase_order)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error updating purchase order", po_id=str(po_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to update purchase order")
|
||||
|
||||
|
||||
@router.delete(route_builder.build_resource_detail_route("purchase-orders", "po_id"))
|
||||
@require_user_role(['admin', 'owner'])
|
||||
async def delete_purchase_order(
|
||||
po_id: UUID = Path(..., description="Purchase order ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete purchase order (soft delete, Admin+ only)"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
|
||||
# Check order exists and belongs to tenant
|
||||
existing_order = await service.get_purchase_order(po_id)
|
||||
if not existing_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
if existing_order.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Capture PO data before deletion
|
||||
po_data = {
|
||||
"po_number": existing_order.order_number,
|
||||
"supplier_id": str(existing_order.supplier_id),
|
||||
"status": existing_order.status.value if existing_order.status else None,
|
||||
"total_amount": float(existing_order.total_amount) if existing_order.total_amount else 0.0,
|
||||
"expected_delivery_date": existing_order.expected_delivery_date.isoformat() if existing_order.expected_delivery_date else None
|
||||
}
|
||||
|
||||
# Delete purchase order (likely soft delete in service)
|
||||
success = await service.delete_purchase_order(po_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
|
||||
# Log audit event for purchase order deletion
|
||||
try:
|
||||
await audit_logger.log_deletion(
|
||||
db_session=db,
|
||||
tenant_id=tenant_id,
|
||||
user_id=current_user["user_id"],
|
||||
resource_type="purchase_order",
|
||||
resource_id=str(po_id),
|
||||
resource_data=po_data,
|
||||
description=f"Admin {current_user.get('email', 'unknown')} deleted purchase order {po_data['po_number']}",
|
||||
endpoint=f"/purchase-orders/{po_id}",
|
||||
method="DELETE"
|
||||
)
|
||||
except Exception as audit_error:
|
||||
logger.warning("Failed to log audit event", error=str(audit_error))
|
||||
|
||||
logger.info("Deleted purchase order",
|
||||
po_id=str(po_id),
|
||||
tenant_id=tenant_id,
|
||||
user_id=current_user["user_id"])
|
||||
|
||||
return {"message": "Purchase order deleted successfully"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error deleting purchase order", po_id=str(po_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to delete purchase order")
|
||||
|
||||
|
||||
@@ -14,12 +14,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.services.supplier_service import SupplierService
|
||||
from app.services.delivery_service import DeliveryService
|
||||
from app.services.purchase_order_service import PurchaseOrderService
|
||||
from app.schemas.suppliers import (
|
||||
SupplierApproval, SupplierResponse, SupplierSummary, SupplierStatistics,
|
||||
DeliveryStatusUpdate, DeliveryReceiptConfirmation, DeliveryResponse, DeliverySummary,
|
||||
PurchaseOrderStatusUpdate, PurchaseOrderApproval, PurchaseOrderResponse, PurchaseOrderSummary
|
||||
SupplierApproval, SupplierResponse, SupplierSummary, SupplierStatistics
|
||||
)
|
||||
from app.models.suppliers import SupplierType
|
||||
from app.models import AuditLog
|
||||
@@ -173,550 +169,6 @@ async def get_suppliers_by_type(
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve suppliers by type")
|
||||
|
||||
|
||||
# ===== Delivery Operations =====
|
||||
|
||||
@router.get(route_builder.build_operations_route("deliveries/today"), response_model=List[DeliverySummary])
|
||||
async def get_todays_deliveries(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get deliveries scheduled for today"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
deliveries = await service.get_todays_deliveries(current_user["tenant_id"])
|
||||
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
|
||||
except Exception as e:
|
||||
logger.error("Error getting today's deliveries", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve today's deliveries")
|
||||
|
||||
|
||||
@router.get(route_builder.build_operations_route("deliveries/overdue"), response_model=List[DeliverySummary])
|
||||
async def get_overdue_deliveries(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get overdue deliveries"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
deliveries = await service.get_overdue_deliveries(current_user["tenant_id"])
|
||||
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
|
||||
except Exception as e:
|
||||
logger.error("Error getting overdue deliveries", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve overdue deliveries")
|
||||
|
||||
|
||||
@router.get(route_builder.build_operations_route("deliveries/scheduled"), response_model=List[DeliverySummary])
|
||||
async def get_scheduled_deliveries(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
date_from: Optional[str] = Query(None, description="From date (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="To date (YYYY-MM-DD)"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get scheduled deliveries for a date range"""
|
||||
try:
|
||||
date_from_parsed = None
|
||||
date_to_parsed = None
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
date_from_parsed = datetime.fromisoformat(date_from)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date_from format")
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
date_to_parsed = datetime.fromisoformat(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date_to format")
|
||||
|
||||
service = DeliveryService(db)
|
||||
deliveries = await service.get_scheduled_deliveries(
|
||||
tenant_id=current_user["tenant_id"],
|
||||
date_from=date_from_parsed,
|
||||
date_to=date_to_parsed
|
||||
)
|
||||
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error getting scheduled deliveries", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve scheduled deliveries")
|
||||
|
||||
|
||||
@router.patch(route_builder.build_nested_resource_route("deliveries", "delivery_id", "status"), response_model=DeliveryResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def update_delivery_status(
|
||||
status_data: DeliveryStatusUpdate,
|
||||
delivery_id: UUID = Path(..., description="Delivery ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update delivery status"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
|
||||
# Check delivery exists and belongs to tenant
|
||||
existing_delivery = await service.get_delivery(delivery_id)
|
||||
if not existing_delivery:
|
||||
raise HTTPException(status_code=404, detail="Delivery not found")
|
||||
if existing_delivery.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
delivery = await service.update_delivery_status(
|
||||
delivery_id=delivery_id,
|
||||
status=status_data.status,
|
||||
updated_by=current_user["user_id"],
|
||||
notes=status_data.notes,
|
||||
update_timestamps=status_data.update_timestamps
|
||||
)
|
||||
|
||||
if not delivery:
|
||||
raise HTTPException(status_code=404, detail="Delivery not found")
|
||||
|
||||
return DeliveryResponse.from_orm(delivery)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error updating delivery status", delivery_id=str(delivery_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to update delivery status")
|
||||
|
||||
|
||||
@router.post(route_builder.build_nested_resource_route("deliveries", "delivery_id", "receive"), response_model=DeliveryResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def receive_delivery(
|
||||
receipt_data: DeliveryReceiptConfirmation,
|
||||
delivery_id: UUID = Path(..., description="Delivery ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Mark delivery as received with inspection details"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
|
||||
# Check delivery exists and belongs to tenant
|
||||
existing_delivery = await service.get_delivery(delivery_id)
|
||||
if not existing_delivery:
|
||||
raise HTTPException(status_code=404, detail="Delivery not found")
|
||||
if existing_delivery.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
delivery = await service.mark_as_received(
|
||||
delivery_id=delivery_id,
|
||||
received_by=current_user["user_id"],
|
||||
inspection_passed=receipt_data.inspection_passed,
|
||||
inspection_notes=receipt_data.inspection_notes,
|
||||
quality_issues=receipt_data.quality_issues,
|
||||
notes=receipt_data.notes
|
||||
)
|
||||
|
||||
if not delivery:
|
||||
raise HTTPException(status_code=404, detail="Delivery not found")
|
||||
|
||||
return DeliveryResponse.from_orm(delivery)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error receiving delivery", delivery_id=str(delivery_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to receive delivery")
|
||||
|
||||
|
||||
@router.get(route_builder.build_resource_detail_route("deliveries/purchase-order", "po_id"), response_model=List[DeliverySummary])
|
||||
async def get_deliveries_by_purchase_order(
|
||||
po_id: UUID = Path(..., description="Purchase order ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all deliveries for a purchase order"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
deliveries = await service.get_deliveries_by_purchase_order(po_id)
|
||||
|
||||
# Check tenant access for first delivery (all should belong to same tenant)
|
||||
if deliveries and deliveries[0].tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
return [DeliverySummary.from_orm(delivery) for delivery in deliveries]
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error getting deliveries by purchase order", po_id=str(po_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve deliveries for purchase order")
|
||||
|
||||
|
||||
# ===== Purchase Order Operations =====
|
||||
|
||||
@router.get(route_builder.build_operations_route("purchase-orders/statistics"), response_model=dict)
|
||||
async def get_purchase_order_statistics(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get purchase order statistics for dashboard"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
stats = await service.get_purchase_order_statistics(current_user["tenant_id"])
|
||||
return stats
|
||||
except Exception as e:
|
||||
logger.error("Error getting purchase order statistics", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve statistics")
|
||||
|
||||
|
||||
@router.get(route_builder.build_operations_route("purchase-orders/pending-approval"), response_model=List[PurchaseOrderSummary])
|
||||
async def get_orders_requiring_approval(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get purchase orders requiring approval"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
orders = await service.get_orders_requiring_approval(current_user["tenant_id"])
|
||||
return [PurchaseOrderSummary.from_orm(order) for order in orders]
|
||||
except Exception as e:
|
||||
logger.error("Error getting orders requiring approval", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve orders requiring approval")
|
||||
|
||||
|
||||
@router.get(route_builder.build_operations_route("purchase-orders/overdue"), response_model=List[PurchaseOrderSummary])
|
||||
async def get_overdue_orders(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get overdue purchase orders"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
orders = await service.get_overdue_orders(current_user["tenant_id"])
|
||||
return [PurchaseOrderSummary.from_orm(order) for order in orders]
|
||||
except Exception as e:
|
||||
logger.error("Error getting overdue orders", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve overdue orders")
|
||||
|
||||
|
||||
@router.patch(route_builder.build_nested_resource_route("purchase-orders", "po_id", "status"), response_model=PurchaseOrderResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def update_purchase_order_status(
|
||||
status_data: PurchaseOrderStatusUpdate,
|
||||
po_id: UUID = Path(..., description="Purchase order ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update purchase order status"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
|
||||
# Check order exists and belongs to tenant
|
||||
existing_order = await service.get_purchase_order(po_id)
|
||||
if not existing_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
if existing_order.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
purchase_order = await service.update_order_status(
|
||||
po_id=po_id,
|
||||
status=status_data.status,
|
||||
updated_by=current_user["user_id"],
|
||||
notes=status_data.notes
|
||||
)
|
||||
|
||||
if not purchase_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
|
||||
return PurchaseOrderResponse.from_orm(purchase_order)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error updating purchase order status", po_id=str(po_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to update purchase order status")
|
||||
|
||||
|
||||
@router.post(route_builder.build_nested_resource_route("purchase-orders", "po_id", "approve"), response_model=PurchaseOrderResponse)
|
||||
@require_user_role(['admin', 'owner'])
|
||||
async def approve_purchase_order(
|
||||
approval_data: PurchaseOrderApproval,
|
||||
po_id: UUID = Path(..., description="Purchase order ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Approve or reject a purchase order (Admin+ only)"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
|
||||
# Check order exists and belongs to tenant
|
||||
existing_order = await service.get_purchase_order(po_id)
|
||||
if not existing_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
if existing_order.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Capture PO details for audit
|
||||
po_details = {
|
||||
"po_number": existing_order.order_number,
|
||||
"supplier_id": str(existing_order.supplier_id),
|
||||
"total_amount": float(existing_order.total_amount) if existing_order.total_amount else 0.0,
|
||||
"expected_delivery_date": existing_order.expected_delivery_date.isoformat() if existing_order.expected_delivery_date else None
|
||||
}
|
||||
|
||||
if approval_data.action == "approve":
|
||||
purchase_order = await service.approve_purchase_order(
|
||||
po_id=po_id,
|
||||
approved_by=current_user["user_id"],
|
||||
approval_notes=approval_data.notes
|
||||
)
|
||||
action = "approve"
|
||||
description = f"Admin {current_user.get('email', 'unknown')} approved purchase order {po_details['po_number']}"
|
||||
elif approval_data.action == "reject":
|
||||
if not approval_data.notes:
|
||||
raise HTTPException(status_code=400, detail="Rejection reason is required")
|
||||
purchase_order = await service.reject_purchase_order(
|
||||
po_id=po_id,
|
||||
rejection_reason=approval_data.notes,
|
||||
rejected_by=current_user["user_id"]
|
||||
)
|
||||
action = "reject"
|
||||
description = f"Admin {current_user.get('email', 'unknown')} rejected purchase order {po_details['po_number']}"
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Invalid action")
|
||||
|
||||
if not purchase_order:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Purchase order is not in pending approval status"
|
||||
)
|
||||
|
||||
# Log HIGH severity audit event for purchase order approval/rejection
|
||||
try:
|
||||
await audit_logger.log_event(
|
||||
db_session=db,
|
||||
tenant_id=tenant_id,
|
||||
user_id=current_user["user_id"],
|
||||
action=action,
|
||||
resource_type="purchase_order",
|
||||
resource_id=str(po_id),
|
||||
severity=AuditSeverity.HIGH.value,
|
||||
description=description,
|
||||
changes={
|
||||
"action": approval_data.action,
|
||||
"notes": approval_data.notes,
|
||||
"po_details": po_details
|
||||
},
|
||||
endpoint=f"/purchase-orders/{po_id}/approve",
|
||||
method="POST"
|
||||
)
|
||||
except Exception as audit_error:
|
||||
logger.warning("Failed to log audit event", error=str(audit_error))
|
||||
|
||||
logger.info("Purchase order approval processed",
|
||||
po_id=str(po_id),
|
||||
action=approval_data.action,
|
||||
tenant_id=tenant_id,
|
||||
user_id=current_user["user_id"])
|
||||
|
||||
return PurchaseOrderResponse.from_orm(purchase_order)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error processing purchase order approval", po_id=str(po_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to process purchase order approval")
|
||||
|
||||
|
||||
@router.post(route_builder.build_nested_resource_route("purchase-orders", "po_id", "send-to-supplier"), response_model=PurchaseOrderResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def send_to_supplier(
|
||||
po_id: UUID = Path(..., description="Purchase order ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
send_email: bool = Query(True, description="Send email notification to supplier"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Send purchase order to supplier"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
|
||||
# Check order exists and belongs to tenant
|
||||
existing_order = await service.get_purchase_order(po_id)
|
||||
if not existing_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
if existing_order.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
purchase_order = await service.send_to_supplier(
|
||||
po_id=po_id,
|
||||
sent_by=current_user["user_id"],
|
||||
send_email=send_email
|
||||
)
|
||||
|
||||
if not purchase_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
|
||||
return PurchaseOrderResponse.from_orm(purchase_order)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error sending purchase order to supplier", po_id=str(po_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to send purchase order to supplier")
|
||||
|
||||
|
||||
@router.post(route_builder.build_nested_resource_route("purchase-orders", "po_id", "confirm-supplier-receipt"), response_model=PurchaseOrderResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def confirm_supplier_receipt(
|
||||
po_id: UUID = Path(..., description="Purchase order ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
supplier_reference: Optional[str] = Query(None, description="Supplier's order reference"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Confirm supplier has received and accepted the order"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
|
||||
# Check order exists and belongs to tenant
|
||||
existing_order = await service.get_purchase_order(po_id)
|
||||
if not existing_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
if existing_order.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
purchase_order = await service.confirm_supplier_receipt(
|
||||
po_id=po_id,
|
||||
supplier_reference=supplier_reference,
|
||||
confirmed_by=current_user["user_id"]
|
||||
)
|
||||
|
||||
if not purchase_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
|
||||
return PurchaseOrderResponse.from_orm(purchase_order)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error confirming supplier receipt", po_id=str(po_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to confirm supplier receipt")
|
||||
|
||||
|
||||
@router.post(route_builder.build_nested_resource_route("purchase-orders", "po_id", "cancel"), response_model=PurchaseOrderResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def cancel_purchase_order(
|
||||
po_id: UUID = Path(..., description="Purchase order ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
cancellation_reason: str = Query(..., description="Reason for cancellation"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Cancel a purchase order"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
|
||||
# Check order exists and belongs to tenant
|
||||
existing_order = await service.get_purchase_order(po_id)
|
||||
if not existing_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
if existing_order.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
purchase_order = await service.cancel_purchase_order(
|
||||
po_id=po_id,
|
||||
cancellation_reason=cancellation_reason,
|
||||
cancelled_by=current_user["user_id"]
|
||||
)
|
||||
|
||||
if not purchase_order:
|
||||
raise HTTPException(status_code=404, detail="Purchase order not found")
|
||||
|
||||
return PurchaseOrderResponse.from_orm(purchase_order)
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Error cancelling purchase order", po_id=str(po_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to cancel purchase order")
|
||||
|
||||
|
||||
@router.get(route_builder.build_resource_detail_route("purchase-orders/supplier", "supplier_id"), response_model=List[PurchaseOrderSummary])
|
||||
async def get_orders_by_supplier(
|
||||
supplier_id: UUID = Path(..., description="Supplier ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Number of orders to return"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get recent purchase orders for a specific supplier"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
orders = await service.get_orders_by_supplier(
|
||||
tenant_id=current_user["tenant_id"],
|
||||
supplier_id=supplier_id,
|
||||
limit=limit
|
||||
)
|
||||
return [PurchaseOrderSummary.from_orm(order) for order in orders]
|
||||
except Exception as e:
|
||||
logger.error("Error getting orders by supplier", supplier_id=str(supplier_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve orders by supplier")
|
||||
|
||||
|
||||
@router.get(route_builder.build_nested_resource_route("purchase-orders/inventory-products", "inventory_product_id", "history"))
|
||||
async def get_inventory_product_purchase_history(
|
||||
inventory_product_id: UUID = Path(..., description="Inventory Product ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
days_back: int = Query(90, ge=1, le=365, description="Number of days to look back"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get purchase history for a specific inventory product"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
history = await service.get_inventory_product_purchase_history(
|
||||
tenant_id=current_user["tenant_id"],
|
||||
inventory_product_id=inventory_product_id,
|
||||
days_back=days_back
|
||||
)
|
||||
return history
|
||||
except Exception as e:
|
||||
logger.error("Error getting inventory product purchase history", inventory_product_id=str(inventory_product_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve inventory product purchase history")
|
||||
|
||||
|
||||
@router.get(route_builder.build_operations_route("purchase-orders/inventory-products/top-purchased"))
|
||||
async def get_top_purchased_inventory_products(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
days_back: int = Query(30, ge=1, le=365, description="Number of days to look back"),
|
||||
limit: int = Query(10, ge=1, le=50, description="Number of top inventory products to return"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get most purchased inventory products by value"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
products = await service.get_top_purchased_inventory_products(
|
||||
tenant_id=current_user["tenant_id"],
|
||||
days_back=days_back,
|
||||
limit=limit
|
||||
)
|
||||
return products
|
||||
except Exception as e:
|
||||
logger.error("Error getting top purchased inventory products", error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve top purchased inventory products")
|
||||
|
||||
|
||||
@router.get(route_builder.build_operations_route("count"))
|
||||
async def get_supplier_count(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
|
||||
@@ -91,6 +91,65 @@ async def list_suppliers(
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve suppliers")
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("batch"), response_model=List[SupplierSummary])
|
||||
async def get_suppliers_batch(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
ids: str = Query(..., description="Comma-separated supplier IDs"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get multiple suppliers in a single call for performance optimization.
|
||||
|
||||
This endpoint is designed to eliminate N+1 query patterns when fetching
|
||||
supplier data for multiple purchase orders or other entities.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
ids: Comma-separated supplier IDs (e.g., "abc123,def456,xyz789")
|
||||
|
||||
Returns:
|
||||
List of supplier summaries for the requested IDs
|
||||
"""
|
||||
try:
|
||||
service = SupplierService(db)
|
||||
|
||||
# Parse comma-separated IDs
|
||||
supplier_ids = [id.strip() for id in ids.split(",") if id.strip()]
|
||||
|
||||
if not supplier_ids:
|
||||
return []
|
||||
|
||||
if len(supplier_ids) > 100:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Maximum 100 supplier IDs allowed per batch request"
|
||||
)
|
||||
|
||||
# Convert to UUIDs
|
||||
try:
|
||||
uuid_ids = [UUID(id) for id in supplier_ids]
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid supplier ID format: {e}")
|
||||
|
||||
# Fetch suppliers
|
||||
suppliers = await service.get_suppliers_batch(tenant_id=UUID(tenant_id), supplier_ids=uuid_ids)
|
||||
|
||||
logger.info(
|
||||
"Batch retrieved suppliers",
|
||||
tenant_id=tenant_id,
|
||||
requested_count=len(supplier_ids),
|
||||
found_count=len(suppliers)
|
||||
)
|
||||
|
||||
return [SupplierSummary.from_orm(supplier) for supplier in suppliers]
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error batch retrieving suppliers", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve suppliers")
|
||||
|
||||
|
||||
@router.get(route_builder.build_resource_detail_route("", "supplier_id"), response_model=SupplierResponse)
|
||||
async def get_supplier(
|
||||
supplier_id: UUID = Path(..., description="Supplier ID"),
|
||||
|
||||
Reference in New Issue
Block a user