455 lines
16 KiB
Python
455 lines
16 KiB
Python
# services/suppliers/app/repositories/supplier_repository.py
|
|
"""
|
|
Supplier repository for database operations
|
|
"""
|
|
|
|
from typing import List, Optional, Dict, Any
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import and_, or_, func, select
|
|
from uuid import UUID
|
|
from datetime import datetime
|
|
|
|
from app.models.suppliers import Supplier, SupplierStatus, SupplierType
|
|
from app.repositories.base import BaseRepository
|
|
|
|
|
|
class SupplierRepository(BaseRepository[Supplier]):
|
|
"""Repository for supplier management operations"""
|
|
|
|
def __init__(self, db: AsyncSession):
|
|
super().__init__(Supplier, db)
|
|
|
|
async def get_by_name(self, tenant_id: UUID, name: str) -> Optional[Supplier]:
|
|
"""Get supplier by name within tenant"""
|
|
stmt = select(self.model).filter(
|
|
and_(
|
|
self.model.tenant_id == tenant_id,
|
|
self.model.name == name
|
|
)
|
|
)
|
|
result = await self.db.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def get_by_supplier_code(self, tenant_id: UUID, supplier_code: str) -> Optional[Supplier]:
|
|
"""Get supplier by supplier code within tenant"""
|
|
stmt = select(self.model).filter(
|
|
and_(
|
|
self.model.tenant_id == tenant_id,
|
|
self.model.supplier_code == supplier_code
|
|
)
|
|
)
|
|
result = await self.db.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def search_suppliers(
|
|
self,
|
|
tenant_id: UUID,
|
|
search_term: Optional[str] = None,
|
|
supplier_type: Optional[SupplierType] = None,
|
|
status: Optional[SupplierStatus] = None,
|
|
limit: int = 50,
|
|
offset: int = 0
|
|
) -> List[Supplier]:
|
|
"""Search suppliers with filters"""
|
|
stmt = select(self.model).filter(self.model.tenant_id == tenant_id)
|
|
|
|
# Search term filter (name, contact person, email)
|
|
if search_term:
|
|
search_filter = or_(
|
|
self.model.name.ilike(f"%{search_term}%"),
|
|
self.model.contact_person.ilike(f"%{search_term}%"),
|
|
self.model.email.ilike(f"%{search_term}%")
|
|
)
|
|
stmt = stmt.filter(search_filter)
|
|
|
|
# Type filter
|
|
if supplier_type:
|
|
stmt = stmt.filter(self.model.supplier_type == supplier_type)
|
|
|
|
# Status filter
|
|
if status:
|
|
stmt = stmt.filter(self.model.status == status)
|
|
|
|
stmt = stmt.order_by(self.model.name).limit(limit).offset(offset)
|
|
result = await self.db.execute(stmt)
|
|
return result.scalars().all()
|
|
|
|
async def get_active_suppliers(self, tenant_id: UUID) -> List[Supplier]:
|
|
"""Get all active suppliers for a tenant"""
|
|
stmt = select(self.model).filter(
|
|
and_(
|
|
self.model.tenant_id == tenant_id,
|
|
self.model.status == SupplierStatus.active
|
|
)
|
|
).order_by(self.model.name)
|
|
result = await self.db.execute(stmt)
|
|
return result.scalars().all()
|
|
|
|
async def get_suppliers_by_ids(self, tenant_id: UUID, supplier_ids: List[UUID]) -> List[Supplier]:
|
|
"""Get multiple suppliers by IDs in a single query (batch fetch)"""
|
|
if not supplier_ids:
|
|
return []
|
|
|
|
stmt = select(self.model).filter(
|
|
and_(
|
|
self.model.tenant_id == tenant_id,
|
|
self.model.id.in_(supplier_ids)
|
|
)
|
|
).order_by(self.model.name)
|
|
result = await self.db.execute(stmt)
|
|
return result.scalars().all()
|
|
|
|
def get_suppliers_by_type(
|
|
self,
|
|
tenant_id: UUID,
|
|
supplier_type: SupplierType
|
|
) -> List[Supplier]:
|
|
"""Get suppliers by type"""
|
|
return (
|
|
self.db.query(self.model)
|
|
.filter(
|
|
and_(
|
|
self.model.tenant_id == tenant_id,
|
|
self.model.supplier_type == supplier_type,
|
|
self.model.status == SupplierStatus.active
|
|
)
|
|
)
|
|
.order_by(self.model.quality_rating.desc(), self.model.name)
|
|
.all()
|
|
)
|
|
|
|
def get_top_suppliers(
|
|
self,
|
|
tenant_id: UUID,
|
|
limit: int = 10
|
|
) -> List[Supplier]:
|
|
"""Get top suppliers by quality rating and order value"""
|
|
return (
|
|
self.db.query(self.model)
|
|
.filter(
|
|
and_(
|
|
self.model.tenant_id == tenant_id,
|
|
self.model.status == SupplierStatus.active
|
|
)
|
|
)
|
|
.order_by(
|
|
self.model.quality_rating.desc(),
|
|
self.model.total_amount.desc()
|
|
)
|
|
.limit(limit)
|
|
.all()
|
|
)
|
|
|
|
def update_supplier_stats(
|
|
self,
|
|
supplier_id: UUID,
|
|
total_orders_increment: int = 0,
|
|
total_amount_increment: float = 0.0,
|
|
new_quality_rating: Optional[float] = None,
|
|
new_delivery_rating: Optional[float] = None
|
|
) -> Optional[Supplier]:
|
|
"""Update supplier performance statistics"""
|
|
supplier = self.get_by_id(supplier_id)
|
|
if not supplier:
|
|
return None
|
|
|
|
# Update counters
|
|
if total_orders_increment:
|
|
supplier.total_orders += total_orders_increment
|
|
|
|
if total_amount_increment:
|
|
supplier.total_amount += total_amount_increment
|
|
|
|
# Update ratings (these should be calculated averages)
|
|
if new_quality_rating is not None:
|
|
supplier.quality_rating = new_quality_rating
|
|
|
|
if new_delivery_rating is not None:
|
|
supplier.delivery_rating = new_delivery_rating
|
|
|
|
supplier.updated_at = datetime.utcnow()
|
|
|
|
self.db.commit()
|
|
self.db.refresh(supplier)
|
|
return supplier
|
|
|
|
def get_suppliers_needing_review(
|
|
self,
|
|
tenant_id: UUID,
|
|
days_since_last_order: int = 30
|
|
) -> List[Supplier]:
|
|
"""Get suppliers that may need performance review"""
|
|
from datetime import datetime, timedelta
|
|
cutoff_date = datetime.utcnow() - timedelta(days=days_since_last_order)
|
|
|
|
return (
|
|
self.db.query(self.model)
|
|
.filter(
|
|
and_(
|
|
self.model.tenant_id == tenant_id,
|
|
self.model.status == SupplierStatus.active,
|
|
or_(
|
|
self.model.quality_rating < 3.0, # Poor rating
|
|
self.model.delivery_rating < 3.0, # Poor delivery
|
|
self.model.updated_at < cutoff_date # Long time since interaction
|
|
)
|
|
)
|
|
)
|
|
.order_by(self.model.quality_rating.asc())
|
|
.all()
|
|
)
|
|
|
|
async def get_supplier_statistics(self, tenant_id: UUID) -> Dict[str, Any]:
|
|
"""Get supplier statistics for dashboard"""
|
|
total_suppliers = await self.count_by_tenant(tenant_id)
|
|
|
|
# Get all suppliers for this tenant to avoid multiple queries and enum casting issues
|
|
all_stmt = select(self.model).filter(self.model.tenant_id == tenant_id)
|
|
all_result = await self.db.execute(all_stmt)
|
|
all_suppliers = all_result.scalars().all()
|
|
|
|
# Calculate statistics in Python to avoid database enum casting issues
|
|
active_suppliers = [s for s in all_suppliers if s.status == SupplierStatus.active]
|
|
pending_suppliers = [s for s in all_suppliers if s.status == SupplierStatus.pending_approval]
|
|
|
|
# Calculate averages from active suppliers
|
|
quality_ratings = [s.quality_rating for s in active_suppliers if s.quality_rating and s.quality_rating > 0]
|
|
avg_quality_rating = sum(quality_ratings) / len(quality_ratings) if quality_ratings else 0.0
|
|
|
|
delivery_ratings = [s.delivery_rating for s in active_suppliers if s.delivery_rating and s.delivery_rating > 0]
|
|
avg_delivery_rating = sum(delivery_ratings) / len(delivery_ratings) if delivery_ratings else 0.0
|
|
|
|
# Total spend for all suppliers
|
|
total_spend = sum(float(s.total_amount or 0) for s in all_suppliers)
|
|
|
|
return {
|
|
"total_suppliers": total_suppliers,
|
|
"active_suppliers": len(active_suppliers),
|
|
"pending_suppliers": len(pending_suppliers),
|
|
"avg_quality_rating": round(float(avg_quality_rating), 2),
|
|
"avg_delivery_rating": round(float(avg_delivery_rating), 2),
|
|
"total_spend": float(total_spend)
|
|
}
|
|
|
|
async def approve_supplier(
|
|
self,
|
|
supplier_id: UUID,
|
|
approved_by: UUID,
|
|
approval_date: Optional[datetime] = None
|
|
) -> Optional[Supplier]:
|
|
"""Approve a pending supplier"""
|
|
supplier = await self.get_by_id(supplier_id)
|
|
if not supplier or supplier.status != SupplierStatus.pending_approval:
|
|
return None
|
|
|
|
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()
|
|
|
|
await self.db.commit()
|
|
await self.db.refresh(supplier)
|
|
return supplier
|
|
|
|
async def reject_supplier(
|
|
self,
|
|
supplier_id: UUID,
|
|
rejection_reason: str,
|
|
approved_by: UUID
|
|
) -> Optional[Supplier]:
|
|
"""Reject a pending supplier"""
|
|
supplier = await self.get_by_id(supplier_id)
|
|
if not supplier or supplier.status != SupplierStatus.pending_approval:
|
|
return None
|
|
|
|
supplier.status = SupplierStatus.inactive
|
|
supplier.rejection_reason = rejection_reason
|
|
supplier.approved_by = approved_by
|
|
supplier.approved_at = datetime.utcnow()
|
|
supplier.updated_at = datetime.utcnow()
|
|
|
|
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()
|
|
}
|
|
|
|
async def get_supplier_price_lists(
|
|
self,
|
|
supplier_id: UUID,
|
|
tenant_id: UUID,
|
|
is_active: bool = True
|
|
) -> List[Any]:
|
|
"""Get all price list items for a supplier"""
|
|
from app.models.suppliers import SupplierPriceList
|
|
|
|
stmt = select(SupplierPriceList).filter(
|
|
and_(
|
|
SupplierPriceList.supplier_id == supplier_id,
|
|
SupplierPriceList.tenant_id == tenant_id
|
|
)
|
|
)
|
|
|
|
if is_active:
|
|
stmt = stmt.filter(SupplierPriceList.is_active == True)
|
|
|
|
result = await self.db.execute(stmt)
|
|
return result.scalars().all()
|
|
|
|
async def get_supplier_price_list(
|
|
self,
|
|
price_list_id: UUID,
|
|
tenant_id: UUID
|
|
) -> Optional[Any]:
|
|
"""Get specific price list item"""
|
|
from app.models.suppliers import SupplierPriceList
|
|
|
|
stmt = select(SupplierPriceList).filter(
|
|
and_(
|
|
SupplierPriceList.id == price_list_id,
|
|
SupplierPriceList.tenant_id == tenant_id
|
|
)
|
|
)
|
|
result = await self.db.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def create_supplier_price_list(
|
|
self,
|
|
create_data: Dict[str, Any]
|
|
) -> Any:
|
|
"""Create a new price list item"""
|
|
from app.models.suppliers import SupplierPriceList
|
|
|
|
price_list = SupplierPriceList(**create_data)
|
|
self.db.add(price_list)
|
|
await self.db.commit()
|
|
await self.db.refresh(price_list)
|
|
return price_list
|
|
|
|
async def update_supplier_price_list(
|
|
self,
|
|
price_list_id: UUID,
|
|
update_data: Dict[str, Any]
|
|
) -> Any:
|
|
"""Update a price list item"""
|
|
from app.models.suppliers import SupplierPriceList
|
|
|
|
stmt = select(SupplierPriceList).filter(SupplierPriceList.id == price_list_id)
|
|
result = await self.db.execute(stmt)
|
|
price_list = result.scalar_one_or_none()
|
|
|
|
if not price_list:
|
|
raise ValueError("Price list item not found")
|
|
|
|
# Update fields
|
|
for key, value in update_data.items():
|
|
if hasattr(price_list, key):
|
|
setattr(price_list, key, value)
|
|
|
|
await self.db.commit()
|
|
await self.db.refresh(price_list)
|
|
return price_list
|
|
|
|
async def delete_supplier_price_list(
|
|
self,
|
|
price_list_id: UUID
|
|
) -> bool:
|
|
"""Delete a price list item"""
|
|
from app.models.suppliers import SupplierPriceList
|
|
from sqlalchemy import delete
|
|
|
|
stmt = delete(SupplierPriceList).filter(SupplierPriceList.id == price_list_id)
|
|
result = await self.db.execute(stmt)
|
|
|
|
await self.db.commit()
|
|
return result.rowcount > 0
|