Create new services: inventory, recipes, suppliers
This commit is contained in:
1
services/suppliers/app/repositories/__init__.py
Normal file
1
services/suppliers/app/repositories/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# services/suppliers/app/repositories/__init__.py
|
||||
96
services/suppliers/app/repositories/base.py
Normal file
96
services/suppliers/app/repositories/base.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# services/suppliers/app/repositories/base.py
|
||||
"""
|
||||
Base repository class for common database operations
|
||||
"""
|
||||
|
||||
from typing import TypeVar, Generic, List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, asc
|
||||
from uuid import UUID
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class BaseRepository(Generic[T]):
|
||||
"""Base repository with common CRUD operations"""
|
||||
|
||||
def __init__(self, model: type, db: Session):
|
||||
self.model = model
|
||||
self.db = db
|
||||
|
||||
def create(self, obj_data: Dict[str, Any]) -> T:
|
||||
"""Create a new record"""
|
||||
db_obj = self.model(**obj_data)
|
||||
self.db.add(db_obj)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def get_by_id(self, record_id: UUID) -> Optional[T]:
|
||||
"""Get record by ID"""
|
||||
return self.db.query(self.model).filter(self.model.id == record_id).first()
|
||||
|
||||
def get_by_tenant_id(self, tenant_id: UUID, limit: int = 100, offset: int = 0) -> List[T]:
|
||||
"""Get records by tenant ID with pagination"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(self.model.tenant_id == tenant_id)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all()
|
||||
)
|
||||
|
||||
def update(self, record_id: UUID, update_data: Dict[str, Any]) -> Optional[T]:
|
||||
"""Update record by ID"""
|
||||
db_obj = self.get_by_id(record_id)
|
||||
if db_obj:
|
||||
for key, value in update_data.items():
|
||||
if hasattr(db_obj, key):
|
||||
setattr(db_obj, key, value)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def delete(self, record_id: UUID) -> bool:
|
||||
"""Delete record by ID"""
|
||||
db_obj = self.get_by_id(record_id)
|
||||
if db_obj:
|
||||
self.db.delete(db_obj)
|
||||
self.db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def count_by_tenant(self, tenant_id: UUID) -> int:
|
||||
"""Count records by tenant"""
|
||||
return self.db.query(self.model).filter(self.model.tenant_id == tenant_id).count()
|
||||
|
||||
def list_with_filters(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
sort_by: str = "created_at",
|
||||
sort_order: str = "desc",
|
||||
limit: int = 100,
|
||||
offset: int = 0
|
||||
) -> List[T]:
|
||||
"""List records with filtering and sorting"""
|
||||
query = self.db.query(self.model).filter(self.model.tenant_id == tenant_id)
|
||||
|
||||
# Apply filters
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
if hasattr(self.model, key) and value is not None:
|
||||
query = query.filter(getattr(self.model, key) == value)
|
||||
|
||||
# Apply sorting
|
||||
if hasattr(self.model, sort_by):
|
||||
if sort_order.lower() == "desc":
|
||||
query = query.order_by(desc(getattr(self.model, sort_by)))
|
||||
else:
|
||||
query = query.order_by(asc(getattr(self.model, sort_by)))
|
||||
|
||||
return query.limit(limit).offset(offset).all()
|
||||
|
||||
def exists(self, record_id: UUID) -> bool:
|
||||
"""Check if record exists"""
|
||||
return self.db.query(self.model).filter(self.model.id == record_id).first() is not None
|
||||
414
services/suppliers/app/repositories/delivery_repository.py
Normal file
414
services/suppliers/app/repositories/delivery_repository.py
Normal file
@@ -0,0 +1,414 @@
|
||||
# services/suppliers/app/repositories/delivery_repository.py
|
||||
"""
|
||||
Delivery repository for database operations
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import and_, or_, func, desc
|
||||
from uuid import UUID
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.models.suppliers import (
|
||||
Delivery, DeliveryItem, DeliveryStatus,
|
||||
PurchaseOrder, Supplier
|
||||
)
|
||||
from app.repositories.base import BaseRepository
|
||||
|
||||
|
||||
class DeliveryRepository(BaseRepository[Delivery]):
|
||||
"""Repository for delivery tracking operations"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
super().__init__(Delivery, db)
|
||||
|
||||
def get_by_delivery_number(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
delivery_number: str
|
||||
) -> Optional[Delivery]:
|
||||
"""Get delivery by delivery number within tenant"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.delivery_number == delivery_number
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_with_items(self, delivery_id: UUID) -> Optional[Delivery]:
|
||||
"""Get delivery with all items loaded"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.options(
|
||||
joinedload(self.model.items),
|
||||
joinedload(self.model.purchase_order),
|
||||
joinedload(self.model.supplier)
|
||||
)
|
||||
.filter(self.model.id == delivery_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_by_purchase_order(self, po_id: UUID) -> List[Delivery]:
|
||||
"""Get all deliveries for a purchase order"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.options(joinedload(self.model.items))
|
||||
.filter(self.model.purchase_order_id == po_id)
|
||||
.order_by(self.model.scheduled_date.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
def search_deliveries(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
supplier_id: Optional[UUID] = None,
|
||||
status: Optional[DeliveryStatus] = None,
|
||||
date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None,
|
||||
search_term: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> List[Delivery]:
|
||||
"""Search deliveries with comprehensive filters"""
|
||||
query = (
|
||||
self.db.query(self.model)
|
||||
.options(
|
||||
joinedload(self.model.supplier),
|
||||
joinedload(self.model.purchase_order)
|
||||
)
|
||||
.filter(self.model.tenant_id == tenant_id)
|
||||
)
|
||||
|
||||
# Supplier filter
|
||||
if supplier_id:
|
||||
query = query.filter(self.model.supplier_id == supplier_id)
|
||||
|
||||
# Status filter
|
||||
if status:
|
||||
query = query.filter(self.model.status == status)
|
||||
|
||||
# Date range filter (scheduled date)
|
||||
if date_from:
|
||||
query = query.filter(self.model.scheduled_date >= date_from)
|
||||
if date_to:
|
||||
query = query.filter(self.model.scheduled_date <= date_to)
|
||||
|
||||
# Search term filter
|
||||
if search_term:
|
||||
search_filter = or_(
|
||||
self.model.delivery_number.ilike(f"%{search_term}%"),
|
||||
self.model.supplier_delivery_note.ilike(f"%{search_term}%"),
|
||||
self.model.tracking_number.ilike(f"%{search_term}%"),
|
||||
self.model.purchase_order.has(PurchaseOrder.po_number.ilike(f"%{search_term}%"))
|
||||
)
|
||||
query = query.filter(search_filter)
|
||||
|
||||
return (
|
||||
query.order_by(desc(self.model.scheduled_date))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_scheduled_deliveries(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None
|
||||
) -> List[Delivery]:
|
||||
"""Get scheduled deliveries for a date range"""
|
||||
if not date_from:
|
||||
date_from = datetime.utcnow()
|
||||
if not date_to:
|
||||
date_to = date_from + timedelta(days=7) # Next week
|
||||
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.options(
|
||||
joinedload(self.model.supplier),
|
||||
joinedload(self.model.purchase_order)
|
||||
)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status.in_([
|
||||
DeliveryStatus.SCHEDULED,
|
||||
DeliveryStatus.IN_TRANSIT,
|
||||
DeliveryStatus.OUT_FOR_DELIVERY
|
||||
]),
|
||||
self.model.scheduled_date >= date_from,
|
||||
self.model.scheduled_date <= date_to
|
||||
)
|
||||
)
|
||||
.order_by(self.model.scheduled_date)
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_todays_deliveries(self, tenant_id: UUID) -> List[Delivery]:
|
||||
"""Get deliveries scheduled for today"""
|
||||
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_end = today_start + timedelta(days=1)
|
||||
|
||||
return self.get_scheduled_deliveries(tenant_id, today_start, today_end)
|
||||
|
||||
def get_overdue_deliveries(self, tenant_id: UUID) -> List[Delivery]:
|
||||
"""Get deliveries that are overdue (scheduled in the past but not completed)"""
|
||||
now = datetime.utcnow()
|
||||
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.options(
|
||||
joinedload(self.model.supplier),
|
||||
joinedload(self.model.purchase_order)
|
||||
)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status.in_([
|
||||
DeliveryStatus.SCHEDULED,
|
||||
DeliveryStatus.IN_TRANSIT,
|
||||
DeliveryStatus.OUT_FOR_DELIVERY
|
||||
]),
|
||||
self.model.scheduled_date < now
|
||||
)
|
||||
)
|
||||
.order_by(self.model.scheduled_date)
|
||||
.all()
|
||||
)
|
||||
|
||||
def generate_delivery_number(self, tenant_id: UUID) -> str:
|
||||
"""Generate next delivery number for tenant"""
|
||||
# Get current date
|
||||
today = datetime.utcnow()
|
||||
date_prefix = f"DEL{today.strftime('%Y%m%d')}"
|
||||
|
||||
# Find highest delivery number for today
|
||||
latest_delivery = (
|
||||
self.db.query(self.model.delivery_number)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.delivery_number.like(f"{date_prefix}%")
|
||||
)
|
||||
)
|
||||
.order_by(self.model.delivery_number.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
if latest_delivery:
|
||||
try:
|
||||
last_number = int(latest_delivery.delivery_number.replace(date_prefix, ""))
|
||||
new_number = last_number + 1
|
||||
except ValueError:
|
||||
new_number = 1
|
||||
else:
|
||||
new_number = 1
|
||||
|
||||
return f"{date_prefix}{new_number:03d}"
|
||||
|
||||
def update_delivery_status(
|
||||
self,
|
||||
delivery_id: UUID,
|
||||
status: DeliveryStatus,
|
||||
updated_by: UUID,
|
||||
notes: Optional[str] = None,
|
||||
update_timestamps: bool = True
|
||||
) -> Optional[Delivery]:
|
||||
"""Update delivery status with appropriate timestamps"""
|
||||
delivery = self.get_by_id(delivery_id)
|
||||
if not delivery:
|
||||
return None
|
||||
|
||||
old_status = delivery.status
|
||||
delivery.status = status
|
||||
delivery.created_by = updated_by # Track who updated
|
||||
|
||||
if update_timestamps:
|
||||
now = datetime.utcnow()
|
||||
|
||||
if status == DeliveryStatus.IN_TRANSIT and not delivery.estimated_arrival:
|
||||
# Set estimated arrival if not already set
|
||||
delivery.estimated_arrival = now + timedelta(hours=4) # Default 4 hours
|
||||
elif status == DeliveryStatus.DELIVERED:
|
||||
delivery.actual_arrival = now
|
||||
delivery.completed_at = now
|
||||
elif status == DeliveryStatus.PARTIALLY_DELIVERED:
|
||||
delivery.actual_arrival = now
|
||||
elif status == DeliveryStatus.FAILED_DELIVERY:
|
||||
delivery.actual_arrival = now
|
||||
|
||||
# Add status change note
|
||||
if notes:
|
||||
existing_notes = delivery.notes or ""
|
||||
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M")
|
||||
status_note = f"[{timestamp}] Status: {old_status.value} → {status.value}: {notes}"
|
||||
delivery.notes = f"{existing_notes}\n{status_note}".strip()
|
||||
|
||||
delivery.updated_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(delivery)
|
||||
return delivery
|
||||
|
||||
def mark_as_received(
|
||||
self,
|
||||
delivery_id: UUID,
|
||||
received_by: UUID,
|
||||
inspection_passed: bool = True,
|
||||
inspection_notes: Optional[str] = None,
|
||||
quality_issues: Optional[Dict[str, Any]] = None
|
||||
) -> Optional[Delivery]:
|
||||
"""Mark delivery as received with inspection details"""
|
||||
delivery = self.get_by_id(delivery_id)
|
||||
if not delivery:
|
||||
return None
|
||||
|
||||
delivery.status = DeliveryStatus.DELIVERED
|
||||
delivery.received_by = received_by
|
||||
delivery.received_at = datetime.utcnow()
|
||||
delivery.completed_at = datetime.utcnow()
|
||||
delivery.inspection_passed = inspection_passed
|
||||
|
||||
if inspection_notes:
|
||||
delivery.inspection_notes = inspection_notes
|
||||
|
||||
if quality_issues:
|
||||
delivery.quality_issues = quality_issues
|
||||
|
||||
if not delivery.actual_arrival:
|
||||
delivery.actual_arrival = datetime.utcnow()
|
||||
|
||||
delivery.updated_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(delivery)
|
||||
return delivery
|
||||
|
||||
def get_delivery_performance_stats(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
days_back: int = 30,
|
||||
supplier_id: Optional[UUID] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get delivery performance statistics"""
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days_back)
|
||||
|
||||
query = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.created_at >= cutoff_date
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if supplier_id:
|
||||
query = query.filter(self.model.supplier_id == supplier_id)
|
||||
|
||||
deliveries = query.all()
|
||||
|
||||
if not deliveries:
|
||||
return {
|
||||
"total_deliveries": 0,
|
||||
"on_time_deliveries": 0,
|
||||
"late_deliveries": 0,
|
||||
"failed_deliveries": 0,
|
||||
"on_time_percentage": 0.0,
|
||||
"avg_delay_hours": 0.0,
|
||||
"quality_pass_rate": 0.0
|
||||
}
|
||||
|
||||
total_count = len(deliveries)
|
||||
on_time_count = 0
|
||||
late_count = 0
|
||||
failed_count = 0
|
||||
total_delay_hours = 0
|
||||
quality_pass_count = 0
|
||||
quality_total = 0
|
||||
|
||||
for delivery in deliveries:
|
||||
if delivery.status == DeliveryStatus.FAILED_DELIVERY:
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
# Check if delivery was on time
|
||||
if delivery.scheduled_date and delivery.actual_arrival:
|
||||
if delivery.actual_arrival <= delivery.scheduled_date:
|
||||
on_time_count += 1
|
||||
else:
|
||||
late_count += 1
|
||||
delay = delivery.actual_arrival - delivery.scheduled_date
|
||||
total_delay_hours += delay.total_seconds() / 3600
|
||||
|
||||
# Check quality inspection
|
||||
if delivery.inspection_passed is not None:
|
||||
quality_total += 1
|
||||
if delivery.inspection_passed:
|
||||
quality_pass_count += 1
|
||||
|
||||
on_time_percentage = (on_time_count / total_count * 100) if total_count > 0 else 0
|
||||
avg_delay_hours = total_delay_hours / late_count if late_count > 0 else 0
|
||||
quality_pass_rate = (quality_pass_count / quality_total * 100) if quality_total > 0 else 0
|
||||
|
||||
return {
|
||||
"total_deliveries": total_count,
|
||||
"on_time_deliveries": on_time_count,
|
||||
"late_deliveries": late_count,
|
||||
"failed_deliveries": failed_count,
|
||||
"on_time_percentage": round(on_time_percentage, 1),
|
||||
"avg_delay_hours": round(avg_delay_hours, 1),
|
||||
"quality_pass_rate": round(quality_pass_rate, 1)
|
||||
}
|
||||
|
||||
def get_upcoming_deliveries_summary(self, tenant_id: UUID) -> Dict[str, Any]:
|
||||
"""Get summary of upcoming deliveries"""
|
||||
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
next_week = today + timedelta(days=7)
|
||||
|
||||
# Today's deliveries
|
||||
todays_deliveries = len(self.get_todays_deliveries(tenant_id))
|
||||
|
||||
# This week's deliveries
|
||||
this_week_deliveries = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.scheduled_date >= today,
|
||||
self.model.scheduled_date <= next_week,
|
||||
self.model.status.in_([
|
||||
DeliveryStatus.SCHEDULED,
|
||||
DeliveryStatus.IN_TRANSIT,
|
||||
DeliveryStatus.OUT_FOR_DELIVERY
|
||||
])
|
||||
)
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Overdue deliveries
|
||||
overdue_count = len(self.get_overdue_deliveries(tenant_id))
|
||||
|
||||
# In transit deliveries
|
||||
in_transit_count = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status == DeliveryStatus.IN_TRANSIT
|
||||
)
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
return {
|
||||
"todays_deliveries": todays_deliveries,
|
||||
"this_week_deliveries": this_week_deliveries,
|
||||
"overdue_deliveries": overdue_count,
|
||||
"in_transit_deliveries": in_transit_count
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
# services/suppliers/app/repositories/purchase_order_item_repository.py
|
||||
"""
|
||||
Purchase Order Item repository for database operations
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, func
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
|
||||
from app.models.suppliers import PurchaseOrderItem
|
||||
from app.repositories.base import BaseRepository
|
||||
|
||||
|
||||
class PurchaseOrderItemRepository(BaseRepository[PurchaseOrderItem]):
|
||||
"""Repository for purchase order item operations"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
super().__init__(PurchaseOrderItem, db)
|
||||
|
||||
def get_by_purchase_order(self, po_id: UUID) -> List[PurchaseOrderItem]:
|
||||
"""Get all items for a purchase order"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(self.model.purchase_order_id == po_id)
|
||||
.order_by(self.model.created_at)
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_by_ingredient(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
ingredient_id: UUID,
|
||||
limit: int = 20
|
||||
) -> List[PurchaseOrderItem]:
|
||||
"""Get recent order items for a specific ingredient"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.ingredient_id == ingredient_id
|
||||
)
|
||||
)
|
||||
.order_by(self.model.created_at.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
def update_received_quantity(
|
||||
self,
|
||||
item_id: UUID,
|
||||
received_quantity: int,
|
||||
update_remaining: bool = True
|
||||
) -> Optional[PurchaseOrderItem]:
|
||||
"""Update received quantity for an item"""
|
||||
item = self.get_by_id(item_id)
|
||||
if not item:
|
||||
return None
|
||||
|
||||
item.received_quantity = max(0, received_quantity)
|
||||
|
||||
if update_remaining:
|
||||
item.remaining_quantity = max(0, item.ordered_quantity - item.received_quantity)
|
||||
|
||||
item.updated_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def add_received_quantity(
|
||||
self,
|
||||
item_id: UUID,
|
||||
quantity_to_add: int
|
||||
) -> Optional[PurchaseOrderItem]:
|
||||
"""Add to received quantity (for partial deliveries)"""
|
||||
item = self.get_by_id(item_id)
|
||||
if not item:
|
||||
return None
|
||||
|
||||
new_received = item.received_quantity + quantity_to_add
|
||||
new_received = min(new_received, item.ordered_quantity) # Cap at ordered quantity
|
||||
|
||||
return self.update_received_quantity(item_id, new_received)
|
||||
|
||||
def get_partially_received_items(self, tenant_id: UUID) -> List[PurchaseOrderItem]:
|
||||
"""Get items that are partially received (have remaining quantity)"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.received_quantity > 0,
|
||||
self.model.remaining_quantity > 0
|
||||
)
|
||||
)
|
||||
.order_by(self.model.updated_at.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_pending_receipt_items(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
ingredient_id: Optional[UUID] = None
|
||||
) -> List[PurchaseOrderItem]:
|
||||
"""Get items pending receipt (not yet delivered)"""
|
||||
query = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.remaining_quantity > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if ingredient_id:
|
||||
query = query.filter(self.model.ingredient_id == ingredient_id)
|
||||
|
||||
return query.order_by(self.model.created_at).all()
|
||||
|
||||
def calculate_line_total(self, item_id: UUID) -> Optional[PurchaseOrderItem]:
|
||||
"""Recalculate line total for an item"""
|
||||
item = self.get_by_id(item_id)
|
||||
if not item:
|
||||
return None
|
||||
|
||||
item.line_total = item.ordered_quantity * item.unit_price
|
||||
item.updated_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(item)
|
||||
return item
|
||||
|
||||
def get_ingredient_purchase_history(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
ingredient_id: UUID,
|
||||
days_back: int = 90
|
||||
) -> Dict[str, Any]:
|
||||
"""Get purchase history and analytics for an ingredient"""
|
||||
from datetime import timedelta
|
||||
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days_back)
|
||||
|
||||
# Get items within date range
|
||||
items = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.ingredient_id == ingredient_id,
|
||||
self.model.created_at >= cutoff_date
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
if not items:
|
||||
return {
|
||||
"total_quantity_ordered": 0,
|
||||
"total_amount_spent": 0.0,
|
||||
"average_unit_price": 0.0,
|
||||
"order_count": 0,
|
||||
"last_order_date": None,
|
||||
"price_trend": "stable"
|
||||
}
|
||||
|
||||
# Calculate statistics
|
||||
total_quantity = sum(item.ordered_quantity for item in items)
|
||||
total_amount = sum(float(item.line_total) for item in items)
|
||||
order_count = len(items)
|
||||
avg_unit_price = total_amount / total_quantity if total_quantity > 0 else 0
|
||||
last_order_date = max(item.created_at for item in items)
|
||||
|
||||
# Price trend analysis (simple)
|
||||
if order_count >= 2:
|
||||
sorted_items = sorted(items, key=lambda x: x.created_at)
|
||||
first_half = sorted_items[:order_count//2]
|
||||
second_half = sorted_items[order_count//2:]
|
||||
|
||||
avg_price_first = sum(float(item.unit_price) for item in first_half) / len(first_half)
|
||||
avg_price_second = sum(float(item.unit_price) for item in second_half) / len(second_half)
|
||||
|
||||
if avg_price_second > avg_price_first * 1.1:
|
||||
price_trend = "increasing"
|
||||
elif avg_price_second < avg_price_first * 0.9:
|
||||
price_trend = "decreasing"
|
||||
else:
|
||||
price_trend = "stable"
|
||||
else:
|
||||
price_trend = "insufficient_data"
|
||||
|
||||
return {
|
||||
"total_quantity_ordered": total_quantity,
|
||||
"total_amount_spent": round(total_amount, 2),
|
||||
"average_unit_price": round(avg_unit_price, 4),
|
||||
"order_count": order_count,
|
||||
"last_order_date": last_order_date,
|
||||
"price_trend": price_trend
|
||||
}
|
||||
|
||||
def get_top_purchased_ingredients(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
days_back: int = 30,
|
||||
limit: int = 10
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get most purchased ingredients by quantity or value"""
|
||||
from datetime import timedelta
|
||||
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days_back)
|
||||
|
||||
# Group by ingredient and calculate totals
|
||||
results = (
|
||||
self.db.query(
|
||||
self.model.ingredient_id,
|
||||
self.model.product_name,
|
||||
self.model.unit_of_measure,
|
||||
func.sum(self.model.ordered_quantity).label('total_quantity'),
|
||||
func.sum(self.model.line_total).label('total_amount'),
|
||||
func.count(self.model.id).label('order_count'),
|
||||
func.avg(self.model.unit_price).label('avg_unit_price')
|
||||
)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.created_at >= cutoff_date
|
||||
)
|
||||
)
|
||||
.group_by(
|
||||
self.model.ingredient_id,
|
||||
self.model.product_name,
|
||||
self.model.unit_of_measure
|
||||
)
|
||||
.order_by(func.sum(self.model.line_total).desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"ingredient_id": str(row.ingredient_id),
|
||||
"product_name": row.product_name,
|
||||
"unit_of_measure": row.unit_of_measure,
|
||||
"total_quantity": int(row.total_quantity),
|
||||
"total_amount": round(float(row.total_amount), 2),
|
||||
"order_count": int(row.order_count),
|
||||
"avg_unit_price": round(float(row.avg_unit_price), 4)
|
||||
}
|
||||
for row in results
|
||||
]
|
||||
|
||||
def bulk_update_items(
|
||||
self,
|
||||
po_id: UUID,
|
||||
item_updates: List[Dict[str, Any]]
|
||||
) -> List[PurchaseOrderItem]:
|
||||
"""Bulk update multiple items in a purchase order"""
|
||||
updated_items = []
|
||||
|
||||
for update_data in item_updates:
|
||||
item_id = update_data.get('id')
|
||||
if not item_id:
|
||||
continue
|
||||
|
||||
item = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.id == item_id,
|
||||
self.model.purchase_order_id == po_id
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if item:
|
||||
# Update allowed fields
|
||||
for key, value in update_data.items():
|
||||
if key != 'id' and hasattr(item, key):
|
||||
setattr(item, key, value)
|
||||
|
||||
# Recalculate line total if quantity or price changed
|
||||
if 'ordered_quantity' in update_data or 'unit_price' in update_data:
|
||||
item.line_total = item.ordered_quantity * item.unit_price
|
||||
item.remaining_quantity = item.ordered_quantity - item.received_quantity
|
||||
|
||||
item.updated_at = datetime.utcnow()
|
||||
updated_items.append(item)
|
||||
|
||||
self.db.commit()
|
||||
|
||||
# Refresh all items
|
||||
for item in updated_items:
|
||||
self.db.refresh(item)
|
||||
|
||||
return updated_items
|
||||
376
services/suppliers/app/repositories/purchase_order_repository.py
Normal file
376
services/suppliers/app/repositories/purchase_order_repository.py
Normal file
@@ -0,0 +1,376 @@
|
||||
# services/suppliers/app/repositories/purchase_order_repository.py
|
||||
"""
|
||||
Purchase Order repository for database operations
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import and_, or_, func, desc
|
||||
from uuid import UUID
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.models.suppliers import (
|
||||
PurchaseOrder, PurchaseOrderItem, PurchaseOrderStatus,
|
||||
Supplier, SupplierPriceList
|
||||
)
|
||||
from app.repositories.base import BaseRepository
|
||||
|
||||
|
||||
class PurchaseOrderRepository(BaseRepository[PurchaseOrder]):
|
||||
"""Repository for purchase order management operations"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
super().__init__(PurchaseOrder, db)
|
||||
|
||||
def get_by_po_number(self, tenant_id: UUID, po_number: str) -> Optional[PurchaseOrder]:
|
||||
"""Get purchase order by PO number within tenant"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.po_number == po_number
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_with_items(self, po_id: UUID) -> Optional[PurchaseOrder]:
|
||||
"""Get purchase order with all items loaded"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.options(joinedload(self.model.items))
|
||||
.filter(self.model.id == po_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
def search_purchase_orders(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
supplier_id: Optional[UUID] = None,
|
||||
status: Optional[PurchaseOrderStatus] = None,
|
||||
priority: Optional[str] = None,
|
||||
date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None,
|
||||
search_term: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> List[PurchaseOrder]:
|
||||
"""Search purchase orders with comprehensive filters"""
|
||||
query = (
|
||||
self.db.query(self.model)
|
||||
.options(joinedload(self.model.supplier))
|
||||
.filter(self.model.tenant_id == tenant_id)
|
||||
)
|
||||
|
||||
# Supplier filter
|
||||
if supplier_id:
|
||||
query = query.filter(self.model.supplier_id == supplier_id)
|
||||
|
||||
# Status filter
|
||||
if status:
|
||||
query = query.filter(self.model.status == status)
|
||||
|
||||
# Priority filter
|
||||
if priority:
|
||||
query = query.filter(self.model.priority == priority)
|
||||
|
||||
# Date range filter
|
||||
if date_from:
|
||||
query = query.filter(self.model.order_date >= date_from)
|
||||
if date_to:
|
||||
query = query.filter(self.model.order_date <= date_to)
|
||||
|
||||
# Search term filter (PO number, reference, supplier name)
|
||||
if search_term:
|
||||
search_filter = or_(
|
||||
self.model.po_number.ilike(f"%{search_term}%"),
|
||||
self.model.reference_number.ilike(f"%{search_term}%"),
|
||||
self.model.supplier.has(Supplier.name.ilike(f"%{search_term}%"))
|
||||
)
|
||||
query = query.filter(search_filter)
|
||||
|
||||
return (
|
||||
query.order_by(desc(self.model.order_date))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_orders_by_status(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
status: PurchaseOrderStatus
|
||||
) -> List[PurchaseOrder]:
|
||||
"""Get all orders with specific status"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.options(joinedload(self.model.supplier))
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status == status
|
||||
)
|
||||
)
|
||||
.order_by(self.model.order_date.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_orders_requiring_approval(self, tenant_id: UUID) -> List[PurchaseOrder]:
|
||||
"""Get orders pending approval"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.options(joinedload(self.model.supplier))
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status == PurchaseOrderStatus.PENDING_APPROVAL,
|
||||
self.model.requires_approval == True
|
||||
)
|
||||
)
|
||||
.order_by(self.model.order_date.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_overdue_orders(self, tenant_id: UUID) -> List[PurchaseOrder]:
|
||||
"""Get orders that are overdue for delivery"""
|
||||
today = datetime.utcnow().date()
|
||||
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.options(joinedload(self.model.supplier))
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status.in_([
|
||||
PurchaseOrderStatus.CONFIRMED,
|
||||
PurchaseOrderStatus.SENT_TO_SUPPLIER,
|
||||
PurchaseOrderStatus.PARTIALLY_RECEIVED
|
||||
]),
|
||||
self.model.required_delivery_date < today
|
||||
)
|
||||
)
|
||||
.order_by(self.model.required_delivery_date.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_orders_by_supplier(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
supplier_id: UUID,
|
||||
limit: int = 20
|
||||
) -> List[PurchaseOrder]:
|
||||
"""Get recent orders for a specific supplier"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.supplier_id == supplier_id
|
||||
)
|
||||
)
|
||||
.order_by(self.model.order_date.desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
def generate_po_number(self, tenant_id: UUID) -> str:
|
||||
"""Generate next PO number for tenant"""
|
||||
# Get current year
|
||||
current_year = datetime.utcnow().year
|
||||
|
||||
# Find highest PO number for current year
|
||||
year_prefix = f"PO{current_year}"
|
||||
|
||||
latest_po = (
|
||||
self.db.query(self.model.po_number)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.po_number.like(f"{year_prefix}%")
|
||||
)
|
||||
)
|
||||
.order_by(self.model.po_number.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
if latest_po:
|
||||
# Extract number and increment
|
||||
try:
|
||||
last_number = int(latest_po.po_number.replace(year_prefix, ""))
|
||||
new_number = last_number + 1
|
||||
except ValueError:
|
||||
new_number = 1
|
||||
else:
|
||||
new_number = 1
|
||||
|
||||
return f"{year_prefix}{new_number:04d}"
|
||||
|
||||
def update_order_status(
|
||||
self,
|
||||
po_id: UUID,
|
||||
status: PurchaseOrderStatus,
|
||||
updated_by: UUID,
|
||||
notes: Optional[str] = None
|
||||
) -> Optional[PurchaseOrder]:
|
||||
"""Update purchase order status with audit trail"""
|
||||
po = self.get_by_id(po_id)
|
||||
if not po:
|
||||
return None
|
||||
|
||||
po.status = status
|
||||
po.updated_by = updated_by
|
||||
po.updated_at = datetime.utcnow()
|
||||
|
||||
if notes:
|
||||
existing_notes = po.internal_notes or ""
|
||||
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M")
|
||||
po.internal_notes = f"{existing_notes}\n[{timestamp}] Status changed to {status.value}: {notes}".strip()
|
||||
|
||||
# Set specific timestamps based on status
|
||||
if status == PurchaseOrderStatus.APPROVED:
|
||||
po.approved_at = datetime.utcnow()
|
||||
elif status == PurchaseOrderStatus.SENT_TO_SUPPLIER:
|
||||
po.sent_to_supplier_at = datetime.utcnow()
|
||||
elif status == PurchaseOrderStatus.CONFIRMED:
|
||||
po.supplier_confirmation_date = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(po)
|
||||
return po
|
||||
|
||||
def approve_order(
|
||||
self,
|
||||
po_id: UUID,
|
||||
approved_by: UUID,
|
||||
approval_notes: Optional[str] = None
|
||||
) -> Optional[PurchaseOrder]:
|
||||
"""Approve a purchase order"""
|
||||
po = self.get_by_id(po_id)
|
||||
if not po or po.status != PurchaseOrderStatus.PENDING_APPROVAL:
|
||||
return None
|
||||
|
||||
po.status = PurchaseOrderStatus.APPROVED
|
||||
po.approved_by = approved_by
|
||||
po.approved_at = datetime.utcnow()
|
||||
po.updated_by = approved_by
|
||||
po.updated_at = datetime.utcnow()
|
||||
|
||||
if approval_notes:
|
||||
po.internal_notes = (po.internal_notes or "") + f"\nApproval notes: {approval_notes}"
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(po)
|
||||
return po
|
||||
|
||||
def calculate_order_totals(self, po_id: UUID) -> Optional[PurchaseOrder]:
|
||||
"""Recalculate order totals based on line items"""
|
||||
po = self.get_with_items(po_id)
|
||||
if not po:
|
||||
return None
|
||||
|
||||
# Calculate subtotal from items
|
||||
subtotal = sum(item.line_total for item in po.items)
|
||||
|
||||
# Keep existing tax, shipping, and discount
|
||||
tax_amount = po.tax_amount or 0
|
||||
shipping_cost = po.shipping_cost or 0
|
||||
discount_amount = po.discount_amount or 0
|
||||
|
||||
# Calculate total
|
||||
total_amount = subtotal + tax_amount + shipping_cost - discount_amount
|
||||
|
||||
# Update PO
|
||||
po.subtotal = subtotal
|
||||
po.total_amount = max(0, total_amount) # Ensure non-negative
|
||||
po.updated_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(po)
|
||||
return po
|
||||
|
||||
def get_purchase_order_statistics(self, tenant_id: UUID) -> Dict[str, Any]:
|
||||
"""Get purchase order statistics for dashboard"""
|
||||
# Total orders
|
||||
total_orders = self.count_by_tenant(tenant_id)
|
||||
|
||||
# Orders by status
|
||||
status_counts = (
|
||||
self.db.query(
|
||||
self.model.status,
|
||||
func.count(self.model.id).label('count')
|
||||
)
|
||||
.filter(self.model.tenant_id == tenant_id)
|
||||
.group_by(self.model.status)
|
||||
.all()
|
||||
)
|
||||
|
||||
status_dict = {status.value: 0 for status in PurchaseOrderStatus}
|
||||
for status, count in status_counts:
|
||||
status_dict[status.value] = count
|
||||
|
||||
# This month's orders
|
||||
first_day_month = datetime.utcnow().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
this_month_orders = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.order_date >= first_day_month
|
||||
)
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Total spend this month
|
||||
this_month_spend = (
|
||||
self.db.query(func.sum(self.model.total_amount))
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.order_date >= first_day_month,
|
||||
self.model.status != PurchaseOrderStatus.CANCELLED
|
||||
)
|
||||
)
|
||||
.scalar()
|
||||
) or 0.0
|
||||
|
||||
# Average order value
|
||||
avg_order_value = (
|
||||
self.db.query(func.avg(self.model.total_amount))
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status != PurchaseOrderStatus.CANCELLED
|
||||
)
|
||||
)
|
||||
.scalar()
|
||||
) or 0.0
|
||||
|
||||
# Overdue orders count
|
||||
today = datetime.utcnow().date()
|
||||
overdue_count = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status.in_([
|
||||
PurchaseOrderStatus.CONFIRMED,
|
||||
PurchaseOrderStatus.SENT_TO_SUPPLIER,
|
||||
PurchaseOrderStatus.PARTIALLY_RECEIVED
|
||||
]),
|
||||
self.model.required_delivery_date < today
|
||||
)
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
return {
|
||||
"total_orders": total_orders,
|
||||
"status_counts": status_dict,
|
||||
"this_month_orders": this_month_orders,
|
||||
"this_month_spend": float(this_month_spend),
|
||||
"avg_order_value": round(float(avg_order_value), 2),
|
||||
"overdue_count": overdue_count,
|
||||
"pending_approval": status_dict.get(PurchaseOrderStatus.PENDING_APPROVAL.value, 0)
|
||||
}
|
||||
298
services/suppliers/app/repositories/supplier_repository.py
Normal file
298
services/suppliers/app/repositories/supplier_repository.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# services/suppliers/app/repositories/supplier_repository.py
|
||||
"""
|
||||
Supplier repository for database operations
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func
|
||||
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: Session):
|
||||
super().__init__(Supplier, db)
|
||||
|
||||
def get_by_name(self, tenant_id: UUID, name: str) -> Optional[Supplier]:
|
||||
"""Get supplier by name within tenant"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.name == name
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_by_supplier_code(self, tenant_id: UUID, supplier_code: str) -> Optional[Supplier]:
|
||||
"""Get supplier by supplier code within tenant"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.supplier_code == supplier_code
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
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"""
|
||||
query = self.db.query(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}%")
|
||||
)
|
||||
query = query.filter(search_filter)
|
||||
|
||||
# Type filter
|
||||
if supplier_type:
|
||||
query = query.filter(self.model.supplier_type == supplier_type)
|
||||
|
||||
# Status filter
|
||||
if status:
|
||||
query = query.filter(self.model.status == status)
|
||||
|
||||
return query.order_by(self.model.name).limit(limit).offset(offset).all()
|
||||
|
||||
def get_active_suppliers(self, tenant_id: UUID) -> List[Supplier]:
|
||||
"""Get all active suppliers for a tenant"""
|
||||
return (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status == SupplierStatus.ACTIVE
|
||||
)
|
||||
)
|
||||
.order_by(self.model.name)
|
||||
.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()
|
||||
)
|
||||
|
||||
def get_supplier_statistics(self, tenant_id: UUID) -> Dict[str, Any]:
|
||||
"""Get supplier statistics for dashboard"""
|
||||
total_suppliers = self.count_by_tenant(tenant_id)
|
||||
|
||||
active_suppliers = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status == SupplierStatus.ACTIVE
|
||||
)
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
pending_suppliers = (
|
||||
self.db.query(self.model)
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status == SupplierStatus.PENDING_APPROVAL
|
||||
)
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
avg_quality_rating = (
|
||||
self.db.query(func.avg(self.model.quality_rating))
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status == SupplierStatus.ACTIVE,
|
||||
self.model.quality_rating > 0
|
||||
)
|
||||
)
|
||||
.scalar()
|
||||
) or 0.0
|
||||
|
||||
avg_delivery_rating = (
|
||||
self.db.query(func.avg(self.model.delivery_rating))
|
||||
.filter(
|
||||
and_(
|
||||
self.model.tenant_id == tenant_id,
|
||||
self.model.status == SupplierStatus.ACTIVE,
|
||||
self.model.delivery_rating > 0
|
||||
)
|
||||
)
|
||||
.scalar()
|
||||
) or 0.0
|
||||
|
||||
total_spend = (
|
||||
self.db.query(func.sum(self.model.total_amount))
|
||||
.filter(self.model.tenant_id == tenant_id)
|
||||
.scalar()
|
||||
) or 0.0
|
||||
|
||||
return {
|
||||
"total_suppliers": total_suppliers,
|
||||
"active_suppliers": active_suppliers,
|
||||
"pending_suppliers": 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)
|
||||
}
|
||||
|
||||
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:
|
||||
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()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(supplier)
|
||||
return supplier
|
||||
|
||||
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:
|
||||
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()
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(supplier)
|
||||
return supplier
|
||||
Reference in New Issue
Block a user