Improve the frontend 3
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user