Improve the frontend modals

This commit is contained in:
Urtzi Alfaro
2025-10-27 16:33:26 +01:00
parent 61376b7a9f
commit 858d985c92
143 changed files with 9289 additions and 2306 deletions

View File

@@ -132,7 +132,6 @@ async def clone_demo_data(
instructions=recipe.instructions,
preparation_notes=recipe.preparation_notes,
storage_instructions=recipe.storage_instructions,
quality_standards=recipe.quality_standards,
serves_count=recipe.serves_count,
nutritional_info=recipe.nutritional_info,
allergen_info=recipe.allergen_info,
@@ -142,9 +141,7 @@ async def clone_demo_data(
maximum_batch_size=recipe.maximum_batch_size,
optimal_production_temperature=recipe.optimal_production_temperature,
optimal_humidity=recipe.optimal_humidity,
quality_check_points=recipe.quality_check_points,
quality_check_configuration=recipe.quality_check_configuration,
common_issues=recipe.common_issues,
status=recipe.status,
is_seasonal=recipe.is_seasonal,
season_start_month=recipe.season_start_month,

View File

@@ -3,7 +3,7 @@
Recipes API - Atomic CRUD operations on Recipe model
"""
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from fastapi import APIRouter, Depends, HTTPException, Header, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from uuid import UUID
@@ -18,6 +18,7 @@ from ..schemas.recipes import (
)
from shared.routing import RouteBuilder, RouteCategory
from shared.auth.access_control import require_user_role
from shared.auth.decorators import get_current_user_dep
from shared.security import create_audit_logger, AuditSeverity, AuditAction
route_builder = RouteBuilder('recipes')
@@ -43,6 +44,7 @@ async def create_recipe(
tenant_id: UUID,
recipe_data: RecipeCreate,
user_id: UUID = Depends(get_user_id),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create a new recipe"""
@@ -86,6 +88,7 @@ async def search_recipes(
difficulty_level: Optional[int] = Query(None, ge=1, le=5),
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Search recipes with filters"""
@@ -135,7 +138,7 @@ async def count_recipes(
return {"count": count}
except Exception as e:
logger.error(f"Error counting recipes for tenant {tenant_id}: {e}")
logger.error(f"Error counting recipes for tenant: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@@ -151,6 +154,7 @@ async def get_recipe(
"""Get recipe by ID with ingredients"""
try:
recipe_service = RecipeService(db)
recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not recipe:
@@ -178,6 +182,7 @@ async def update_recipe(
recipe_id: UUID,
recipe_data: RecipeUpdate,
user_id: UUID = Depends(get_user_id),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update an existing recipe"""
@@ -224,6 +229,7 @@ async def delete_recipe(
tenant_id: UUID,
recipe_id: UUID,
user_id: UUID = Depends(get_user_id),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Delete a recipe (Admin+ only)"""
@@ -237,6 +243,20 @@ async def delete_recipe(
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
# Check if deletion is safe
summary = await recipe_service.get_deletion_summary(recipe_id)
if not summary["success"]:
raise HTTPException(status_code=500, detail=summary["error"])
if not summary["data"]["can_delete"]:
raise HTTPException(
status_code=400,
detail={
"message": "Cannot delete recipe with active dependencies",
"warnings": summary["data"]["warnings"]
}
)
# Capture recipe data before deletion
recipe_data = {
"recipe_name": existing_recipe.get("name"),
@@ -281,3 +301,91 @@ async def delete_recipe(
except Exception as e:
logger.error(f"Error deleting recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.patch(
route_builder.build_custom_route(RouteCategory.OPERATIONS, ["{recipe_id}", "archive"])
)
@require_user_role(['admin', 'owner'])
async def archive_recipe(
tenant_id: UUID,
recipe_id: UUID,
user_id: UUID = Depends(get_user_id),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Archive (soft delete) a recipe by setting status to ARCHIVED"""
try:
recipe_service = RecipeService(db)
existing_recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Not authorized")
# Check status transitions (business rule)
current_status = existing_recipe.get("status")
if current_status == "DISCONTINUED":
raise HTTPException(
status_code=400,
detail="Cannot archive a discontinued recipe. Use hard delete instead."
)
# Update status to ARCHIVED
from ..schemas.recipes import RecipeUpdate, RecipeStatus
update_data = RecipeUpdate(status=RecipeStatus.ARCHIVED)
updated_recipe = await recipe_service.update_recipe(
recipe_id,
update_data.dict(exclude_unset=True),
user_id
)
if not updated_recipe["success"]:
raise HTTPException(status_code=400, detail=updated_recipe["error"])
logger.info(f"Archived recipe {recipe_id} by user {user_id}")
return RecipeResponse(**updated_recipe["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error archiving recipe: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get(
route_builder.build_custom_route(RouteCategory.OPERATIONS, ["{recipe_id}", "deletion-summary"])
)
@require_user_role(['admin', 'owner'])
async def get_recipe_deletion_summary(
tenant_id: UUID,
recipe_id: UUID,
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get summary of what will be affected by deleting this recipe"""
try:
recipe_service = RecipeService(db)
existing_recipe = await recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Not authorized")
summary = await recipe_service.get_deletion_summary(recipe_id)
if not summary["success"]:
raise HTTPException(status_code=500, detail=summary["error"])
return summary["data"]
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting deletion summary: {e}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -95,7 +95,6 @@ class Recipe(Base):
instructions = Column(JSONB, nullable=True) # Structured step-by-step instructions
preparation_notes = Column(Text, nullable=True)
storage_instructions = Column(Text, nullable=True)
quality_standards = Column(Text, nullable=True)
# Recipe metadata
serves_count = Column(Integer, nullable=True) # How many people/portions
@@ -111,9 +110,7 @@ class Recipe(Base):
optimal_humidity = Column(Float, nullable=True) # Percentage
# Quality control
quality_check_points = Column(JSONB, nullable=True) # Key checkpoints during production
quality_check_configuration = Column(JSONB, nullable=True) # Stage-based quality check config
common_issues = Column(JSONB, nullable=True) # Known issues and solutions
# Status and lifecycle
status = Column(SQLEnum(RecipeStatus), nullable=False, default=RecipeStatus.DRAFT, index=True)
@@ -170,7 +167,6 @@ class Recipe(Base):
'instructions': self.instructions,
'preparation_notes': self.preparation_notes,
'storage_instructions': self.storage_instructions,
'quality_standards': self.quality_standards,
'serves_count': self.serves_count,
'nutritional_info': self.nutritional_info,
'allergen_info': self.allergen_info,
@@ -180,9 +176,7 @@ class Recipe(Base):
'maximum_batch_size': self.maximum_batch_size,
'optimal_production_temperature': self.optimal_production_temperature,
'optimal_humidity': self.optimal_humidity,
'quality_check_points': self.quality_check_points,
'quality_check_configuration': self.quality_check_configuration,
'common_issues': self.common_issues,
'status': self.status.value if self.status else None,
'is_seasonal': self.is_seasonal,
'season_start_month': self.season_start_month,

View File

@@ -47,8 +47,8 @@ class RecipeRepository(BaseRepository[Recipe, RecipeCreate, RecipeUpdate]):
"category": recipe.category,
"cuisine_type": recipe.cuisine_type,
"difficulty_level": recipe.difficulty_level,
"yield_quantity": recipe.yield_quantity,
"yield_unit": recipe.yield_unit.value if recipe.yield_unit else None,
"yield_quantity": float(recipe.yield_quantity),
"yield_unit": recipe.yield_unit.value if hasattr(recipe.yield_unit, 'value') else recipe.yield_unit,
"prep_time_minutes": recipe.prep_time_minutes,
"cook_time_minutes": recipe.cook_time_minutes,
"total_time_minutes": recipe.total_time_minutes,
@@ -61,29 +61,46 @@ class RecipeRepository(BaseRepository[Recipe, RecipeCreate, RecipeUpdate]):
"instructions": recipe.instructions,
"preparation_notes": recipe.preparation_notes,
"storage_instructions": recipe.storage_instructions,
"quality_standards": recipe.quality_standards,
"quality_check_configuration": recipe.quality_check_configuration,
"serves_count": recipe.serves_count,
"nutritional_info": recipe.nutritional_info,
"allergen_info": recipe.allergen_info,
"dietary_tags": recipe.dietary_tags,
"batch_size_multiplier": recipe.batch_size_multiplier,
"minimum_batch_size": recipe.minimum_batch_size,
"maximum_batch_size": recipe.maximum_batch_size,
"status": recipe.status,
"batch_size_multiplier": float(recipe.batch_size_multiplier),
"minimum_batch_size": float(recipe.minimum_batch_size) if recipe.minimum_batch_size else None,
"maximum_batch_size": float(recipe.maximum_batch_size) if recipe.maximum_batch_size else None,
"optimal_production_temperature": float(recipe.optimal_production_temperature) if recipe.optimal_production_temperature else None,
"optimal_humidity": float(recipe.optimal_humidity) if recipe.optimal_humidity else None,
"status": recipe.status.value if hasattr(recipe.status, 'value') else recipe.status,
"is_seasonal": recipe.is_seasonal,
"season_start_month": recipe.season_start_month,
"season_end_month": recipe.season_end_month,
"is_signature_item": recipe.is_signature_item,
"created_at": recipe.created_at.isoformat() if recipe.created_at else None,
"updated_at": recipe.updated_at.isoformat() if recipe.updated_at else None,
"created_by": str(recipe.created_by) if recipe.created_by else None,
"updated_by": str(recipe.updated_by) if hasattr(recipe, 'updated_by') and recipe.updated_by else None,
"ingredients": [
{
"id": str(ingredient.id),
"tenant_id": str(ingredient.tenant_id),
"recipe_id": str(ingredient.recipe_id),
"ingredient_id": str(ingredient.ingredient_id),
"quantity": float(ingredient.quantity),
"unit": ingredient.unit,
"unit": ingredient.unit.value if hasattr(ingredient.unit, 'value') else ingredient.unit,
"quantity_in_base_unit": float(ingredient.quantity_in_base_unit) if ingredient.quantity_in_base_unit else None,
"alternative_quantity": float(ingredient.alternative_quantity) if ingredient.alternative_quantity else None,
"alternative_unit": ingredient.alternative_unit.value if hasattr(ingredient.alternative_unit, 'value') and ingredient.alternative_unit else None,
"preparation_method": ingredient.preparation_method,
"notes": ingredient.notes
"ingredient_notes": ingredient.ingredient_notes,
"is_optional": ingredient.is_optional,
"ingredient_order": ingredient.ingredient_order,
"ingredient_group": ingredient.ingredient_group,
"substitution_options": ingredient.substitution_options,
"substitution_ratio": float(ingredient.substitution_ratio) if ingredient.substitution_ratio else None,
"unit_cost": float(ingredient.unit_cost) if hasattr(ingredient, 'unit_cost') and ingredient.unit_cost else None,
"total_cost": float(ingredient.total_cost) if hasattr(ingredient, 'total_cost') and ingredient.total_cost else None,
"cost_updated_at": ingredient.cost_updated_at.isoformat() if hasattr(ingredient, 'cost_updated_at') and ingredient.cost_updated_at else None
}
for ingredient in recipe.ingredients
] if hasattr(recipe, 'ingredients') else []
@@ -151,8 +168,8 @@ class RecipeRepository(BaseRepository[Recipe, RecipeCreate, RecipeUpdate]):
"category": recipe.category,
"cuisine_type": recipe.cuisine_type,
"difficulty_level": recipe.difficulty_level,
"yield_quantity": recipe.yield_quantity,
"yield_unit": recipe.yield_unit.value if recipe.yield_unit else None,
"yield_quantity": float(recipe.yield_quantity),
"yield_unit": recipe.yield_unit.value if hasattr(recipe.yield_unit, 'value') else recipe.yield_unit,
"prep_time_minutes": recipe.prep_time_minutes,
"cook_time_minutes": recipe.cook_time_minutes,
"total_time_minutes": recipe.total_time_minutes,
@@ -165,21 +182,26 @@ class RecipeRepository(BaseRepository[Recipe, RecipeCreate, RecipeUpdate]):
"instructions": recipe.instructions,
"preparation_notes": recipe.preparation_notes,
"storage_instructions": recipe.storage_instructions,
"quality_standards": recipe.quality_standards,
"quality_check_configuration": recipe.quality_check_configuration,
"serves_count": recipe.serves_count,
"nutritional_info": recipe.nutritional_info,
"allergen_info": recipe.allergen_info,
"dietary_tags": recipe.dietary_tags,
"batch_size_multiplier": recipe.batch_size_multiplier,
"minimum_batch_size": recipe.minimum_batch_size,
"maximum_batch_size": recipe.maximum_batch_size,
"status": recipe.status,
"batch_size_multiplier": float(recipe.batch_size_multiplier),
"minimum_batch_size": float(recipe.minimum_batch_size) if recipe.minimum_batch_size else None,
"maximum_batch_size": float(recipe.maximum_batch_size) if recipe.maximum_batch_size else None,
"optimal_production_temperature": float(recipe.optimal_production_temperature) if recipe.optimal_production_temperature else None,
"optimal_humidity": float(recipe.optimal_humidity) if recipe.optimal_humidity else None,
"status": recipe.status.value if hasattr(recipe.status, 'value') else recipe.status,
"is_seasonal": recipe.is_seasonal,
"season_start_month": recipe.season_start_month,
"season_end_month": recipe.season_end_month,
"is_signature_item": recipe.is_signature_item,
"created_at": recipe.created_at.isoformat() if recipe.created_at else None,
"updated_at": recipe.updated_at.isoformat() if recipe.updated_at else None
"updated_at": recipe.updated_at.isoformat() if recipe.updated_at else None,
"created_by": str(recipe.created_by) if recipe.created_by else None,
"updated_by": str(recipe.updated_by) if hasattr(recipe, 'updated_by') and recipe.updated_by else None,
"ingredients": [] # For list view, don't load ingredients to improve performance
}
for recipe in recipes
]

View File

@@ -117,7 +117,6 @@ class RecipeCreate(BaseModel):
instructions: Optional[Dict[str, Any]] = None
preparation_notes: Optional[str] = None
storage_instructions: Optional[str] = None
quality_standards: Optional[str] = None
quality_check_configuration: Optional[RecipeQualityConfiguration] = None
serves_count: Optional[int] = Field(None, ge=1)
nutritional_info: Optional[Dict[str, Any]] = None
@@ -128,8 +127,6 @@ class RecipeCreate(BaseModel):
maximum_batch_size: Optional[float] = Field(None, gt=0)
optimal_production_temperature: Optional[float] = None
optimal_humidity: Optional[float] = Field(None, ge=0, le=100)
quality_check_points: Optional[Dict[str, Any]] = None
common_issues: Optional[Dict[str, Any]] = None
is_seasonal: bool = False
season_start_month: Optional[int] = Field(None, ge=1, le=12)
season_end_month: Optional[int] = Field(None, ge=1, le=12)
@@ -156,7 +153,6 @@ class RecipeUpdate(BaseModel):
instructions: Optional[Dict[str, Any]] = None
preparation_notes: Optional[str] = None
storage_instructions: Optional[str] = None
quality_standards: Optional[str] = None
quality_check_configuration: Optional[RecipeQualityConfigurationUpdate] = None
serves_count: Optional[int] = Field(None, ge=1)
nutritional_info: Optional[Dict[str, Any]] = None
@@ -167,8 +163,6 @@ class RecipeUpdate(BaseModel):
maximum_batch_size: Optional[float] = Field(None, gt=0)
optimal_production_temperature: Optional[float] = None
optimal_humidity: Optional[float] = Field(None, ge=0, le=100)
quality_check_points: Optional[Dict[str, Any]] = None
common_issues: Optional[Dict[str, Any]] = None
status: Optional[RecipeStatus] = None
is_seasonal: Optional[bool] = None
season_start_month: Optional[int] = Field(None, ge=1, le=12)
@@ -204,7 +198,6 @@ class RecipeResponse(BaseModel):
instructions: Optional[Dict[str, Any]] = None
preparation_notes: Optional[str] = None
storage_instructions: Optional[str] = None
quality_standards: Optional[str] = None
quality_check_configuration: Optional[RecipeQualityConfiguration] = None
serves_count: Optional[int] = None
nutritional_info: Optional[Dict[str, Any]] = None
@@ -215,8 +208,6 @@ class RecipeResponse(BaseModel):
maximum_batch_size: Optional[float] = None
optimal_production_temperature: Optional[float] = None
optimal_humidity: Optional[float] = None
quality_check_points: Optional[Dict[str, Any]] = None
common_issues: Optional[Dict[str, Any]] = None
status: str
is_seasonal: bool
season_start_month: Optional[int] = None
@@ -232,6 +223,20 @@ class RecipeResponse(BaseModel):
from_attributes = True
class RecipeDeletionSummary(BaseModel):
"""Summary of what will be deleted when hard-deleting a recipe"""
recipe_id: UUID
recipe_name: str
recipe_code: str
production_batches_count: int
recipe_ingredients_count: int
dependent_recipes_count: int # Recipes that use this as ingredient/sub-recipe
affected_orders_count: int # Orders that include this recipe
last_used_date: Optional[datetime] = None
can_delete: bool
warnings: List[str] = []
class RecipeSearchRequest(BaseModel):
"""Schema for recipe search requests"""
search_term: Optional[str] = None

View File

@@ -67,6 +67,83 @@ class RecipeService:
logger.error(f"Error getting recipe statistics: {e}")
return {"total_recipes": 0, "active_recipes": 0, "signature_recipes": 0, "seasonal_recipes": 0}
async def get_deletion_summary(self, recipe_id: UUID) -> Dict[str, Any]:
"""Get summary of what will be affected by deleting this recipe"""
try:
from sqlalchemy import select, func
from ..models.recipes import RecipeIngredient
# Get recipe info
recipe = await self.recipe_repo.get_by_id(recipe_id)
if not recipe:
return {"success": False, "error": "Recipe not found"}
# Count recipe ingredients
ingredients_result = await self.session.execute(
select(func.count(RecipeIngredient.id))
.where(RecipeIngredient.recipe_id == recipe_id)
)
ingredients_count = ingredients_result.scalar() or 0
# Count production batches using this recipe (if production tables exist)
production_batches_count = 0
try:
# Try to import production models if they exist
production_batches_result = await self.session.execute(
select(func.count()).select_from(
select(1).where(
# This would need actual production_batches table reference
# For now, set to 0
).subquery()
)
)
production_batches_count = 0 # Set to 0 for now
except:
production_batches_count = 0
# Count dependent recipes (recipes using this as ingredient) - future feature
dependent_recipes_count = 0
# Count affected orders - would need orders service integration
affected_orders_count = 0
# Determine if deletion is safe
warnings = []
can_delete = True
if production_batches_count > 0:
warnings.append(f"Esta receta tiene {production_batches_count} lotes de producción asociados")
can_delete = False
if affected_orders_count > 0:
warnings.append(f"Esta receta está en {affected_orders_count} pedidos")
can_delete = False
if dependent_recipes_count > 0:
warnings.append(f"{dependent_recipes_count} recetas dependen de esta")
if recipe.status == RecipeStatus.ACTIVE:
warnings.append("Esta receta está activa. Considera archivarla primero.")
return {
"success": True,
"data": {
"recipe_id": str(recipe.id),
"recipe_name": recipe.name,
"recipe_code": recipe.recipe_code or "",
"production_batches_count": production_batches_count,
"recipe_ingredients_count": ingredients_count,
"dependent_recipes_count": dependent_recipes_count,
"affected_orders_count": affected_orders_count,
"last_used_date": None,
"can_delete": can_delete,
"warnings": warnings
}
}
except Exception as e:
logger.error(f"Error getting deletion summary: {e}")
return {"success": False, "error": str(e)}
async def create_recipe(
self,
recipe_data: Dict[str, Any],
@@ -74,17 +151,35 @@ class RecipeService:
created_by: UUID
) -> Dict[str, Any]:
"""Create a new recipe with ingredients"""
from ..models.recipes import Recipe, RecipeIngredient, RecipeStatus
try:
# Add metadata
recipe_data["created_by"] = created_by
recipe_data["created_at"] = datetime.utcnow()
recipe_data["updated_at"] = datetime.utcnow()
recipe_data["status"] = recipe_data.get("status", RecipeStatus.DRAFT)
# Use the shared repository's create method
recipe_create = RecipeCreate(**recipe_data)
recipe = await self.recipe_repo.create(recipe_create)
# Create Recipe model directly (without ingredients)
recipe = Recipe(**recipe_data)
self.session.add(recipe)
await self.session.flush() # Get the recipe ID
# Get the created recipe with ingredients (if the repository supports it)
# Now create ingredients with the recipe_id and tenant_id
for ing_data in ingredients_data:
ingredient = RecipeIngredient(
recipe_id=recipe.id,
tenant_id=recipe.tenant_id, # Add tenant_id from recipe
**ing_data
)
self.session.add(ingredient)
await self.session.flush()
# Commit the transaction to persist changes
await self.session.commit()
# Get the created recipe with ingredients
result = await self.recipe_repo.get_recipe_with_ingredients(recipe.id)
return {
@@ -117,6 +212,45 @@ class RecipeService:
"error": "Recipe not found"
}
# Status transition business rules
if "status" in recipe_data:
from ..models.recipes import RecipeStatus
new_status = recipe_data["status"]
current_status = existing_recipe.status
# Cannot reactivate discontinued recipes
if current_status == RecipeStatus.DISCONTINUED:
if new_status != RecipeStatus.DISCONTINUED:
return {
"success": False,
"error": "Cannot reactivate a discontinued recipe. Create a new version instead."
}
# Can only archive active or testing recipes
if new_status == RecipeStatus.ARCHIVED:
if current_status not in [RecipeStatus.ACTIVE, RecipeStatus.TESTING]:
return {
"success": False,
"error": "Can only archive active or testing recipes."
}
# Cannot activate drafts without ingredients
if new_status == RecipeStatus.ACTIVE and current_status == RecipeStatus.DRAFT:
# Check if recipe has ingredients
from sqlalchemy import select, func
from ..models.recipes import RecipeIngredient
result = await self.session.execute(
select(func.count(RecipeIngredient.id)).where(RecipeIngredient.recipe_id == recipe_id)
)
ingredient_count = result.scalar()
if ingredient_count == 0:
return {
"success": False,
"error": "Cannot activate a recipe without ingredients."
}
# Add metadata
if updated_by:
recipe_data["updated_by"] = updated_by

View File

@@ -0,0 +1,34 @@
"""remove legacy quality fields
Revision ID: 20251027_remove_quality
Revises: 3c4d0f57a312
Create Date: 2025-10-27
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '20251027_remove_quality'
down_revision = '3c4d0f57a312'
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Remove deprecated quality fields from recipes table"""
# Drop columns that are no longer used
# Using batch operations for safer column drops
with op.batch_alter_table('recipes', schema=None) as batch_op:
batch_op.drop_column('quality_standards')
batch_op.drop_column('quality_check_points')
batch_op.drop_column('common_issues')
def downgrade() -> None:
"""Restore deprecated quality fields (for rollback purposes only)"""
# Add back the columns in case of rollback
op.add_column('recipes', sa.Column('quality_standards', sa.Text(), nullable=True))
op.add_column('recipes', sa.Column('quality_check_points', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
op.add_column('recipes', sa.Column('common_issues', postgresql.JSONB(astext_type=sa.Text()), nullable=True))

View File

@@ -50,7 +50,7 @@
},
"preparation_notes": "Es crucial usar vapor al inicio del horneado para lograr una corteza crujiente. La temperatura del agua debe estar entre 18-20°C.",
"storage_instructions": "Consumir el mismo día de producción. Se puede congelar después del horneado.",
"quality_standards": "Color dorado uniforme, corteza muy crujiente, miga alveolada con alveolos irregulares, aroma característico a trigo.",
"is_seasonal": false,
"is_signature_item": true,
"ingredientes": [
@@ -147,7 +147,7 @@
},
"preparation_notes": "La mantequilla para laminar debe estar a 15-16°C, flexible pero no blanda. Trabajar en ambiente fresco.",
"storage_instructions": "Consumir el día de producción. Se puede congelar la masa formada antes de la fermentación final.",
"quality_standards": "Laminado perfecto con capas visibles, color marrón brillante, estructura hojaldrada bien definida, aroma intenso a mantequilla.",
"is_seasonal": false,
"is_signature_item": true,
"ingredientes": [
@@ -280,7 +280,7 @@
},
"preparation_notes": "La masa madre debe estar activa y en su punto óptimo. La temperatura final de la masa debe ser 24-25°C.",
"storage_instructions": "Se conserva hasta 5-7 días en bolsa de papel. Mejora al segundo día.",
"quality_standards": "Corteza gruesa y oscura, miga densa pero húmeda, alveolos irregulares, sabor complejo ligeramente ácido.",
"is_seasonal": false,
"is_signature_item": true,
"ingredientes": [
@@ -378,7 +378,7 @@
},
"preparation_notes": "El chocolate debe ser de buena calidad para un mejor resultado. No sobrecargar de chocolate.",
"storage_instructions": "Consumir preferiblemente el día de producción.",
"quality_standards": "Hojaldre bien desarrollado, chocolate fundido en el interior, color dorado brillante.",
"is_seasonal": false,
"is_signature_item": false,
"ingredientes": [

View File

@@ -149,7 +149,7 @@ async def seed_recipes_for_tenant(
instructions=recipe_data.get("instructions"),
preparation_notes=recipe_data.get("preparation_notes"),
storage_instructions=recipe_data.get("storage_instructions"),
quality_standards=recipe_data.get("quality_standards"),
quality_check_configuration=recipe_data.get("quality_check_configuration"),
status=RecipeStatus.ACTIVE,
is_seasonal=recipe_data.get("is_seasonal", False),
is_signature_item=recipe_data.get("is_signature_item", False),