Fix new services implementation 3

This commit is contained in:
Urtzi Alfaro
2025-08-14 16:47:34 +02:00
parent 0951547e92
commit 03737430ee
51 changed files with 657 additions and 982 deletions

View File

@@ -464,47 +464,47 @@ async def get_orders_by_supplier(
raise HTTPException(status_code=500, detail="Failed to retrieve orders by supplier")
@router.get("/ingredients/{ingredient_id}/history")
async def get_ingredient_purchase_history(
ingredient_id: UUID = Path(..., description="Ingredient ID"),
@router.get("/inventory-products/{inventory_product_id}/history")
async def get_inventory_product_purchase_history(
inventory_product_id: UUID = Path(..., description="Inventory Product ID"),
days_back: int = Query(90, ge=1, le=365, description="Number of days to look back"),
current_user: UserInfo = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get purchase history for a specific ingredient"""
"""Get purchase history for a specific inventory product"""
require_permissions(current_user, ["purchase_orders:read"])
try:
service = PurchaseOrderService(db)
history = await service.get_ingredient_purchase_history(
history = await service.get_inventory_product_purchase_history(
tenant_id=current_user.tenant_id,
ingredient_id=ingredient_id,
inventory_product_id=inventory_product_id,
days_back=days_back
)
return history
except Exception as e:
logger.error("Error getting ingredient purchase history", ingredient_id=str(ingredient_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve ingredient purchase history")
logger.error("Error getting inventory product purchase history", inventory_product_id=str(inventory_product_id), error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve inventory product purchase history")
@router.get("/ingredients/top-purchased")
async def get_top_purchased_ingredients(
@router.get("/inventory-products/top-purchased")
async def get_top_purchased_inventory_products(
days_back: int = Query(30, ge=1, le=365, description="Number of days to look back"),
limit: int = Query(10, ge=1, le=50, description="Number of top ingredients to return"),
limit: int = Query(10, ge=1, le=50, description="Number of top inventory products to return"),
current_user: UserInfo = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get most purchased ingredients by value"""
"""Get most purchased inventory products by value"""
require_permissions(current_user, ["purchase_orders:read"])
try:
service = PurchaseOrderService(db)
ingredients = await service.get_top_purchased_ingredients(
products = await service.get_top_purchased_inventory_products(
tenant_id=current_user.tenant_id,
days_back=days_back,
limit=limit
)
return ingredients
return products
except Exception as e:
logger.error("Error getting top purchased ingredients", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve top purchased ingredients")
logger.error("Error getting top purchased inventory products", error=str(e))
raise HTTPException(status_code=500, detail="Failed to retrieve top purchased inventory products")

View File

@@ -186,9 +186,8 @@ class SupplierPriceList(Base):
supplier_id = Column(UUID(as_uuid=True), ForeignKey('suppliers.id'), nullable=False, index=True)
# Product identification (references inventory service)
ingredient_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to inventory.ingredients
inventory_product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to inventory products
product_code = Column(String(100), nullable=True) # Supplier's product code
product_name = Column(String(255), nullable=False)
# Pricing information
unit_price = Column(Numeric(10, 4), nullable=False)
@@ -228,7 +227,7 @@ class SupplierPriceList(Base):
# Indexes
__table_args__ = (
Index('ix_price_lists_tenant_supplier', 'tenant_id', 'supplier_id'),
Index('ix_price_lists_ingredient', 'ingredient_id'),
Index('ix_price_lists_inventory_product', 'inventory_product_id'),
Index('ix_price_lists_active', 'is_active'),
Index('ix_price_lists_effective_date', 'effective_date'),
)
@@ -317,9 +316,8 @@ class PurchaseOrderItem(Base):
price_list_item_id = Column(UUID(as_uuid=True), ForeignKey('supplier_price_lists.id'), nullable=True, index=True)
# Product identification
ingredient_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to inventory.ingredients
inventory_product_id = Column(UUID(as_uuid=True), nullable=False, index=True) # Reference to inventory products
product_code = Column(String(100), nullable=True) # Supplier's product code
product_name = Column(String(255), nullable=False)
# Order quantities
ordered_quantity = Column(Integer, nullable=False)
@@ -347,7 +345,7 @@ class PurchaseOrderItem(Base):
# Indexes
__table_args__ = (
Index('ix_po_items_tenant_po', 'tenant_id', 'purchase_order_id'),
Index('ix_po_items_ingredient', 'ingredient_id'),
Index('ix_po_items_inventory_product', 'inventory_product_id'),
)
@@ -421,8 +419,7 @@ class DeliveryItem(Base):
purchase_order_item_id = Column(UUID(as_uuid=True), ForeignKey('purchase_order_items.id'), nullable=False, index=True)
# Product identification
ingredient_id = Column(UUID(as_uuid=True), nullable=False, index=True)
product_name = Column(String(255), nullable=False)
inventory_product_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Delivery quantities
ordered_quantity = Column(Integer, nullable=False)
@@ -451,7 +448,7 @@ class DeliveryItem(Base):
# Indexes
__table_args__ = (
Index('ix_delivery_items_tenant_delivery', 'tenant_id', 'delivery_id'),
Index('ix_delivery_items_ingredient', 'ingredient_id'),
Index('ix_delivery_items_inventory_product', 'inventory_product_id'),
)

View File

@@ -28,19 +28,19 @@ class PurchaseOrderItemRepository(BaseRepository[PurchaseOrderItem]):
.all()
)
def get_by_ingredient(
def get_by_inventory_product(
self,
tenant_id: UUID,
ingredient_id: UUID,
inventory_product_id: UUID,
limit: int = 20
) -> List[PurchaseOrderItem]:
"""Get recent order items for a specific ingredient"""
"""Get recent order items for a specific inventory product"""
return (
self.db.query(self.model)
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.ingredient_id == ingredient_id
self.model.inventory_product_id == inventory_product_id
)
)
.order_by(self.model.created_at.desc())
@@ -103,7 +103,7 @@ class PurchaseOrderItemRepository(BaseRepository[PurchaseOrderItem]):
def get_pending_receipt_items(
self,
tenant_id: UUID,
ingredient_id: Optional[UUID] = None
inventory_product_id: Optional[UUID] = None
) -> List[PurchaseOrderItem]:
"""Get items pending receipt (not yet delivered)"""
query = (
@@ -116,8 +116,8 @@ class PurchaseOrderItemRepository(BaseRepository[PurchaseOrderItem]):
)
)
if ingredient_id:
query = query.filter(self.model.ingredient_id == ingredient_id)
if inventory_product_id:
query = query.filter(self.model.inventory_product_id == inventory_product_id)
return query.order_by(self.model.created_at).all()
@@ -134,13 +134,13 @@ class PurchaseOrderItemRepository(BaseRepository[PurchaseOrderItem]):
self.db.refresh(item)
return item
def get_ingredient_purchase_history(
def get_inventory_product_purchase_history(
self,
tenant_id: UUID,
ingredient_id: UUID,
inventory_product_id: UUID,
days_back: int = 90
) -> Dict[str, Any]:
"""Get purchase history and analytics for an ingredient"""
"""Get purchase history and analytics for an inventory product"""
from datetime import timedelta
cutoff_date = datetime.utcnow() - timedelta(days=days_back)
@@ -151,7 +151,7 @@ class PurchaseOrderItemRepository(BaseRepository[PurchaseOrderItem]):
.filter(
and_(
self.model.tenant_id == tenant_id,
self.model.ingredient_id == ingredient_id,
self.model.inventory_product_id == inventory_product_id,
self.model.created_at >= cutoff_date
)
)
@@ -202,22 +202,21 @@ class PurchaseOrderItemRepository(BaseRepository[PurchaseOrderItem]):
"price_trend": price_trend
}
def get_top_purchased_ingredients(
def get_top_purchased_inventory_products(
self,
tenant_id: UUID,
days_back: int = 30,
limit: int = 10
) -> List[Dict[str, Any]]:
"""Get most purchased ingredients by quantity or value"""
"""Get most purchased inventory products by quantity or value"""
from datetime import timedelta
cutoff_date = datetime.utcnow() - timedelta(days=days_back)
# Group by ingredient and calculate totals
# Group by inventory product and calculate totals
results = (
self.db.query(
self.model.ingredient_id,
self.model.product_name,
self.model.inventory_product_id,
self.model.unit_of_measure,
func.sum(self.model.ordered_quantity).label('total_quantity'),
func.sum(self.model.line_total).label('total_amount'),
@@ -231,8 +230,7 @@ class PurchaseOrderItemRepository(BaseRepository[PurchaseOrderItem]):
)
)
.group_by(
self.model.ingredient_id,
self.model.product_name,
self.model.inventory_product_id,
self.model.unit_of_measure
)
.order_by(func.sum(self.model.line_total).desc())
@@ -242,8 +240,7 @@ class PurchaseOrderItemRepository(BaseRepository[PurchaseOrderItem]):
return [
{
"ingredient_id": str(row.ingredient_id),
"product_name": row.product_name,
"inventory_product_id": str(row.inventory_product_id),
"unit_of_measure": row.unit_of_measure,
"total_quantity": int(row.total_quantity),
"total_amount": round(float(row.total_amount), 2),

View File

@@ -186,9 +186,8 @@ class SupplierSummary(BaseModel):
class PurchaseOrderItemCreate(BaseModel):
"""Schema for creating purchase order items"""
ingredient_id: UUID
inventory_product_id: UUID
product_code: Optional[str] = Field(None, max_length=100)
product_name: str = Field(..., min_length=1, max_length=255)
ordered_quantity: int = Field(..., gt=0)
unit_of_measure: str = Field(..., max_length=20)
unit_price: Decimal = Field(..., gt=0)
@@ -210,9 +209,8 @@ class PurchaseOrderItemResponse(BaseModel):
tenant_id: UUID
purchase_order_id: UUID
price_list_item_id: Optional[UUID] = None
ingredient_id: UUID
inventory_product_id: UUID
product_code: Optional[str] = None
product_name: str
ordered_quantity: int
unit_of_measure: str
unit_price: Decimal
@@ -376,8 +374,7 @@ class PurchaseOrderSummary(BaseModel):
class DeliveryItemCreate(BaseModel):
"""Schema for creating delivery items"""
purchase_order_item_id: UUID
ingredient_id: UUID
product_name: str = Field(..., min_length=1, max_length=255)
inventory_product_id: UUID
ordered_quantity: int = Field(..., gt=0)
delivered_quantity: int = Field(..., ge=0)
accepted_quantity: int = Field(..., ge=0)
@@ -400,8 +397,7 @@ class DeliveryItemResponse(BaseModel):
tenant_id: UUID
delivery_id: UUID
purchase_order_item_id: UUID
ingredient_id: UUID
product_name: str
inventory_product_id: UUID
ordered_quantity: int
delivered_quantity: int
accepted_quantity: int

View File

@@ -444,24 +444,24 @@ class PurchaseOrderService:
return to_status in valid_transitions.get(from_status, [])
async def get_ingredient_purchase_history(
async def get_inventory_product_purchase_history(
self,
tenant_id: UUID,
ingredient_id: UUID,
inventory_product_id: UUID,
days_back: int = 90
) -> Dict[str, Any]:
"""Get purchase history for an ingredient"""
return self.item_repository.get_ingredient_purchase_history(
tenant_id, ingredient_id, days_back
"""Get purchase history for an inventory product"""
return self.item_repository.get_inventory_product_purchase_history(
tenant_id, inventory_product_id, days_back
)
async def get_top_purchased_ingredients(
async def get_top_purchased_inventory_products(
self,
tenant_id: UUID,
days_back: int = 30,
limit: int = 10
) -> List[Dict[str, Any]]:
"""Get most purchased ingredients"""
return self.item_repository.get_top_purchased_ingredients(
"""Get most purchased inventory products"""
return self.item_repository.get_top_purchased_inventory_products(
tenant_id, days_back, limit
)

View File

@@ -1,404 +0,0 @@
"""Initial supplier and procurement tables
Revision ID: 001_initial_supplier_tables
Revises:
Create Date: 2024-01-15 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID, JSONB
# revision identifiers, used by Alembic.
revision = '001_initial_supplier_tables'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create suppliers table
op.create_table('suppliers',
sa.Column('id', UUID(as_uuid=True), nullable=False, primary_key=True),
sa.Column('tenant_id', UUID(as_uuid=True), nullable=False),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('supplier_code', sa.String(50), nullable=True),
sa.Column('tax_id', sa.String(50), nullable=True),
sa.Column('registration_number', sa.String(100), nullable=True),
sa.Column('supplier_type', sa.Enum('INGREDIENTS', 'PACKAGING', 'EQUIPMENT', 'SERVICES', 'UTILITIES', 'MULTI', name='suppliertype'), nullable=False),
sa.Column('status', sa.Enum('ACTIVE', 'INACTIVE', 'PENDING_APPROVAL', 'SUSPENDED', 'BLACKLISTED', name='supplierstatus'), nullable=False, default='PENDING_APPROVAL'),
sa.Column('contact_person', sa.String(200), nullable=True),
sa.Column('email', sa.String(254), nullable=True),
sa.Column('phone', sa.String(30), nullable=True),
sa.Column('mobile', sa.String(30), nullable=True),
sa.Column('website', sa.String(255), nullable=True),
sa.Column('address_line1', sa.String(255), nullable=True),
sa.Column('address_line2', sa.String(255), nullable=True),
sa.Column('city', sa.String(100), nullable=True),
sa.Column('state_province', sa.String(100), nullable=True),
sa.Column('postal_code', sa.String(20), nullable=True),
sa.Column('country', sa.String(100), nullable=True),
sa.Column('payment_terms', sa.Enum('CASH_ON_DELIVERY', 'NET_15', 'NET_30', 'NET_45', 'NET_60', 'PREPAID', 'CREDIT_TERMS', name='paymentterms'), nullable=False, default='NET_30'),
sa.Column('credit_limit', sa.Numeric(12, 2), nullable=True),
sa.Column('currency', sa.String(3), nullable=False, default='EUR'),
sa.Column('standard_lead_time', sa.Integer(), nullable=False, default=3),
sa.Column('minimum_order_amount', sa.Numeric(10, 2), nullable=True),
sa.Column('delivery_area', sa.String(255), nullable=True),
sa.Column('quality_rating', sa.Float(), nullable=True, default=0.0),
sa.Column('delivery_rating', sa.Float(), nullable=True, default=0.0),
sa.Column('total_orders', sa.Integer(), nullable=False, default=0),
sa.Column('total_amount', sa.Numeric(12, 2), nullable=False, default=0.0),
sa.Column('approved_by', UUID(as_uuid=True), nullable=True),
sa.Column('approved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('rejection_reason', sa.Text(), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('certifications', JSONB, nullable=True),
sa.Column('business_hours', JSONB, nullable=True),
sa.Column('specializations', JSONB, nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', UUID(as_uuid=True), nullable=False),
sa.Column('updated_by', UUID(as_uuid=True), nullable=False)
)
# Create supplier_price_lists table
op.create_table('supplier_price_lists',
sa.Column('id', UUID(as_uuid=True), nullable=False, primary_key=True),
sa.Column('tenant_id', UUID(as_uuid=True), nullable=False),
sa.Column('supplier_id', UUID(as_uuid=True), nullable=False),
sa.Column('ingredient_id', UUID(as_uuid=True), nullable=False),
sa.Column('product_code', sa.String(100), nullable=True),
sa.Column('product_name', sa.String(255), nullable=False),
sa.Column('unit_price', sa.Numeric(10, 4), nullable=False),
sa.Column('unit_of_measure', sa.String(20), nullable=False),
sa.Column('minimum_order_quantity', sa.Integer(), nullable=True, default=1),
sa.Column('price_per_unit', sa.Numeric(10, 4), nullable=False),
sa.Column('tier_pricing', JSONB, nullable=True),
sa.Column('effective_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('expiry_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False, default=True),
sa.Column('brand', sa.String(100), nullable=True),
sa.Column('packaging_size', sa.String(50), nullable=True),
sa.Column('origin_country', sa.String(100), nullable=True),
sa.Column('shelf_life_days', sa.Integer(), nullable=True),
sa.Column('storage_requirements', sa.Text(), nullable=True),
sa.Column('quality_specs', JSONB, nullable=True),
sa.Column('allergens', JSONB, nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', UUID(as_uuid=True), nullable=False),
sa.Column('updated_by', UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'])
)
# Create purchase_orders table
op.create_table('purchase_orders',
sa.Column('id', UUID(as_uuid=True), nullable=False, primary_key=True),
sa.Column('tenant_id', UUID(as_uuid=True), nullable=False),
sa.Column('supplier_id', UUID(as_uuid=True), nullable=False),
sa.Column('po_number', sa.String(50), nullable=False),
sa.Column('reference_number', sa.String(100), nullable=True),
sa.Column('status', sa.Enum('DRAFT', 'PENDING_APPROVAL', 'APPROVED', 'SENT_TO_SUPPLIER', 'CONFIRMED', 'PARTIALLY_RECEIVED', 'COMPLETED', 'CANCELLED', 'DISPUTED', name='purchaseorderstatus'), nullable=False, default='DRAFT'),
sa.Column('priority', sa.String(20), nullable=False, default='normal'),
sa.Column('order_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('required_delivery_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('estimated_delivery_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('subtotal', sa.Numeric(12, 2), nullable=False, default=0.0),
sa.Column('tax_amount', sa.Numeric(12, 2), nullable=False, default=0.0),
sa.Column('shipping_cost', sa.Numeric(10, 2), nullable=False, default=0.0),
sa.Column('discount_amount', sa.Numeric(10, 2), nullable=False, default=0.0),
sa.Column('total_amount', sa.Numeric(12, 2), nullable=False, default=0.0),
sa.Column('currency', sa.String(3), nullable=False, default='EUR'),
sa.Column('delivery_address', sa.Text(), nullable=True),
sa.Column('delivery_instructions', sa.Text(), nullable=True),
sa.Column('delivery_contact', sa.String(200), nullable=True),
sa.Column('delivery_phone', sa.String(30), nullable=True),
sa.Column('requires_approval', sa.Boolean(), nullable=False, default=False),
sa.Column('approved_by', UUID(as_uuid=True), nullable=True),
sa.Column('approved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('rejection_reason', sa.Text(), nullable=True),
sa.Column('sent_to_supplier_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('supplier_confirmation_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('supplier_reference', sa.String(100), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('internal_notes', sa.Text(), nullable=True),
sa.Column('terms_and_conditions', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', UUID(as_uuid=True), nullable=False),
sa.Column('updated_by', UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'])
)
# Create purchase_order_items table
op.create_table('purchase_order_items',
sa.Column('id', UUID(as_uuid=True), nullable=False, primary_key=True),
sa.Column('tenant_id', UUID(as_uuid=True), nullable=False),
sa.Column('purchase_order_id', UUID(as_uuid=True), nullable=False),
sa.Column('price_list_item_id', UUID(as_uuid=True), nullable=True),
sa.Column('ingredient_id', UUID(as_uuid=True), nullable=False),
sa.Column('product_code', sa.String(100), nullable=True),
sa.Column('product_name', sa.String(255), nullable=False),
sa.Column('ordered_quantity', sa.Integer(), nullable=False),
sa.Column('unit_of_measure', sa.String(20), nullable=False),
sa.Column('unit_price', sa.Numeric(10, 4), nullable=False),
sa.Column('line_total', sa.Numeric(12, 2), nullable=False),
sa.Column('received_quantity', sa.Integer(), nullable=False, default=0),
sa.Column('remaining_quantity', sa.Integer(), nullable=False, default=0),
sa.Column('quality_requirements', sa.Text(), nullable=True),
sa.Column('item_notes', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['purchase_order_id'], ['purchase_orders.id']),
sa.ForeignKeyConstraint(['price_list_item_id'], ['supplier_price_lists.id'])
)
# Create deliveries table
op.create_table('deliveries',
sa.Column('id', UUID(as_uuid=True), nullable=False, primary_key=True),
sa.Column('tenant_id', UUID(as_uuid=True), nullable=False),
sa.Column('purchase_order_id', UUID(as_uuid=True), nullable=False),
sa.Column('supplier_id', UUID(as_uuid=True), nullable=False),
sa.Column('delivery_number', sa.String(50), nullable=False),
sa.Column('supplier_delivery_note', sa.String(100), nullable=True),
sa.Column('status', sa.Enum('SCHEDULED', 'IN_TRANSIT', 'OUT_FOR_DELIVERY', 'DELIVERED', 'PARTIALLY_DELIVERED', 'FAILED_DELIVERY', 'RETURNED', name='deliverystatus'), nullable=False, default='SCHEDULED'),
sa.Column('scheduled_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('estimated_arrival', sa.DateTime(timezone=True), nullable=True),
sa.Column('actual_arrival', sa.DateTime(timezone=True), nullable=True),
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('delivery_address', sa.Text(), nullable=True),
sa.Column('delivery_contact', sa.String(200), nullable=True),
sa.Column('delivery_phone', sa.String(30), nullable=True),
sa.Column('carrier_name', sa.String(200), nullable=True),
sa.Column('tracking_number', sa.String(100), nullable=True),
sa.Column('inspection_passed', sa.Boolean(), nullable=True),
sa.Column('inspection_notes', sa.Text(), nullable=True),
sa.Column('quality_issues', JSONB, nullable=True),
sa.Column('received_by', UUID(as_uuid=True), nullable=True),
sa.Column('received_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('photos', JSONB, nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['purchase_order_id'], ['purchase_orders.id']),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'])
)
# Create delivery_items table
op.create_table('delivery_items',
sa.Column('id', UUID(as_uuid=True), nullable=False, primary_key=True),
sa.Column('tenant_id', UUID(as_uuid=True), nullable=False),
sa.Column('delivery_id', UUID(as_uuid=True), nullable=False),
sa.Column('purchase_order_item_id', UUID(as_uuid=True), nullable=False),
sa.Column('ingredient_id', UUID(as_uuid=True), nullable=False),
sa.Column('product_name', sa.String(255), nullable=False),
sa.Column('ordered_quantity', sa.Integer(), nullable=False),
sa.Column('delivered_quantity', sa.Integer(), nullable=False),
sa.Column('accepted_quantity', sa.Integer(), nullable=False),
sa.Column('rejected_quantity', sa.Integer(), nullable=False, default=0),
sa.Column('batch_lot_number', sa.String(100), nullable=True),
sa.Column('expiry_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('quality_grade', sa.String(20), nullable=True),
sa.Column('quality_issues', sa.Text(), nullable=True),
sa.Column('rejection_reason', sa.Text(), nullable=True),
sa.Column('item_notes', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['delivery_id'], ['deliveries.id']),
sa.ForeignKeyConstraint(['purchase_order_item_id'], ['purchase_order_items.id'])
)
# Create supplier_quality_reviews table
op.create_table('supplier_quality_reviews',
sa.Column('id', UUID(as_uuid=True), nullable=False, primary_key=True),
sa.Column('tenant_id', UUID(as_uuid=True), nullable=False),
sa.Column('supplier_id', UUID(as_uuid=True), nullable=False),
sa.Column('purchase_order_id', UUID(as_uuid=True), nullable=True),
sa.Column('delivery_id', UUID(as_uuid=True), nullable=True),
sa.Column('review_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('review_type', sa.String(50), nullable=False),
sa.Column('quality_rating', sa.Enum('EXCELLENT', 'GOOD', 'AVERAGE', 'POOR', 'VERY_POOR', name='qualityrating'), nullable=False),
sa.Column('delivery_rating', sa.Enum('EXCELLENT', 'GOOD', 'AVERAGE', 'POOR', 'VERY_POOR', name='deliveryrating'), nullable=False),
sa.Column('communication_rating', sa.Integer(), nullable=False),
sa.Column('overall_rating', sa.Float(), nullable=False),
sa.Column('quality_comments', sa.Text(), nullable=True),
sa.Column('delivery_comments', sa.Text(), nullable=True),
sa.Column('communication_comments', sa.Text(), nullable=True),
sa.Column('improvement_suggestions', sa.Text(), nullable=True),
sa.Column('quality_issues', JSONB, nullable=True),
sa.Column('corrective_actions', sa.Text(), nullable=True),
sa.Column('follow_up_required', sa.Boolean(), nullable=False, default=False),
sa.Column('follow_up_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_final', sa.Boolean(), nullable=False, default=True),
sa.Column('approved_by', UUID(as_uuid=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('reviewed_by', UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id']),
sa.ForeignKeyConstraint(['purchase_order_id'], ['purchase_orders.id']),
sa.ForeignKeyConstraint(['delivery_id'], ['deliveries.id'])
)
# Create supplier_invoices table
op.create_table('supplier_invoices',
sa.Column('id', UUID(as_uuid=True), nullable=False, primary_key=True),
sa.Column('tenant_id', UUID(as_uuid=True), nullable=False),
sa.Column('supplier_id', UUID(as_uuid=True), nullable=False),
sa.Column('purchase_order_id', UUID(as_uuid=True), nullable=True),
sa.Column('invoice_number', sa.String(50), nullable=False),
sa.Column('supplier_invoice_number', sa.String(100), nullable=False),
sa.Column('status', sa.Enum('PENDING', 'APPROVED', 'PAID', 'OVERDUE', 'DISPUTED', 'CANCELLED', name='invoicestatus'), nullable=False, default='PENDING'),
sa.Column('invoice_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('due_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('received_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('subtotal', sa.Numeric(12, 2), nullable=False),
sa.Column('tax_amount', sa.Numeric(12, 2), nullable=False, default=0.0),
sa.Column('shipping_cost', sa.Numeric(10, 2), nullable=False, default=0.0),
sa.Column('discount_amount', sa.Numeric(10, 2), nullable=False, default=0.0),
sa.Column('total_amount', sa.Numeric(12, 2), nullable=False),
sa.Column('currency', sa.String(3), nullable=False, default='EUR'),
sa.Column('paid_amount', sa.Numeric(12, 2), nullable=False, default=0.0),
sa.Column('payment_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('payment_reference', sa.String(100), nullable=True),
sa.Column('approved_by', UUID(as_uuid=True), nullable=True),
sa.Column('approved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('rejection_reason', sa.Text(), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('invoice_document_url', sa.String(500), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_by', UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id']),
sa.ForeignKeyConstraint(['purchase_order_id'], ['purchase_orders.id'])
)
# Create indexes
op.create_index('ix_suppliers_tenant_id', 'suppliers', ['tenant_id'])
op.create_index('ix_suppliers_name', 'suppliers', ['name'])
op.create_index('ix_suppliers_tenant_name', 'suppliers', ['tenant_id', 'name'])
op.create_index('ix_suppliers_tenant_status', 'suppliers', ['tenant_id', 'status'])
op.create_index('ix_suppliers_tenant_type', 'suppliers', ['tenant_id', 'supplier_type'])
op.create_index('ix_suppliers_quality_rating', 'suppliers', ['quality_rating'])
op.create_index('ix_suppliers_status', 'suppliers', ['status'])
op.create_index('ix_suppliers_supplier_type', 'suppliers', ['supplier_type'])
op.create_index('ix_price_lists_tenant_id', 'supplier_price_lists', ['tenant_id'])
op.create_index('ix_price_lists_supplier_id', 'supplier_price_lists', ['supplier_id'])
op.create_index('ix_price_lists_tenant_supplier', 'supplier_price_lists', ['tenant_id', 'supplier_id'])
op.create_index('ix_price_lists_ingredient', 'supplier_price_lists', ['ingredient_id'])
op.create_index('ix_price_lists_active', 'supplier_price_lists', ['is_active'])
op.create_index('ix_price_lists_effective_date', 'supplier_price_lists', ['effective_date'])
op.create_index('ix_purchase_orders_tenant_id', 'purchase_orders', ['tenant_id'])
op.create_index('ix_purchase_orders_supplier_id', 'purchase_orders', ['supplier_id'])
op.create_index('ix_purchase_orders_tenant_supplier', 'purchase_orders', ['tenant_id', 'supplier_id'])
op.create_index('ix_purchase_orders_tenant_status', 'purchase_orders', ['tenant_id', 'status'])
op.create_index('ix_purchase_orders_po_number', 'purchase_orders', ['po_number'])
op.create_index('ix_purchase_orders_order_date', 'purchase_orders', ['order_date'])
op.create_index('ix_purchase_orders_delivery_date', 'purchase_orders', ['required_delivery_date'])
op.create_index('ix_purchase_orders_status', 'purchase_orders', ['status'])
op.create_index('ix_po_items_tenant_id', 'purchase_order_items', ['tenant_id'])
op.create_index('ix_po_items_purchase_order_id', 'purchase_order_items', ['purchase_order_id'])
op.create_index('ix_po_items_tenant_po', 'purchase_order_items', ['tenant_id', 'purchase_order_id'])
op.create_index('ix_po_items_ingredient', 'purchase_order_items', ['ingredient_id'])
op.create_index('ix_deliveries_tenant_id', 'deliveries', ['tenant_id'])
op.create_index('ix_deliveries_tenant_status', 'deliveries', ['tenant_id', 'status'])
op.create_index('ix_deliveries_scheduled_date', 'deliveries', ['scheduled_date'])
op.create_index('ix_deliveries_delivery_number', 'deliveries', ['delivery_number'])
op.create_index('ix_delivery_items_tenant_id', 'delivery_items', ['tenant_id'])
op.create_index('ix_delivery_items_delivery_id', 'delivery_items', ['delivery_id'])
op.create_index('ix_delivery_items_tenant_delivery', 'delivery_items', ['tenant_id', 'delivery_id'])
op.create_index('ix_delivery_items_ingredient', 'delivery_items', ['ingredient_id'])
op.create_index('ix_quality_reviews_tenant_id', 'supplier_quality_reviews', ['tenant_id'])
op.create_index('ix_quality_reviews_supplier_id', 'supplier_quality_reviews', ['supplier_id'])
op.create_index('ix_quality_reviews_tenant_supplier', 'supplier_quality_reviews', ['tenant_id', 'supplier_id'])
op.create_index('ix_quality_reviews_date', 'supplier_quality_reviews', ['review_date'])
op.create_index('ix_quality_reviews_overall_rating', 'supplier_quality_reviews', ['overall_rating'])
op.create_index('ix_invoices_tenant_id', 'supplier_invoices', ['tenant_id'])
op.create_index('ix_invoices_supplier_id', 'supplier_invoices', ['supplier_id'])
op.create_index('ix_invoices_tenant_supplier', 'supplier_invoices', ['tenant_id', 'supplier_id'])
op.create_index('ix_invoices_tenant_status', 'supplier_invoices', ['tenant_id', 'status'])
op.create_index('ix_invoices_due_date', 'supplier_invoices', ['due_date'])
op.create_index('ix_invoices_invoice_number', 'supplier_invoices', ['invoice_number'])
def downgrade() -> None:
# Drop indexes
op.drop_index('ix_invoices_invoice_number', 'supplier_invoices')
op.drop_index('ix_invoices_due_date', 'supplier_invoices')
op.drop_index('ix_invoices_tenant_status', 'supplier_invoices')
op.drop_index('ix_invoices_tenant_supplier', 'supplier_invoices')
op.drop_index('ix_invoices_supplier_id', 'supplier_invoices')
op.drop_index('ix_invoices_tenant_id', 'supplier_invoices')
op.drop_index('ix_quality_reviews_overall_rating', 'supplier_quality_reviews')
op.drop_index('ix_quality_reviews_date', 'supplier_quality_reviews')
op.drop_index('ix_quality_reviews_tenant_supplier', 'supplier_quality_reviews')
op.drop_index('ix_quality_reviews_supplier_id', 'supplier_quality_reviews')
op.drop_index('ix_quality_reviews_tenant_id', 'supplier_quality_reviews')
op.drop_index('ix_delivery_items_ingredient', 'delivery_items')
op.drop_index('ix_delivery_items_tenant_delivery', 'delivery_items')
op.drop_index('ix_delivery_items_delivery_id', 'delivery_items')
op.drop_index('ix_delivery_items_tenant_id', 'delivery_items')
op.drop_index('ix_deliveries_delivery_number', 'deliveries')
op.drop_index('ix_deliveries_scheduled_date', 'deliveries')
op.drop_index('ix_deliveries_tenant_status', 'deliveries')
op.drop_index('ix_deliveries_tenant_id', 'deliveries')
op.drop_index('ix_po_items_ingredient', 'purchase_order_items')
op.drop_index('ix_po_items_tenant_po', 'purchase_order_items')
op.drop_index('ix_po_items_purchase_order_id', 'purchase_order_items')
op.drop_index('ix_po_items_tenant_id', 'purchase_order_items')
op.drop_index('ix_purchase_orders_status', 'purchase_orders')
op.drop_index('ix_purchase_orders_delivery_date', 'purchase_orders')
op.drop_index('ix_purchase_orders_order_date', 'purchase_orders')
op.drop_index('ix_purchase_orders_po_number', 'purchase_orders')
op.drop_index('ix_purchase_orders_tenant_status', 'purchase_orders')
op.drop_index('ix_purchase_orders_tenant_supplier', 'purchase_orders')
op.drop_index('ix_purchase_orders_supplier_id', 'purchase_orders')
op.drop_index('ix_purchase_orders_tenant_id', 'purchase_orders')
op.drop_index('ix_price_lists_effective_date', 'supplier_price_lists')
op.drop_index('ix_price_lists_active', 'supplier_price_lists')
op.drop_index('ix_price_lists_ingredient', 'supplier_price_lists')
op.drop_index('ix_price_lists_tenant_supplier', 'supplier_price_lists')
op.drop_index('ix_price_lists_supplier_id', 'supplier_price_lists')
op.drop_index('ix_price_lists_tenant_id', 'supplier_price_lists')
op.drop_index('ix_suppliers_supplier_type', 'suppliers')
op.drop_index('ix_suppliers_status', 'suppliers')
op.drop_index('ix_suppliers_quality_rating', 'suppliers')
op.drop_index('ix_suppliers_tenant_type', 'suppliers')
op.drop_index('ix_suppliers_tenant_status', 'suppliers')
op.drop_index('ix_suppliers_tenant_name', 'suppliers')
op.drop_index('ix_suppliers_name', 'suppliers')
op.drop_index('ix_suppliers_tenant_id', 'suppliers')
# Drop tables
op.drop_table('supplier_invoices')
op.drop_table('supplier_quality_reviews')
op.drop_table('delivery_items')
op.drop_table('deliveries')
op.drop_table('purchase_order_items')
op.drop_table('purchase_orders')
op.drop_table('supplier_price_lists')
op.drop_table('suppliers')
# Drop enums
op.execute('DROP TYPE IF EXISTS invoicestatus')
op.execute('DROP TYPE IF EXISTS deliveryrating')
op.execute('DROP TYPE IF EXISTS qualityrating')
op.execute('DROP TYPE IF EXISTS deliverystatus')
op.execute('DROP TYPE IF EXISTS purchaseorderstatus')
op.execute('DROP TYPE IF EXISTS paymentterms')
op.execute('DROP TYPE IF EXISTS supplierstatus')
op.execute('DROP TYPE IF EXISTS suppliertype')

View File

@@ -0,0 +1,151 @@
"""Standardize product references to inventory_product_id
Revision ID: 001_standardize_product_references
Revises:
Create Date: 2025-01-15 12:00:00.000000
This migration standardizes product references across the suppliers service by:
1. Renaming ingredient_id columns to inventory_product_id
2. Removing redundant product_name columns where UUID references exist
3. Updating indexes to match new column names
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID
# revision identifiers
revision = '001_standardize_product_references'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
"""Apply the changes to standardize product references"""
# 1. Update supplier_price_lists table
print("Updating supplier_price_lists table...")
# Rename ingredient_id to inventory_product_id
op.alter_column('supplier_price_lists', 'ingredient_id',
new_column_name='inventory_product_id')
# Drop the product_name column (redundant with UUID reference)
op.drop_column('supplier_price_lists', 'product_name')
# Update index name
op.drop_index('ix_price_lists_ingredient')
op.create_index('ix_price_lists_inventory_product', 'supplier_price_lists',
['inventory_product_id'])
# 2. Update purchase_order_items table
print("Updating purchase_order_items table...")
# Rename ingredient_id to inventory_product_id
op.alter_column('purchase_order_items', 'ingredient_id',
new_column_name='inventory_product_id')
# Drop the product_name column (redundant with UUID reference)
op.drop_column('purchase_order_items', 'product_name')
# Update index name
op.drop_index('ix_po_items_ingredient')
op.create_index('ix_po_items_inventory_product', 'purchase_order_items',
['inventory_product_id'])
# 3. Update delivery_items table
print("Updating delivery_items table...")
# Rename ingredient_id to inventory_product_id
op.alter_column('delivery_items', 'ingredient_id',
new_column_name='inventory_product_id')
# Drop the product_name column (redundant with UUID reference)
op.drop_column('delivery_items', 'product_name')
# Update index name
op.drop_index('ix_delivery_items_ingredient')
op.create_index('ix_delivery_items_inventory_product', 'delivery_items',
['inventory_product_id'])
print("Migration completed successfully!")
def downgrade():
"""Revert the changes (for rollback purposes)"""
print("Rolling back product reference standardization...")
# 1. Revert delivery_items table
print("Reverting delivery_items table...")
# Revert index name
op.drop_index('ix_delivery_items_inventory_product')
op.create_index('ix_delivery_items_ingredient', 'delivery_items',
['inventory_product_id']) # Will rename back to ingredient_id below
# Add back product_name column (will be empty initially)
op.add_column('delivery_items',
sa.Column('product_name', sa.String(255), nullable=False,
server_default='Unknown Product'))
# Rename inventory_product_id back to ingredient_id
op.alter_column('delivery_items', 'inventory_product_id',
new_column_name='ingredient_id')
# Update index to use ingredient_id
op.drop_index('ix_delivery_items_ingredient')
op.create_index('ix_delivery_items_ingredient', 'delivery_items',
['ingredient_id'])
# 2. Revert purchase_order_items table
print("Reverting purchase_order_items table...")
# Revert index name
op.drop_index('ix_po_items_inventory_product')
op.create_index('ix_po_items_ingredient', 'purchase_order_items',
['inventory_product_id']) # Will rename back to ingredient_id below
# Add back product_name column (will be empty initially)
op.add_column('purchase_order_items',
sa.Column('product_name', sa.String(255), nullable=False,
server_default='Unknown Product'))
# Rename inventory_product_id back to ingredient_id
op.alter_column('purchase_order_items', 'inventory_product_id',
new_column_name='ingredient_id')
# Update index to use ingredient_id
op.drop_index('ix_po_items_ingredient')
op.create_index('ix_po_items_ingredient', 'purchase_order_items',
['ingredient_id'])
# 3. Revert supplier_price_lists table
print("Reverting supplier_price_lists table...")
# Revert index name
op.drop_index('ix_price_lists_inventory_product')
op.create_index('ix_price_lists_ingredient', 'supplier_price_lists',
['inventory_product_id']) # Will rename back to ingredient_id below
# Add back product_name column (will be empty initially)
op.add_column('supplier_price_lists',
sa.Column('product_name', sa.String(255), nullable=False,
server_default='Unknown Product'))
# Rename inventory_product_id back to ingredient_id
op.alter_column('supplier_price_lists', 'inventory_product_id',
new_column_name='ingredient_id')
# Update index to use ingredient_id
op.drop_index('ix_price_lists_ingredient')
op.create_index('ix_price_lists_ingredient', 'supplier_price_lists',
['ingredient_id'])
print("Rollback completed successfully!")