Fix enum mismatch: Update Python enums and seed data to match database uppercase values
- Fixed ProductType enum values from lowercase to uppercase (INGREDIENT, FINISHED_PRODUCT) - Fixed UnitOfMeasure enum values from lowercase/abbreviated to uppercase (KILOGRAMS, LITERS, etc.) - Fixed IngredientCategory enum values from lowercase to uppercase (FLOUR, YEAST, etc.) - Fixed ProductCategory enum values from lowercase to uppercase (BREAD, CROISSANTS, etc.) - Updated seed data files to use correct uppercase enum values - Fixed hardcoded enum references throughout the codebase - This resolves the InvalidTextRepresentationError when inserting inventory data Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -280,8 +280,8 @@ async def classify_products_batch(
|
|||||||
))
|
))
|
||||||
|
|
||||||
# Analyze business model
|
# Analyze business model
|
||||||
ingredient_count = sum(1 for s in suggestions if s.product_type.value == 'ingredient')
|
ingredient_count = sum(1 for s in suggestions if s.product_type.value == 'INGREDIENT')
|
||||||
finished_count = sum(1 for s in suggestions if s.product_type.value == 'finished_product')
|
finished_count = sum(1 for s in suggestions if s.product_type.value == 'FINISHED_PRODUCT')
|
||||||
semi_finished_count = sum(1 for s in suggestions if 'semi' in s.suggested_name.lower() or 'frozen' in s.suggested_name.lower() or 'pre' in s.suggested_name.lower())
|
semi_finished_count = sum(1 for s in suggestions if 'semi' in s.suggested_name.lower() or 'frozen' in s.suggested_name.lower() or 'pre' in s.suggested_name.lower())
|
||||||
total = len(suggestions)
|
total = len(suggestions)
|
||||||
ingredient_ratio = ingredient_count / total if total > 0 else 0
|
ingredient_ratio = ingredient_count / total if total > 0 else 0
|
||||||
|
|||||||
@@ -17,51 +17,51 @@ from shared.database.base import Base
|
|||||||
|
|
||||||
class UnitOfMeasure(enum.Enum):
|
class UnitOfMeasure(enum.Enum):
|
||||||
"""Standard units of measure for ingredients"""
|
"""Standard units of measure for ingredients"""
|
||||||
KILOGRAMS = "kg"
|
KILOGRAMS = "KILOGRAMS"
|
||||||
GRAMS = "g"
|
GRAMS = "GRAMS"
|
||||||
LITERS = "l"
|
LITERS = "LITERS"
|
||||||
MILLILITERS = "ml"
|
MILLILITERS = "MILLILITERS"
|
||||||
UNITS = "units"
|
UNITS = "UNITS"
|
||||||
PIECES = "pcs"
|
PIECES = "PIECES"
|
||||||
PACKAGES = "pkg"
|
PACKAGES = "PACKAGES"
|
||||||
BAGS = "bags"
|
BAGS = "BAGS"
|
||||||
BOXES = "boxes"
|
BOXES = "BOXES"
|
||||||
|
|
||||||
|
|
||||||
class IngredientCategory(enum.Enum):
|
class IngredientCategory(enum.Enum):
|
||||||
"""Bakery ingredient categories"""
|
"""Bakery ingredient categories"""
|
||||||
FLOUR = "flour"
|
FLOUR = "FLOUR"
|
||||||
YEAST = "yeast"
|
YEAST = "YEAST"
|
||||||
DAIRY = "dairy"
|
DAIRY = "DAIRY"
|
||||||
EGGS = "eggs"
|
EGGS = "EGGS"
|
||||||
SUGAR = "sugar"
|
SUGAR = "SUGAR"
|
||||||
FATS = "fats"
|
FATS = "FATS"
|
||||||
SALT = "salt"
|
SALT = "SALT"
|
||||||
SPICES = "spices"
|
SPICES = "SPICES"
|
||||||
ADDITIVES = "additives"
|
ADDITIVES = "ADDITIVES"
|
||||||
PACKAGING = "packaging"
|
PACKAGING = "PACKAGING"
|
||||||
CLEANING = "cleaning"
|
CLEANING = "CLEANING"
|
||||||
OTHER = "other"
|
OTHER = "OTHER"
|
||||||
|
|
||||||
|
|
||||||
class ProductCategory(enum.Enum):
|
class ProductCategory(enum.Enum):
|
||||||
"""Finished bakery product categories for retail/distribution model"""
|
"""Finished bakery product categories for retail/distribution model"""
|
||||||
BREAD = "bread"
|
BREAD = "BREAD"
|
||||||
CROISSANTS = "croissants"
|
CROISSANTS = "CROISSANTS"
|
||||||
PASTRIES = "pastries"
|
PASTRIES = "PASTRIES"
|
||||||
CAKES = "cakes"
|
CAKES = "CAKES"
|
||||||
COOKIES = "cookies"
|
COOKIES = "COOKIES"
|
||||||
MUFFINS = "muffins"
|
MUFFINS = "MUFFINS"
|
||||||
SANDWICHES = "sandwiches"
|
SANDWICHES = "SANDWICHES"
|
||||||
SEASONAL = "seasonal"
|
SEASONAL = "SEASONAL"
|
||||||
BEVERAGES = "beverages"
|
BEVERAGES = "BEVERAGES"
|
||||||
OTHER_PRODUCTS = "other_products"
|
OTHER_PRODUCTS = "OTHER_PRODUCTS"
|
||||||
|
|
||||||
|
|
||||||
class ProductType(enum.Enum):
|
class ProductType(enum.Enum):
|
||||||
"""Type of product in inventory"""
|
"""Type of product in inventory"""
|
||||||
INGREDIENT = "ingredient" # Raw materials (flour, yeast, etc.)
|
INGREDIENT = "INGREDIENT" # Raw materials (flour, yeast, etc.)
|
||||||
FINISHED_PRODUCT = "finished_product" # Ready-to-sell items (bread, croissants, etc.)
|
FINISHED_PRODUCT = "FINISHED_PRODUCT" # Ready-to-sell items (bread, croissants, etc.)
|
||||||
|
|
||||||
|
|
||||||
class ProductionStage(enum.Enum):
|
class ProductionStage(enum.Enum):
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ class DashboardRepository:
|
|||||||
query = text("""
|
query = text("""
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_ingredients,
|
COUNT(*) as total_ingredients,
|
||||||
COUNT(CASE WHEN product_type::text = 'finished_product' THEN 1 END) as finished_products,
|
COUNT(CASE WHEN product_type::text = 'FINISHED_PRODUCT' THEN 1 END) as finished_products,
|
||||||
COUNT(CASE WHEN product_type::text = 'ingredient' THEN 1 END) as raw_ingredients,
|
COUNT(CASE WHEN product_type::text = 'INGREDIENT' THEN 1 END) as raw_ingredients,
|
||||||
COUNT(DISTINCT st.supplier_id) as supplier_count,
|
COUNT(DISTINCT st.supplier_id) as supplier_count,
|
||||||
AVG(CASE WHEN s.available_quantity IS NOT NULL THEN s.available_quantity ELSE 0 END) as avg_stock_level
|
AVG(CASE WHEN s.available_quantity IS NOT NULL THEN s.available_quantity ELSE 0 END) as avg_stock_level
|
||||||
FROM ingredients i
|
FROM ingredients i
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class IngredientRepository(BaseRepository[Ingredient, IngredientCreate, Ingredie
|
|||||||
ingredient_name=create_data.get('name'),
|
ingredient_name=create_data.get('name'),
|
||||||
tenant_id=tenant_id
|
tenant_id=tenant_id
|
||||||
)
|
)
|
||||||
product_type_value = 'ingredient'
|
product_type_value = 'INGREDIENT'
|
||||||
|
|
||||||
if 'product_type' in create_data:
|
if 'product_type' in create_data:
|
||||||
from app.models.inventory import ProductType
|
from app.models.inventory import ProductType
|
||||||
@@ -73,7 +73,7 @@ class IngredientRepository(BaseRepository[Ingredient, IngredientCreate, Ingredie
|
|||||||
if 'category' in create_data:
|
if 'category' in create_data:
|
||||||
category_value = create_data.pop('category')
|
category_value = create_data.pop('category')
|
||||||
|
|
||||||
if product_type_value == 'finished_product' or product_type_value == 'FINISHED_PRODUCT':
|
if product_type_value == 'FINISHED_PRODUCT':
|
||||||
# Map to product_category for finished products
|
# Map to product_category for finished products
|
||||||
from app.models.inventory import ProductCategory
|
from app.models.inventory import ProductCategory
|
||||||
if category_value:
|
if category_value:
|
||||||
@@ -166,15 +166,15 @@ class IngredientRepository(BaseRepository[Ingredient, IngredientCreate, Ingredie
|
|||||||
# Handle category mapping based on product type
|
# Handle category mapping based on product type
|
||||||
if 'category' in update_data:
|
if 'category' in update_data:
|
||||||
category_value = update_data.pop('category')
|
category_value = update_data.pop('category')
|
||||||
product_type_value = update_data.get('product_type', 'ingredient')
|
product_type_value = update_data.get('product_type', 'INGREDIENT')
|
||||||
|
|
||||||
# Get current product if we need to determine type
|
# Get current product if we need to determine type
|
||||||
if 'product_type' not in update_data:
|
if 'product_type' not in update_data:
|
||||||
current_record = await self.get_by_id(record_id)
|
current_record = await self.get_by_id(record_id)
|
||||||
if current_record:
|
if current_record:
|
||||||
product_type_value = current_record.product_type.value if current_record.product_type else 'ingredient'
|
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':
|
if product_type_value == 'FINISHED_PRODUCT':
|
||||||
# Map to product_category for finished products
|
# Map to product_category for finished products
|
||||||
from app.models.inventory import ProductCategory
|
from app.models.inventory import ProductCategory
|
||||||
if category_value:
|
if category_value:
|
||||||
@@ -560,3 +560,109 @@ class IngredientRepository(BaseRepository[Ingredient, IngredientCreate, Ingredie
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to get active tenants from ingredients", error=str(e))
|
logger.error("Failed to get active tenants from ingredients", error=str(e))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def get_critical_stock_shortages(self) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get critical stock shortages across all tenants using CTE analysis.
|
||||||
|
Returns ingredients that are critically low on stock.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from sqlalchemy import text
|
||||||
|
query = text("""
|
||||||
|
WITH stock_analysis AS (
|
||||||
|
SELECT
|
||||||
|
i.id as ingredient_id,
|
||||||
|
i.name as ingredient_name,
|
||||||
|
i.tenant_id,
|
||||||
|
i.reorder_point,
|
||||||
|
COALESCE(SUM(s.current_quantity), 0) as current_quantity,
|
||||||
|
i.low_stock_threshold,
|
||||||
|
GREATEST(0, i.low_stock_threshold - COALESCE(SUM(s.current_quantity), 0)) as shortage_amount,
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE(SUM(s.current_quantity), 0) < i.low_stock_threshold THEN 'critical'
|
||||||
|
WHEN COALESCE(SUM(s.current_quantity), 0) < i.low_stock_threshold * 1.2 THEN 'low'
|
||||||
|
ELSE 'normal'
|
||||||
|
END as status
|
||||||
|
FROM ingredients i
|
||||||
|
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
||||||
|
WHERE i.is_active = true
|
||||||
|
GROUP BY i.id, i.name, i.tenant_id, i.reorder_point, i.low_stock_threshold
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ingredient_id,
|
||||||
|
ingredient_name,
|
||||||
|
tenant_id,
|
||||||
|
current_quantity,
|
||||||
|
reorder_point,
|
||||||
|
shortage_amount
|
||||||
|
FROM stock_analysis
|
||||||
|
WHERE status = 'critical'
|
||||||
|
ORDER BY shortage_amount DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
result = await self.session.execute(query)
|
||||||
|
rows = result.fetchall()
|
||||||
|
|
||||||
|
shortages = []
|
||||||
|
for row in rows:
|
||||||
|
shortages.append({
|
||||||
|
'ingredient_id': row.ingredient_id,
|
||||||
|
'ingredient_name': row.ingredient_name,
|
||||||
|
'tenant_id': row.tenant_id,
|
||||||
|
'current_quantity': float(row.current_quantity) if row.current_quantity else 0,
|
||||||
|
'required_quantity': float(row.reorder_point) if row.reorder_point else 0,
|
||||||
|
'shortage_amount': float(row.shortage_amount) if row.shortage_amount else 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return shortages
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get critical stock shortages", error=str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_stock_issues(self, tenant_id: UUID) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get stock level issues with CTE analysis for a specific tenant
|
||||||
|
Returns list of critical, low, and overstock situations
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from sqlalchemy import text
|
||||||
|
query = text("""
|
||||||
|
WITH stock_analysis AS (
|
||||||
|
SELECT
|
||||||
|
i.id, i.name, i.tenant_id,
|
||||||
|
COALESCE(SUM(s.current_quantity), 0) as current_stock,
|
||||||
|
i.low_stock_threshold as minimum_stock,
|
||||||
|
i.max_stock_level as maximum_stock,
|
||||||
|
i.reorder_point,
|
||||||
|
0 as tomorrow_needed,
|
||||||
|
0 as avg_daily_usage,
|
||||||
|
7 as lead_time_days,
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE(SUM(s.current_quantity), 0) < i.low_stock_threshold THEN 'critical'
|
||||||
|
WHEN COALESCE(SUM(s.current_quantity), 0) < i.low_stock_threshold * 1.2 THEN 'low'
|
||||||
|
WHEN i.max_stock_level IS NOT NULL AND COALESCE(SUM(s.current_quantity), 0) > i.max_stock_level THEN 'overstock'
|
||||||
|
ELSE 'normal'
|
||||||
|
END as status,
|
||||||
|
GREATEST(0, i.low_stock_threshold - COALESCE(SUM(s.current_quantity), 0)) as shortage_amount
|
||||||
|
FROM ingredients i
|
||||||
|
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
||||||
|
WHERE i.tenant_id = :tenant_id AND i.is_active = true
|
||||||
|
GROUP BY i.id, i.name, i.tenant_id, i.low_stock_threshold, i.max_stock_level, i.reorder_point
|
||||||
|
)
|
||||||
|
SELECT * FROM stock_analysis WHERE status != 'normal'
|
||||||
|
ORDER BY
|
||||||
|
CASE status
|
||||||
|
WHEN 'critical' THEN 1
|
||||||
|
WHEN 'low' THEN 2
|
||||||
|
WHEN 'overstock' THEN 3
|
||||||
|
END,
|
||||||
|
shortage_amount DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
result = await self.session.execute(query, {"tenant_id": tenant_id})
|
||||||
|
return [dict(row._mapping) for row in result.fetchall()]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to get stock issues", error=str(e), tenant_id=str(tenant_id))
|
||||||
|
raise
|
||||||
@@ -1069,14 +1069,14 @@ class InventoryService:
|
|||||||
|
|
||||||
ingredient_create = IngredientCreate(
|
ingredient_create = IngredientCreate(
|
||||||
name=ingredient_data.get('name'),
|
name=ingredient_data.get('name'),
|
||||||
product_type=ingredient_data.get('type', 'finished_product'),
|
product_type=ingredient_data.get('type', 'FINISHED_PRODUCT'),
|
||||||
unit_of_measure=ingredient_data.get('unit', 'units'),
|
unit_of_measure=ingredient_data.get('unit', 'UNITS'),
|
||||||
low_stock_threshold=ingredient_data.get('current_stock', 0),
|
low_stock_threshold=ingredient_data.get('current_stock', 0),
|
||||||
reorder_point=max(ingredient_data.get('reorder_point', 1),
|
reorder_point=max(ingredient_data.get('reorder_point', 1),
|
||||||
ingredient_data.get('current_stock', 0) + 1),
|
ingredient_data.get('current_stock', 0) + 1),
|
||||||
average_cost=ingredient_data.get('cost_per_unit', 0.0),
|
average_cost=ingredient_data.get('cost_per_unit', 0.0),
|
||||||
ingredient_category=ingredient_data.get('category') if ingredient_data.get('type') == 'ingredient' else None,
|
ingredient_category=ingredient_data.get('category') if ingredient_data.get('type') == 'INGREDIENT' else None,
|
||||||
product_category=ingredient_data.get('category') if ingredient_data.get('type') == 'finished_product' else None
|
product_category=ingredient_data.get('category') if ingredient_data.get('type') == 'FINISHED_PRODUCT' else None
|
||||||
)
|
)
|
||||||
|
|
||||||
ingredient = await repository.create_ingredient(ingredient_create, tenant_id)
|
ingredient = await repository.create_ingredient(ingredient_create, tenant_id)
|
||||||
|
|||||||
181
shared/demo/fixtures/enterprise/parent/03-inventory.json
Normal file
181
shared/demo/fixtures/enterprise/parent/03-inventory.json
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
{
|
||||||
|
"ingredients": [
|
||||||
|
{
|
||||||
|
"id": "10000000-0000-0000-0000-000000000001",
|
||||||
|
"tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||||
|
"name": "Harina de Trigo T55 - Enterprise Grade",
|
||||||
|
"sku": "HAR-T55-ENT-001",
|
||||||
|
"barcode": null,
|
||||||
|
"product_type": "INGREDIENT",
|
||||||
|
"ingredient_category": "FLOUR",
|
||||||
|
"product_category": "BREAD",
|
||||||
|
"subcategory": null,
|
||||||
|
"description": "Premium harina de trigo tipo 55 para uso en todas las ubicaciones",
|
||||||
|
"brand": "Molinos San José - Enterprise",
|
||||||
|
"unit_of_measure": "KILOGRAMS",
|
||||||
|
"package_size": null,
|
||||||
|
"average_cost": 0.80,
|
||||||
|
"last_purchase_price": null,
|
||||||
|
"standard_cost": null,
|
||||||
|
"low_stock_threshold": 500.0,
|
||||||
|
"reorder_point": 750.0,
|
||||||
|
"reorder_quantity": null,
|
||||||
|
"max_stock_level": null,
|
||||||
|
"shelf_life_days": null,
|
||||||
|
"display_life_hours": null,
|
||||||
|
"best_before_hours": null,
|
||||||
|
"storage_instructions": null,
|
||||||
|
"central_baker_product_code": null,
|
||||||
|
"delivery_days": null,
|
||||||
|
"minimum_order_quantity": null,
|
||||||
|
"pack_size": null,
|
||||||
|
"is_active": true,
|
||||||
|
"is_perishable": false,
|
||||||
|
"allergen_info": [
|
||||||
|
"gluten"
|
||||||
|
],
|
||||||
|
"nutritional_info": null,
|
||||||
|
"produced_locally": false,
|
||||||
|
"recipe_id": null,
|
||||||
|
"created_at": "2025-01-15T06:00:00Z",
|
||||||
|
"updated_at": "2025-01-15T06:00:00Z",
|
||||||
|
"created_by": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
|
||||||
|
"enterprise_shared": true,
|
||||||
|
"shared_locations": ["Madrid Centro", "Barcelona Gràcia", "Valencia Ruzafa"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10000000-0000-0000-0000-000000000002",
|
||||||
|
"tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||||
|
"name": "Mantequilla Francesa AOP - Enterprise",
|
||||||
|
"sku": "MAN-FRA-ENT-001",
|
||||||
|
"barcode": null,
|
||||||
|
"product_type": "INGREDIENT",
|
||||||
|
"ingredient_category": "DAIRY",
|
||||||
|
"product_category": "PASTRY",
|
||||||
|
"subcategory": null,
|
||||||
|
"description": "Mantequilla francesa AOP para uso en croissants y pastelería fina",
|
||||||
|
"brand": "Lescure - Enterprise",
|
||||||
|
"unit_of_measure": "KILOGRAMS",
|
||||||
|
"package_size": null,
|
||||||
|
"average_cost": 4.20,
|
||||||
|
"last_purchase_price": null,
|
||||||
|
"standard_cost": null,
|
||||||
|
"low_stock_threshold": 200.0,
|
||||||
|
"reorder_point": 300.0,
|
||||||
|
"reorder_quantity": null,
|
||||||
|
"max_stock_level": null,
|
||||||
|
"shelf_life_days": null,
|
||||||
|
"display_life_hours": null,
|
||||||
|
"best_before_hours": null,
|
||||||
|
"storage_instructions": null,
|
||||||
|
"central_baker_product_code": null,
|
||||||
|
"delivery_days": null,
|
||||||
|
"minimum_order_quantity": null,
|
||||||
|
"pack_size": null,
|
||||||
|
"is_active": true,
|
||||||
|
"is_perishable": true,
|
||||||
|
"allergen_info": [
|
||||||
|
"milk"
|
||||||
|
],
|
||||||
|
"nutritional_info": null,
|
||||||
|
"produced_locally": false,
|
||||||
|
"recipe_id": null,
|
||||||
|
"created_at": "2025-01-15T06:00:00Z",
|
||||||
|
"updated_at": "2025-01-15T06:00:00Z",
|
||||||
|
"created_by": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
|
||||||
|
"enterprise_shared": true,
|
||||||
|
"shared_locations": ["Madrid Centro", "Barcelona Gràcia", "Valencia Ruzafa"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "20000000-0000-0000-0000-000000000001",
|
||||||
|
"tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||||
|
"name": "Baguette Premium - Enterprise Standard",
|
||||||
|
"sku": "BAG-PRE-ENT-001",
|
||||||
|
"barcode": null,
|
||||||
|
"product_type": "FINISHED_PRODUCT",
|
||||||
|
"ingredient_category": null,
|
||||||
|
"product_category": "BREAD",
|
||||||
|
"subcategory": "FRENCH",
|
||||||
|
"description": "Baguette premium estándar para todas las ubicaciones enterprise",
|
||||||
|
"brand": "Panadería Central",
|
||||||
|
"unit_of_measure": "UNITS",
|
||||||
|
"package_size": null,
|
||||||
|
"average_cost": 1.80,
|
||||||
|
"last_purchase_price": null,
|
||||||
|
"standard_cost": null,
|
||||||
|
"low_stock_threshold": 100.0,
|
||||||
|
"reorder_point": 150.0,
|
||||||
|
"reorder_quantity": null,
|
||||||
|
"max_stock_level": null,
|
||||||
|
"shelf_life_days": 1,
|
||||||
|
"display_life_hours": 24,
|
||||||
|
"best_before_hours": 12,
|
||||||
|
"storage_instructions": null,
|
||||||
|
"central_baker_product_code": null,
|
||||||
|
"delivery_days": null,
|
||||||
|
"minimum_order_quantity": null,
|
||||||
|
"pack_size": null,
|
||||||
|
"is_active": true,
|
||||||
|
"is_perishable": true,
|
||||||
|
"allergen_info": [
|
||||||
|
"gluten",
|
||||||
|
"may_contain_sesame"
|
||||||
|
],
|
||||||
|
"nutritional_info": null,
|
||||||
|
"produced_locally": true,
|
||||||
|
"recipe_id": "30000000-0000-0000-0000-000000000001",
|
||||||
|
"created_at": "2025-01-15T06:00:00Z",
|
||||||
|
"updated_at": "2025-01-15T06:00:00Z",
|
||||||
|
"created_by": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
|
||||||
|
"enterprise_shared": true,
|
||||||
|
"shared_locations": ["Madrid Centro", "Barcelona Gràcia", "Valencia Ruzafa"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stock": [
|
||||||
|
{
|
||||||
|
"id": "10000000-0000-0000-0000-000000001001",
|
||||||
|
"tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||||
|
"ingredient_id": "10000000-0000-0000-0000-000000000001",
|
||||||
|
"quantity": 850.0,
|
||||||
|
"location": "Central Warehouse - Madrid",
|
||||||
|
"production_stage": "RAW_MATERIAL",
|
||||||
|
"quality_status": "APPROVED",
|
||||||
|
"expiration_date": "2025-07-15T00:00:00Z",
|
||||||
|
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||||
|
"batch_number": "ENT-HAR-20250115-001",
|
||||||
|
"created_at": "2025-01-15T06:00:00Z",
|
||||||
|
"updated_at": "2025-01-15T06:00:00Z",
|
||||||
|
"enterprise_shared": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10000000-0000-0000-0000-000000001002",
|
||||||
|
"tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||||
|
"ingredient_id": "10000000-0000-0000-0000-000000000002",
|
||||||
|
"quantity": 280.0,
|
||||||
|
"location": "Central Warehouse - Madrid",
|
||||||
|
"production_stage": "RAW_MATERIAL",
|
||||||
|
"quality_status": "APPROVED",
|
||||||
|
"expiration_date": "2025-02-15T00:00:00Z",
|
||||||
|
"supplier_id": "40000000-0000-0000-0000-000000000002",
|
||||||
|
"batch_number": "ENT-MAN-20250115-001",
|
||||||
|
"created_at": "2025-01-15T06:00:00Z",
|
||||||
|
"updated_at": "2025-01-15T06:00:00Z",
|
||||||
|
"enterprise_shared": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "20000000-0000-0000-0000-000000001001",
|
||||||
|
"tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||||
|
"ingredient_id": "20000000-0000-0000-0000-000000000001",
|
||||||
|
"quantity": 120.0,
|
||||||
|
"location": "Central Warehouse - Madrid",
|
||||||
|
"production_stage": "FINISHED_PRODUCT",
|
||||||
|
"quality_status": "APPROVED",
|
||||||
|
"expiration_date": "2025-01-16T06:00:00Z",
|
||||||
|
"supplier_id": null,
|
||||||
|
"batch_number": "ENT-BAG-20250115-001",
|
||||||
|
"created_at": "2025-01-15T06:00:00Z",
|
||||||
|
"updated_at": "2025-01-15T06:00:00Z",
|
||||||
|
"enterprise_shared": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1020
shared/demo/fixtures/professional/03-inventory.json
Normal file
1020
shared/demo/fixtures/professional/03-inventory.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user