Improve the inventory page
This commit is contained in:
@@ -64,6 +64,15 @@ class ProductType(enum.Enum):
|
||||
FINISHED_PRODUCT = "finished_product" # Ready-to-sell items (bread, croissants, etc.)
|
||||
|
||||
|
||||
class ProductionStage(enum.Enum):
|
||||
"""Production stages for bakery products"""
|
||||
RAW_INGREDIENT = "raw_ingredient" # Basic ingredients (flour, yeast)
|
||||
PAR_BAKED = "par_baked" # Pre-baked items needing final baking
|
||||
FULLY_BAKED = "fully_baked" # Completed products ready for sale
|
||||
PREPARED_DOUGH = "prepared_dough" # Prepared but unbaked dough
|
||||
FROZEN_PRODUCT = "frozen_product" # Frozen intermediate products
|
||||
|
||||
|
||||
class StockMovementType(enum.Enum):
|
||||
"""Types of inventory movements"""
|
||||
PURCHASE = "purchase"
|
||||
@@ -73,6 +82,7 @@ class StockMovementType(enum.Enum):
|
||||
TRANSFER = "transfer"
|
||||
RETURN = "return"
|
||||
INITIAL_STOCK = "initial_stock"
|
||||
TRANSFORMATION = "transformation" # Converting between production stages
|
||||
|
||||
|
||||
class Ingredient(Base):
|
||||
@@ -227,16 +237,20 @@ class Ingredient(Base):
|
||||
class Stock(Base):
|
||||
"""Current stock levels and batch tracking"""
|
||||
__tablename__ = "stock"
|
||||
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
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)
|
||||
|
||||
|
||||
# Stock identification
|
||||
batch_number = Column(String(100), nullable=True, index=True)
|
||||
lot_number = Column(String(100), nullable=True, index=True)
|
||||
supplier_batch_ref = Column(String(100), nullable=True)
|
||||
|
||||
|
||||
# Production stage tracking
|
||||
production_stage = Column(String(20), nullable=False, default='raw_ingredient', index=True)
|
||||
transformation_reference = Column(String(100), nullable=True, index=True) # Links related transformations
|
||||
|
||||
# Quantities
|
||||
current_quantity = Column(Float, nullable=False, default=0.0)
|
||||
reserved_quantity = Column(Float, nullable=False, default=0.0) # Reserved for production
|
||||
@@ -246,6 +260,11 @@ class Stock(Base):
|
||||
received_date = Column(DateTime(timezone=True), nullable=True)
|
||||
expiration_date = Column(DateTime(timezone=True), nullable=True, index=True)
|
||||
best_before_date = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Stage-specific expiration tracking
|
||||
original_expiration_date = Column(DateTime(timezone=True), nullable=True) # Original batch expiration (for par-baked)
|
||||
transformation_date = Column(DateTime(timezone=True), nullable=True) # When product was transformed
|
||||
final_expiration_date = Column(DateTime(timezone=True), nullable=True) # Final product expiration after transformation
|
||||
|
||||
# Cost tracking
|
||||
unit_cost = Column(Numeric(10, 2), nullable=True)
|
||||
@@ -276,6 +295,9 @@ class Stock(Base):
|
||||
Index('idx_stock_batch', 'tenant_id', 'batch_number'),
|
||||
Index('idx_stock_low_levels', 'tenant_id', 'current_quantity', 'is_available'),
|
||||
Index('idx_stock_quality', 'tenant_id', 'quality_status', 'is_available'),
|
||||
Index('idx_stock_production_stage', 'tenant_id', 'production_stage', 'is_available'),
|
||||
Index('idx_stock_transformation', 'tenant_id', 'transformation_reference'),
|
||||
Index('idx_stock_final_expiration', 'tenant_id', 'final_expiration_date', 'is_available'),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
@@ -287,12 +309,17 @@ class Stock(Base):
|
||||
'batch_number': self.batch_number,
|
||||
'lot_number': self.lot_number,
|
||||
'supplier_batch_ref': self.supplier_batch_ref,
|
||||
'production_stage': self.production_stage if self.production_stage else None,
|
||||
'transformation_reference': self.transformation_reference,
|
||||
'current_quantity': self.current_quantity,
|
||||
'reserved_quantity': self.reserved_quantity,
|
||||
'available_quantity': self.available_quantity,
|
||||
'received_date': self.received_date.isoformat() if self.received_date else None,
|
||||
'expiration_date': self.expiration_date.isoformat() if self.expiration_date else None,
|
||||
'best_before_date': self.best_before_date.isoformat() if self.best_before_date else None,
|
||||
'original_expiration_date': self.original_expiration_date.isoformat() if self.original_expiration_date else None,
|
||||
'transformation_date': self.transformation_date.isoformat() if self.transformation_date else None,
|
||||
'final_expiration_date': self.final_expiration_date.isoformat() if self.final_expiration_date else None,
|
||||
'unit_cost': float(self.unit_cost) if self.unit_cost else None,
|
||||
'total_cost': float(self.total_cost) if self.total_cost else None,
|
||||
'storage_location': self.storage_location,
|
||||
@@ -375,6 +402,83 @@ class StockMovement(Base):
|
||||
}
|
||||
|
||||
|
||||
class ProductTransformation(Base):
|
||||
"""Track product transformations (e.g., par-baked to fully baked)"""
|
||||
__tablename__ = "product_transformations"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Transformation details
|
||||
transformation_reference = Column(String(100), nullable=False, index=True)
|
||||
source_ingredient_id = Column(UUID(as_uuid=True), ForeignKey('ingredients.id'), nullable=False)
|
||||
target_ingredient_id = Column(UUID(as_uuid=True), ForeignKey('ingredients.id'), nullable=False)
|
||||
|
||||
# Stage transformation
|
||||
source_stage = Column(String(20), nullable=False)
|
||||
target_stage = Column(String(20), nullable=False)
|
||||
|
||||
# Quantities and conversion
|
||||
source_quantity = Column(Float, nullable=False) # Input quantity
|
||||
target_quantity = Column(Float, nullable=False) # Output quantity
|
||||
conversion_ratio = Column(Float, nullable=False, default=1.0) # target/source ratio
|
||||
|
||||
# Expiration logic
|
||||
expiration_calculation_method = Column(String(50), nullable=False, default="days_from_transformation") # days_from_transformation, preserve_original
|
||||
expiration_days_offset = Column(Integer, nullable=True) # Days from transformation date
|
||||
|
||||
# Process tracking
|
||||
transformation_date = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
process_notes = Column(Text, nullable=True)
|
||||
performed_by = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
# Batch tracking
|
||||
source_batch_numbers = Column(Text, nullable=True) # JSON array of source batch numbers
|
||||
target_batch_number = Column(String(100), nullable=True)
|
||||
|
||||
# Status
|
||||
is_completed = Column(Boolean, default=True)
|
||||
is_reversed = Column(Boolean, default=False)
|
||||
|
||||
# Audit fields
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
created_by = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_transformations_tenant_date', 'tenant_id', 'transformation_date'),
|
||||
Index('idx_transformations_reference', 'transformation_reference'),
|
||||
Index('idx_transformations_source', 'tenant_id', 'source_ingredient_id'),
|
||||
Index('idx_transformations_target', 'tenant_id', 'target_ingredient_id'),
|
||||
Index('idx_transformations_stages', 'source_stage', 'target_stage'),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert model to dictionary for API responses"""
|
||||
return {
|
||||
'id': str(self.id),
|
||||
'tenant_id': str(self.tenant_id),
|
||||
'transformation_reference': self.transformation_reference,
|
||||
'source_ingredient_id': str(self.source_ingredient_id),
|
||||
'target_ingredient_id': str(self.target_ingredient_id),
|
||||
'source_stage': self.source_stage if self.source_stage else None,
|
||||
'target_stage': self.target_stage if self.target_stage else None,
|
||||
'source_quantity': self.source_quantity,
|
||||
'target_quantity': self.target_quantity,
|
||||
'conversion_ratio': self.conversion_ratio,
|
||||
'expiration_calculation_method': self.expiration_calculation_method,
|
||||
'expiration_days_offset': self.expiration_days_offset,
|
||||
'transformation_date': self.transformation_date.isoformat() if self.transformation_date else None,
|
||||
'process_notes': self.process_notes,
|
||||
'performed_by': str(self.performed_by) if self.performed_by else None,
|
||||
'source_batch_numbers': self.source_batch_numbers,
|
||||
'target_batch_number': self.target_batch_number,
|
||||
'is_completed': self.is_completed,
|
||||
'is_reversed': self.is_reversed,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'created_by': str(self.created_by) if self.created_by else None,
|
||||
}
|
||||
|
||||
|
||||
class StockAlert(Base):
|
||||
"""Automated stock alerts for low stock, expiration, etc."""
|
||||
__tablename__ = "stock_alerts"
|
||||
|
||||
Reference in New Issue
Block a user