Improve the frontend 2

This commit is contained in:
Urtzi Alfaro
2025-10-29 06:58:05 +01:00
parent 858d985c92
commit 36217a2729
98 changed files with 6652 additions and 4230 deletions

View File

@@ -16,6 +16,7 @@ from app.schemas.suppliers import (
PurchaseOrderSearchParams
)
from app.models.suppliers import PurchaseOrderStatus
from app.models import AuditLog
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
from shared.auth.access_control import require_user_role
@@ -27,7 +28,7 @@ route_builder = RouteBuilder('suppliers')
router = APIRouter(tags=["purchase-orders"])
logger = structlog.get_logger()
audit_logger = create_audit_logger("suppliers-service")
audit_logger = create_audit_logger("suppliers-service", AuditLog)
@router.post(route_builder.build_base_route("purchase-orders"), response_model=PurchaseOrderResponse)

View File

@@ -22,6 +22,7 @@ from app.schemas.suppliers import (
PurchaseOrderStatusUpdate, PurchaseOrderApproval, PurchaseOrderResponse, PurchaseOrderSummary
)
from app.models.suppliers import SupplierType
from app.models import AuditLog
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
from shared.auth.access_control import require_user_role
@@ -33,7 +34,7 @@ route_builder = RouteBuilder('suppliers')
router = APIRouter(tags=["supplier-operations"])
logger = structlog.get_logger()
audit_logger = create_audit_logger("suppliers-service")
audit_logger = create_audit_logger("suppliers-service", AuditLog)
# ===== Supplier Operations =====

View File

@@ -13,9 +13,11 @@ from sqlalchemy import select
from app.core.database import get_db
from app.services.supplier_service import SupplierService
from app.models.suppliers import SupplierPriceList
from app.models import AuditLog
from app.schemas.suppliers import (
SupplierCreate, SupplierUpdate, SupplierResponse, SupplierSummary,
SupplierSearchParams, SupplierDeletionSummary
SupplierSearchParams, SupplierDeletionSummary,
SupplierPriceListCreate, SupplierPriceListUpdate, SupplierPriceListResponse
)
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
@@ -28,7 +30,7 @@ route_builder = RouteBuilder('suppliers')
router = APIRouter(tags=["suppliers"])
logger = structlog.get_logger()
audit_logger = create_audit_logger("suppliers-service")
audit_logger = create_audit_logger("suppliers-service", AuditLog)
@router.post(route_builder.build_base_route(""), response_model=SupplierResponse)
@require_user_role(['admin', 'owner', 'member'])
@@ -359,4 +361,252 @@ async def get_supplier_products(
raise HTTPException(
status_code=500,
detail="Failed to retrieve supplier products"
)
)
@router.get(
route_builder.build_resource_action_route("", "supplier_id", "price-lists"),
response_model=List[SupplierPriceListResponse]
)
async def get_supplier_price_lists(
supplier_id: UUID = Path(..., description="Supplier ID"),
tenant_id: str = Path(..., description="Tenant ID"),
is_active: bool = Query(True, description="Filter by active price lists"),
db: AsyncSession = Depends(get_db)
):
"""Get all price list items for a supplier"""
try:
service = SupplierService(db)
price_lists = await service.get_supplier_price_lists(
supplier_id=supplier_id,
tenant_id=UUID(tenant_id),
is_active=is_active
)
logger.info(
"Retrieved supplier price lists",
supplier_id=str(supplier_id),
count=len(price_lists)
)
return [SupplierPriceListResponse.from_orm(pl) for pl in price_lists]
except Exception as e:
logger.error(
"Error getting supplier price lists",
supplier_id=str(supplier_id),
error=str(e)
)
raise HTTPException(
status_code=500,
detail="Failed to retrieve supplier price lists"
)
@router.get(
route_builder.build_resource_action_route("", "supplier_id", "price-lists/{price_list_id}"),
response_model=SupplierPriceListResponse
)
async def get_supplier_price_list(
supplier_id: UUID = Path(..., description="Supplier ID"),
price_list_id: UUID = Path(..., description="Price List ID"),
tenant_id: str = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Get specific price list item for a supplier"""
try:
service = SupplierService(db)
price_list = await service.get_supplier_price_list(
price_list_id=price_list_id,
tenant_id=UUID(tenant_id)
)
if not price_list:
raise HTTPException(status_code=404, detail="Price list item not found")
logger.info(
"Retrieved supplier price list item",
supplier_id=str(supplier_id),
price_list_id=str(price_list_id)
)
return SupplierPriceListResponse.from_orm(price_list)
except HTTPException:
raise
except Exception as e:
logger.error(
"Error getting supplier price list item",
supplier_id=str(supplier_id),
price_list_id=str(price_list_id),
error=str(e)
)
raise HTTPException(
status_code=500,
detail="Failed to retrieve supplier price list item"
)
@router.post(
route_builder.build_resource_action_route("", "supplier_id", "price-lists"),
response_model=SupplierPriceListResponse
)
@require_user_role(['admin', 'owner', 'member'])
async def create_supplier_price_list(
supplier_id: UUID = Path(..., description="Supplier ID"),
price_list_data: SupplierPriceListCreate = None,
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create a new price list item for a supplier"""
try:
service = SupplierService(db)
# Verify supplier exists
supplier = await service.get_supplier(supplier_id)
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
price_list = await service.create_supplier_price_list(
supplier_id=supplier_id,
price_list_data=price_list_data,
tenant_id=UUID(tenant_id),
created_by=UUID(current_user["user_id"])
)
logger.info(
"Created supplier price list item",
supplier_id=str(supplier_id),
price_list_id=str(price_list.id)
)
return SupplierPriceListResponse.from_orm(price_list)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(
"Error creating supplier price list item",
supplier_id=str(supplier_id),
error=str(e)
)
raise HTTPException(
status_code=500,
detail="Failed to create supplier price list item"
)
@router.put(
route_builder.build_resource_action_route("", "supplier_id", "price-lists/{price_list_id}"),
response_model=SupplierPriceListResponse
)
@require_user_role(['admin', 'owner', 'member'])
async def update_supplier_price_list(
supplier_id: UUID = Path(..., description="Supplier ID"),
price_list_id: UUID = Path(..., description="Price List ID"),
price_list_data: SupplierPriceListUpdate = None,
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update a price list item for a supplier"""
try:
service = SupplierService(db)
# Verify supplier and price list exist
supplier = await service.get_supplier(supplier_id)
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
price_list = await service.get_supplier_price_list(
price_list_id=price_list_id,
tenant_id=UUID(tenant_id)
)
if not price_list:
raise HTTPException(status_code=404, detail="Price list item not found")
updated_price_list = await service.update_supplier_price_list(
price_list_id=price_list_id,
price_list_data=price_list_data,
tenant_id=UUID(tenant_id),
updated_by=UUID(current_user["user_id"])
)
logger.info(
"Updated supplier price list item",
supplier_id=str(supplier_id),
price_list_id=str(price_list_id)
)
return SupplierPriceListResponse.from_orm(updated_price_list)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(
"Error updating supplier price list item",
supplier_id=str(supplier_id),
price_list_id=str(price_list_id),
error=str(e)
)
raise HTTPException(
status_code=500,
detail="Failed to update supplier price list item"
)
@router.delete(
route_builder.build_resource_action_route("", "supplier_id", "price-lists/{price_list_id}")
)
@require_user_role(['admin', 'owner'])
async def delete_supplier_price_list(
supplier_id: UUID = Path(..., description="Supplier ID"),
price_list_id: UUID = Path(..., description="Price List ID"),
tenant_id: str = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Delete a price list item for a supplier"""
try:
service = SupplierService(db)
# Verify supplier and price list exist
supplier = await service.get_supplier(supplier_id)
if not supplier:
raise HTTPException(status_code=404, detail="Supplier not found")
price_list = await service.get_supplier_price_list(
price_list_id=price_list_id,
tenant_id=UUID(tenant_id)
)
if not price_list:
raise HTTPException(status_code=404, detail="Price list item not found")
success = await service.delete_supplier_price_list(
price_list_id=price_list_id,
tenant_id=UUID(tenant_id)
)
if not success:
raise HTTPException(status_code=404, detail="Price list item not found")
logger.info(
"Deleted supplier price list item",
supplier_id=str(supplier_id),
price_list_id=str(price_list_id)
)
return {"message": "Price list item deleted successfully"}
except Exception as e:
logger.error(
"Error deleting supplier price list item",
supplier_id=str(supplier_id),
price_list_id=str(price_list_id),
error=str(e)
)
raise HTTPException(
status_code=500,
detail="Failed to delete supplier price list item"
)

View File

@@ -348,3 +348,93 @@ class SupplierRepository(BaseRepository[Supplier]):
"deleted_scorecards": scorecards_count,
"deletion_timestamp": datetime.utcnow()
}
async def get_supplier_price_lists(
self,
supplier_id: UUID,
tenant_id: UUID,
is_active: bool = True
) -> List[Any]:
"""Get all price list items for a supplier"""
from app.models.suppliers import SupplierPriceList
stmt = select(SupplierPriceList).filter(
and_(
SupplierPriceList.supplier_id == supplier_id,
SupplierPriceList.tenant_id == tenant_id
)
)
if is_active:
stmt = stmt.filter(SupplierPriceList.is_active == True)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_supplier_price_list(
self,
price_list_id: UUID,
tenant_id: UUID
) -> Optional[Any]:
"""Get specific price list item"""
from app.models.suppliers import SupplierPriceList
stmt = select(SupplierPriceList).filter(
and_(
SupplierPriceList.id == price_list_id,
SupplierPriceList.tenant_id == tenant_id
)
)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def create_supplier_price_list(
self,
create_data: Dict[str, Any]
) -> Any:
"""Create a new price list item"""
from app.models.suppliers import SupplierPriceList
price_list = SupplierPriceList(**create_data)
self.db.add(price_list)
await self.db.commit()
await self.db.refresh(price_list)
return price_list
async def update_supplier_price_list(
self,
price_list_id: UUID,
update_data: Dict[str, Any]
) -> Any:
"""Update a price list item"""
from app.models.suppliers import SupplierPriceList
stmt = select(SupplierPriceList).filter(SupplierPriceList.id == price_list_id)
result = await self.db.execute(stmt)
price_list = result.scalar_one_or_none()
if not price_list:
raise ValueError("Price list item not found")
# Update fields
for key, value in update_data.items():
if hasattr(price_list, key):
setattr(price_list, key, value)
await self.db.commit()
await self.db.refresh(price_list)
return price_list
async def delete_supplier_price_list(
self,
price_list_id: UUID
) -> bool:
"""Delete a price list item"""
from app.models.suppliers import SupplierPriceList
from sqlalchemy import delete
stmt = delete(SupplierPriceList).filter(SupplierPriceList.id == price_list_id)
result = await self.db.execute(stmt)
await self.db.commit()
return result.rowcount > 0

View File

@@ -600,6 +600,80 @@ class DeliverySearchParams(BaseModel):
offset: int = Field(default=0, ge=0)
# ============================================================================
# SUPPLIER PRICE LIST SCHEMAS
# ============================================================================
class SupplierPriceListCreate(BaseModel):
"""Schema for creating supplier price list items"""
inventory_product_id: UUID
product_code: Optional[str] = Field(None, max_length=100)
unit_price: Decimal = Field(..., gt=0)
unit_of_measure: str = Field(..., max_length=20)
minimum_order_quantity: Optional[int] = Field(None, ge=1)
price_per_unit: Decimal = Field(..., gt=0)
tier_pricing: Optional[Dict[str, Any]] = None # [{quantity: 100, price: 2.50}, ...]
effective_date: Optional[datetime] = Field(default_factory=lambda: datetime.now())
expiry_date: Optional[datetime] = None
is_active: bool = True
brand: Optional[str] = Field(None, max_length=100)
packaging_size: Optional[str] = Field(None, max_length=50)
origin_country: Optional[str] = Field(None, max_length=100)
shelf_life_days: Optional[int] = None
storage_requirements: Optional[str] = None
quality_specs: Optional[Dict[str, Any]] = None
allergens: Optional[Dict[str, Any]] = None
class SupplierPriceListUpdate(BaseModel):
"""Schema for updating supplier price list items"""
unit_price: Optional[Decimal] = Field(None, gt=0)
unit_of_measure: Optional[str] = Field(None, max_length=20)
minimum_order_quantity: Optional[int] = Field(None, ge=1)
tier_pricing: Optional[Dict[str, Any]] = None
effective_date: Optional[datetime] = None
expiry_date: Optional[datetime] = None
is_active: Optional[bool] = None
brand: Optional[str] = Field(None, max_length=100)
packaging_size: Optional[str] = Field(None, max_length=50)
origin_country: Optional[str] = Field(None, max_length=100)
shelf_life_days: Optional[int] = None
storage_requirements: Optional[str] = None
quality_specs: Optional[Dict[str, Any]] = None
allergens: Optional[Dict[str, Any]] = None
class SupplierPriceListResponse(BaseModel):
"""Schema for supplier price list responses"""
id: UUID
tenant_id: UUID
supplier_id: UUID
inventory_product_id: UUID
product_code: Optional[str] = None
unit_price: Decimal
unit_of_measure: str
minimum_order_quantity: Optional[int] = None
price_per_unit: Decimal
tier_pricing: Optional[Dict[str, Any]] = None
effective_date: datetime
expiry_date: Optional[datetime] = None
is_active: bool
brand: Optional[str] = None
packaging_size: Optional[str] = None
origin_country: Optional[str] = None
shelf_life_days: Optional[int] = None
storage_requirements: Optional[str] = None
quality_specs: Optional[Dict[str, Any]] = None
allergens: Optional[Dict[str, Any]] = None
created_at: datetime
updated_at: datetime
created_by: UUID
updated_by: UUID
class Config:
from_attributes = True
# ============================================================================
# STATISTICS AND REPORTING SCHEMAS
# ============================================================================
@@ -641,4 +715,4 @@ class DeliverySummaryStats(BaseModel):
todays_deliveries: int
this_week_deliveries: int
overdue_deliveries: int
in_transit_deliveries: int
in_transit_deliveries: int

View File

@@ -13,7 +13,8 @@ from app.repositories.supplier_repository import SupplierRepository
from app.models.suppliers import Supplier, SupplierStatus, SupplierType
from app.schemas.suppliers import (
SupplierCreate, SupplierUpdate, SupplierResponse,
SupplierSearchParams, SupplierStatistics
SupplierSearchParams, SupplierStatistics,
SupplierPriceListCreate, SupplierPriceListUpdate, SupplierPriceListResponse
)
from app.core.config import settings
@@ -378,3 +379,132 @@ class SupplierService:
errors['minimum_order_amount'] = "Minimum order amount cannot be negative"
return errors
async def get_supplier_price_lists(
self,
supplier_id: UUID,
tenant_id: UUID,
is_active: bool = True
) -> List[Any]:
"""Get all price list items for a supplier"""
logger.info(
"Getting supplier price lists",
supplier_id=str(supplier_id),
tenant_id=str(tenant_id),
is_active=is_active
)
return await self.repository.get_supplier_price_lists(
supplier_id=supplier_id,
tenant_id=tenant_id,
is_active=is_active
)
async def get_supplier_price_list(
self,
price_list_id: UUID,
tenant_id: UUID
) -> Optional[Any]:
"""Get specific price list item"""
logger.info(
"Getting supplier price list item",
price_list_id=str(price_list_id),
tenant_id=str(tenant_id)
)
return await self.repository.get_supplier_price_list(
price_list_id=price_list_id,
tenant_id=tenant_id
)
async def create_supplier_price_list(
self,
supplier_id: UUID,
price_list_data: SupplierPriceListCreate,
tenant_id: UUID,
created_by: UUID
) -> Any:
"""Create a new price list item for a supplier"""
logger.info(
"Creating supplier price list item",
supplier_id=str(supplier_id),
tenant_id=str(tenant_id)
)
# Prepare creation data
create_data = price_list_data.model_dump(exclude_unset=True)
create_data.update({
'tenant_id': tenant_id,
'supplier_id': supplier_id,
'created_by': created_by,
'updated_by': created_by,
})
# Calculate price_per_unit if not provided
if 'price_per_unit' not in create_data or create_data['price_per_unit'] is None:
create_data['price_per_unit'] = create_data['unit_price']
price_list = await self.repository.create_supplier_price_list(create_data)
logger.info(
"Supplier price list item created successfully",
price_list_id=str(price_list.id),
supplier_id=str(supplier_id)
)
return price_list
async def update_supplier_price_list(
self,
price_list_id: UUID,
price_list_data: SupplierPriceListUpdate,
tenant_id: UUID,
updated_by: UUID
) -> Any:
"""Update a price list item"""
logger.info(
"Updating supplier price list item",
price_list_id=str(price_list_id),
tenant_id=str(tenant_id)
)
# Prepare update data
update_data = price_list_data.model_dump(exclude_unset=True)
update_data['updated_by'] = updated_by
update_data['updated_at'] = datetime.now()
price_list = await self.repository.update_supplier_price_list(
price_list_id=price_list_id,
update_data=update_data
)
logger.info(
"Supplier price list item updated successfully",
price_list_id=str(price_list_id)
)
return price_list
async def delete_supplier_price_list(
self,
price_list_id: UUID,
tenant_id: UUID
) -> bool:
"""Delete a price list item"""
logger.info(
"Deleting supplier price list item",
price_list_id=str(price_list_id),
tenant_id=str(tenant_id)
)
success = await self.repository.delete_supplier_price_list(
price_list_id=price_list_id
)
logger.info(
"Supplier price list item deletion completed",
price_list_id=str(price_list_id),
success=success
)
return success