Add supplier and imporve inventory frontend

This commit is contained in:
Urtzi Alfaro
2025-09-18 23:32:53 +02:00
parent ae77a0e1c5
commit d61056df33
40 changed files with 2022 additions and 629 deletions

View File

@@ -4,8 +4,8 @@ Base repository class for common database operations
"""
from typing import TypeVar, Generic, List, Optional, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import desc, asc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import desc, asc, select, func
from uuid import UUID
T = TypeVar('T')
@@ -14,55 +14,59 @@ T = TypeVar('T')
class BaseRepository(Generic[T]):
"""Base repository with common CRUD operations"""
def __init__(self, model: type, db: Session):
def __init__(self, model: type, db: AsyncSession):
self.model = model
self.db = db
def create(self, obj_data: Dict[str, Any]) -> T:
async def create(self, obj_data: Dict[str, Any]) -> T:
"""Create a new record"""
db_obj = self.model(**obj_data)
self.db.add(db_obj)
self.db.commit()
self.db.refresh(db_obj)
await self.db.commit()
await self.db.refresh(db_obj)
return db_obj
def get_by_id(self, record_id: UUID) -> Optional[T]:
async def get_by_id(self, record_id: UUID) -> Optional[T]:
"""Get record by ID"""
return self.db.query(self.model).filter(self.model.id == record_id).first()
stmt = select(self.model).filter(self.model.id == record_id)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
def get_by_tenant_id(self, tenant_id: UUID, limit: int = 100, offset: int = 0) -> List[T]:
async def get_by_tenant_id(self, tenant_id: UUID, limit: int = 100, offset: int = 0) -> List[T]:
"""Get records by tenant ID with pagination"""
return (
self.db.query(self.model)
.filter(self.model.tenant_id == tenant_id)
.limit(limit)
.offset(offset)
.all()
)
stmt = select(self.model).filter(
self.model.tenant_id == tenant_id
).limit(limit).offset(offset)
result = await self.db.execute(stmt)
return result.scalars().all()
def update(self, record_id: UUID, update_data: Dict[str, Any]) -> Optional[T]:
async def update(self, record_id: UUID, update_data: Dict[str, Any]) -> Optional[T]:
"""Update record by ID"""
db_obj = self.get_by_id(record_id)
db_obj = await self.get_by_id(record_id)
if db_obj:
for key, value in update_data.items():
if hasattr(db_obj, key):
setattr(db_obj, key, value)
self.db.commit()
self.db.refresh(db_obj)
await self.db.commit()
await self.db.refresh(db_obj)
return db_obj
def delete(self, record_id: UUID) -> bool:
async def delete(self, record_id: UUID) -> bool:
"""Delete record by ID"""
db_obj = self.get_by_id(record_id)
db_obj = await self.get_by_id(record_id)
if db_obj:
self.db.delete(db_obj)
self.db.commit()
await self.db.delete(db_obj)
await self.db.commit()
return True
return False
def count_by_tenant(self, tenant_id: UUID) -> int:
async def count_by_tenant(self, tenant_id: UUID) -> int:
"""Count records by tenant"""
return self.db.query(self.model).filter(self.model.tenant_id == tenant_id).count()
stmt = select(func.count()).select_from(self.model).filter(
self.model.tenant_id == tenant_id
)
result = await self.db.execute(stmt)
return result.scalar() or 0
def list_with_filters(
self,

View File

@@ -4,8 +4,8 @@ Supplier repository for database operations
"""
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import and_, or_, func, select
from uuid import UUID
from datetime import datetime
@@ -16,36 +16,32 @@ from app.repositories.base import BaseRepository
class SupplierRepository(BaseRepository[Supplier]):
"""Repository for supplier management operations"""
def __init__(self, db: Session):
def __init__(self, db: AsyncSession):
super().__init__(Supplier, db)
def get_by_name(self, tenant_id: UUID, name: str) -> Optional[Supplier]:
async def get_by_name(self, tenant_id: UUID, name: str) -> Optional[Supplier]:
"""Get supplier by name within tenant"""
return (
self.db.query(self.model)
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.name == name
)
stmt = select(self.model).filter(
and_(
self.model.tenant_id == tenant_id,
self.model.name == name
)
.first()
)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
def get_by_supplier_code(self, tenant_id: UUID, supplier_code: str) -> Optional[Supplier]:
async def get_by_supplier_code(self, tenant_id: UUID, supplier_code: str) -> Optional[Supplier]:
"""Get supplier by supplier code within tenant"""
return (
self.db.query(self.model)
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.supplier_code == supplier_code
)
stmt = select(self.model).filter(
and_(
self.model.tenant_id == tenant_id,
self.model.supplier_code == supplier_code
)
.first()
)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
def search_suppliers(
async def search_suppliers(
self,
tenant_id: UUID,
search_term: Optional[str] = None,
@@ -55,8 +51,8 @@ class SupplierRepository(BaseRepository[Supplier]):
offset: int = 0
) -> List[Supplier]:
"""Search suppliers with filters"""
query = self.db.query(self.model).filter(self.model.tenant_id == tenant_id)
stmt = select(self.model).filter(self.model.tenant_id == tenant_id)
# Search term filter (name, contact person, email)
if search_term:
search_filter = or_(
@@ -64,31 +60,30 @@ class SupplierRepository(BaseRepository[Supplier]):
self.model.contact_person.ilike(f"%{search_term}%"),
self.model.email.ilike(f"%{search_term}%")
)
query = query.filter(search_filter)
stmt = stmt.filter(search_filter)
# Type filter
if supplier_type:
query = query.filter(self.model.supplier_type == supplier_type)
stmt = stmt.filter(self.model.supplier_type == supplier_type)
# Status filter
if status:
query = query.filter(self.model.status == status)
return query.order_by(self.model.name).limit(limit).offset(offset).all()
stmt = stmt.filter(self.model.status == status)
stmt = stmt.order_by(self.model.name).limit(limit).offset(offset)
result = await self.db.execute(stmt)
return result.scalars().all()
def get_active_suppliers(self, tenant_id: UUID) -> List[Supplier]:
async def get_active_suppliers(self, tenant_id: UUID) -> List[Supplier]:
"""Get all active suppliers for a tenant"""
return (
self.db.query(self.model)
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.status == SupplierStatus.ACTIVE
)
stmt = select(self.model).filter(
and_(
self.model.tenant_id == tenant_id,
self.model.status == SupplierStatus.active
)
.order_by(self.model.name)
.all()
)
).order_by(self.model.name)
result = await self.db.execute(stmt)
return result.scalars().all()
def get_suppliers_by_type(
self,
@@ -102,7 +97,7 @@ class SupplierRepository(BaseRepository[Supplier]):
and_(
self.model.tenant_id == tenant_id,
self.model.supplier_type == supplier_type,
self.model.status == SupplierStatus.ACTIVE
self.model.status == SupplierStatus.active
)
)
.order_by(self.model.quality_rating.desc(), self.model.name)
@@ -120,7 +115,7 @@ class SupplierRepository(BaseRepository[Supplier]):
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.status == SupplierStatus.ACTIVE
self.model.status == SupplierStatus.active
)
)
.order_by(
@@ -178,7 +173,7 @@ class SupplierRepository(BaseRepository[Supplier]):
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.status == SupplierStatus.ACTIVE,
self.model.status == SupplierStatus.active,
or_(
self.model.quality_rating < 3.0, # Poor rating
self.model.delivery_rating < 3.0, # Poor delivery
@@ -190,66 +185,33 @@ class SupplierRepository(BaseRepository[Supplier]):
.all()
)
def get_supplier_statistics(self, tenant_id: UUID) -> Dict[str, Any]:
async def get_supplier_statistics(self, tenant_id: UUID) -> Dict[str, Any]:
"""Get supplier statistics for dashboard"""
total_suppliers = self.count_by_tenant(tenant_id)
active_suppliers = (
self.db.query(self.model)
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.status == SupplierStatus.ACTIVE
)
)
.count()
)
pending_suppliers = (
self.db.query(self.model)
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.status == SupplierStatus.PENDING_APPROVAL
)
)
.count()
)
avg_quality_rating = (
self.db.query(func.avg(self.model.quality_rating))
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.status == SupplierStatus.ACTIVE,
self.model.quality_rating > 0
)
)
.scalar()
) or 0.0
avg_delivery_rating = (
self.db.query(func.avg(self.model.delivery_rating))
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.status == SupplierStatus.ACTIVE,
self.model.delivery_rating > 0
)
)
.scalar()
) or 0.0
total_spend = (
self.db.query(func.sum(self.model.total_amount))
.filter(self.model.tenant_id == tenant_id)
.scalar()
) or 0.0
total_suppliers = await self.count_by_tenant(tenant_id)
# Get all suppliers for this tenant to avoid multiple queries and enum casting issues
all_stmt = select(self.model).filter(self.model.tenant_id == tenant_id)
all_result = await self.db.execute(all_stmt)
all_suppliers = all_result.scalars().all()
# Calculate statistics in Python to avoid database enum casting issues
active_suppliers = [s for s in all_suppliers if s.status == SupplierStatus.active]
pending_suppliers = [s for s in all_suppliers if s.status == SupplierStatus.pending_approval]
# Calculate averages from active suppliers
quality_ratings = [s.quality_rating for s in active_suppliers if s.quality_rating and s.quality_rating > 0]
avg_quality_rating = sum(quality_ratings) / len(quality_ratings) if quality_ratings else 0.0
delivery_ratings = [s.delivery_rating for s in active_suppliers if s.delivery_rating and s.delivery_rating > 0]
avg_delivery_rating = sum(delivery_ratings) / len(delivery_ratings) if delivery_ratings else 0.0
# Total spend for all suppliers
total_spend = sum(float(s.total_amount or 0) for s in all_suppliers)
return {
"total_suppliers": total_suppliers,
"active_suppliers": active_suppliers,
"pending_suppliers": pending_suppliers,
"active_suppliers": len(active_suppliers),
"pending_suppliers": len(pending_suppliers),
"avg_quality_rating": round(float(avg_quality_rating), 2),
"avg_delivery_rating": round(float(avg_delivery_rating), 2),
"total_spend": float(total_spend)