Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
# ================================================================
# services/procurement/app/repositories/base_repository.py
# ================================================================
"""
Base Repository Pattern for Procurement Service
"""
from typing import Generic, TypeVar, Type, Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from shared.database.base import Base
ModelType = TypeVar("ModelType", bound=Base)
class BaseRepository(Generic[ModelType]):
"""Base repository with common database operations"""
def __init__(self, model: Type[ModelType]):
self.model = model
async def get_by_id(self, db: AsyncSession, id: Any) -> Optional[ModelType]:
"""Get entity by ID"""
result = await db.execute(select(self.model).where(self.model.id == id))
return result.scalar_one_or_none()
async def get_all(self, db: AsyncSession, skip: int = 0, limit: int = 100) -> List[ModelType]:
"""Get all entities with pagination"""
result = await db.execute(select(self.model).offset(skip).limit(limit))
return result.scalars().all()
async def create(self, db: AsyncSession, **kwargs) -> ModelType:
"""Create new entity"""
instance = self.model(**kwargs)
db.add(instance)
await db.flush()
await db.refresh(instance)
return instance
async def update(self, db: AsyncSession, id: Any, **kwargs) -> Optional[ModelType]:
"""Update entity"""
instance = await self.get_by_id(db, id)
if not instance:
return None
for key, value in kwargs.items():
if hasattr(instance, key):
setattr(instance, key, value)
await db.flush()
await db.refresh(instance)
return instance
async def delete(self, db: AsyncSession, id: Any) -> bool:
"""Delete entity"""
instance = await self.get_by_id(db, id)
if not instance:
return False
await db.delete(instance)
await db.flush()
return True

View File

@@ -0,0 +1,254 @@
# ================================================================
# services/procurement/app/repositories/procurement_plan_repository.py
# ================================================================
"""
Procurement Plan Repository - Database operations for procurement plans and requirements
"""
import uuid
from datetime import datetime, date
from typing import List, Optional, Dict, Any
from sqlalchemy import select, and_, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.procurement_plan import ProcurementPlan, ProcurementRequirement
from app.repositories.base_repository import BaseRepository
class ProcurementPlanRepository(BaseRepository):
"""Repository for procurement plan operations"""
def __init__(self, db: AsyncSession):
super().__init__(ProcurementPlan)
self.db = db
async def create_plan(self, plan_data: Dict[str, Any]) -> ProcurementPlan:
"""Create a new procurement plan"""
plan = ProcurementPlan(**plan_data)
self.db.add(plan)
await self.db.flush()
return plan
async def get_plan_by_id(self, plan_id: uuid.UUID, tenant_id: uuid.UUID) -> Optional[ProcurementPlan]:
"""Get procurement plan by ID"""
stmt = select(ProcurementPlan).where(
and_(
ProcurementPlan.id == plan_id,
ProcurementPlan.tenant_id == tenant_id
)
).options(selectinload(ProcurementPlan.requirements))
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def get_plan_by_date(self, plan_date: date, tenant_id: uuid.UUID) -> Optional[ProcurementPlan]:
"""Get procurement plan for a specific date"""
stmt = select(ProcurementPlan).where(
and_(
ProcurementPlan.plan_date == plan_date,
ProcurementPlan.tenant_id == tenant_id
)
).options(selectinload(ProcurementPlan.requirements))
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def get_current_plan(self, tenant_id: uuid.UUID) -> Optional[ProcurementPlan]:
"""Get the current day's procurement plan"""
today = date.today()
return await self.get_plan_by_date(today, tenant_id)
async def list_plans(
self,
tenant_id: uuid.UUID,
status: Optional[str] = None,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
limit: int = 50,
offset: int = 0
) -> List[ProcurementPlan]:
"""List procurement plans with filters"""
conditions = [ProcurementPlan.tenant_id == tenant_id]
if status:
conditions.append(ProcurementPlan.status == status)
if start_date:
conditions.append(ProcurementPlan.plan_date >= start_date)
if end_date:
conditions.append(ProcurementPlan.plan_date <= end_date)
stmt = (
select(ProcurementPlan)
.where(and_(*conditions))
.order_by(desc(ProcurementPlan.plan_date))
.limit(limit)
.offset(offset)
.options(selectinload(ProcurementPlan.requirements))
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_plans_by_tenant(
self,
tenant_id: uuid.UUID,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> List[ProcurementPlan]:
"""Get all procurement plans for a tenant with optional date filters"""
conditions = [ProcurementPlan.tenant_id == tenant_id]
if start_date:
conditions.append(ProcurementPlan.created_at >= start_date)
if end_date:
conditions.append(ProcurementPlan.created_at <= end_date)
stmt = (
select(ProcurementPlan)
.where(and_(*conditions))
.order_by(desc(ProcurementPlan.created_at))
.options(selectinload(ProcurementPlan.requirements))
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def update_plan(self, plan_id: uuid.UUID, tenant_id: uuid.UUID, updates: Dict[str, Any]) -> Optional[ProcurementPlan]:
"""Update procurement plan"""
plan = await self.get_plan_by_id(plan_id, tenant_id)
if not plan:
return None
for key, value in updates.items():
if hasattr(plan, key):
setattr(plan, key, value)
plan.updated_at = datetime.utcnow()
await self.db.flush()
return plan
async def delete_plan(self, plan_id: uuid.UUID, tenant_id: uuid.UUID) -> bool:
"""Delete procurement plan"""
plan = await self.get_plan_by_id(plan_id, tenant_id)
if not plan:
return False
await self.db.delete(plan)
return True
async def generate_plan_number(self, tenant_id: uuid.UUID, plan_date: date) -> str:
"""Generate unique plan number"""
date_str = plan_date.strftime("%Y%m%d")
# Count existing plans for the same date
stmt = select(func.count(ProcurementPlan.id)).where(
and_(
ProcurementPlan.tenant_id == tenant_id,
ProcurementPlan.plan_date == plan_date
)
)
result = await self.db.execute(stmt)
count = result.scalar() or 0
return f"PP-{date_str}-{count + 1:03d}"
class ProcurementRequirementRepository(BaseRepository):
"""Repository for procurement requirement operations"""
def __init__(self, db: AsyncSession):
super().__init__(ProcurementRequirement)
self.db = db
async def create_requirement(self, requirement_data: Dict[str, Any]) -> ProcurementRequirement:
"""Create a new procurement requirement"""
requirement = ProcurementRequirement(**requirement_data)
self.db.add(requirement)
await self.db.flush()
return requirement
async def create_requirements_batch(self, requirements_data: List[Dict[str, Any]]) -> List[ProcurementRequirement]:
"""Create multiple procurement requirements"""
requirements = [ProcurementRequirement(**data) for data in requirements_data]
self.db.add_all(requirements)
await self.db.flush()
return requirements
async def get_requirement_by_id(self, requirement_id: uuid.UUID, tenant_id: uuid.UUID) -> Optional[ProcurementRequirement]:
"""Get procurement requirement by ID"""
stmt = select(ProcurementRequirement).join(ProcurementPlan).where(
and_(
ProcurementRequirement.id == requirement_id,
ProcurementPlan.tenant_id == tenant_id
)
)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def get_requirements_by_plan(self, plan_id: uuid.UUID) -> List[ProcurementRequirement]:
"""Get all requirements for a specific plan"""
stmt = select(ProcurementRequirement).where(
ProcurementRequirement.plan_id == plan_id
).order_by(ProcurementRequirement.priority.desc(), ProcurementRequirement.required_by_date)
result = await self.db.execute(stmt)
return result.scalars().all()
async def update_requirement(
self,
requirement_id: uuid.UUID,
updates: Dict[str, Any]
) -> Optional[ProcurementRequirement]:
"""Update procurement requirement"""
stmt = select(ProcurementRequirement).where(
ProcurementRequirement.id == requirement_id
)
result = await self.db.execute(stmt)
requirement = result.scalar_one_or_none()
if not requirement:
return None
for key, value in updates.items():
if hasattr(requirement, key):
setattr(requirement, key, value)
requirement.updated_at = datetime.utcnow()
await self.db.flush()
return requirement
async def generate_requirement_number(self, plan_id: uuid.UUID) -> str:
"""Generate unique requirement number within a plan"""
stmt = select(func.count(ProcurementRequirement.id)).where(
ProcurementRequirement.plan_id == plan_id
)
result = await self.db.execute(stmt)
count = result.scalar() or 0
return f"REQ-{count + 1:05d}"
async def get_requirements_by_tenant(
self,
tenant_id: uuid.UUID,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> List[ProcurementRequirement]:
"""Get all procurement requirements for a tenant with optional date filters"""
conditions = [ProcurementPlan.tenant_id == tenant_id]
if start_date:
conditions.append(ProcurementRequirement.created_at >= start_date)
if end_date:
conditions.append(ProcurementRequirement.created_at <= end_date)
stmt = (
select(ProcurementRequirement)
.join(ProcurementPlan)
.where(and_(*conditions))
.order_by(desc(ProcurementRequirement.created_at))
)
result = await self.db.execute(stmt)
return result.scalars().all()

View File

@@ -0,0 +1,318 @@
# ================================================================
# services/procurement/app/repositories/purchase_order_repository.py
# ================================================================
"""
Purchase Order Repository - Database operations for purchase orders
Migrated from Suppliers Service
"""
import uuid
from datetime import datetime, date
from typing import List, Optional, Dict, Any
from sqlalchemy import select, and_, or_, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.purchase_order import (
PurchaseOrder,
PurchaseOrderItem,
PurchaseOrderStatus,
Delivery,
DeliveryStatus,
SupplierInvoice,
)
from app.repositories.base_repository import BaseRepository
class PurchaseOrderRepository(BaseRepository):
"""Repository for purchase order operations"""
def __init__(self, db: AsyncSession):
super().__init__(PurchaseOrder)
self.db = db
async def create_po(self, po_data: Dict[str, Any]) -> PurchaseOrder:
"""Create a new purchase order"""
po = PurchaseOrder(**po_data)
self.db.add(po)
await self.db.flush()
return po
async def get_po_by_id(self, po_id: uuid.UUID, tenant_id: uuid.UUID) -> Optional[PurchaseOrder]:
"""Get purchase order by ID with items loaded"""
stmt = select(PurchaseOrder).where(
and_(
PurchaseOrder.id == po_id,
PurchaseOrder.tenant_id == tenant_id
)
).options(
selectinload(PurchaseOrder.items),
selectinload(PurchaseOrder.deliveries),
selectinload(PurchaseOrder.invoices)
)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def get_po_by_number(self, po_number: str, tenant_id: uuid.UUID) -> Optional[PurchaseOrder]:
"""Get purchase order by PO number"""
stmt = select(PurchaseOrder).where(
and_(
PurchaseOrder.po_number == po_number,
PurchaseOrder.tenant_id == tenant_id
)
).options(selectinload(PurchaseOrder.items))
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def list_purchase_orders(
self,
tenant_id: uuid.UUID,
status: Optional[PurchaseOrderStatus] = None,
supplier_id: Optional[uuid.UUID] = None,
priority: Optional[str] = None,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
limit: int = 50,
offset: int = 0
) -> List[PurchaseOrder]:
"""List purchase orders with filters"""
conditions = [PurchaseOrder.tenant_id == tenant_id]
if status:
conditions.append(PurchaseOrder.status == status)
if supplier_id:
conditions.append(PurchaseOrder.supplier_id == supplier_id)
if priority:
conditions.append(PurchaseOrder.priority == priority)
if start_date:
conditions.append(PurchaseOrder.order_date >= start_date)
if end_date:
conditions.append(PurchaseOrder.order_date <= end_date)
stmt = (
select(PurchaseOrder)
.where(and_(*conditions))
.order_by(desc(PurchaseOrder.order_date))
.limit(limit)
.offset(offset)
.options(selectinload(PurchaseOrder.items))
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_pending_approval(self, tenant_id: uuid.UUID) -> List[PurchaseOrder]:
"""Get purchase orders pending approval"""
stmt = select(PurchaseOrder).where(
and_(
PurchaseOrder.tenant_id == tenant_id,
PurchaseOrder.status == PurchaseOrderStatus.pending_approval
)
).order_by(PurchaseOrder.total_amount.desc())
result = await self.db.execute(stmt)
return result.scalars().all()
async def update_po(self, po_id: uuid.UUID, tenant_id: uuid.UUID, updates: Dict[str, Any]) -> Optional[PurchaseOrder]:
"""Update purchase order"""
po = await self.get_po_by_id(po_id, tenant_id)
if not po:
return None
for key, value in updates.items():
if hasattr(po, key):
setattr(po, key, value)
po.updated_at = datetime.utcnow()
await self.db.flush()
return po
async def generate_po_number(self, tenant_id: uuid.UUID) -> str:
"""Generate unique PO number"""
today = date.today()
date_str = today.strftime("%Y%m%d")
# Count existing POs for today
stmt = select(func.count(PurchaseOrder.id)).where(
and_(
PurchaseOrder.tenant_id == tenant_id,
func.date(PurchaseOrder.order_date) == today
)
)
result = await self.db.execute(stmt)
count = result.scalar() or 0
return f"PO-{date_str}-{count + 1:04d}"
class PurchaseOrderItemRepository(BaseRepository):
"""Repository for purchase order item operations"""
def __init__(self, db: AsyncSession):
super().__init__(PurchaseOrderItem)
self.db = db
async def create_item(self, item_data: Dict[str, Any]) -> PurchaseOrderItem:
"""Create a purchase order item"""
item = PurchaseOrderItem(**item_data)
self.db.add(item)
await self.db.flush()
return item
async def create_items_batch(self, items_data: List[Dict[str, Any]]) -> List[PurchaseOrderItem]:
"""Create multiple purchase order items"""
items = [PurchaseOrderItem(**data) for data in items_data]
self.db.add_all(items)
await self.db.flush()
return items
async def get_items_by_po(self, po_id: uuid.UUID) -> List[PurchaseOrderItem]:
"""Get all items for a purchase order"""
stmt = select(PurchaseOrderItem).where(
PurchaseOrderItem.purchase_order_id == po_id
)
result = await self.db.execute(stmt)
return result.scalars().all()
class DeliveryRepository(BaseRepository):
"""Repository for delivery operations"""
def __init__(self, db: AsyncSession):
super().__init__(Delivery)
self.db = db
async def create_delivery(self, delivery_data: Dict[str, Any]) -> Delivery:
"""Create a new delivery"""
delivery = Delivery(**delivery_data)
self.db.add(delivery)
await self.db.flush()
return delivery
async def get_delivery_by_id(self, delivery_id: uuid.UUID, tenant_id: uuid.UUID) -> Optional[Delivery]:
"""Get delivery by ID with items loaded"""
stmt = select(Delivery).where(
and_(
Delivery.id == delivery_id,
Delivery.tenant_id == tenant_id
)
).options(selectinload(Delivery.items))
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def get_deliveries_by_po(self, po_id: uuid.UUID) -> List[Delivery]:
"""Get all deliveries for a purchase order"""
stmt = select(Delivery).where(
Delivery.purchase_order_id == po_id
).options(selectinload(Delivery.items))
result = await self.db.execute(stmt)
return result.scalars().all()
async def create_delivery_item(self, item_data: Dict[str, Any]):
"""Create a delivery item"""
from app.models.purchase_order import DeliveryItem
item = DeliveryItem(**item_data)
self.db.add(item)
await self.db.flush()
return item
async def update_delivery(
self,
delivery_id: uuid.UUID,
tenant_id: uuid.UUID,
updates: Dict[str, Any]
) -> Optional[Delivery]:
"""Update delivery"""
delivery = await self.get_delivery_by_id(delivery_id, tenant_id)
if not delivery:
return None
for key, value in updates.items():
if hasattr(delivery, key):
setattr(delivery, key, value)
delivery.updated_at = datetime.utcnow()
await self.db.flush()
return delivery
async def generate_delivery_number(self, tenant_id: uuid.UUID) -> str:
"""Generate unique delivery number"""
today = date.today()
date_str = today.strftime("%Y%m%d")
stmt = select(func.count(Delivery.id)).where(
and_(
Delivery.tenant_id == tenant_id,
func.date(Delivery.created_at) == today
)
)
result = await self.db.execute(stmt)
count = result.scalar() or 0
return f"DEL-{date_str}-{count + 1:04d}"
class SupplierInvoiceRepository(BaseRepository):
"""Repository for supplier invoice operations"""
def __init__(self, db: AsyncSession):
super().__init__(SupplierInvoice)
self.db = db
async def create_invoice(self, invoice_data: Dict[str, Any]) -> SupplierInvoice:
"""Create a new supplier invoice"""
invoice = SupplierInvoice(**invoice_data)
self.db.add(invoice)
await self.db.flush()
return invoice
async def get_invoice_by_id(self, invoice_id: uuid.UUID, tenant_id: uuid.UUID) -> Optional[SupplierInvoice]:
"""Get invoice by ID"""
stmt = select(SupplierInvoice).where(
and_(
SupplierInvoice.id == invoice_id,
SupplierInvoice.tenant_id == tenant_id
)
)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def get_invoices_by_po(self, po_id: uuid.UUID) -> List[SupplierInvoice]:
"""Get all invoices for a purchase order"""
stmt = select(SupplierInvoice).where(
SupplierInvoice.purchase_order_id == po_id
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_invoices_by_supplier(self, supplier_id: uuid.UUID, tenant_id: uuid.UUID) -> List[SupplierInvoice]:
"""Get all invoices for a supplier"""
stmt = select(SupplierInvoice).where(
and_(
SupplierInvoice.supplier_id == supplier_id,
SupplierInvoice.tenant_id == tenant_id
)
).order_by(SupplierInvoice.invoice_date.desc())
result = await self.db.execute(stmt)
return result.scalars().all()
async def generate_invoice_number(self, tenant_id: uuid.UUID) -> str:
"""Generate unique invoice number"""
today = date.today()
date_str = today.strftime("%Y%m%d")
stmt = select(func.count(SupplierInvoice.id)).where(
and_(
SupplierInvoice.tenant_id == tenant_id,
func.date(SupplierInvoice.created_at) == today
)
)
result = await self.db.execute(stmt)
count = result.scalar() or 0
return f"INV-{date_str}-{count + 1:04d}"

View File

@@ -0,0 +1,315 @@
"""
Replenishment Plan Repository
Provides database operations for replenishment planning, inventory projections,
and supplier allocations.
"""
from typing import List, Optional, Dict, Any
from datetime import date
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, func
from sqlalchemy.orm import selectinload
from app.models.replenishment import (
ReplenishmentPlan,
ReplenishmentPlanItem,
InventoryProjection,
SupplierAllocation
)
from app.repositories.base_repository import BaseRepository
import structlog
logger = structlog.get_logger()
class ReplenishmentPlanRepository(BaseRepository[ReplenishmentPlan]):
"""Repository for replenishment plan operations"""
def __init__(self):
super().__init__(ReplenishmentPlan)
async def list_plans(
self,
db: AsyncSession,
tenant_id: UUID,
skip: int = 0,
limit: int = 100,
status: Optional[str] = None
) -> List[Dict[str, Any]]:
"""List replenishment plans for a tenant"""
try:
query = select(ReplenishmentPlan).where(
ReplenishmentPlan.tenant_id == tenant_id
)
if status:
query = query.where(ReplenishmentPlan.status == status)
query = query.offset(skip).limit(limit).order_by(
ReplenishmentPlan.created_at.desc()
)
result = await db.execute(query)
plans = result.scalars().all()
return [
{
"id": str(plan.id),
"tenant_id": str(plan.tenant_id),
"planning_date": plan.planning_date,
"projection_horizon_days": plan.projection_horizon_days,
"total_items": plan.total_items,
"urgent_items": plan.urgent_items,
"high_risk_items": plan.high_risk_items,
"total_estimated_cost": float(plan.total_estimated_cost),
"status": plan.status,
"created_at": plan.created_at,
"updated_at": plan.updated_at
}
for plan in plans
]
except Exception as e:
logger.error("Failed to list replenishment plans", error=str(e), tenant_id=tenant_id)
raise
async def get_plan_by_id(
self,
db: AsyncSession,
plan_id: UUID,
tenant_id: UUID
) -> Optional[Dict[str, Any]]:
"""Get a specific replenishment plan with items"""
try:
query = select(ReplenishmentPlan).where(
and_(
ReplenishmentPlan.id == plan_id,
ReplenishmentPlan.tenant_id == tenant_id
)
).options(selectinload(ReplenishmentPlan.items))
result = await db.execute(query)
plan = result.scalar_one_or_none()
if not plan:
return None
return {
"id": str(plan.id),
"tenant_id": str(plan.tenant_id),
"planning_date": plan.planning_date,
"projection_horizon_days": plan.projection_horizon_days,
"forecast_id": str(plan.forecast_id) if plan.forecast_id else None,
"production_schedule_id": str(plan.production_schedule_id) if plan.production_schedule_id else None,
"total_items": plan.total_items,
"urgent_items": plan.urgent_items,
"high_risk_items": plan.high_risk_items,
"total_estimated_cost": float(plan.total_estimated_cost),
"status": plan.status,
"created_at": plan.created_at,
"updated_at": plan.updated_at,
"executed_at": plan.executed_at,
"items": [
{
"id": str(item.id),
"ingredient_id": str(item.ingredient_id),
"ingredient_name": item.ingredient_name,
"unit_of_measure": item.unit_of_measure,
"base_quantity": float(item.base_quantity),
"safety_stock_quantity": float(item.safety_stock_quantity),
"final_order_quantity": float(item.final_order_quantity),
"order_date": item.order_date,
"delivery_date": item.delivery_date,
"required_by_date": item.required_by_date,
"lead_time_days": item.lead_time_days,
"is_urgent": item.is_urgent,
"urgency_reason": item.urgency_reason,
"waste_risk": item.waste_risk,
"stockout_risk": item.stockout_risk,
"supplier_id": str(item.supplier_id) if item.supplier_id else None
}
for item in plan.items
]
}
except Exception as e:
logger.error("Failed to get replenishment plan", error=str(e), plan_id=plan_id)
raise
class InventoryProjectionRepository(BaseRepository[InventoryProjection]):
"""Repository for inventory projection operations"""
def __init__(self):
super().__init__(InventoryProjection)
async def list_projections(
self,
db: AsyncSession,
tenant_id: UUID,
ingredient_id: Optional[UUID] = None,
projection_date: Optional[date] = None,
stockout_only: bool = False,
skip: int = 0,
limit: int = 100
) -> List[Dict[str, Any]]:
"""List inventory projections"""
try:
query = select(InventoryProjection).where(
InventoryProjection.tenant_id == tenant_id
)
if ingredient_id:
query = query.where(InventoryProjection.ingredient_id == ingredient_id)
if projection_date:
query = query.where(InventoryProjection.projection_date == projection_date)
if stockout_only:
query = query.where(InventoryProjection.is_stockout == True)
query = query.offset(skip).limit(limit).order_by(
InventoryProjection.projection_date.asc()
)
result = await db.execute(query)
projections = result.scalars().all()
return [
{
"id": str(proj.id),
"tenant_id": str(proj.tenant_id),
"ingredient_id": str(proj.ingredient_id),
"ingredient_name": proj.ingredient_name,
"projection_date": proj.projection_date,
"starting_stock": float(proj.starting_stock),
"forecasted_consumption": float(proj.forecasted_consumption),
"scheduled_receipts": float(proj.scheduled_receipts),
"projected_ending_stock": float(proj.projected_ending_stock),
"is_stockout": proj.is_stockout,
"coverage_gap": float(proj.coverage_gap),
"created_at": proj.created_at
}
for proj in projections
]
except Exception as e:
logger.error("Failed to list inventory projections", error=str(e), tenant_id=tenant_id)
raise
class SupplierAllocationRepository(BaseRepository[SupplierAllocation]):
"""Repository for supplier allocation operations"""
def __init__(self):
super().__init__(SupplierAllocation)
async def list_allocations(
self,
db: AsyncSession,
tenant_id: UUID,
requirement_id: Optional[UUID] = None,
supplier_id: Optional[UUID] = None,
skip: int = 0,
limit: int = 100
) -> List[Dict[str, Any]]:
"""List supplier allocations
Note: SupplierAllocation model doesn't have tenant_id, so we filter by requirements
"""
try:
# Build base query - no tenant_id filter since model doesn't have it
query = select(SupplierAllocation)
if requirement_id:
query = query.where(SupplierAllocation.requirement_id == requirement_id)
if supplier_id:
query = query.where(SupplierAllocation.supplier_id == supplier_id)
query = query.offset(skip).limit(limit).order_by(
SupplierAllocation.created_at.desc()
)
result = await db.execute(query)
allocations = result.scalars().all()
return [
{
"id": str(alloc.id),
"requirement_id": str(alloc.requirement_id) if alloc.requirement_id else None,
"replenishment_plan_item_id": str(alloc.replenishment_plan_item_id) if alloc.replenishment_plan_item_id else None,
"supplier_id": str(alloc.supplier_id),
"supplier_name": alloc.supplier_name,
"allocation_type": alloc.allocation_type,
"allocated_quantity": float(alloc.allocated_quantity),
"allocation_percentage": float(alloc.allocation_percentage),
"unit_price": float(alloc.unit_price),
"total_cost": float(alloc.total_cost),
"lead_time_days": alloc.lead_time_days,
"supplier_score": float(alloc.supplier_score),
"allocation_reason": alloc.allocation_reason,
"created_at": alloc.created_at
}
for alloc in allocations
]
except Exception as e:
logger.error("Failed to list supplier allocations", error=str(e))
raise
class ReplenishmentAnalyticsRepository:
"""Repository for replenishment analytics"""
async def get_analytics(
self,
db: AsyncSession,
tenant_id: UUID,
start_date: Optional[date] = None,
end_date: Optional[date] = None
) -> Dict[str, Any]:
"""Get replenishment planning analytics"""
try:
# Build base query
query = select(ReplenishmentPlan).where(
ReplenishmentPlan.tenant_id == tenant_id
)
if start_date:
query = query.where(ReplenishmentPlan.planning_date >= start_date)
if end_date:
query = query.where(ReplenishmentPlan.planning_date <= end_date)
result = await db.execute(query)
plans = result.scalars().all()
# Calculate analytics
total_plans = len(plans)
total_items = sum(plan.total_items for plan in plans)
total_urgent = sum(plan.urgent_items for plan in plans)
total_high_risk = sum(plan.high_risk_items for plan in plans)
total_cost = sum(plan.total_estimated_cost for plan in plans)
# Status breakdown
status_counts = {}
for plan in plans:
status_counts[plan.status] = status_counts.get(plan.status, 0) + 1
return {
"total_plans": total_plans,
"total_items": total_items,
"total_urgent_items": total_urgent,
"total_high_risk_items": total_high_risk,
"total_estimated_cost": float(total_cost),
"status_breakdown": status_counts,
"average_items_per_plan": total_items / total_plans if total_plans > 0 else 0,
"urgent_item_percentage": (total_urgent / total_items * 100) if total_items > 0 else 0
}
except Exception as e:
logger.error("Failed to get replenishment analytics", error=str(e), tenant_id=tenant_id)
raise