Improve the frontend 3

This commit is contained in:
Urtzi Alfaro
2025-10-30 21:08:07 +01:00
parent 36217a2729
commit 63f5c6d512
184 changed files with 21512 additions and 7442 deletions

View File

@@ -114,6 +114,46 @@ class TenantSettings(Base):
"delivery_tracking_enabled": True
})
# Replenishment Planning Settings (Orchestrator Service)
replenishment_settings = Column(JSON, nullable=False, default=lambda: {
"projection_horizon_days": 7,
"service_level": 0.95,
"buffer_days": 1,
"enable_auto_replenishment": True,
"min_order_quantity": 1.0,
"max_order_quantity": 1000.0,
"demand_forecast_days": 14
})
# Safety Stock Settings (Orchestrator Service)
safety_stock_settings = Column(JSON, nullable=False, default=lambda: {
"service_level": 0.95,
"method": "statistical",
"min_safety_stock": 0.0,
"max_safety_stock": 100.0,
"reorder_point_calculation": "safety_stock_plus_lead_time_demand"
})
# MOQ Aggregation Settings (Orchestrator Service)
moq_settings = Column(JSON, nullable=False, default=lambda: {
"consolidation_window_days": 7,
"allow_early_ordering": True,
"enable_batch_optimization": True,
"min_batch_size": 1.0,
"max_batch_size": 1000.0
})
# Supplier Selection Settings (Orchestrator Service)
supplier_selection_settings = Column(JSON, nullable=False, default=lambda: {
"price_weight": 0.40,
"lead_time_weight": 0.20,
"quality_weight": 0.20,
"reliability_weight": 0.20,
"diversification_threshold": 1000,
"max_single_percentage": 0.70,
"enable_supplier_score_optimization": True
})
# Timestamps
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
@@ -208,5 +248,37 @@ class TenantSettings(Base):
"dynamic_pricing_enabled": False,
"discount_enabled": True,
"delivery_tracking_enabled": True
},
"replenishment_settings": {
"projection_horizon_days": 7,
"service_level": 0.95,
"buffer_days": 1,
"enable_auto_replenishment": True,
"min_order_quantity": 1.0,
"max_order_quantity": 1000.0,
"demand_forecast_days": 14
},
"safety_stock_settings": {
"service_level": 0.95,
"method": "statistical",
"min_safety_stock": 0.0,
"max_safety_stock": 100.0,
"reorder_point_calculation": "safety_stock_plus_lead_time_demand"
},
"moq_settings": {
"consolidation_window_days": 7,
"allow_early_ordering": True,
"enable_batch_optimization": True,
"min_batch_size": 1.0,
"max_batch_size": 1000.0
},
"supplier_selection_settings": {
"price_weight": 0.40,
"lead_time_weight": 0.20,
"quality_weight": 0.20,
"reliability_weight": 0.20,
"diversification_threshold": 1000,
"max_single_percentage": 0.70,
"enable_supplier_score_optimization": True
}
}

View File

@@ -143,6 +143,55 @@ class OrderSettings(BaseModel):
delivery_tracking_enabled: bool = True
class ReplenishmentSettings(BaseModel):
"""Replenishment planning settings"""
projection_horizon_days: int = Field(7, ge=1, le=30)
service_level: float = Field(0.95, ge=0.0, le=1.0)
buffer_days: int = Field(1, ge=0, le=14)
enable_auto_replenishment: bool = True
min_order_quantity: float = Field(1.0, ge=0.1, le=1000.0)
max_order_quantity: float = Field(1000.0, ge=1.0, le=10000.0)
demand_forecast_days: int = Field(14, ge=1, le=90)
class SafetyStockSettings(BaseModel):
"""Safety stock settings"""
service_level: float = Field(0.95, ge=0.0, le=1.0)
method: str = Field("statistical", description="Method for safety stock calculation")
min_safety_stock: float = Field(0.0, ge=0.0, le=1000.0)
max_safety_stock: float = Field(100.0, ge=0.0, le=1000.0)
reorder_point_calculation: str = Field("safety_stock_plus_lead_time_demand", description="Method for reorder point calculation")
class MOQSettings(BaseModel):
"""MOQ aggregation settings"""
consolidation_window_days: int = Field(7, ge=1, le=30)
allow_early_ordering: bool = True
enable_batch_optimization: bool = True
min_batch_size: float = Field(1.0, ge=0.1, le=1000.0)
max_batch_size: float = Field(1000.0, ge=1.0, le=10000.0)
class SupplierSelectionSettings(BaseModel):
"""Supplier selection settings"""
price_weight: float = Field(0.40, ge=0.0, le=1.0)
lead_time_weight: float = Field(0.20, ge=0.0, le=1.0)
quality_weight: float = Field(0.20, ge=0.0, le=1.0)
reliability_weight: float = Field(0.20, ge=0.0, le=1.0)
diversification_threshold: int = Field(1000, ge=0, le=1000)
max_single_percentage: float = Field(0.70, ge=0.0, le=1.0)
enable_supplier_score_optimization: bool = True
@validator('price_weight', 'lead_time_weight', 'quality_weight', 'reliability_weight')
def validate_weights_sum(cls, v, values):
weights = [values.get('price_weight', 0.40), values.get('lead_time_weight', 0.20),
values.get('quality_weight', 0.20), values.get('reliability_weight', 0.20)]
total = sum(weights)
if total > 1.0:
raise ValueError('Weights must sum to 1.0 or less')
return v
# ================================================================
# REQUEST/RESPONSE SCHEMAS
# ================================================================
@@ -157,6 +206,10 @@ class TenantSettingsResponse(BaseModel):
supplier_settings: SupplierSettings
pos_settings: POSSettings
order_settings: OrderSettings
replenishment_settings: ReplenishmentSettings
safety_stock_settings: SafetyStockSettings
moq_settings: MOQSettings
supplier_selection_settings: SupplierSelectionSettings
created_at: datetime
updated_at: datetime
@@ -172,6 +225,10 @@ class TenantSettingsUpdate(BaseModel):
supplier_settings: Optional[SupplierSettings] = None
pos_settings: Optional[POSSettings] = None
order_settings: Optional[OrderSettings] = None
replenishment_settings: Optional[ReplenishmentSettings] = None
safety_stock_settings: Optional[SafetyStockSettings] = None
moq_settings: Optional[MOQSettings] = None
supplier_selection_settings: Optional[SupplierSelectionSettings] = None
class CategoryUpdateRequest(BaseModel):

View File

@@ -19,7 +19,11 @@ from ..schemas.tenant_settings import (
ProductionSettings,
SupplierSettings,
POSSettings,
OrderSettings
OrderSettings,
ReplenishmentSettings,
SafetyStockSettings,
MOQSettings,
SupplierSelectionSettings
)
logger = structlog.get_logger()
@@ -38,7 +42,11 @@ class TenantSettingsService:
"production": ProductionSettings,
"supplier": SupplierSettings,
"pos": POSSettings,
"order": OrderSettings
"order": OrderSettings,
"replenishment": ReplenishmentSettings,
"safety_stock": SafetyStockSettings,
"moq": MOQSettings,
"supplier_selection": SupplierSelectionSettings
}
# Map category names to database column names
@@ -48,7 +56,11 @@ class TenantSettingsService:
"production": "production_settings",
"supplier": "supplier_settings",
"pos": "pos_settings",
"order": "order_settings"
"order": "order_settings",
"replenishment": "replenishment_settings",
"safety_stock": "safety_stock_settings",
"moq": "moq_settings",
"supplier_selection": "supplier_selection_settings"
}
def __init__(self, db: AsyncSession):
@@ -125,6 +137,18 @@ class TenantSettingsService:
if updates.order_settings is not None:
settings.order_settings = updates.order_settings.dict()
if updates.replenishment_settings is not None:
settings.replenishment_settings = updates.replenishment_settings.dict()
if updates.safety_stock_settings is not None:
settings.safety_stock_settings = updates.safety_stock_settings.dict()
if updates.moq_settings is not None:
settings.moq_settings = updates.moq_settings.dict()
if updates.supplier_selection_settings is not None:
settings.supplier_selection_settings = updates.supplier_selection_settings.dict()
return await self.repository.update(settings)
async def get_category(self, tenant_id: UUID, category: str) -> Dict[str, Any]:
@@ -247,7 +271,11 @@ class TenantSettingsService:
production_settings=defaults["production_settings"],
supplier_settings=defaults["supplier_settings"],
pos_settings=defaults["pos_settings"],
order_settings=defaults["order_settings"]
order_settings=defaults["order_settings"],
replenishment_settings=defaults["replenishment_settings"],
safety_stock_settings=defaults["safety_stock_settings"],
moq_settings=defaults["moq_settings"],
supplier_selection_settings=defaults["supplier_selection_settings"]
)
return await self.repository.create(settings)

View File

@@ -0,0 +1,102 @@
"""add missing settings columns to tenant settings
Revision ID: 20251030_add_missing_settings
Revises: 20251028_remove_sub_tier
Create Date: 2025-10-30
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from uuid import uuid4
import json
# revision identifiers, used by Alembic.
revision = '20251030_add_missing_settings'
down_revision = '20251028_remove_sub_tier'
branch_labels = None
depends_on = None
def get_default_settings():
"""Get default settings for the new categories"""
return {
"replenishment_settings": {
"projection_horizon_days": 7,
"service_level": 0.95,
"buffer_days": 1,
"enable_auto_replenishment": True,
"min_order_quantity": 1.0,
"max_order_quantity": 1000.0,
"demand_forecast_days": 14
},
"safety_stock_settings": {
"service_level": 0.95,
"method": "statistical",
"min_safety_stock": 0.0,
"max_safety_stock": 100.0,
"reorder_point_calculation": "safety_stock_plus_lead_time_demand"
},
"moq_settings": {
"consolidation_window_days": 7,
"allow_early_ordering": True,
"enable_batch_optimization": True,
"min_batch_size": 1.0,
"max_batch_size": 1000.0
},
"supplier_selection_settings": {
"price_weight": 0.40,
"lead_time_weight": 0.20,
"quality_weight": 0.20,
"reliability_weight": 0.20,
"diversification_threshold": 1000,
"max_single_percentage": 0.70,
"enable_supplier_score_optimization": True
}
}
def upgrade():
"""Add missing settings columns to tenant_settings table"""
# Add the missing columns with default values
default_settings = get_default_settings()
# Add replenishment_settings column
op.add_column('tenant_settings',
sa.Column('replenishment_settings', postgresql.JSON(),
nullable=False,
server_default=str(default_settings["replenishment_settings"]).replace("'", '"').replace("True", "true").replace("False", "false"))
)
# Add safety_stock_settings column
op.add_column('tenant_settings',
sa.Column('safety_stock_settings', postgresql.JSON(),
nullable=False,
server_default=str(default_settings["safety_stock_settings"]).replace("'", '"').replace("True", "true").replace("False", "false"))
)
# Add moq_settings column
op.add_column('tenant_settings',
sa.Column('moq_settings', postgresql.JSON(),
nullable=False,
server_default=str(default_settings["moq_settings"]).replace("'", '"').replace("True", "true").replace("False", "false"))
)
# Add supplier_selection_settings column
op.add_column('tenant_settings',
sa.Column('supplier_selection_settings', postgresql.JSON(),
nullable=False,
server_default=str(default_settings["supplier_selection_settings"]).replace("'", '"').replace("True", "true").replace("False", "false"))
)
# Update the updated_at timestamp for all existing rows
connection = op.get_bind()
connection.execute(sa.text("UPDATE tenant_settings SET updated_at = now()"))
def downgrade():
"""Remove the added settings columns from tenant_settings table"""
op.drop_column('tenant_settings', 'supplier_selection_settings')
op.drop_column('tenant_settings', 'moq_settings')
op.drop_column('tenant_settings', 'safety_stock_settings')
op.drop_column('tenant_settings', 'replenishment_settings')