Initial commit - production deployment
This commit is contained in:
0
services/procurement/app/repositories/__init__.py
Normal file
0
services/procurement/app/repositories/__init__.py
Normal file
62
services/procurement/app/repositories/base_repository.py
Normal file
62
services/procurement/app/repositories/base_repository.py
Normal 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
|
||||
@@ -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()
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user