Add supplier and imporve inventory frontend
This commit is contained in:
@@ -106,7 +106,6 @@ class Ingredient(Base):
|
||||
# Product details
|
||||
description = Column(Text, nullable=True)
|
||||
brand = Column(String(100), nullable=True) # Brand or central baker name
|
||||
supplier_name = Column(String(200), nullable=True) # Central baker or distributor
|
||||
unit_of_measure = Column(SQLEnum(UnitOfMeasure), nullable=False)
|
||||
package_size = Column(Float, nullable=True) # Size per package/unit
|
||||
|
||||
@@ -158,7 +157,6 @@ class Ingredient(Base):
|
||||
Index('idx_ingredients_ingredient_category', 'tenant_id', 'ingredient_category', 'is_active'),
|
||||
Index('idx_ingredients_product_category', 'tenant_id', 'product_category', 'is_active'),
|
||||
Index('idx_ingredients_stock_levels', 'tenant_id', 'low_stock_threshold', 'reorder_point'),
|
||||
Index('idx_ingredients_central_baker', 'tenant_id', 'supplier_name', 'product_type'),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
@@ -194,7 +192,6 @@ class Ingredient(Base):
|
||||
'subcategory': self.subcategory,
|
||||
'description': self.description,
|
||||
'brand': self.brand,
|
||||
'supplier_name': self.supplier_name,
|
||||
'unit_of_measure': self.unit_of_measure.value if self.unit_of_measure else None,
|
||||
'package_size': self.package_size,
|
||||
'average_cost': float(self.average_cost) if self.average_cost else None,
|
||||
@@ -230,6 +227,9 @@ class Stock(Base):
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
ingredient_id = Column(UUID(as_uuid=True), ForeignKey('ingredients.id'), nullable=False, index=True)
|
||||
|
||||
# Supplier association
|
||||
supplier_id = Column(UUID(as_uuid=True), nullable=True, index=True)
|
||||
|
||||
# Stock identification
|
||||
batch_number = Column(String(100), nullable=True, index=True)
|
||||
lot_number = Column(String(100), nullable=True, index=True)
|
||||
|
||||
@@ -61,13 +61,6 @@ class IngredientCreate(InventoryBaseSchema):
|
||||
is_perishable: bool = Field(False, description="Is perishable")
|
||||
allergen_info: Optional[Dict[str, Any]] = Field(None, description="Allergen information")
|
||||
|
||||
@validator('storage_temperature_max')
|
||||
def validate_temperature_range(cls, v, values):
|
||||
if v is not None and 'storage_temperature_min' in values and values['storage_temperature_min'] is not None:
|
||||
if v <= values['storage_temperature_min']:
|
||||
raise ValueError('Max temperature must be greater than min temperature')
|
||||
return v
|
||||
|
||||
@validator('reorder_point')
|
||||
def validate_reorder_point(cls, v, values):
|
||||
if 'low_stock_threshold' in values and v <= values['low_stock_threshold']:
|
||||
@@ -147,6 +140,7 @@ class IngredientResponse(InventoryBaseSchema):
|
||||
class StockCreate(InventoryBaseSchema):
|
||||
"""Schema for creating stock entries"""
|
||||
ingredient_id: str = Field(..., description="Ingredient ID")
|
||||
supplier_id: Optional[str] = Field(None, description="Supplier ID")
|
||||
batch_number: Optional[str] = Field(None, max_length=100, description="Batch number")
|
||||
lot_number: Optional[str] = Field(None, max_length=100, description="Lot number")
|
||||
supplier_batch_ref: Optional[str] = Field(None, max_length=100, description="Supplier batch reference")
|
||||
@@ -181,9 +175,16 @@ class StockCreate(InventoryBaseSchema):
|
||||
shelf_life_days: Optional[int] = Field(None, gt=0, description="Batch-specific shelf life in days")
|
||||
storage_instructions: Optional[str] = Field(None, description="Batch-specific storage instructions")
|
||||
|
||||
|
||||
@validator('storage_temperature_max')
|
||||
def validate_temperature_range(cls, v, values):
|
||||
min_temp = values.get('storage_temperature_min')
|
||||
if v is not None and min_temp is not None and v <= min_temp:
|
||||
raise ValueError('Max temperature must be greater than min temperature')
|
||||
return v
|
||||
|
||||
class StockUpdate(InventoryBaseSchema):
|
||||
"""Schema for updating stock entries"""
|
||||
supplier_id: Optional[str] = Field(None, description="Supplier ID")
|
||||
batch_number: Optional[str] = Field(None, max_length=100, description="Batch number")
|
||||
lot_number: Optional[str] = Field(None, max_length=100, description="Lot number")
|
||||
supplier_batch_ref: Optional[str] = Field(None, max_length=100, description="Supplier batch reference")
|
||||
@@ -226,6 +227,7 @@ class StockResponse(InventoryBaseSchema):
|
||||
id: str
|
||||
tenant_id: str
|
||||
ingredient_id: str
|
||||
supplier_id: Optional[str]
|
||||
batch_number: Optional[str]
|
||||
lot_number: Optional[str]
|
||||
supplier_batch_ref: Optional[str]
|
||||
|
||||
@@ -451,17 +451,17 @@ class DashboardService:
|
||||
SELECT
|
||||
'stock_movement' as activity_type,
|
||||
CASE
|
||||
WHEN movement_type = 'purchase' THEN 'Stock added: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'production_use' THEN 'Stock consumed: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'waste' THEN 'Stock wasted: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'adjustment' THEN 'Stock adjusted: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'PURCHASE' THEN 'Stock added: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'PRODUCTION_USE' THEN 'Stock consumed: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'WASTE' THEN 'Stock wasted: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
WHEN movement_type = 'ADJUSTMENT' THEN 'Stock adjusted: ' || i.name || ' (' || sm.quantity || ' ' || i.unit_of_measure::text || ')'
|
||||
ELSE 'Stock movement: ' || i.name
|
||||
END as description,
|
||||
sm.movement_date as timestamp,
|
||||
sm.created_by as user_id,
|
||||
CASE
|
||||
WHEN movement_type = 'waste' THEN 'high'
|
||||
WHEN movement_type = 'adjustment' THEN 'medium'
|
||||
WHEN movement_type = 'WASTE' THEN 'high'
|
||||
WHEN movement_type = 'ADJUSTMENT' THEN 'medium'
|
||||
ELSE 'low'
|
||||
END as impact_level,
|
||||
sm.id as entity_id,
|
||||
@@ -613,17 +613,22 @@ class DashboardService:
|
||||
|
||||
# Get ingredient metrics
|
||||
query = """
|
||||
SELECT
|
||||
SELECT
|
||||
COUNT(*) as total_ingredients,
|
||||
COUNT(CASE WHEN product_type = 'finished_product' THEN 1 END) as finished_products,
|
||||
COUNT(CASE WHEN product_type = 'ingredient' THEN 1 END) as raw_ingredients,
|
||||
COUNT(DISTINCT supplier_name) 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
|
||||
FROM ingredients i
|
||||
LEFT JOIN (
|
||||
SELECT ingredient_id, SUM(available_quantity) as available_quantity
|
||||
FROM stock WHERE tenant_id = :tenant_id GROUP BY ingredient_id
|
||||
) s ON i.id = s.ingredient_id
|
||||
LEFT JOIN (
|
||||
SELECT ingredient_id, supplier_id
|
||||
FROM stock WHERE tenant_id = :tenant_id AND supplier_id IS NOT NULL
|
||||
GROUP BY ingredient_id, supplier_id
|
||||
) st ON i.id = st.ingredient_id
|
||||
WHERE i.tenant_id = :tenant_id AND i.is_active = true
|
||||
"""
|
||||
|
||||
|
||||
@@ -428,17 +428,17 @@ class InventoryAlertService(BaseAlertService, AlertServiceMixin):
|
||||
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'
|
||||
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 = 'production_use'
|
||||
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 = 'production_use') as last_used
|
||||
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.is_active = true AND i.tenant_id = :tenant_id
|
||||
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'
|
||||
HAVING COUNT(sm.id) FILTER (WHERE sm.movement_type = 'PRODUCTION_USE'
|
||||
AND sm.created_at > CURRENT_DATE - INTERVAL '30 days') >= 3
|
||||
),
|
||||
recommendations AS (
|
||||
|
||||
Reference in New Issue
Block a user