174 lines
6.4 KiB
Python
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() |