Improve the frontend modals

This commit is contained in:
Urtzi Alfaro
2025-10-27 16:33:26 +01:00
parent 61376b7a9f
commit 858d985c92
143 changed files with 9289 additions and 2306 deletions

View File

@@ -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))

View File

@@ -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:

View File

@@ -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}

View File

@@ -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(

View File

@@ -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)

View File

@@ -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()
}

View File

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

View File

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