Add fixes to procurement logic and fix rel-time connections

This commit is contained in:
Urtzi Alfaro
2025-10-02 13:20:30 +02:00
parent c9d8d1d071
commit 1243c2ca6d
24 changed files with 4984 additions and 348 deletions

View File

@@ -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
# ================================================================