Fix issues 4

This commit is contained in:
Urtzi Alfaro
2025-08-17 15:21:10 +02:00
parent cafd316c4b
commit f33f5d242a
6 changed files with 255 additions and 45 deletions

View File

@@ -547,6 +547,9 @@ export class InventoryService {
*/ */
async getProductsList(tenantId: string): Promise<ProductInfo[]> { async getProductsList(tenantId: string): Promise<ProductInfo[]> {
try { try {
console.log('🔍 Fetching products for forecasting...', { tenantId });
// First try to get finished products (preferred for forecasting)
const response = await apiClient.get(`/tenants/${tenantId}/ingredients`, { const response = await apiClient.get(`/tenants/${tenantId}/ingredients`, {
params: { params: {
limit: 100, limit: 100,
@@ -606,7 +609,47 @@ export class InventoryService {
})) }))
.filter(product => product.inventory_product_id && product.name); .filter(product => product.inventory_product_id && product.name);
console.log('📋 Processed inventory products:', products); console.log('📋 Processed finished products:', products);
// If no finished products found, try to get all products as fallback
if (products.length === 0) {
console.log('⚠️ No finished products found, trying to get all products as fallback...');
const fallbackResponse = await apiClient.get(`/tenants/${tenantId}/ingredients`, {
params: {
limit: 100,
// No product_type filter to get all products
},
});
console.log('🔍 Fallback API Response:', fallbackResponse);
const fallbackDataToProcess = fallbackResponse?.data || fallbackResponse;
let fallbackProductsArray: any[] = [];
if (Array.isArray(fallbackDataToProcess)) {
fallbackProductsArray = fallbackDataToProcess;
} else if (fallbackDataToProcess && typeof fallbackDataToProcess === 'object') {
const keys = Object.keys(fallbackDataToProcess);
if (keys.length > 0 && keys.every(key => !isNaN(Number(key)))) {
fallbackProductsArray = Object.values(fallbackDataToProcess);
}
}
const fallbackProducts: ProductInfo[] = fallbackProductsArray
.map((product: any) => ({
inventory_product_id: product.id || product.inventory_product_id,
name: product.name || product.product_name || `Product ${product.id || ''}`,
category: product.category,
current_stock: product.current_stock,
unit: product.unit,
cost_per_unit: product.cost_per_unit
}))
.filter(product => product.inventory_product_id && product.name);
console.log('📋 Processed fallback products (all inventory items):', fallbackProducts);
return fallbackProducts;
}
return products; return products;

View File

@@ -87,12 +87,10 @@ const ForecastPage: React.FC = () => {
try { try {
const products = await getProductsList(tenantId); const products = await getProductsList(tenantId);
// If no finished products found, use fallback products with trained models // Always use real inventory products - no hardcoded fallbacks
if (products.length === 0) { if (products.length === 0) {
setInventoryItems([ console.warn('⚠️ No finished products found in inventory for forecasting');
{ id: '3ae0afca-3e75-4f93-b6af-2d24c24bfcd5', name: 'Croissants' }, setInventoryItems([]);
{ id: 'b2341c5d-db5d-418e-a978-6d24cd9f039e', name: 'Pan de molde' }
]);
} else { } else {
// Map products to the expected format // Map products to the expected format
setInventoryItems(products.map(p => ({ setInventoryItems(products.map(p => ({
@@ -102,11 +100,8 @@ const ForecastPage: React.FC = () => {
} }
} catch (error) { } catch (error) {
console.error('Failed to load products:', error); console.error('Failed to load products:', error);
// Use fallback products with trained models // Don't use fake fallback products - show empty state instead
setInventoryItems([ setInventoryItems([]);
{ id: '3ae0afca-3e75-4f93-b6af-2d24c24bfcd5', name: 'Croissants' },
{ id: 'b2341c5d-db5d-418e-a978-6d24cd9f039e', name: 'Pan de molde' }
]);
} }
} }
}; };
@@ -462,7 +457,19 @@ const ForecastPage: React.FC = () => {
</div> </div>
)} )}
{forecastData.length === 0 && !isLoading && !isGenerating && ( {inventoryItems.length === 0 && !isLoading && !isGenerating && (
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center">
<AlertTriangle className="h-5 w-5 text-yellow-600 mr-2" />
<span className="text-yellow-800 text-sm">
<strong>No hay productos disponibles para predicciones.</strong>
<br />Para usar esta funcionalidad, necesitas crear productos terminados (como pan, croissants, etc.) en tu inventario con tipo "Producto terminado".
</span>
</div>
</div>
)}
{inventoryItems.length > 0 && forecastData.length === 0 && !isLoading && !isGenerating && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg"> <div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center"> <div className="flex items-center">
<Info className="h-5 w-5 text-blue-600 mr-2" /> <Info className="h-5 w-5 text-blue-600 mr-2" />

View File

@@ -30,24 +30,61 @@ class IngredientRepository(BaseRepository[Ingredient, IngredientCreate, Ingredie
create_data = ingredient_data.model_dump() create_data = ingredient_data.model_dump()
create_data['tenant_id'] = tenant_id create_data['tenant_id'] = tenant_id
# Map 'category' from schema to appropriate model fields # Handle product_type enum conversion
product_type_value = create_data.get('product_type', 'ingredient')
if 'product_type' in create_data:
from app.models.inventory import ProductType
try:
# Convert string to enum object
if isinstance(product_type_value, str):
for enum_member in ProductType:
if enum_member.value == product_type_value or enum_member.name == product_type_value:
create_data['product_type'] = enum_member
break
else:
# If not found, default to INGREDIENT
create_data['product_type'] = ProductType.INGREDIENT
# If it's already an enum, keep it
except Exception:
# Fallback to INGREDIENT if any issues
create_data['product_type'] = ProductType.INGREDIENT
# Handle category mapping based on product type
if 'category' in create_data: if 'category' in create_data:
category_value = create_data.pop('category') category_value = create_data.pop('category')
# For now, assume all items are ingredients and map to ingredient_category
# Convert string to enum object if product_type_value == 'finished_product' or product_type_value == 'FINISHED_PRODUCT':
from app.models.inventory import IngredientCategory # Map to product_category for finished products
try: from app.models.inventory import ProductCategory
# Find the enum member by value if category_value:
for enum_member in IngredientCategory: try:
if enum_member.value == category_value: # Find the enum member by value
create_data['ingredient_category'] = enum_member for enum_member in ProductCategory:
break if enum_member.value == category_value:
else: create_data['product_category'] = enum_member
# If not found, default to OTHER break
create_data['ingredient_category'] = IngredientCategory.OTHER else:
except Exception: # If not found, default to OTHER
# Fallback to OTHER if any issues create_data['product_category'] = ProductCategory.OTHER_PRODUCTS
create_data['ingredient_category'] = IngredientCategory.OTHER except Exception:
# Fallback to OTHER if any issues
create_data['product_category'] = ProductCategory.OTHER_PRODUCTS
else:
# Map to ingredient_category for ingredients
from app.models.inventory import IngredientCategory
if category_value:
try:
# Find the enum member by value
for enum_member in IngredientCategory:
if enum_member.value == category_value:
create_data['ingredient_category'] = enum_member
break
else:
# If not found, default to OTHER
create_data['ingredient_category'] = IngredientCategory.OTHER
except Exception:
# Fallback to OTHER if any issues
create_data['ingredient_category'] = IngredientCategory.OTHER
# Convert unit_of_measure string to enum object # Convert unit_of_measure string to enum object
if 'unit_of_measure' in create_data: if 'unit_of_measure' in create_data:
@@ -81,6 +118,92 @@ class IngredientRepository(BaseRepository[Ingredient, IngredientCreate, Ingredie
logger.error("Failed to create ingredient", error=str(e), tenant_id=tenant_id) logger.error("Failed to create ingredient", error=str(e), tenant_id=tenant_id)
raise raise
async def update(self, record_id: Any, obj_in: IngredientUpdate, **kwargs) -> Optional[Ingredient]:
"""Override update to handle product_type and category enum conversions"""
try:
# Prepare data and map schema fields to model fields
update_data = obj_in.model_dump(exclude_unset=True)
# Handle product_type enum conversion
if 'product_type' in update_data:
product_type_value = update_data['product_type']
from app.models.inventory import ProductType
try:
# Convert string to enum object
if isinstance(product_type_value, str):
for enum_member in ProductType:
if enum_member.value == product_type_value or enum_member.name == product_type_value:
update_data['product_type'] = enum_member
break
else:
# If not found, keep original value (don't update)
del update_data['product_type']
# If it's already an enum, keep it
except Exception:
# Remove invalid product_type to avoid update
del update_data['product_type']
# Handle category mapping based on product type
if 'category' in update_data:
category_value = update_data.pop('category')
product_type_value = update_data.get('product_type', 'ingredient')
# Get current product if we need to determine type
if 'product_type' not in update_data:
current_record = await self.get_by_id(record_id)
if current_record:
product_type_value = current_record.product_type.value if current_record.product_type else 'ingredient'
if product_type_value == 'finished_product' or product_type_value == 'FINISHED_PRODUCT':
# Map to product_category for finished products
from app.models.inventory import ProductCategory
if category_value:
try:
for enum_member in ProductCategory:
if enum_member.value == category_value:
update_data['product_category'] = enum_member
# Clear ingredient_category when setting product_category
update_data['ingredient_category'] = None
break
except Exception:
pass
else:
# Map to ingredient_category for ingredients
from app.models.inventory import IngredientCategory
if category_value:
try:
for enum_member in IngredientCategory:
if enum_member.value == category_value:
update_data['ingredient_category'] = enum_member
# Clear product_category when setting ingredient_category
update_data['product_category'] = None
break
except Exception:
pass
# Handle unit_of_measure enum conversion
if 'unit_of_measure' in update_data:
unit_value = update_data['unit_of_measure']
from app.models.inventory import UnitOfMeasure
try:
if isinstance(unit_value, str):
for enum_member in UnitOfMeasure:
if enum_member.value == unit_value:
update_data['unit_of_measure'] = enum_member
break
else:
# If not found, keep original value
del update_data['unit_of_measure']
except Exception:
del update_data['unit_of_measure']
# Call parent update method
return await super().update(record_id, update_data, **kwargs)
except Exception as e:
logger.error("Failed to update ingredient", error=str(e), record_id=record_id)
raise
async def get_ingredients_by_tenant( async def get_ingredients_by_tenant(
self, self,
tenant_id: UUID, tenant_id: UUID,

View File

@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field, validator
from typing import Generic, TypeVar from typing import Generic, TypeVar
from enum import Enum from enum import Enum
from app.models.inventory import UnitOfMeasure, IngredientCategory, StockMovementType from app.models.inventory import UnitOfMeasure, IngredientCategory, StockMovementType, ProductType, ProductCategory
T = TypeVar('T') T = TypeVar('T')
@@ -32,11 +32,12 @@ class InventoryBaseSchema(BaseModel):
# ===== INGREDIENT SCHEMAS ===== # ===== INGREDIENT SCHEMAS =====
class IngredientCreate(InventoryBaseSchema): class IngredientCreate(InventoryBaseSchema):
"""Schema for creating ingredients""" """Schema for creating ingredients and finished products"""
name: str = Field(..., max_length=255, description="Ingredient name") name: str = Field(..., max_length=255, description="Product name")
product_type: ProductType = Field(ProductType.INGREDIENT, description="Type of product (ingredient or finished_product)")
sku: Optional[str] = Field(None, max_length=100, description="SKU code") sku: Optional[str] = Field(None, max_length=100, description="SKU code")
barcode: Optional[str] = Field(None, max_length=50, description="Barcode") barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
category: IngredientCategory = Field(..., description="Ingredient category") category: Optional[str] = Field(None, description="Product category (ingredient or finished product category)")
subcategory: Optional[str] = Field(None, max_length=100, description="Subcategory") subcategory: Optional[str] = Field(None, max_length=100, description="Subcategory")
description: Optional[str] = Field(None, description="Ingredient description") description: Optional[str] = Field(None, description="Ingredient description")
brand: Optional[str] = Field(None, max_length=100, description="Brand name") brand: Optional[str] = Field(None, max_length=100, description="Brand name")
@@ -83,11 +84,12 @@ class IngredientCreate(InventoryBaseSchema):
class IngredientUpdate(InventoryBaseSchema): class IngredientUpdate(InventoryBaseSchema):
"""Schema for updating ingredients""" """Schema for updating ingredients and finished products"""
name: Optional[str] = Field(None, max_length=255, description="Ingredient name") name: Optional[str] = Field(None, max_length=255, description="Product name")
product_type: Optional[ProductType] = Field(None, description="Type of product (ingredient or finished_product)")
sku: Optional[str] = Field(None, max_length=100, description="SKU code") sku: Optional[str] = Field(None, max_length=100, description="SKU code")
barcode: Optional[str] = Field(None, max_length=50, description="Barcode") barcode: Optional[str] = Field(None, max_length=50, description="Barcode")
category: Optional[IngredientCategory] = Field(None, description="Ingredient category") category: Optional[str] = Field(None, description="Product category")
subcategory: Optional[str] = Field(None, max_length=100, description="Subcategory") subcategory: Optional[str] = Field(None, max_length=100, description="Subcategory")
description: Optional[str] = Field(None, description="Ingredient description") description: Optional[str] = Field(None, description="Ingredient description")
brand: Optional[str] = Field(None, max_length=100, description="Brand name") brand: Optional[str] = Field(None, max_length=100, description="Brand name")
@@ -122,13 +124,14 @@ class IngredientUpdate(InventoryBaseSchema):
class IngredientResponse(InventoryBaseSchema): class IngredientResponse(InventoryBaseSchema):
"""Schema for ingredient API responses""" """Schema for ingredient and finished product API responses"""
id: str id: str
tenant_id: str tenant_id: str
name: str name: str
product_type: ProductType
sku: Optional[str] sku: Optional[str]
barcode: Optional[str] barcode: Optional[str]
category: IngredientCategory category: Optional[str] # Will be populated from ingredient_category or product_category
subcategory: Optional[str] subcategory: Optional[str]
description: Optional[str] description: Optional[str]
brand: Optional[str] brand: Optional[str]

View File

@@ -61,7 +61,15 @@ class InventoryService:
ingredient = await repository.create_ingredient(ingredient_data, tenant_id) ingredient = await repository.create_ingredient(ingredient_data, tenant_id)
# Convert to response schema # Convert to response schema
response = IngredientResponse(**ingredient.to_dict()) ingredient_dict = ingredient.to_dict()
# Map category field based on product type
if ingredient.product_type and ingredient.product_type.value == 'finished_product':
ingredient_dict['category'] = ingredient.product_category.value if ingredient.product_category else None
else:
ingredient_dict['category'] = ingredient.ingredient_category.value if ingredient.ingredient_category else None
response = IngredientResponse(**ingredient_dict)
# Add computed fields # Add computed fields
response.current_stock = 0.0 response.current_stock = 0.0
@@ -90,7 +98,15 @@ class InventoryService:
stock_totals = await stock_repo.get_total_stock_by_ingredient(tenant_id, ingredient_id) stock_totals = await stock_repo.get_total_stock_by_ingredient(tenant_id, ingredient_id)
# Convert to response schema # Convert to response schema
response = IngredientResponse(**ingredient.to_dict()) ingredient_dict = ingredient.to_dict()
# Map category field based on product type
if ingredient.product_type and ingredient.product_type.value == 'finished_product':
ingredient_dict['category'] = ingredient.product_category.value if ingredient.product_category else None
else:
ingredient_dict['category'] = ingredient.ingredient_category.value if ingredient.ingredient_category else None
response = IngredientResponse(**ingredient_dict)
response.current_stock = stock_totals['total_available'] response.current_stock = stock_totals['total_available']
response.is_low_stock = stock_totals['total_available'] <= ingredient.low_stock_threshold response.is_low_stock = stock_totals['total_available'] <= ingredient.low_stock_threshold
response.needs_reorder = stock_totals['total_available'] <= ingredient.reorder_point response.needs_reorder = stock_totals['total_available'] <= ingredient.reorder_point
@@ -138,7 +154,15 @@ class InventoryService:
stock_totals = await stock_repo.get_total_stock_by_ingredient(tenant_id, ingredient_id) stock_totals = await stock_repo.get_total_stock_by_ingredient(tenant_id, ingredient_id)
# Convert to response schema # Convert to response schema
response = IngredientResponse(**updated_ingredient.to_dict()) ingredient_dict = updated_ingredient.to_dict()
# Map category field based on product type
if updated_ingredient.product_type and updated_ingredient.product_type.value == 'finished_product':
ingredient_dict['category'] = updated_ingredient.product_category.value if updated_ingredient.product_category else None
else:
ingredient_dict['category'] = updated_ingredient.ingredient_category.value if updated_ingredient.ingredient_category else None
response = IngredientResponse(**ingredient_dict)
response.current_stock = stock_totals['total_available'] response.current_stock = stock_totals['total_available']
response.is_low_stock = stock_totals['total_available'] <= updated_ingredient.low_stock_threshold response.is_low_stock = stock_totals['total_available'] <= updated_ingredient.low_stock_threshold
response.needs_reorder = stock_totals['total_available'] <= updated_ingredient.reorder_point response.needs_reorder = stock_totals['total_available'] <= updated_ingredient.reorder_point
@@ -173,7 +197,15 @@ class InventoryService:
stock_totals = await stock_repo.get_total_stock_by_ingredient(tenant_id, ingredient.id) stock_totals = await stock_repo.get_total_stock_by_ingredient(tenant_id, ingredient.id)
# Convert to response schema # Convert to response schema
response = IngredientResponse(**ingredient.to_dict()) ingredient_dict = ingredient.to_dict()
# Map category field based on product type
if ingredient.product_type and ingredient.product_type.value == 'finished_product':
ingredient_dict['category'] = ingredient.product_category.value if ingredient.product_category else None
else:
ingredient_dict['category'] = ingredient.ingredient_category.value if ingredient.ingredient_category else None
response = IngredientResponse(**ingredient_dict)
response.current_stock = stock_totals['total_available'] response.current_stock = stock_totals['total_available']
response.is_low_stock = stock_totals['total_available'] <= ingredient.low_stock_threshold response.is_low_stock = stock_totals['total_available'] <= ingredient.low_stock_threshold
response.needs_reorder = stock_totals['total_available'] <= ingredient.reorder_point response.needs_reorder = stock_totals['total_available'] <= ingredient.reorder_point

View File

@@ -627,13 +627,14 @@ class EnhancedTrainingService:
"status": status or "pending", "status": status or "pending",
"progress": progress or 0, "progress": progress or 0,
"current_step": current_step or "initializing", "current_step": current_step or "initializing",
"start_time": datetime.utcnow() "start_time": datetime.now(timezone.utc)
} }
if error_message: if error_message:
log_data["error_message"] = error_message log_data["error_message"] = error_message
if results: if results:
log_data["results"] = results # Ensure results are JSON-serializable before storing
log_data["results"] = make_json_serializable(results)
await self.training_log_repo.create_training_log(log_data) await self.training_log_repo.create_training_log(log_data)
logger.info("Created initial training log", job_id=job_id, tenant_id=tenant_id) logger.info("Created initial training log", job_id=job_id, tenant_id=tenant_id)
@@ -655,9 +656,10 @@ class EnhancedTrainingService:
if error_message: if error_message:
update_data["error_message"] = error_message update_data["error_message"] = error_message
if results: if results:
update_data["results"] = results # Ensure results are JSON-serializable before storing
update_data["results"] = make_json_serializable(results)
if status in ["completed", "failed"]: if status in ["completed", "failed"]:
update_data["end_time"] = datetime.utcnow() update_data["end_time"] = datetime.now(timezone.utc)
if update_data: if update_data:
await self.training_log_repo.update(existing_log.id, update_data) await self.training_log_repo.update(existing_log.id, update_data)