import uuid from datetime import datetime from typing import Any, Dict, List, Optional, Union from sqlalchemy.orm import Session from app.models.inventory import Inventory from app.models.order import Order, OrderStatus from app.models.shipment import Shipment, ShipmentItem, ShipmentStatus from app.schemas.shipment import ShipmentCreate, ShipmentUpdate from app.services import inventory as inventory_service def generate_tracking_number() -> str: """Generate a unique tracking number""" # Format: TRK-{random_uuid} return f"TRK-{uuid.uuid4().hex[:12].upper()}" def get(db: Session, shipment_id: int) -> Optional[Shipment]: return db.query(Shipment).filter(Shipment.id == shipment_id).first() def get_by_tracking_number(db: Session, tracking_number: str) -> Optional[Shipment]: return db.query(Shipment).filter(Shipment.tracking_number == tracking_number).first() def get_multi( db: Session, *, skip: int = 0, limit: int = 100, order_id: Optional[int] = None ) -> List[Shipment]: query = db.query(Shipment) if order_id: query = query.filter(Shipment.order_id == order_id) return query.order_by(Shipment.created_at.desc()).offset(skip).limit(limit).all() def create(db: Session, *, obj_in: ShipmentCreate) -> Shipment: # Validate shipment data if not obj_in.destination_warehouse_id and not obj_in.customer_address: raise ValueError("Either destination_warehouse_id or customer_address must be provided") # Create shipment db_obj = Shipment( tracking_number=generate_tracking_number(), order_id=obj_in.order_id, origin_warehouse_id=obj_in.origin_warehouse_id, destination_warehouse_id=obj_in.destination_warehouse_id, customer_address=obj_in.customer_address, status=obj_in.status or ShipmentStatus.PENDING, carrier=obj_in.carrier, estimated_delivery=obj_in.estimated_delivery, shipping_cost=obj_in.shipping_cost or 0.0, created_by_id=obj_in.created_by_id, created_at=datetime.now(), ) db.add(db_obj) db.flush() # Get the shipment ID without committing transaction # Create shipment items and update inventory for item in obj_in.items: shipment_item = ShipmentItem( shipment_id=db_obj.id, product_id=item.product_id, quantity=item.quantity ) db.add(shipment_item) # Reduce inventory at origin warehouse inventory_item = inventory_service.get_by_product_and_warehouse( db, product_id=item.product_id, warehouse_id=obj_in.origin_warehouse_id ) if not inventory_item: raise ValueError(f"Product {item.product_id} not available in warehouse {obj_in.origin_warehouse_id}") if inventory_item.quantity < item.quantity: raise ValueError(f"Insufficient quantity for product {item.product_id} in warehouse {obj_in.origin_warehouse_id}") inventory_item.quantity -= item.quantity db.add(inventory_item) # If this is warehouse-to-warehouse transfer, increase inventory at destination if obj_in.destination_warehouse_id: dest_inventory = inventory_service.get_by_product_and_warehouse( db, product_id=item.product_id, warehouse_id=obj_in.destination_warehouse_id ) if dest_inventory: dest_inventory.quantity += item.quantity db.add(dest_inventory) else: # Create new inventory record at destination new_dest_inventory = Inventory( product_id=item.product_id, warehouse_id=obj_in.destination_warehouse_id, quantity=item.quantity, ) db.add(new_dest_inventory) # If this shipment is related to an order, update order status if obj_in.order_id: order = db.query(Order).get(obj_in.order_id) if order and order.status == OrderStatus.PROCESSING: order.status = OrderStatus.SHIPPED order.updated_at = datetime.now() db.add(order) db.commit() db.refresh(db_obj) return db_obj def update( db: Session, *, db_obj: Shipment, obj_in: Union[ShipmentUpdate, Dict[str, Any]] ) -> Shipment: if isinstance(obj_in, dict): update_data = obj_in else: update_data = obj_in.dict(exclude_unset=True) # Don't allow changing tracking_number or created_at update_data.pop("tracking_number", None) update_data.pop("created_at", None) # Set updated_at update_data["updated_at"] = datetime.now() # Check for status changes current_status = db_obj.status new_status = update_data.get("status") if new_status and new_status != current_status: # Handle status change logic if new_status == ShipmentStatus.DELIVERED: # Set actual delivery time if not already set if not update_data.get("actual_delivery"): update_data["actual_delivery"] = datetime.now() # If this shipment is related to an order, update order status if db_obj.order_id: order = db.query(Order).get(db_obj.order_id) if order and order.status != OrderStatus.DELIVERED: order.status = OrderStatus.DELIVERED order.updated_at = datetime.now() db.add(order) # Update shipment fields for field in update_data: setattr(db_obj, field, update_data[field]) db.add(db_obj) db.commit() db.refresh(db_obj) return db_obj def update_status( db: Session, *, shipment_id: int, status: ShipmentStatus, actual_delivery: Optional[datetime] = None ) -> Optional[Shipment]: """Update shipment status""" shipment = get(db, shipment_id) if not shipment: return None update_data = {"status": status, "updated_at": datetime.now()} if status == ShipmentStatus.DELIVERED and not actual_delivery: update_data["actual_delivery"] = datetime.now() elif actual_delivery: update_data["actual_delivery"] = actual_delivery return update(db, db_obj=shipment, obj_in=update_data) def get_shipment_items(db: Session, *, shipment_id: int) -> List[ShipmentItem]: """Get all items for a specific shipment""" return db.query(ShipmentItem).filter(ShipmentItem.shipment_id == shipment_id).all()