Improve the frontend modals
This commit is contained in:
@@ -67,7 +67,7 @@ async def get_delivery_performance_stats(
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
stats = await service.get_delivery_performance_stats(
|
||||
tenant_id=current_user.tenant_id,
|
||||
tenant_id=current_user["tenant_id"],
|
||||
days_back=days_back,
|
||||
supplier_id=supplier_id
|
||||
)
|
||||
@@ -89,7 +89,7 @@ async def get_delivery_summary_stats(
|
||||
"""Get delivery summary statistics for dashboard"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
stats = await service.get_upcoming_deliveries_summary(current_user.tenant_id)
|
||||
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))
|
||||
|
||||
@@ -41,9 +41,9 @@ async def create_delivery(
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
delivery = await service.create_delivery(
|
||||
tenant_id=current_user.tenant_id,
|
||||
tenant_id=current_user["tenant_id"],
|
||||
delivery_data=delivery_data,
|
||||
created_by=current_user.user_id
|
||||
created_by=current_user["user_id"]
|
||||
)
|
||||
return DeliveryResponse.from_orm(delivery)
|
||||
except ValueError as e:
|
||||
@@ -106,7 +106,7 @@ async def list_deliveries(
|
||||
)
|
||||
|
||||
deliveries = await service.search_deliveries(
|
||||
tenant_id=current_user.tenant_id,
|
||||
tenant_id=current_user["tenant_id"],
|
||||
search_params=search_params
|
||||
)
|
||||
|
||||
@@ -135,7 +135,7 @@ async def get_delivery(
|
||||
raise HTTPException(status_code=404, detail="Delivery not found")
|
||||
|
||||
# Check tenant access
|
||||
if delivery.tenant_id != current_user.tenant_id:
|
||||
if delivery.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
return DeliveryResponse.from_orm(delivery)
|
||||
@@ -164,13 +164,13 @@ async def update_delivery(
|
||||
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:
|
||||
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
|
||||
updated_by=current_user["user_id"]
|
||||
)
|
||||
|
||||
if not delivery:
|
||||
|
||||
@@ -102,7 +102,7 @@ async def get_suppliers_needing_review(
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve suppliers needing review")
|
||||
|
||||
|
||||
@router.post(route_builder.build_nested_resource_route("suppliers", "supplier_id", "approve"), response_model=SupplierResponse)
|
||||
@router.post(route_builder.build_resource_action_route("", "supplier_id", "approve"), response_model=SupplierResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def approve_supplier(
|
||||
approval_data: SupplierApproval,
|
||||
@@ -123,7 +123,7 @@ async def approve_supplier(
|
||||
if approval_data.action == "approve":
|
||||
supplier = await service.approve_supplier(
|
||||
supplier_id=supplier_id,
|
||||
approved_by=current_user.user_id,
|
||||
approved_by=current_user["user_id"],
|
||||
notes=approval_data.notes
|
||||
)
|
||||
elif approval_data.action == "reject":
|
||||
@@ -132,7 +132,7 @@ async def approve_supplier(
|
||||
supplier = await service.reject_supplier(
|
||||
supplier_id=supplier_id,
|
||||
rejection_reason=approval_data.notes,
|
||||
rejected_by=current_user.user_id
|
||||
rejected_by=current_user["user_id"]
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Invalid action")
|
||||
@@ -148,7 +148,7 @@ async def approve_supplier(
|
||||
raise HTTPException(status_code=500, detail="Failed to process supplier approval")
|
||||
|
||||
|
||||
@router.get(route_builder.build_resource_detail_route("suppliers/types", "supplier_type"), response_model=List[SupplierSummary])
|
||||
@router.get(route_builder.build_resource_detail_route("types", "supplier_type"), response_model=List[SupplierSummary])
|
||||
async def get_suppliers_by_type(
|
||||
supplier_type: str = Path(..., description="Supplier type"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
@@ -183,7 +183,7 @@ async def get_todays_deliveries(
|
||||
"""Get deliveries scheduled for today"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
deliveries = await service.get_todays_deliveries(current_user.tenant_id)
|
||||
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))
|
||||
@@ -199,7 +199,7 @@ async def get_overdue_deliveries(
|
||||
"""Get overdue deliveries"""
|
||||
try:
|
||||
service = DeliveryService(db)
|
||||
deliveries = await service.get_overdue_deliveries(current_user.tenant_id)
|
||||
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))
|
||||
@@ -233,7 +233,7 @@ async def get_scheduled_deliveries(
|
||||
|
||||
service = DeliveryService(db)
|
||||
deliveries = await service.get_scheduled_deliveries(
|
||||
tenant_id=current_user.tenant_id,
|
||||
tenant_id=current_user["tenant_id"],
|
||||
date_from=date_from_parsed,
|
||||
date_to=date_to_parsed
|
||||
)
|
||||
@@ -262,13 +262,13 @@ async def update_delivery_status(
|
||||
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:
|
||||
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,
|
||||
updated_by=current_user["user_id"],
|
||||
notes=status_data.notes,
|
||||
update_timestamps=status_data.update_timestamps
|
||||
)
|
||||
@@ -303,12 +303,12 @@ async def receive_delivery(
|
||||
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:
|
||||
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,
|
||||
received_by=current_user["user_id"],
|
||||
inspection_passed=receipt_data.inspection_passed,
|
||||
inspection_notes=receipt_data.inspection_notes,
|
||||
quality_issues=receipt_data.quality_issues,
|
||||
@@ -341,7 +341,7 @@ async def get_deliveries_by_purchase_order(
|
||||
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:
|
||||
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]
|
||||
@@ -363,7 +363,7 @@ async def get_purchase_order_statistics(
|
||||
"""Get purchase order statistics for dashboard"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
stats = await service.get_purchase_order_statistics(current_user.tenant_id)
|
||||
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))
|
||||
@@ -379,7 +379,7 @@ async def get_orders_requiring_approval(
|
||||
"""Get purchase orders requiring approval"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
orders = await service.get_orders_requiring_approval(current_user.tenant_id)
|
||||
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))
|
||||
@@ -395,7 +395,7 @@ async def get_overdue_orders(
|
||||
"""Get overdue purchase orders"""
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
orders = await service.get_overdue_orders(current_user.tenant_id)
|
||||
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))
|
||||
@@ -419,13 +419,13 @@ async def update_purchase_order_status(
|
||||
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:
|
||||
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,
|
||||
updated_by=current_user["user_id"],
|
||||
notes=status_data.notes
|
||||
)
|
||||
|
||||
@@ -459,7 +459,7 @@ async def approve_purchase_order(
|
||||
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:
|
||||
if existing_order.tenant_id != current_user["tenant_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
# Capture PO details for audit
|
||||
@@ -473,7 +473,7 @@ async def approve_purchase_order(
|
||||
if approval_data.action == "approve":
|
||||
purchase_order = await service.approve_purchase_order(
|
||||
po_id=po_id,
|
||||
approved_by=current_user.user_id,
|
||||
approved_by=current_user["user_id"],
|
||||
approval_notes=approval_data.notes
|
||||
)
|
||||
action = "approve"
|
||||
@@ -484,7 +484,7 @@ async def approve_purchase_order(
|
||||
purchase_order = await service.reject_purchase_order(
|
||||
po_id=po_id,
|
||||
rejection_reason=approval_data.notes,
|
||||
rejected_by=current_user.user_id
|
||||
rejected_by=current_user["user_id"]
|
||||
)
|
||||
action = "reject"
|
||||
description = f"Admin {current_user.get('email', 'unknown')} rejected purchase order {po_details['po_number']}"
|
||||
@@ -550,12 +550,12 @@ async def send_to_supplier(
|
||||
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:
|
||||
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,
|
||||
sent_by=current_user["user_id"],
|
||||
send_email=send_email
|
||||
)
|
||||
|
||||
@@ -589,13 +589,13 @@ async def confirm_supplier_receipt(
|
||||
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:
|
||||
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
|
||||
confirmed_by=current_user["user_id"]
|
||||
)
|
||||
|
||||
if not purchase_order:
|
||||
@@ -628,13 +628,13 @@ async def cancel_purchase_order(
|
||||
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:
|
||||
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
|
||||
cancelled_by=current_user["user_id"]
|
||||
)
|
||||
|
||||
if not purchase_order:
|
||||
@@ -662,7 +662,7 @@ async def get_orders_by_supplier(
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
orders = await service.get_orders_by_supplier(
|
||||
tenant_id=current_user.tenant_id,
|
||||
tenant_id=current_user["tenant_id"],
|
||||
supplier_id=supplier_id,
|
||||
limit=limit
|
||||
)
|
||||
@@ -684,7 +684,7 @@ async def get_inventory_product_purchase_history(
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
history = await service.get_inventory_product_purchase_history(
|
||||
tenant_id=current_user.tenant_id,
|
||||
tenant_id=current_user["tenant_id"],
|
||||
inventory_product_id=inventory_product_id,
|
||||
days_back=days_back
|
||||
)
|
||||
@@ -706,7 +706,7 @@ async def get_top_purchased_inventory_products(
|
||||
try:
|
||||
service = PurchaseOrderService(db)
|
||||
products = await service.get_top_purchased_inventory_products(
|
||||
tenant_id=current_user.tenant_id,
|
||||
tenant_id=current_user["tenant_id"],
|
||||
days_back=days_back,
|
||||
limit=limit
|
||||
)
|
||||
@@ -732,7 +732,7 @@ async def get_supplier_count(
|
||||
|
||||
try:
|
||||
service = SupplierService(db)
|
||||
suppliers = await service.get_suppliers(tenant_id=current_user.tenant_id)
|
||||
suppliers = await service.get_suppliers(tenant_id=current_user["tenant_id"])
|
||||
count = len(suppliers)
|
||||
|
||||
return {"count": count}
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.services.supplier_service import SupplierService
|
||||
from app.models.suppliers import SupplierPriceList
|
||||
from app.schemas.suppliers import (
|
||||
SupplierCreate, SupplierUpdate, SupplierResponse, SupplierSummary,
|
||||
SupplierSearchParams
|
||||
SupplierSearchParams, SupplierDeletionSummary
|
||||
)
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing import RouteBuilder
|
||||
@@ -30,7 +30,7 @@ router = APIRouter(tags=["suppliers"])
|
||||
logger = structlog.get_logger()
|
||||
audit_logger = create_audit_logger("suppliers-service")
|
||||
|
||||
@router.post(route_builder.build_base_route("suppliers"), response_model=SupplierResponse)
|
||||
@router.post(route_builder.build_base_route(""), response_model=SupplierResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def create_supplier(
|
||||
supplier_data: SupplierCreate,
|
||||
@@ -41,10 +41,15 @@ async def create_supplier(
|
||||
"""Create a new supplier"""
|
||||
try:
|
||||
service = SupplierService(db)
|
||||
|
||||
# Get user role from current_user dict
|
||||
user_role = current_user.get("role", "member").lower()
|
||||
|
||||
supplier = await service.create_supplier(
|
||||
tenant_id=UUID(tenant_id),
|
||||
supplier_data=supplier_data,
|
||||
created_by=current_user.user_id
|
||||
created_by=current_user["user_id"],
|
||||
created_by_role=user_role
|
||||
)
|
||||
return SupplierResponse.from_orm(supplier)
|
||||
except ValueError as e:
|
||||
@@ -54,7 +59,7 @@ async def create_supplier(
|
||||
raise HTTPException(status_code=500, detail="Failed to create supplier")
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("suppliers"), response_model=List[SupplierSummary])
|
||||
@router.get(route_builder.build_base_route(""), response_model=List[SupplierSummary])
|
||||
async def list_suppliers(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
search_term: Optional[str] = Query(None, description="Search term"),
|
||||
@@ -84,7 +89,7 @@ async def list_suppliers(
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve suppliers")
|
||||
|
||||
|
||||
@router.get(route_builder.build_resource_detail_route("suppliers", "supplier_id"), response_model=SupplierResponse)
|
||||
@router.get(route_builder.build_resource_detail_route("", "supplier_id"), response_model=SupplierResponse)
|
||||
async def get_supplier(
|
||||
supplier_id: UUID = Path(..., description="Supplier ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
@@ -106,7 +111,7 @@ async def get_supplier(
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve supplier")
|
||||
|
||||
|
||||
@router.put(route_builder.build_resource_detail_route("suppliers", "supplier_id"), response_model=SupplierResponse)
|
||||
@router.put(route_builder.build_resource_detail_route("", "supplier_id"), response_model=SupplierResponse)
|
||||
@require_user_role(['admin', 'owner', 'member'])
|
||||
async def update_supplier(
|
||||
supplier_data: SupplierUpdate,
|
||||
@@ -126,7 +131,7 @@ async def update_supplier(
|
||||
supplier = await service.update_supplier(
|
||||
supplier_id=supplier_id,
|
||||
supplier_data=supplier_data,
|
||||
updated_by=current_user.user_id
|
||||
updated_by=current_user["user_id"]
|
||||
)
|
||||
|
||||
if not supplier:
|
||||
@@ -142,7 +147,7 @@ async def update_supplier(
|
||||
raise HTTPException(status_code=500, detail="Failed to update supplier")
|
||||
|
||||
|
||||
@router.delete(route_builder.build_resource_detail_route("suppliers", "supplier_id"))
|
||||
@router.delete(route_builder.build_resource_detail_route("", "supplier_id"))
|
||||
@require_user_role(['admin', 'owner'])
|
||||
async def delete_supplier(
|
||||
supplier_id: UUID = Path(..., description="Supplier ID"),
|
||||
@@ -207,6 +212,77 @@ async def delete_supplier(
|
||||
raise HTTPException(status_code=500, detail="Failed to delete supplier")
|
||||
|
||||
|
||||
@router.delete(
|
||||
route_builder.build_resource_action_route("", "supplier_id", "hard"),
|
||||
response_model=SupplierDeletionSummary
|
||||
)
|
||||
@require_user_role(['admin', 'owner'])
|
||||
async def hard_delete_supplier(
|
||||
supplier_id: UUID = Path(..., description="Supplier ID"),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Hard delete supplier and all associated data (Admin/Owner only, permanent)"""
|
||||
try:
|
||||
service = SupplierService(db)
|
||||
|
||||
# Check supplier exists
|
||||
existing_supplier = await service.get_supplier(supplier_id)
|
||||
if not existing_supplier:
|
||||
raise HTTPException(status_code=404, detail="Supplier not found")
|
||||
|
||||
# Capture supplier data before deletion
|
||||
supplier_data = {
|
||||
"id": str(existing_supplier.id),
|
||||
"name": existing_supplier.name,
|
||||
"status": existing_supplier.status.value,
|
||||
"supplier_code": existing_supplier.supplier_code
|
||||
}
|
||||
|
||||
# Perform hard deletion
|
||||
deletion_summary = await service.hard_delete_supplier(supplier_id, UUID(tenant_id))
|
||||
|
||||
# Log audit event for hard deletion
|
||||
try:
|
||||
# Get sync db session for audit logging
|
||||
from app.core.database import SessionLocal
|
||||
sync_db = SessionLocal()
|
||||
try:
|
||||
await audit_logger.log_deletion(
|
||||
db_session=sync_db,
|
||||
tenant_id=tenant_id,
|
||||
user_id=current_user["user_id"],
|
||||
resource_type="supplier",
|
||||
resource_id=str(supplier_id),
|
||||
resource_data=supplier_data,
|
||||
description=f"Hard deleted supplier '{supplier_data['name']}' and all associated data",
|
||||
endpoint=f"/suppliers/{supplier_id}/hard",
|
||||
method="DELETE",
|
||||
metadata=deletion_summary
|
||||
)
|
||||
sync_db.commit()
|
||||
finally:
|
||||
sync_db.close()
|
||||
except Exception as audit_error:
|
||||
logger.warning("Failed to log audit event", error=str(audit_error))
|
||||
|
||||
logger.info("Hard deleted supplier",
|
||||
supplier_id=str(supplier_id),
|
||||
tenant_id=tenant_id,
|
||||
user_id=current_user["user_id"],
|
||||
deletion_summary=deletion_summary)
|
||||
|
||||
return deletion_summary
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error hard deleting supplier", supplier_id=str(supplier_id), error=str(e))
|
||||
raise HTTPException(status_code=500, detail="Failed to hard delete supplier")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("count"),
|
||||
response_model=dict
|
||||
@@ -237,7 +313,7 @@ async def count_suppliers(
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_resource_action_route("suppliers", "supplier_id", "products"),
|
||||
route_builder.build_resource_action_route("", "supplier_id", "products"),
|
||||
response_model=List[Dict[str, Any]]
|
||||
)
|
||||
async def get_supplier_products(
|
||||
|
||||
@@ -102,11 +102,13 @@ app = service.create_app()
|
||||
service.setup_standard_endpoints()
|
||||
|
||||
# Include API routers
|
||||
service.add_router(suppliers.router)
|
||||
service.add_router(deliveries.router)
|
||||
service.add_router(purchase_orders.router)
|
||||
service.add_router(supplier_operations.router)
|
||||
service.add_router(analytics.router)
|
||||
# IMPORTANT: Order matters! More specific routes must come first
|
||||
# to avoid path parameter matching issues
|
||||
service.add_router(purchase_orders.router) # /suppliers/purchase-orders/...
|
||||
service.add_router(deliveries.router) # /suppliers/deliveries/...
|
||||
service.add_router(supplier_operations.router) # /suppliers/operations/...
|
||||
service.add_router(analytics.router) # /suppliers/analytics/...
|
||||
service.add_router(suppliers.router) # /suppliers/{supplier_id} - catch-all, must be last
|
||||
service.add_router(internal_demo.router)
|
||||
|
||||
|
||||
|
||||
@@ -217,44 +217,134 @@ class SupplierRepository(BaseRepository[Supplier]):
|
||||
"total_spend": float(total_spend)
|
||||
}
|
||||
|
||||
def approve_supplier(
|
||||
async def approve_supplier(
|
||||
self,
|
||||
supplier_id: UUID,
|
||||
approved_by: UUID,
|
||||
approval_date: Optional[datetime] = None
|
||||
) -> Optional[Supplier]:
|
||||
"""Approve a pending supplier"""
|
||||
supplier = self.get_by_id(supplier_id)
|
||||
if not supplier or supplier.status != SupplierStatus.PENDING_APPROVAL:
|
||||
supplier = await self.get_by_id(supplier_id)
|
||||
if not supplier or supplier.status != SupplierStatus.pending_approval:
|
||||
return None
|
||||
|
||||
supplier.status = SupplierStatus.ACTIVE
|
||||
|
||||
supplier.status = SupplierStatus.active
|
||||
supplier.approved_by = approved_by
|
||||
supplier.approved_at = approval_date or datetime.utcnow()
|
||||
supplier.rejection_reason = None
|
||||
supplier.updated_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(supplier)
|
||||
|
||||
await self.db.commit()
|
||||
await self.db.refresh(supplier)
|
||||
return supplier
|
||||
|
||||
def reject_supplier(
|
||||
async def reject_supplier(
|
||||
self,
|
||||
supplier_id: UUID,
|
||||
rejection_reason: str,
|
||||
approved_by: UUID
|
||||
) -> Optional[Supplier]:
|
||||
"""Reject a pending supplier"""
|
||||
supplier = self.get_by_id(supplier_id)
|
||||
if not supplier or supplier.status != SupplierStatus.PENDING_APPROVAL:
|
||||
supplier = await self.get_by_id(supplier_id)
|
||||
if not supplier or supplier.status != SupplierStatus.pending_approval:
|
||||
return None
|
||||
|
||||
supplier.status = SupplierStatus.INACTIVE
|
||||
|
||||
supplier.status = SupplierStatus.inactive
|
||||
supplier.rejection_reason = rejection_reason
|
||||
supplier.approved_by = approved_by
|
||||
supplier.approved_at = datetime.utcnow()
|
||||
supplier.updated_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(supplier)
|
||||
return supplier
|
||||
|
||||
await self.db.commit()
|
||||
await self.db.refresh(supplier)
|
||||
return supplier
|
||||
async def hard_delete_supplier(self, supplier_id: UUID) -> Dict[str, Any]:
|
||||
"""
|
||||
Hard delete supplier and all associated data
|
||||
Returns counts of deleted records
|
||||
"""
|
||||
from app.models.suppliers import (
|
||||
SupplierPriceList, SupplierQualityReview,
|
||||
SupplierAlert, SupplierScorecard, PurchaseOrderStatus, PurchaseOrder
|
||||
)
|
||||
from app.models.performance import SupplierPerformanceMetric
|
||||
from sqlalchemy import delete
|
||||
|
||||
# Get supplier first
|
||||
supplier = await self.get_by_id(supplier_id)
|
||||
if not supplier:
|
||||
return None
|
||||
|
||||
# Check for active purchase orders (block deletion if any exist)
|
||||
active_statuses = [
|
||||
PurchaseOrderStatus.draft,
|
||||
PurchaseOrderStatus.pending_approval,
|
||||
PurchaseOrderStatus.approved,
|
||||
PurchaseOrderStatus.sent_to_supplier,
|
||||
PurchaseOrderStatus.confirmed
|
||||
]
|
||||
|
||||
stmt = select(PurchaseOrder).where(
|
||||
PurchaseOrder.supplier_id == supplier_id,
|
||||
PurchaseOrder.status.in_(active_statuses)
|
||||
)
|
||||
result = await self.db.execute(stmt)
|
||||
active_pos = result.scalars().all()
|
||||
|
||||
if active_pos:
|
||||
raise ValueError(
|
||||
f"Cannot delete supplier with {len(active_pos)} active purchase orders. "
|
||||
"Complete or cancel all purchase orders first."
|
||||
)
|
||||
|
||||
# Count related records before deletion
|
||||
stmt = select(SupplierPriceList).where(SupplierPriceList.supplier_id == supplier_id)
|
||||
result = await self.db.execute(stmt)
|
||||
price_lists_count = len(result.scalars().all())
|
||||
|
||||
stmt = select(SupplierQualityReview).where(SupplierQualityReview.supplier_id == supplier_id)
|
||||
result = await self.db.execute(stmt)
|
||||
quality_reviews_count = len(result.scalars().all())
|
||||
|
||||
stmt = select(SupplierPerformanceMetric).where(SupplierPerformanceMetric.supplier_id == supplier_id)
|
||||
result = await self.db.execute(stmt)
|
||||
metrics_count = len(result.scalars().all())
|
||||
|
||||
stmt = select(SupplierAlert).where(SupplierAlert.supplier_id == supplier_id)
|
||||
result = await self.db.execute(stmt)
|
||||
alerts_count = len(result.scalars().all())
|
||||
|
||||
stmt = select(SupplierScorecard).where(SupplierScorecard.supplier_id == supplier_id)
|
||||
result = await self.db.execute(stmt)
|
||||
scorecards_count = len(result.scalars().all())
|
||||
|
||||
# Delete related records (in reverse dependency order)
|
||||
stmt = delete(SupplierScorecard).where(SupplierScorecard.supplier_id == supplier_id)
|
||||
await self.db.execute(stmt)
|
||||
|
||||
stmt = delete(SupplierAlert).where(SupplierAlert.supplier_id == supplier_id)
|
||||
await self.db.execute(stmt)
|
||||
|
||||
stmt = delete(SupplierPerformanceMetric).where(SupplierPerformanceMetric.supplier_id == supplier_id)
|
||||
await self.db.execute(stmt)
|
||||
|
||||
stmt = delete(SupplierQualityReview).where(SupplierQualityReview.supplier_id == supplier_id)
|
||||
await self.db.execute(stmt)
|
||||
|
||||
stmt = delete(SupplierPriceList).where(SupplierPriceList.supplier_id == supplier_id)
|
||||
await self.db.execute(stmt)
|
||||
|
||||
# Delete the supplier itself
|
||||
await self.delete(supplier_id)
|
||||
|
||||
await self.db.commit()
|
||||
|
||||
return {
|
||||
"supplier_name": supplier.name,
|
||||
"deleted_price_lists": price_lists_count,
|
||||
"deleted_quality_reviews": quality_reviews_count,
|
||||
"deleted_performance_metrics": metrics_count,
|
||||
"deleted_alerts": alerts_count,
|
||||
"deleted_scorecards": scorecards_count,
|
||||
"deletion_timestamp": datetime.utcnow()
|
||||
}
|
||||
|
||||
@@ -170,6 +170,13 @@ class SupplierSummary(BaseModel):
|
||||
phone: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
|
||||
# Business terms - Added for list view
|
||||
payment_terms: PaymentTerms
|
||||
standard_lead_time: int
|
||||
minimum_order_amount: Optional[Decimal] = None
|
||||
|
||||
# Performance metrics
|
||||
quality_rating: Optional[float] = None
|
||||
delivery_rating: Optional[float] = None
|
||||
total_orders: int
|
||||
@@ -180,6 +187,20 @@ class SupplierSummary(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class SupplierDeletionSummary(BaseModel):
|
||||
"""Schema for supplier deletion summary"""
|
||||
supplier_name: str
|
||||
deleted_price_lists: int = 0
|
||||
deleted_quality_reviews: int = 0
|
||||
deleted_performance_metrics: int = 0
|
||||
deleted_alerts: int = 0
|
||||
deleted_scorecards: int = 0
|
||||
deletion_timestamp: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PURCHASE ORDER SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
@@ -31,11 +31,12 @@ class SupplierService:
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
supplier_data: SupplierCreate,
|
||||
created_by: UUID
|
||||
created_by: UUID,
|
||||
created_by_role: str = "member"
|
||||
) -> Supplier:
|
||||
"""Create a new supplier"""
|
||||
logger.info("Creating supplier", tenant_id=str(tenant_id), name=supplier_data.name)
|
||||
|
||||
"""Create a new supplier with role-based auto-approval"""
|
||||
logger.info("Creating supplier", tenant_id=str(tenant_id), name=supplier_data.name, role=created_by_role)
|
||||
|
||||
# Check for duplicate name
|
||||
existing = await self.repository.get_by_name(tenant_id, supplier_data.name)
|
||||
if existing:
|
||||
@@ -50,18 +51,48 @@ class SupplierService:
|
||||
raise ValueError(
|
||||
f"Supplier with code '{supplier_data.supplier_code}' already exists"
|
||||
)
|
||||
|
||||
|
||||
# Generate supplier code if not provided
|
||||
supplier_code = supplier_data.supplier_code
|
||||
if not supplier_code:
|
||||
supplier_code = self._generate_supplier_code(supplier_data.name)
|
||||
|
||||
|
||||
# Fetch tenant supplier settings to determine approval workflow
|
||||
try:
|
||||
from shared.clients.tenant_client import create_tenant_client
|
||||
tenant_client = create_tenant_client(settings)
|
||||
supplier_settings = await tenant_client.get_supplier_settings(str(tenant_id)) or {}
|
||||
except Exception as e:
|
||||
logger.warning("Failed to fetch tenant settings, using defaults", error=str(e))
|
||||
supplier_settings = {}
|
||||
|
||||
# Determine initial status based on settings and role
|
||||
require_approval = supplier_settings.get('require_supplier_approval', True)
|
||||
auto_approve_admin = supplier_settings.get('auto_approve_for_admin_owner', True)
|
||||
|
||||
# Auto-approval logic
|
||||
if not require_approval:
|
||||
# Workflow disabled globally - always auto-approve
|
||||
initial_status = SupplierStatus.active
|
||||
auto_approved = True
|
||||
logger.info("Supplier approval workflow disabled - auto-approving")
|
||||
elif auto_approve_admin and created_by_role.lower() in ['admin', 'owner']:
|
||||
# Auto-approve for admin/owner roles
|
||||
initial_status = SupplierStatus.active
|
||||
auto_approved = True
|
||||
logger.info("Auto-approving supplier created by admin/owner", role=created_by_role)
|
||||
else:
|
||||
# Require approval for other roles
|
||||
initial_status = SupplierStatus.pending_approval
|
||||
auto_approved = False
|
||||
logger.info("Supplier requires approval", role=created_by_role)
|
||||
|
||||
# Create supplier data
|
||||
create_data = supplier_data.model_dump(exclude_unset=True)
|
||||
create_data.update({
|
||||
'tenant_id': tenant_id,
|
||||
'supplier_code': supplier_code,
|
||||
'status': SupplierStatus.pending_approval,
|
||||
'status': initial_status,
|
||||
'created_by': created_by,
|
||||
'updated_by': created_by,
|
||||
'quality_rating': 0.0,
|
||||
@@ -69,16 +100,23 @@ class SupplierService:
|
||||
'total_orders': 0,
|
||||
'total_amount': 0.0
|
||||
})
|
||||
|
||||
|
||||
# Set approval fields if auto-approved
|
||||
if auto_approved:
|
||||
create_data['approved_by'] = created_by
|
||||
create_data['approved_at'] = datetime.utcnow()
|
||||
|
||||
supplier = await self.repository.create(create_data)
|
||||
|
||||
|
||||
logger.info(
|
||||
"Supplier created successfully",
|
||||
tenant_id=str(tenant_id),
|
||||
supplier_id=str(supplier.id),
|
||||
name=supplier.name
|
||||
name=supplier.name,
|
||||
status=initial_status.value,
|
||||
auto_approved=auto_approved
|
||||
)
|
||||
|
||||
|
||||
return supplier
|
||||
|
||||
async def get_supplier(self, supplier_id: UUID) -> Optional[Supplier]:
|
||||
@@ -144,7 +182,28 @@ class SupplierService:
|
||||
|
||||
logger.info("Supplier deleted successfully", supplier_id=str(supplier_id))
|
||||
return True
|
||||
|
||||
|
||||
async def hard_delete_supplier(self, supplier_id: UUID, tenant_id: UUID) -> Dict[str, Any]:
|
||||
"""
|
||||
Hard delete supplier and all associated data (permanent deletion)
|
||||
Returns deletion summary for audit purposes
|
||||
"""
|
||||
logger.info("Hard deleting supplier", supplier_id=str(supplier_id), tenant_id=str(tenant_id))
|
||||
|
||||
# Delegate to repository layer - all DB access is done there
|
||||
deletion_summary = await self.repository.hard_delete_supplier(supplier_id)
|
||||
|
||||
if not deletion_summary:
|
||||
raise ValueError("Supplier not found")
|
||||
|
||||
logger.info(
|
||||
"Supplier hard deleted successfully",
|
||||
supplier_id=str(supplier_id),
|
||||
**deletion_summary
|
||||
)
|
||||
|
||||
return deletion_summary
|
||||
|
||||
async def search_suppliers(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
@@ -184,18 +243,18 @@ class SupplierService:
|
||||
) -> Optional[Supplier]:
|
||||
"""Approve a pending supplier"""
|
||||
logger.info("Approving supplier", supplier_id=str(supplier_id))
|
||||
|
||||
supplier = self.repository.approve_supplier(supplier_id, approved_by)
|
||||
|
||||
supplier = await self.repository.approve_supplier(supplier_id, approved_by)
|
||||
if not supplier:
|
||||
logger.warning("Failed to approve supplier - not found or not pending")
|
||||
return None
|
||||
|
||||
|
||||
if notes:
|
||||
self.repository.update(supplier_id, {
|
||||
await self.repository.update(supplier_id, {
|
||||
'notes': (supplier.notes or "") + f"\nApproval notes: {notes}",
|
||||
'updated_at': datetime.utcnow()
|
||||
})
|
||||
|
||||
|
||||
logger.info("Supplier approved successfully", supplier_id=str(supplier_id))
|
||||
return supplier
|
||||
|
||||
@@ -207,14 +266,14 @@ class SupplierService:
|
||||
) -> Optional[Supplier]:
|
||||
"""Reject a pending supplier"""
|
||||
logger.info("Rejecting supplier", supplier_id=str(supplier_id))
|
||||
|
||||
supplier = self.repository.reject_supplier(
|
||||
|
||||
supplier = await self.repository.reject_supplier(
|
||||
supplier_id, rejection_reason, rejected_by
|
||||
)
|
||||
if not supplier:
|
||||
logger.warning("Failed to reject supplier - not found or not pending")
|
||||
return None
|
||||
|
||||
|
||||
logger.info("Supplier rejected successfully", supplier_id=str(supplier_id))
|
||||
return supplier
|
||||
|
||||
@@ -318,4 +377,4 @@ class SupplierService:
|
||||
if min_order is not None and min_order < 0:
|
||||
errors['minimum_order_amount'] = "Minimum order amount cannot be negative"
|
||||
|
||||
return errors
|
||||
return errors
|
||||
|
||||
Reference in New Issue
Block a user