Add fixes to procurement logic and fix rel-time connections
This commit is contained in:
@@ -75,10 +75,13 @@ async def get_current_tenant(
|
||||
|
||||
|
||||
async def get_procurement_service(db: AsyncSession = Depends(get_db)) -> ProcurementService:
|
||||
"""Get procurement service instance"""
|
||||
"""Get procurement service instance with all required clients"""
|
||||
from shared.clients.suppliers_client import SuppliersServiceClient
|
||||
|
||||
inventory_client = InventoryServiceClient(service_settings)
|
||||
forecast_client = ForecastServiceClient(service_settings, "orders-service")
|
||||
return ProcurementService(db, service_settings, inventory_client, forecast_client)
|
||||
suppliers_client = SuppliersServiceClient(service_settings)
|
||||
return ProcurementService(db, service_settings, inventory_client, forecast_client, suppliers_client)
|
||||
|
||||
|
||||
# ================================================================
|
||||
@@ -406,6 +409,307 @@ async def get_critical_requirements(
|
||||
)
|
||||
|
||||
|
||||
# ================================================================
|
||||
# NEW FEATURE ENDPOINTS
|
||||
# ================================================================
|
||||
|
||||
@router.post("/procurement/plans/{plan_id}/recalculate", response_model=GeneratePlanResponse)
|
||||
@monitor_performance("recalculate_procurement_plan")
|
||||
async def recalculate_procurement_plan(
|
||||
tenant_id: uuid.UUID,
|
||||
plan_id: uuid.UUID,
|
||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||
):
|
||||
"""
|
||||
Recalculate an existing procurement plan (Edge Case #3)
|
||||
Useful when inventory has changed significantly after plan creation
|
||||
"""
|
||||
try:
|
||||
if tenant_access.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this tenant"
|
||||
)
|
||||
|
||||
result = await procurement_service.recalculate_plan(tenant_id, plan_id)
|
||||
|
||||
if not result.success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=result.message
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error recalculating procurement plan: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/procurement/requirements/{requirement_id}/link-purchase-order")
|
||||
@monitor_performance("link_requirement_to_po")
|
||||
async def link_requirement_to_purchase_order(
|
||||
tenant_id: uuid.UUID,
|
||||
requirement_id: uuid.UUID,
|
||||
purchase_order_id: uuid.UUID,
|
||||
purchase_order_number: str,
|
||||
ordered_quantity: float,
|
||||
expected_delivery_date: Optional[date] = None,
|
||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||
):
|
||||
"""
|
||||
Link a procurement requirement to a purchase order (Bug #4 FIX, Feature #1)
|
||||
Updates requirement status and tracks PO information
|
||||
"""
|
||||
try:
|
||||
if tenant_access.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this tenant"
|
||||
)
|
||||
|
||||
from decimal import Decimal
|
||||
success = await procurement_service.link_requirement_to_purchase_order(
|
||||
tenant_id=tenant_id,
|
||||
requirement_id=requirement_id,
|
||||
purchase_order_id=purchase_order_id,
|
||||
purchase_order_number=purchase_order_number,
|
||||
ordered_quantity=Decimal(str(ordered_quantity)),
|
||||
expected_delivery_date=expected_delivery_date
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Requirement not found or unauthorized"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Requirement linked to purchase order successfully",
|
||||
"requirement_id": str(requirement_id),
|
||||
"purchase_order_id": str(purchase_order_id)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error linking requirement to PO: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/procurement/requirements/{requirement_id}/delivery-status")
|
||||
@monitor_performance("update_delivery_status")
|
||||
async def update_requirement_delivery_status(
|
||||
tenant_id: uuid.UUID,
|
||||
requirement_id: uuid.UUID,
|
||||
delivery_status: str,
|
||||
received_quantity: Optional[float] = None,
|
||||
actual_delivery_date: Optional[date] = None,
|
||||
quality_rating: Optional[float] = None,
|
||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||
):
|
||||
"""
|
||||
Update delivery status for a requirement (Feature #2)
|
||||
Tracks received quantities, delivery dates, and quality ratings
|
||||
"""
|
||||
try:
|
||||
if tenant_access.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this tenant"
|
||||
)
|
||||
|
||||
from decimal import Decimal
|
||||
success = await procurement_service.update_delivery_status(
|
||||
tenant_id=tenant_id,
|
||||
requirement_id=requirement_id,
|
||||
delivery_status=delivery_status,
|
||||
received_quantity=Decimal(str(received_quantity)) if received_quantity is not None else None,
|
||||
actual_delivery_date=actual_delivery_date,
|
||||
quality_rating=Decimal(str(quality_rating)) if quality_rating is not None else None
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Requirement not found or unauthorized"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Delivery status updated successfully",
|
||||
"requirement_id": str(requirement_id),
|
||||
"delivery_status": delivery_status
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error updating delivery status: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/procurement/plans/{plan_id}/approve")
|
||||
@monitor_performance("approve_procurement_plan")
|
||||
async def approve_procurement_plan(
|
||||
tenant_id: uuid.UUID,
|
||||
plan_id: uuid.UUID,
|
||||
approval_notes: Optional[str] = None,
|
||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||
):
|
||||
"""
|
||||
Approve a procurement plan (Edge Case #7: Enhanced approval workflow)
|
||||
Includes approval notes and workflow tracking
|
||||
"""
|
||||
try:
|
||||
if tenant_access.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this tenant"
|
||||
)
|
||||
|
||||
try:
|
||||
user_id = uuid.UUID(tenant_access.user_id)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid user ID"
|
||||
)
|
||||
|
||||
result = await procurement_service.update_plan_status(
|
||||
tenant_id=tenant_id,
|
||||
plan_id=plan_id,
|
||||
status="approved",
|
||||
updated_by=user_id,
|
||||
approval_notes=approval_notes
|
||||
)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Plan not found"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error approving plan: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/procurement/plans/{plan_id}/reject")
|
||||
@monitor_performance("reject_procurement_plan")
|
||||
async def reject_procurement_plan(
|
||||
tenant_id: uuid.UUID,
|
||||
plan_id: uuid.UUID,
|
||||
rejection_notes: Optional[str] = None,
|
||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||
):
|
||||
"""
|
||||
Reject a procurement plan (Edge Case #7: Enhanced approval workflow)
|
||||
Marks plan as cancelled with rejection notes
|
||||
"""
|
||||
try:
|
||||
if tenant_access.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this tenant"
|
||||
)
|
||||
|
||||
try:
|
||||
user_id = uuid.UUID(tenant_access.user_id)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid user ID"
|
||||
)
|
||||
|
||||
result = await procurement_service.update_plan_status(
|
||||
tenant_id=tenant_id,
|
||||
plan_id=plan_id,
|
||||
status="cancelled",
|
||||
updated_by=user_id,
|
||||
approval_notes=f"REJECTED: {rejection_notes}" if rejection_notes else "REJECTED"
|
||||
)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Plan not found"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error rejecting plan: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/procurement/plans/{plan_id}/create-purchase-orders")
|
||||
@monitor_performance("create_pos_from_plan")
|
||||
async def create_purchase_orders_from_plan(
|
||||
tenant_id: uuid.UUID,
|
||||
plan_id: uuid.UUID,
|
||||
auto_approve: bool = False,
|
||||
tenant_access: TenantAccess = Depends(get_current_tenant),
|
||||
procurement_service: ProcurementService = Depends(get_procurement_service)
|
||||
):
|
||||
"""
|
||||
Automatically create purchase orders from procurement plan (Feature #1)
|
||||
Groups requirements by supplier and creates POs automatically
|
||||
"""
|
||||
try:
|
||||
if tenant_access.tenant_id != tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to this tenant"
|
||||
)
|
||||
|
||||
result = await procurement_service.create_purchase_orders_from_plan(
|
||||
tenant_id=tenant_id,
|
||||
plan_id=plan_id,
|
||||
auto_approve=auto_approve
|
||||
)
|
||||
|
||||
if not result.get('success'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=result.get('error', 'Failed to create purchase orders')
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating purchase orders: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# ================================================================
|
||||
# UTILITY ENDPOINTS
|
||||
# ================================================================
|
||||
|
||||
Reference in New Issue
Block a user