from typing import Any, Dict, List, Optional from datetime import datetime, timedelta from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import func, and_, desc from sqlalchemy.orm import Session from app.api.deps import get_db, get_current_active_user, get_current_admin_user from app.models.user import User as UserModel from app.models.product import Product from app.models.inventory import Inventory, InventoryTransaction from app.models.purchase_order import PurchaseOrder, PurchaseOrderItem from app.models.sale import Sale, SaleItem router = APIRouter() @router.get("/inventory-value") def inventory_value_report( db: Session = Depends(get_db), current_user: UserModel = Depends(get_current_active_user), ) -> Any: """ Get current inventory value report. Calculates total inventory value (quantity * cost price). """ # Join Product and Inventory to calculate value inventory_value = db.query( func.sum(Product.cost_price * Inventory.quantity).label("total_value"), func.count(Inventory.id).label("items_count"), func.sum(Inventory.quantity).label("total_quantity") ).join( Product, Product.id == Inventory.product_id ).filter( Inventory.quantity > 0 ).first() # Get top products by value top_products = db.query( Product.id, Product.name, Product.sku, Product.cost_price, Inventory.quantity, (Product.cost_price * Inventory.quantity).label("value") ).join( Inventory, Product.id == Inventory.product_id ).filter( Inventory.quantity > 0 ).order_by( desc("value") ).limit(10).all() top_products_result = [] for p in top_products: top_products_result.append({ "id": p.id, "name": p.name, "sku": p.sku, "cost_price": float(p.cost_price) if p.cost_price else 0, "quantity": p.quantity, "value": float(p.value) if p.value else 0 }) return { "total_value": float(inventory_value.total_value) if inventory_value.total_value else 0, "items_count": inventory_value.items_count or 0, "total_quantity": inventory_value.total_quantity or 0, "top_products_by_value": top_products_result, "report_date": datetime.now() } @router.get("/low-stock") def low_stock_report( db: Session = Depends(get_db), threshold: int = 5, current_user: UserModel = Depends(get_current_active_user), ) -> Any: """ Get low stock report. Shows products with inventory below specified threshold. """ # Get products with low stock low_stock_products = db.query( Product.id, Product.name, Product.sku, Product.barcode, func.sum(Inventory.quantity).label("total_quantity") ).outerjoin( Inventory, Product.id == Inventory.product_id ).group_by( Product.id ).having( func.coalesce(func.sum(Inventory.quantity), 0) < threshold ).all() result = [] for p in low_stock_products: result.append({ "id": p.id, "name": p.name, "sku": p.sku, "barcode": p.barcode, "quantity": p.total_quantity or 0 }) return { "threshold": threshold, "count": len(result), "products": result, "report_date": datetime.now() } @router.get("/sales-summary") def sales_summary_report( db: Session = Depends(get_db), start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, current_user: UserModel = Depends(get_current_active_user), ) -> Any: """ Get sales summary report for a specified date range. If no dates provided, defaults to the last 30 days. """ # Set default date range if not provided if not end_date: end_date = datetime.now() if not start_date: start_date = end_date - timedelta(days=30) # Get summary of all sales in date range sales_summary = db.query( func.count(Sale.id).label("total_sales"), func.sum(SaleItem.quantity * SaleItem.unit_price).label("total_revenue"), func.sum(SaleItem.quantity).label("total_items_sold") ).join( SaleItem, Sale.id == SaleItem.sale_id ).filter( Sale.status == "completed", Sale.created_at >= start_date, Sale.created_at <= end_date ).first() # Get top selling products top_products = db.query( Product.id, Product.name, Product.sku, func.sum(SaleItem.quantity).label("quantity_sold"), func.sum(SaleItem.quantity * SaleItem.unit_price).label("revenue") ).join( SaleItem, Product.id == SaleItem.product_id ).join( Sale, SaleItem.sale_id == Sale.id ).filter( Sale.status == "completed", Sale.created_at >= start_date, Sale.created_at <= end_date ).group_by( Product.id ).order_by( desc("quantity_sold") ).limit(10).all() top_products_result = [] for p in top_products: top_products_result.append({ "id": p.id, "name": p.name, "sku": p.sku, "quantity_sold": p.quantity_sold, "revenue": float(p.revenue) if p.revenue else 0 }) return { "start_date": start_date, "end_date": end_date, "total_sales": sales_summary.total_sales or 0, "total_revenue": float(sales_summary.total_revenue) if sales_summary.total_revenue else 0, "total_items_sold": sales_summary.total_items_sold or 0, "top_selling_products": top_products_result, "report_date": datetime.now() } @router.get("/purchases-summary") def purchases_summary_report( db: Session = Depends(get_db), start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, current_user: UserModel = Depends(get_current_active_user), ) -> Any: """ Get purchases summary report for a specified date range. If no dates provided, defaults to the last 30 days. """ # Set default date range if not provided if not end_date: end_date = datetime.now() if not start_date: start_date = end_date - timedelta(days=30) # Get summary of all received purchase orders in date range purchases_summary = db.query( func.count(PurchaseOrder.id).label("total_purchase_orders"), func.sum(PurchaseOrderItem.quantity * PurchaseOrderItem.unit_price).label("total_cost"), func.sum(PurchaseOrderItem.quantity).label("total_items_purchased") ).join( PurchaseOrderItem, PurchaseOrder.id == PurchaseOrderItem.purchase_order_id ).filter( PurchaseOrder.status == "received", PurchaseOrder.created_at >= start_date, PurchaseOrder.created_at <= end_date ).first() # Get top suppliers top_suppliers = db.query( PurchaseOrder.supplier_name, func.count(PurchaseOrder.id).label("order_count"), func.sum(PurchaseOrderItem.quantity * PurchaseOrderItem.unit_price).label("total_spend") ).join( PurchaseOrderItem, PurchaseOrder.id == PurchaseOrderItem.purchase_order_id ).filter( PurchaseOrder.status == "received", PurchaseOrder.created_at >= start_date, PurchaseOrder.created_at <= end_date ).group_by( PurchaseOrder.supplier_name ).order_by( desc("total_spend") ).limit(5).all() top_suppliers_result = [] for s in top_suppliers: top_suppliers_result.append({ "supplier_name": s.supplier_name, "order_count": s.order_count, "total_spend": float(s.total_spend) if s.total_spend else 0 }) return { "start_date": start_date, "end_date": end_date, "total_purchase_orders": purchases_summary.total_purchase_orders or 0, "total_cost": float(purchases_summary.total_cost) if purchases_summary.total_cost else 0, "total_items_purchased": purchases_summary.total_items_purchased or 0, "top_suppliers": top_suppliers_result, "report_date": datetime.now() } @router.get("/inventory-movements") def inventory_movements_report( db: Session = Depends(get_db), product_id: Optional[int] = None, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, current_user: UserModel = Depends(get_current_active_user), ) -> Any: """ Get inventory movements report for a specified date range and product. If no dates provided, defaults to the last 30 days. """ # Set default date range if not provided if not end_date: end_date = datetime.now() if not start_date: start_date = end_date - timedelta(days=30) # Build query for inventory transactions query = db.query( InventoryTransaction.id, InventoryTransaction.product_id, Product.name.label("product_name"), Product.sku, InventoryTransaction.quantity, InventoryTransaction.transaction_type, InventoryTransaction.reference_id, InventoryTransaction.reason, InventoryTransaction.timestamp, InventoryTransaction.location ).join( Product, InventoryTransaction.product_id == Product.id ).filter( InventoryTransaction.timestamp >= start_date, InventoryTransaction.timestamp <= end_date ) # Filter by product if specified if product_id: query = query.filter(InventoryTransaction.product_id == product_id) # Execute query transactions = query.order_by(InventoryTransaction.timestamp.desc()).all() result = [] for t in transactions: result.append({ "id": t.id, "product_id": t.product_id, "product_name": t.product_name, "sku": t.sku, "quantity": t.quantity, "transaction_type": t.transaction_type, "reference_id": t.reference_id, "reason": t.reason, "timestamp": t.timestamp, "location": t.location }) # Get summary by transaction type summary = db.query( InventoryTransaction.transaction_type, func.sum(InventoryTransaction.quantity).label("total_quantity") ).filter( InventoryTransaction.timestamp >= start_date, InventoryTransaction.timestamp <= end_date ) if product_id: summary = summary.filter(InventoryTransaction.product_id == product_id) summary = summary.group_by(InventoryTransaction.transaction_type).all() summary_result = {} for s in summary: summary_result[s.transaction_type] = s.total_quantity return { "start_date": start_date, "end_date": end_date, "product_id": product_id, "transaction_count": len(result), "summary_by_type": summary_result, "transactions": result, "report_date": datetime.now() }