Start integrating the onboarding flow with backend 12
This commit is contained in:
@@ -25,9 +25,9 @@ ENV PYTHONPATH=/app
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5)" || exit 1
|
||||
CMD python -c "import requests; requests.get('http://localhost:8000/health/', timeout=5)" || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -79,22 +79,25 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
query = """
|
||||
WITH stock_analysis AS (
|
||||
SELECT
|
||||
i.*,
|
||||
COALESCE(p.scheduled_quantity, 0) as tomorrow_needed,
|
||||
COALESCE(s.avg_daily_usage, 0) as avg_daily_usage,
|
||||
COALESCE(s.lead_time_days, 7) as lead_time_days,
|
||||
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 i.current_stock < i.minimum_stock THEN 'critical'
|
||||
WHEN i.current_stock < i.minimum_stock * 1.2 THEN 'low'
|
||||
WHEN i.current_stock > i.maximum_stock THEN 'overstock'
|
||||
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.minimum_stock - i.current_stock) as shortage_amount
|
||||
FROM inventory_items i
|
||||
LEFT JOIN production_schedule p ON p.ingredient_id = i.id
|
||||
AND p.date = CURRENT_DATE + INTERVAL '1 day'
|
||||
LEFT JOIN supplier_items s ON s.ingredient_id = i.id
|
||||
WHERE i.tenant_id = $1 AND i.active = true
|
||||
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 = $1 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
|
||||
@@ -212,15 +215,16 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
i.id, i.name, i.current_stock, i.tenant_id,
|
||||
b.id as batch_id, b.expiry_date, b.quantity,
|
||||
EXTRACT(days FROM (b.expiry_date - CURRENT_DATE)) as days_to_expiry
|
||||
FROM inventory_items i
|
||||
JOIN inventory_batches b ON b.ingredient_id = i.id
|
||||
WHERE b.expiry_date <= CURRENT_DATE + INTERVAL '7 days'
|
||||
AND b.quantity > 0
|
||||
AND b.status = 'active'
|
||||
ORDER BY b.expiry_date ASC
|
||||
i.id, i.name, i.tenant_id,
|
||||
s.id as stock_id, s.expiration_date, s.current_quantity,
|
||||
EXTRACT(days FROM (s.expiration_date - CURRENT_DATE)) as days_to_expiry
|
||||
FROM ingredients i
|
||||
JOIN stock s ON s.ingredient_id = i.id
|
||||
WHERE s.expiration_date <= CURRENT_DATE + INTERVAL '7 days'
|
||||
AND s.current_quantity > 0
|
||||
AND s.is_available = true
|
||||
AND s.expiration_date IS NOT NULL
|
||||
ORDER BY s.expiration_date ASC
|
||||
"""
|
||||
|
||||
from sqlalchemy import text
|
||||
@@ -275,8 +279,8 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
{
|
||||
'id': str(item['id']),
|
||||
'name': item['name'],
|
||||
'batch_id': str(item['batch_id']),
|
||||
'quantity': float(item['quantity']),
|
||||
'stock_id': str(item['stock_id']),
|
||||
'quantity': float(item['current_quantity']),
|
||||
'days_expired': abs(item['days_to_expiry'])
|
||||
} for item in expired
|
||||
]
|
||||
@@ -294,9 +298,9 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
'actions': ['Usar inmediatamente', 'Promoción especial', 'Revisar recetas', 'Documentar'],
|
||||
'metadata': {
|
||||
'ingredient_id': str(item['id']),
|
||||
'batch_id': str(item['batch_id']),
|
||||
'stock_id': str(item['stock_id']),
|
||||
'days_to_expiry': item['days_to_expiry'],
|
||||
'quantity': float(item['quantity'])
|
||||
'quantity': float(item['current_quantity'])
|
||||
}
|
||||
}, item_type='alert')
|
||||
|
||||
@@ -312,14 +316,16 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
t.id, t.sensor_id, t.location, t.temperature,
|
||||
t.max_threshold, t.tenant_id,
|
||||
EXTRACT(minutes FROM (NOW() - t.first_breach_time)) as breach_duration_minutes
|
||||
FROM temperature_readings t
|
||||
WHERE t.temperature > t.max_threshold
|
||||
AND t.breach_duration_minutes >= 30 -- Only after 30 minutes
|
||||
AND t.last_alert_sent < NOW() - INTERVAL '15 minutes' -- Avoid spam
|
||||
ORDER BY t.temperature DESC, t.breach_duration_minutes DESC
|
||||
t.id, t.equipment_id as sensor_id, t.storage_location as location,
|
||||
t.temperature_celsius as temperature,
|
||||
t.target_temperature_max as max_threshold, t.tenant_id,
|
||||
COALESCE(t.deviation_minutes, 0) as breach_duration_minutes
|
||||
FROM temperature_logs t
|
||||
WHERE t.temperature_celsius > COALESCE(t.target_temperature_max, 25)
|
||||
AND NOT t.is_within_range
|
||||
AND COALESCE(t.deviation_minutes, 0) >= 30 -- Only after 30 minutes
|
||||
AND (t.recorded_at < NOW() - INTERVAL '15 minutes' OR t.alert_triggered = false) -- Avoid spam
|
||||
ORDER BY t.temperature_celsius DESC, t.deviation_minutes DESC
|
||||
"""
|
||||
|
||||
from sqlalchemy import text
|
||||
@@ -371,11 +377,14 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
}
|
||||
}, item_type='alert')
|
||||
|
||||
# Update last alert sent time to avoid spam
|
||||
await self.db_manager.execute(
|
||||
"UPDATE temperature_readings SET last_alert_sent = NOW() WHERE id = $1",
|
||||
breach['id']
|
||||
)
|
||||
# Update alert triggered flag to avoid spam
|
||||
from sqlalchemy import text
|
||||
async with self.db_manager.get_session() as session:
|
||||
await session.execute(
|
||||
text("UPDATE temperature_logs SET alert_triggered = true WHERE id = :id"),
|
||||
{"id": breach['id']}
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error processing temperature breach",
|
||||
@@ -391,24 +400,27 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
query = """
|
||||
WITH usage_analysis AS (
|
||||
SELECT
|
||||
i.id, i.name, i.tenant_id, i.minimum_stock, i.maximum_stock,
|
||||
i.current_stock,
|
||||
AVG(sm.quantity) FILTER (WHERE sm.movement_type = 'out'
|
||||
i.id, i.name, i.tenant_id,
|
||||
i.low_stock_threshold as minimum_stock,
|
||||
i.max_stock_level as maximum_stock,
|
||||
COALESCE(SUM(s.current_quantity), 0) as current_stock,
|
||||
AVG(sm.quantity) FILTER (WHERE sm.movement_type = 'production_use'
|
||||
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') as avg_daily_usage,
|
||||
COUNT(sm.id) FILTER (WHERE sm.movement_type = 'out'
|
||||
COUNT(sm.id) FILTER (WHERE sm.movement_type = 'production_use'
|
||||
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') as usage_days,
|
||||
MAX(sm.created_at) FILTER (WHERE sm.movement_type = 'out') as last_used
|
||||
FROM inventory_items i
|
||||
MAX(sm.created_at) FILTER (WHERE sm.movement_type = 'production_use') as last_used
|
||||
FROM ingredients i
|
||||
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
||||
LEFT JOIN stock_movements sm ON sm.ingredient_id = i.id
|
||||
WHERE i.active = true AND i.tenant_id = $1
|
||||
GROUP BY i.id
|
||||
HAVING COUNT(sm.id) FILTER (WHERE sm.movement_type = 'out'
|
||||
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') >= 5
|
||||
WHERE i.is_active = true AND i.tenant_id = $1
|
||||
GROUP BY i.id, i.name, i.tenant_id, i.low_stock_threshold, i.max_stock_level
|
||||
HAVING COUNT(sm.id) FILTER (WHERE sm.movement_type = 'production_use'
|
||||
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') >= 3
|
||||
),
|
||||
recommendations AS (
|
||||
SELECT *,
|
||||
CASE
|
||||
WHEN avg_daily_usage * 7 > maximum_stock THEN 'increase_max'
|
||||
WHEN avg_daily_usage * 7 > maximum_stock AND maximum_stock IS NOT NULL THEN 'increase_max'
|
||||
WHEN avg_daily_usage * 3 < minimum_stock THEN 'decrease_min'
|
||||
WHEN current_stock / NULLIF(avg_daily_usage, 0) > 14 THEN 'reduce_stock'
|
||||
WHEN avg_daily_usage > 0 AND minimum_stock / avg_daily_usage < 3 THEN 'increase_min'
|
||||
@@ -500,20 +512,21 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
async def generate_waste_reduction_recommendations(self):
|
||||
"""Generate waste reduction recommendations"""
|
||||
try:
|
||||
# Analyze waste patterns
|
||||
# Analyze waste patterns from stock movements
|
||||
query = """
|
||||
SELECT
|
||||
i.id, i.name, i.tenant_id,
|
||||
SUM(w.quantity) as total_waste_30d,
|
||||
COUNT(w.id) as waste_incidents,
|
||||
AVG(w.quantity) as avg_waste_per_incident,
|
||||
w.waste_reason
|
||||
FROM inventory_items i
|
||||
JOIN waste_logs w ON w.ingredient_id = i.id
|
||||
WHERE w.created_at > CURRENT_DATE - INTERVAL '30 days'
|
||||
SUM(sm.quantity) as total_waste_30d,
|
||||
COUNT(sm.id) as waste_incidents,
|
||||
AVG(sm.quantity) as avg_waste_per_incident,
|
||||
COALESCE(sm.reason_code, 'unknown') as waste_reason
|
||||
FROM ingredients i
|
||||
JOIN stock_movements sm ON sm.ingredient_id = i.id
|
||||
WHERE sm.movement_type = 'waste'
|
||||
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days'
|
||||
AND i.tenant_id = $1
|
||||
GROUP BY i.id, w.waste_reason
|
||||
HAVING SUM(w.quantity) > 5 -- More than 5kg wasted
|
||||
GROUP BY i.id, i.name, i.tenant_id, sm.reason_code
|
||||
HAVING SUM(sm.quantity) > 5 -- More than 5kg wasted
|
||||
ORDER BY total_waste_30d DESC
|
||||
"""
|
||||
|
||||
@@ -694,10 +707,14 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
"""Get stock information after hypothetical order"""
|
||||
try:
|
||||
query = """
|
||||
SELECT id, name, current_stock, minimum_stock,
|
||||
(current_stock - $2) as remaining
|
||||
FROM inventory_items
|
||||
WHERE id = $1
|
||||
SELECT i.id, i.name,
|
||||
COALESCE(SUM(s.current_quantity), 0) as current_stock,
|
||||
i.low_stock_threshold as minimum_stock,
|
||||
(COALESCE(SUM(s.current_quantity), 0) - $2) as remaining
|
||||
FROM ingredients i
|
||||
LEFT JOIN stock s ON s.ingredient_id = i.id AND s.is_available = true
|
||||
WHERE i.id = $1
|
||||
GROUP BY i.id, i.name, i.low_stock_threshold
|
||||
"""
|
||||
|
||||
result = await self.db_manager.fetchrow(query, ingredient_id, order_quantity)
|
||||
|
||||
Reference in New Issue
Block a user