Fix few issues

This commit is contained in:
Urtzi Alfaro
2025-09-26 12:12:17 +02:00
parent d573c38621
commit a27f159e24
32 changed files with 2694 additions and 575 deletions

View File

@@ -1302,4 +1302,169 @@ async def duplicate_quality_template(
except Exception as e:
logger.error("Error duplicating quality template",
error=str(e), tenant_id=str(tenant_id), template_id=str(template_id))
raise HTTPException(status_code=500, detail="Failed to duplicate quality template")
raise HTTPException(status_code=500, detail="Failed to duplicate quality template")
# ================================================================
# TRANSFORMATION ENDPOINTS
# ================================================================
@router.post("/tenants/{tenant_id}/production/batches/{batch_id}/complete-with-transformation", response_model=dict)
async def complete_batch_with_transformation(
transformation_data: Optional[dict] = None,
completion_data: Optional[dict] = None,
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Complete batch and apply transformation (e.g. par-baked to fully baked)"""
try:
result = await production_service.complete_production_batch_with_transformation(
tenant_id, batch_id, completion_data, transformation_data
)
logger.info("Completed batch with transformation",
batch_id=str(batch_id),
has_transformation=bool(transformation_data),
tenant_id=str(tenant_id))
return result
except ValueError as e:
logger.warning("Invalid batch completion with transformation", error=str(e), batch_id=str(batch_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error completing batch with transformation",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to complete batch with transformation")
@router.post("/tenants/{tenant_id}/production/transformations/par-baked-to-fresh", response_model=dict)
async def transform_par_baked_products(
source_ingredient_id: UUID = Query(..., description="Par-baked ingredient ID"),
target_ingredient_id: UUID = Query(..., description="Fresh baked ingredient ID"),
quantity: float = Query(..., gt=0, description="Quantity to transform"),
batch_reference: Optional[str] = Query(None, description="Production batch reference"),
expiration_hours: int = Query(24, ge=1, le=72, description="Hours until expiration after transformation"),
notes: Optional[str] = Query(None, description="Transformation notes"),
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Transform par-baked products to fresh baked products"""
try:
result = await production_service.transform_par_baked_products(
tenant_id=tenant_id,
source_ingredient_id=source_ingredient_id,
target_ingredient_id=target_ingredient_id,
quantity=quantity,
batch_reference=batch_reference,
expiration_hours=expiration_hours,
notes=notes
)
if not result:
raise HTTPException(status_code=400, detail="Failed to create transformation")
logger.info("Transformed par-baked products to fresh",
transformation_id=result.get('transformation_id'),
quantity=quantity, tenant_id=str(tenant_id))
return result
except HTTPException:
raise
except ValueError as e:
logger.warning("Invalid transformation data", error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error transforming par-baked products",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to transform par-baked products")
@router.get("/tenants/{tenant_id}/production/transformations", response_model=dict)
async def get_production_transformations(
tenant_id: UUID = Path(...),
days_back: int = Query(30, ge=1, le=365, description="Days back to retrieve transformations"),
limit: int = Query(100, ge=1, le=500, description="Maximum number of transformations to retrieve"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get transformations related to production processes"""
try:
transformations = await production_service.get_production_transformations(
tenant_id, days_back, limit
)
result = {
"transformations": transformations,
"total_count": len(transformations),
"period_days": days_back,
"retrieved_at": datetime.now().isoformat()
}
logger.info("Retrieved production transformations",
count=len(transformations), tenant_id=str(tenant_id))
return result
except Exception as e:
logger.error("Error getting production transformations",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get production transformations")
@router.get("/tenants/{tenant_id}/production/analytics/transformation-efficiency", response_model=dict)
async def get_transformation_efficiency_analytics(
tenant_id: UUID = Path(...),
days_back: int = Query(30, ge=1, le=365, description="Days back for efficiency analysis"),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get transformation efficiency metrics for analytics"""
try:
metrics = await production_service.get_transformation_efficiency_metrics(
tenant_id, days_back
)
logger.info("Retrieved transformation efficiency analytics",
total_transformations=metrics.get('total_transformations', 0),
tenant_id=str(tenant_id))
return metrics
except Exception as e:
logger.error("Error getting transformation efficiency analytics",
error=str(e), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get transformation efficiency analytics")
@router.get("/tenants/{tenant_id}/production/batches/{batch_id}/transformations", response_model=dict)
async def get_batch_transformations(
tenant_id: UUID = Path(...),
batch_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
production_service: ProductionService = Depends(get_production_service)
):
"""Get batch details with associated transformations"""
try:
result = await production_service.get_batch_with_transformations(tenant_id, batch_id)
if not result:
raise HTTPException(status_code=404, detail="Batch not found")
logger.info("Retrieved batch with transformations",
batch_id=str(batch_id),
transformation_count=result.get('transformation_count', 0),
tenant_id=str(tenant_id))
return result
except HTTPException:
raise
except Exception as e:
logger.error("Error getting batch transformations",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise HTTPException(status_code=500, detail="Failed to get batch transformations")

View File

@@ -658,6 +658,128 @@ class ProductionService:
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise
async def complete_production_batch_with_transformation(
self,
tenant_id: UUID,
batch_id: UUID,
completion_data: Optional[Dict[str, Any]] = None,
transformation_data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Complete production batch and apply transformation if needed"""
try:
async with self.database_manager.get_session() as session:
batch_repo = ProductionBatchRepository(session)
# Complete the batch first
batch = await batch_repo.complete_batch(batch_id, completion_data or {})
# Update inventory for the completed batch
if batch.actual_quantity:
await self._update_inventory_on_completion(tenant_id, batch, batch.actual_quantity)
result = {
"batch": batch.to_dict(),
"transformation": None
}
# Apply transformation if requested and batch produces par-baked goods
if transformation_data and batch.actual_quantity:
transformation_result = await self._apply_batch_transformation(
tenant_id, batch, transformation_data
)
result["transformation"] = transformation_result
logger.info("Completed production batch with transformation",
batch_id=str(batch_id),
has_transformation=bool(transformation_data),
tenant_id=str(tenant_id))
return result
except Exception as e:
logger.error("Error completing production batch with transformation",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
raise
async def transform_par_baked_products(
self,
tenant_id: UUID,
source_ingredient_id: UUID,
target_ingredient_id: UUID,
quantity: float,
batch_reference: Optional[str] = None,
expiration_hours: int = 24,
notes: Optional[str] = None
) -> Optional[Dict[str, Any]]:
"""Transform par-baked products to finished products"""
try:
# Use the inventory client to create the transformation
transformation_result = await self.inventory_client.create_par_bake_transformation(
source_ingredient_id=source_ingredient_id,
target_ingredient_id=target_ingredient_id,
quantity=quantity,
tenant_id=str(tenant_id),
target_batch_number=batch_reference,
expiration_hours=expiration_hours,
notes=notes
)
if transformation_result:
logger.info("Created par-baked transformation",
transformation_id=transformation_result.get('transformation_id'),
source_ingredient=str(source_ingredient_id),
target_ingredient=str(target_ingredient_id),
quantity=quantity,
tenant_id=str(tenant_id))
return transformation_result
except Exception as e:
logger.error("Error transforming par-baked products",
error=str(e),
source_ingredient=str(source_ingredient_id),
target_ingredient=str(target_ingredient_id),
tenant_id=str(tenant_id))
raise
async def _apply_batch_transformation(
self,
tenant_id: UUID,
batch: ProductionBatch,
transformation_data: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
"""Apply transformation after batch completion"""
try:
# Extract transformation parameters
source_ingredient_id = transformation_data.get('source_ingredient_id')
target_ingredient_id = transformation_data.get('target_ingredient_id')
transform_quantity = transformation_data.get('quantity', batch.actual_quantity)
expiration_hours = transformation_data.get('expiration_hours', 24)
notes = transformation_data.get('notes', f"Transformation from batch {batch.batch_number}")
if not source_ingredient_id or not target_ingredient_id:
logger.warning("Missing ingredient IDs for transformation",
batch_id=str(batch.id), transformation_data=transformation_data)
return None
# Create the transformation
transformation_result = await self.transform_par_baked_products(
tenant_id=tenant_id,
source_ingredient_id=UUID(source_ingredient_id),
target_ingredient_id=UUID(target_ingredient_id),
quantity=transform_quantity,
batch_reference=batch.batch_number,
expiration_hours=expiration_hours,
notes=notes
)
return transformation_result
except Exception as e:
logger.error("Error applying batch transformation",
error=str(e), batch_id=str(batch.id), tenant_id=str(tenant_id))
return None
async def get_batch_statistics(
self,
tenant_id: UUID,
@@ -1116,4 +1238,152 @@ class ProductionService:
except Exception as e:
logger.error("Error generating analytics report",
error=str(e), tenant_id=str(tenant_id))
raise
raise
# ================================================================
# TRANSFORMATION METHODS FOR PRODUCTION
# ================================================================
async def get_production_transformations(
self,
tenant_id: UUID,
days_back: int = 30,
limit: int = 100
) -> List[Dict[str, Any]]:
"""Get transformations related to production processes"""
try:
transformations = await self.inventory_client.get_transformations(
tenant_id=str(tenant_id),
source_stage="PAR_BAKED",
target_stage="FULLY_BAKED",
days_back=days_back,
limit=limit
)
logger.info("Retrieved production transformations",
count=len(transformations), tenant_id=str(tenant_id))
return transformations
except Exception as e:
logger.error("Error getting production transformations",
error=str(e), tenant_id=str(tenant_id))
return []
async def get_transformation_efficiency_metrics(
self,
tenant_id: UUID,
days_back: int = 30
) -> Dict[str, Any]:
"""Get transformation efficiency metrics for production analytics"""
try:
# Get transformation summary from inventory service
summary = await self.inventory_client.get_transformation_summary(
tenant_id=str(tenant_id),
days_back=days_back
)
if not summary:
return {
"par_baked_to_fully_baked": {
"total_transformations": 0,
"total_quantity_transformed": 0.0,
"average_conversion_ratio": 0.0,
"efficiency_percentage": 0.0
},
"period_days": days_back,
"transformation_rate": 0.0
}
# Extract par-baked to fully baked metrics
par_baked_metrics = summary.get("par_baked_to_fully_baked", {})
total_transformations = summary.get("total_transformations", 0)
# Calculate transformation rate (transformations per day)
transformation_rate = total_transformations / max(days_back, 1)
result = {
"par_baked_to_fully_baked": {
"total_transformations": par_baked_metrics.get("count", 0),
"total_quantity_transformed": par_baked_metrics.get("total_source_quantity", 0.0),
"average_conversion_ratio": par_baked_metrics.get("average_conversion_ratio", 0.0),
"efficiency_percentage": par_baked_metrics.get("average_conversion_ratio", 0.0) * 100
},
"period_days": days_back,
"transformation_rate": round(transformation_rate, 2),
"total_transformations": total_transformations
}
logger.info("Retrieved transformation efficiency metrics",
total_transformations=total_transformations,
transformation_rate=transformation_rate,
tenant_id=str(tenant_id))
return result
except Exception as e:
logger.error("Error getting transformation efficiency metrics",
error=str(e), tenant_id=str(tenant_id))
return {
"par_baked_to_fully_baked": {
"total_transformations": 0,
"total_quantity_transformed": 0.0,
"average_conversion_ratio": 0.0,
"efficiency_percentage": 0.0
},
"period_days": days_back,
"transformation_rate": 0.0,
"total_transformations": 0
}
async def get_batch_with_transformations(
self,
tenant_id: UUID,
batch_id: UUID
) -> Dict[str, Any]:
"""Get batch details with associated transformations"""
try:
async with self.database_manager.get_session() as session:
batch_repo = ProductionBatchRepository(session)
# Get batch details
batch = await batch_repo.get(batch_id)
if not batch or str(batch.tenant_id) != str(tenant_id):
return {}
batch_data = batch.to_dict()
# Get related transformations from inventory service
# Look for transformations that reference this batch
transformations = await self.inventory_client.get_transformations(
tenant_id=str(tenant_id),
days_back=7, # Look in recent transformations
limit=50
)
# Filter transformations related to this batch
batch_transformations = []
batch_number = batch.batch_number
for transformation in transformations:
# Check if transformation references this batch
if (transformation.get('target_batch_number') == batch_number or
transformation.get('process_notes', '').find(batch_number) >= 0):
batch_transformations.append(transformation)
result = {
"batch": batch_data,
"transformations": batch_transformations,
"transformation_count": len(batch_transformations)
}
logger.info("Retrieved batch with transformations",
batch_id=str(batch_id),
transformation_count=len(batch_transformations),
tenant_id=str(tenant_id))
return result
except Exception as e:
logger.error("Error getting batch with transformations",
error=str(e), batch_id=str(batch_id), tenant_id=str(tenant_id))
return {}