2025-06-02 11:37:11 +00:00

174 lines
6.4 KiB
Python

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