Create new services: inventory, recipes, suppliers
This commit is contained in:
355
services/suppliers/app/services/delivery_service.py
Normal file
355
services/suppliers/app/services/delivery_service.py
Normal file
@@ -0,0 +1,355 @@
|
||||
# services/suppliers/app/services/delivery_service.py
|
||||
"""
|
||||
Delivery service for business logic operations
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.repositories.delivery_repository import DeliveryRepository
|
||||
from app.repositories.purchase_order_repository import PurchaseOrderRepository
|
||||
from app.repositories.supplier_repository import SupplierRepository
|
||||
from app.models.suppliers import (
|
||||
Delivery, DeliveryItem, DeliveryStatus,
|
||||
PurchaseOrder, PurchaseOrderStatus, SupplierStatus
|
||||
)
|
||||
from app.schemas.suppliers import (
|
||||
DeliveryCreate, DeliveryUpdate, DeliverySearchParams
|
||||
)
|
||||
from app.core.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DeliveryService:
|
||||
"""Service for delivery management operations"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.repository = DeliveryRepository(db)
|
||||
self.po_repository = PurchaseOrderRepository(db)
|
||||
self.supplier_repository = SupplierRepository(db)
|
||||
|
||||
async def create_delivery(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
delivery_data: DeliveryCreate,
|
||||
created_by: UUID
|
||||
) -> Delivery:
|
||||
"""Create a new delivery"""
|
||||
logger.info(
|
||||
"Creating delivery",
|
||||
tenant_id=str(tenant_id),
|
||||
po_id=str(delivery_data.purchase_order_id)
|
||||
)
|
||||
|
||||
# Validate purchase order exists and belongs to tenant
|
||||
po = self.po_repository.get_by_id(delivery_data.purchase_order_id)
|
||||
if not po:
|
||||
raise ValueError("Purchase order not found")
|
||||
if po.tenant_id != tenant_id:
|
||||
raise ValueError("Purchase order does not belong to this tenant")
|
||||
if po.status not in [
|
||||
PurchaseOrderStatus.CONFIRMED,
|
||||
PurchaseOrderStatus.PARTIALLY_RECEIVED
|
||||
]:
|
||||
raise ValueError("Purchase order must be confirmed before creating deliveries")
|
||||
|
||||
# Validate supplier
|
||||
supplier = self.supplier_repository.get_by_id(delivery_data.supplier_id)
|
||||
if not supplier:
|
||||
raise ValueError("Supplier not found")
|
||||
if supplier.id != po.supplier_id:
|
||||
raise ValueError("Supplier does not match purchase order supplier")
|
||||
|
||||
# Generate delivery number
|
||||
delivery_number = self.repository.generate_delivery_number(tenant_id)
|
||||
|
||||
# Create delivery
|
||||
delivery_create_data = delivery_data.model_dump(exclude={'items'})
|
||||
delivery_create_data.update({
|
||||
'tenant_id': tenant_id,
|
||||
'delivery_number': delivery_number,
|
||||
'status': DeliveryStatus.SCHEDULED,
|
||||
'created_by': created_by
|
||||
})
|
||||
|
||||
# Set default scheduled date if not provided
|
||||
if not delivery_create_data.get('scheduled_date'):
|
||||
delivery_create_data['scheduled_date'] = datetime.utcnow()
|
||||
|
||||
delivery = self.repository.create(delivery_create_data)
|
||||
|
||||
# Create delivery items
|
||||
from app.repositories.purchase_order_item_repository import PurchaseOrderItemRepository
|
||||
item_repo = PurchaseOrderItemRepository(self.db)
|
||||
|
||||
for item_data in delivery_data.items:
|
||||
# Validate purchase order item
|
||||
po_item = item_repo.get_by_id(item_data.purchase_order_item_id)
|
||||
if not po_item or po_item.purchase_order_id != po.id:
|
||||
raise ValueError("Invalid purchase order item")
|
||||
|
||||
# Create delivery item
|
||||
from app.models.suppliers import DeliveryItem
|
||||
item_create_data = item_data.model_dump()
|
||||
item_create_data.update({
|
||||
'tenant_id': tenant_id,
|
||||
'delivery_id': delivery.id
|
||||
})
|
||||
|
||||
delivery_item = DeliveryItem(**item_create_data)
|
||||
self.db.add(delivery_item)
|
||||
|
||||
self.db.commit()
|
||||
|
||||
logger.info(
|
||||
"Delivery created successfully",
|
||||
tenant_id=str(tenant_id),
|
||||
delivery_id=str(delivery.id),
|
||||
delivery_number=delivery_number
|
||||
)
|
||||
|
||||
return delivery
|
||||
|
||||
async def get_delivery(self, delivery_id: UUID) -> Optional[Delivery]:
|
||||
"""Get delivery by ID with items"""
|
||||
return self.repository.get_with_items(delivery_id)
|
||||
|
||||
async def update_delivery(
|
||||
self,
|
||||
delivery_id: UUID,
|
||||
delivery_data: DeliveryUpdate,
|
||||
updated_by: UUID
|
||||
) -> Optional[Delivery]:
|
||||
"""Update delivery information"""
|
||||
logger.info("Updating delivery", delivery_id=str(delivery_id))
|
||||
|
||||
delivery = self.repository.get_by_id(delivery_id)
|
||||
if not delivery:
|
||||
return None
|
||||
|
||||
# Check if delivery can be modified
|
||||
if delivery.status in [DeliveryStatus.DELIVERED, DeliveryStatus.FAILED_DELIVERY]:
|
||||
raise ValueError("Cannot modify completed deliveries")
|
||||
|
||||
# Prepare update data
|
||||
update_data = delivery_data.model_dump(exclude_unset=True)
|
||||
update_data['created_by'] = updated_by # Track who updated
|
||||
update_data['updated_at'] = datetime.utcnow()
|
||||
|
||||
delivery = self.repository.update(delivery_id, update_data)
|
||||
|
||||
logger.info("Delivery updated successfully", delivery_id=str(delivery_id))
|
||||
return delivery
|
||||
|
||||
async 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"""
|
||||
logger.info("Updating delivery status", delivery_id=str(delivery_id), status=status.value)
|
||||
|
||||
return self.repository.update_delivery_status(
|
||||
delivery_id=delivery_id,
|
||||
status=status,
|
||||
updated_by=updated_by,
|
||||
notes=notes,
|
||||
update_timestamps=update_timestamps
|
||||
)
|
||||
|
||||
async 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,
|
||||
notes: Optional[str] = None
|
||||
) -> Optional[Delivery]:
|
||||
"""Mark delivery as received with inspection details"""
|
||||
logger.info("Marking delivery as received", delivery_id=str(delivery_id))
|
||||
|
||||
delivery = self.repository.mark_as_received(
|
||||
delivery_id=delivery_id,
|
||||
received_by=received_by,
|
||||
inspection_passed=inspection_passed,
|
||||
inspection_notes=inspection_notes,
|
||||
quality_issues=quality_issues
|
||||
)
|
||||
|
||||
if not delivery:
|
||||
return None
|
||||
|
||||
# Add custom notes if provided
|
||||
if notes:
|
||||
existing_notes = delivery.notes or ""
|
||||
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M")
|
||||
delivery.notes = f"{existing_notes}\n[{timestamp}] Receipt notes: {notes}".strip()
|
||||
self.repository.update(delivery_id, {'notes': delivery.notes})
|
||||
|
||||
# Update purchase order item received quantities
|
||||
await self._update_purchase_order_received_quantities(delivery)
|
||||
|
||||
# Check if purchase order is fully received
|
||||
await self._check_purchase_order_completion(delivery.purchase_order_id)
|
||||
|
||||
logger.info("Delivery marked as received", delivery_id=str(delivery_id))
|
||||
return delivery
|
||||
|
||||
async def search_deliveries(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
search_params: DeliverySearchParams
|
||||
) -> List[Delivery]:
|
||||
"""Search deliveries with filters"""
|
||||
return self.repository.search_deliveries(
|
||||
tenant_id=tenant_id,
|
||||
supplier_id=search_params.supplier_id,
|
||||
status=search_params.status,
|
||||
date_from=search_params.date_from,
|
||||
date_to=search_params.date_to,
|
||||
search_term=search_params.search_term,
|
||||
limit=search_params.limit,
|
||||
offset=search_params.offset
|
||||
)
|
||||
|
||||
async def get_deliveries_by_purchase_order(self, po_id: UUID) -> List[Delivery]:
|
||||
"""Get all deliveries for a purchase order"""
|
||||
return self.repository.get_by_purchase_order(po_id)
|
||||
|
||||
async def get_todays_deliveries(self, tenant_id: UUID) -> List[Delivery]:
|
||||
"""Get deliveries scheduled for today"""
|
||||
return self.repository.get_todays_deliveries(tenant_id)
|
||||
|
||||
async def get_overdue_deliveries(self, tenant_id: UUID) -> List[Delivery]:
|
||||
"""Get overdue deliveries"""
|
||||
return self.repository.get_overdue_deliveries(tenant_id)
|
||||
|
||||
async def get_scheduled_deliveries(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None
|
||||
) -> List[Delivery]:
|
||||
"""Get scheduled deliveries for date range"""
|
||||
return self.repository.get_scheduled_deliveries(tenant_id, date_from, date_to)
|
||||
|
||||
async 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"""
|
||||
return self.repository.get_delivery_performance_stats(
|
||||
tenant_id, days_back, supplier_id
|
||||
)
|
||||
|
||||
async def get_upcoming_deliveries_summary(self, tenant_id: UUID) -> Dict[str, Any]:
|
||||
"""Get summary of upcoming deliveries"""
|
||||
return self.repository.get_upcoming_deliveries_summary(tenant_id)
|
||||
|
||||
async def _update_purchase_order_received_quantities(self, delivery: Delivery):
|
||||
"""Update purchase order item received quantities based on delivery"""
|
||||
from app.repositories.purchase_order_item_repository import PurchaseOrderItemRepository
|
||||
|
||||
item_repo = PurchaseOrderItemRepository(self.db)
|
||||
|
||||
# Get delivery items with accepted quantities
|
||||
delivery_with_items = self.repository.get_with_items(delivery.id)
|
||||
if not delivery_with_items or not delivery_with_items.items:
|
||||
return
|
||||
|
||||
for delivery_item in delivery_with_items.items:
|
||||
# Update purchase order item received quantity
|
||||
item_repo.add_received_quantity(
|
||||
delivery_item.purchase_order_item_id,
|
||||
delivery_item.accepted_quantity
|
||||
)
|
||||
|
||||
async def _check_purchase_order_completion(self, po_id: UUID):
|
||||
"""Check if purchase order is fully received and update status"""
|
||||
from app.repositories.purchase_order_item_repository import PurchaseOrderItemRepository
|
||||
|
||||
item_repo = PurchaseOrderItemRepository(self.db)
|
||||
po_items = item_repo.get_by_purchase_order(po_id)
|
||||
|
||||
if not po_items:
|
||||
return
|
||||
|
||||
# Check if all items are fully received
|
||||
fully_received = all(item.remaining_quantity == 0 for item in po_items)
|
||||
partially_received = any(item.received_quantity > 0 for item in po_items)
|
||||
|
||||
if fully_received:
|
||||
# Mark purchase order as completed
|
||||
self.po_repository.update_order_status(
|
||||
po_id,
|
||||
PurchaseOrderStatus.COMPLETED,
|
||||
po_items[0].tenant_id, # Use tenant_id as updated_by placeholder
|
||||
"All items received"
|
||||
)
|
||||
elif partially_received:
|
||||
# Mark as partially received if not already
|
||||
po = self.po_repository.get_by_id(po_id)
|
||||
if po and po.status == PurchaseOrderStatus.CONFIRMED:
|
||||
self.po_repository.update_order_status(
|
||||
po_id,
|
||||
PurchaseOrderStatus.PARTIALLY_RECEIVED,
|
||||
po.tenant_id, # Use tenant_id as updated_by placeholder
|
||||
"Partial delivery received"
|
||||
)
|
||||
|
||||
async def generate_delivery_tracking_info(self, delivery_id: UUID) -> Dict[str, Any]:
|
||||
"""Generate delivery tracking information"""
|
||||
delivery = self.repository.get_with_items(delivery_id)
|
||||
if not delivery:
|
||||
return {}
|
||||
|
||||
# Calculate delivery metrics
|
||||
total_items = len(delivery.items) if delivery.items else 0
|
||||
delivered_items = sum(
|
||||
1 for item in (delivery.items or [])
|
||||
if item.delivered_quantity > 0
|
||||
)
|
||||
accepted_items = sum(
|
||||
1 for item in (delivery.items or [])
|
||||
if item.accepted_quantity > 0
|
||||
)
|
||||
rejected_items = sum(
|
||||
1 for item in (delivery.items or [])
|
||||
if item.rejected_quantity > 0
|
||||
)
|
||||
|
||||
# Calculate timing metrics
|
||||
on_time = False
|
||||
delay_hours = 0
|
||||
if delivery.scheduled_date and delivery.actual_arrival:
|
||||
delay_seconds = (delivery.actual_arrival - delivery.scheduled_date).total_seconds()
|
||||
delay_hours = delay_seconds / 3600
|
||||
on_time = delay_hours <= 0
|
||||
|
||||
return {
|
||||
"delivery_id": str(delivery.id),
|
||||
"delivery_number": delivery.delivery_number,
|
||||
"status": delivery.status.value,
|
||||
"total_items": total_items,
|
||||
"delivered_items": delivered_items,
|
||||
"accepted_items": accepted_items,
|
||||
"rejected_items": rejected_items,
|
||||
"inspection_passed": delivery.inspection_passed,
|
||||
"on_time": on_time,
|
||||
"delay_hours": round(delay_hours, 1) if delay_hours > 0 else 0,
|
||||
"quality_issues": delivery.quality_issues or {},
|
||||
"scheduled_date": delivery.scheduled_date.isoformat() if delivery.scheduled_date else None,
|
||||
"actual_arrival": delivery.actual_arrival.isoformat() if delivery.actual_arrival else None,
|
||||
"completed_at": delivery.completed_at.isoformat() if delivery.completed_at else None
|
||||
}
|
||||
Reference in New Issue
Block a user