Spaces:
Sleeping
Sleeping
update config
Browse files
app.py
CHANGED
|
@@ -97,13 +97,31 @@ def calculate_stockout_forecast(method, api_token):
|
|
| 97 |
# Get current inventory (demo data if API unavailable)
|
| 98 |
wb_client = initialize_wb_client(api_token)
|
| 99 |
|
| 100 |
-
if
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
else:
|
| 103 |
inventory_data = utils.load_demo_inventory_data()
|
|
|
|
| 104 |
|
| 105 |
-
# Get sales data for forecasting
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
# Initialize forecaster
|
| 109 |
forecaster = InventoryForecaster()
|
|
@@ -113,52 +131,66 @@ def calculate_stockout_forecast(method, api_token):
|
|
| 113 |
for _, item in inventory_data.iterrows():
|
| 114 |
product_sales = sales_data[sales_data['product_id'] == item['product_id']]
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
days_left = forecaster.weighted_average_method(
|
| 132 |
item['current_stock'],
|
| 133 |
product_sales
|
| 134 |
)
|
| 135 |
else:
|
| 136 |
-
days_left = forecaster.
|
| 137 |
item['current_stock'],
|
| 138 |
-
|
| 139 |
-
seasonal_factor=1.0
|
| 140 |
)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
# Create results DataFrame
|
| 158 |
results_df = pd.DataFrame(forecasts)
|
| 159 |
|
| 160 |
if results_df.empty:
|
| 161 |
-
|
|
|
|
| 162 |
|
| 163 |
# Sort by days until stockout
|
| 164 |
results_df = results_df.sort_values('Days Until Stockout')
|
|
@@ -167,8 +199,9 @@ def calculate_stockout_forecast(method, api_token):
|
|
| 167 |
critical_items = len(results_df[results_df['Days Until Stockout'] < 7])
|
| 168 |
warning_items = len(results_df[results_df['Days Until Stockout'].between(7, 14)])
|
| 169 |
|
|
|
|
| 170 |
summary = f"""
|
| 171 |
-
## Inventory Forecast - {method.replace('_', ' ').title()} Method
|
| 172 |
|
| 173 |
- **Critical Items** (< 7 days): {critical_items}
|
| 174 |
- **Warning Items** (7-14 days): {warning_items}
|
|
@@ -183,7 +216,8 @@ def calculate_stockout_forecast(method, api_token):
|
|
| 183 |
except Exception as e:
|
| 184 |
error_msg = f"Error calculating forecast: {str(e)}"
|
| 185 |
gr.Error(error_msg)
|
| 186 |
-
|
|
|
|
| 187 |
|
| 188 |
def update_status(api_token):
|
| 189 |
"""Update API status based on token"""
|
|
|
|
| 97 |
# Get current inventory (demo data if API unavailable)
|
| 98 |
wb_client = initialize_wb_client(api_token)
|
| 99 |
|
| 100 |
+
# Determine if we're in demo mode
|
| 101 |
+
use_demo_mode = not api_token or api_token.strip() == ""
|
| 102 |
+
|
| 103 |
+
if wb_client and not use_demo_mode:
|
| 104 |
+
try:
|
| 105 |
+
inventory_data = wb_client.get_stocks()
|
| 106 |
+
# If API returns empty data, fall back to demo
|
| 107 |
+
if inventory_data.empty:
|
| 108 |
+
inventory_data = utils.load_demo_inventory_data()
|
| 109 |
+
use_demo_mode = True
|
| 110 |
+
except Exception:
|
| 111 |
+
inventory_data = utils.load_demo_inventory_data()
|
| 112 |
+
use_demo_mode = True
|
| 113 |
else:
|
| 114 |
inventory_data = utils.load_demo_inventory_data()
|
| 115 |
+
use_demo_mode = True
|
| 116 |
|
| 117 |
+
# Get sales data for forecasting - ensure consistency with inventory data source
|
| 118 |
+
if use_demo_mode:
|
| 119 |
+
sales_data = utils.load_demo_sales_data("month")
|
| 120 |
+
else:
|
| 121 |
+
sales_data = get_sales_data("month", api_token)
|
| 122 |
+
# If API sales data is empty, fall back to demo
|
| 123 |
+
if sales_data.empty:
|
| 124 |
+
sales_data = utils.load_demo_sales_data("month")
|
| 125 |
|
| 126 |
# Initialize forecaster
|
| 127 |
forecaster = InventoryForecaster()
|
|
|
|
| 131 |
for _, item in inventory_data.iterrows():
|
| 132 |
product_sales = sales_data[sales_data['product_id'] == item['product_id']]
|
| 133 |
|
| 134 |
+
# If no sales data for this product, use average daily sales of 1
|
| 135 |
+
if product_sales.empty:
|
| 136 |
+
avg_daily_sales = 1
|
| 137 |
+
max_daily_sales = 1
|
| 138 |
+
else:
|
| 139 |
+
avg_daily_sales = product_sales['quantity'].mean()
|
| 140 |
+
max_daily_sales = product_sales['quantity'].max()
|
| 141 |
+
|
| 142 |
+
if method == "simple":
|
| 143 |
+
days_left = forecaster.simple_division_method(
|
| 144 |
+
item['current_stock'],
|
| 145 |
+
avg_daily_sales
|
| 146 |
+
)
|
| 147 |
+
elif method == "safety_stock":
|
| 148 |
+
days_left = forecaster.safety_stock_method(
|
| 149 |
+
item['current_stock'],
|
| 150 |
+
avg_daily_sales,
|
| 151 |
+
max_daily_sales,
|
| 152 |
+
avg_lead_time=7,
|
| 153 |
+
max_lead_time=14
|
| 154 |
+
)
|
| 155 |
+
elif method == "weighted":
|
| 156 |
+
if not product_sales.empty:
|
| 157 |
days_left = forecaster.weighted_average_method(
|
| 158 |
item['current_stock'],
|
| 159 |
product_sales
|
| 160 |
)
|
| 161 |
else:
|
| 162 |
+
days_left = forecaster.simple_division_method(
|
| 163 |
item['current_stock'],
|
| 164 |
+
avg_daily_sales
|
|
|
|
| 165 |
)
|
| 166 |
+
else:
|
| 167 |
+
days_left = forecaster.seasonal_adjustment_method(
|
| 168 |
+
item['current_stock'],
|
| 169 |
+
avg_daily_sales,
|
| 170 |
+
seasonal_factor=1.0
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
# Risk categorization
|
| 174 |
+
if days_left < 7:
|
| 175 |
+
risk_level = "🔴 Critical"
|
| 176 |
+
elif days_left < 14:
|
| 177 |
+
risk_level = "🟡 Warning"
|
| 178 |
+
else:
|
| 179 |
+
risk_level = "🟢 Safe"
|
| 180 |
+
|
| 181 |
+
forecasts.append({
|
| 182 |
+
'Product': item['product_name'],
|
| 183 |
+
'Current Stock': item['current_stock'],
|
| 184 |
+
'Days Until Stockout': round(days_left, 1),
|
| 185 |
+
'Risk Level': risk_level
|
| 186 |
+
})
|
| 187 |
|
| 188 |
# Create results DataFrame
|
| 189 |
results_df = pd.DataFrame(forecasts)
|
| 190 |
|
| 191 |
if results_df.empty:
|
| 192 |
+
# Return all 3 required values for Gradio
|
| 193 |
+
return "No inventory data available.", pd.DataFrame(), None
|
| 194 |
|
| 195 |
# Sort by days until stockout
|
| 196 |
results_df = results_df.sort_values('Days Until Stockout')
|
|
|
|
| 199 |
critical_items = len(results_df[results_df['Days Until Stockout'] < 7])
|
| 200 |
warning_items = len(results_df[results_df['Days Until Stockout'].between(7, 14)])
|
| 201 |
|
| 202 |
+
mode_indicator = "Demo Mode" if use_demo_mode else "Live Data"
|
| 203 |
summary = f"""
|
| 204 |
+
## Inventory Forecast - {method.replace('_', ' ').title()} Method ({mode_indicator})
|
| 205 |
|
| 206 |
- **Critical Items** (< 7 days): {critical_items}
|
| 207 |
- **Warning Items** (7-14 days): {warning_items}
|
|
|
|
| 216 |
except Exception as e:
|
| 217 |
error_msg = f"Error calculating forecast: {str(e)}"
|
| 218 |
gr.Error(error_msg)
|
| 219 |
+
# Return all 3 required values for Gradio
|
| 220 |
+
return error_msg, pd.DataFrame(), None
|
| 221 |
|
| 222 |
def update_status(api_token):
|
| 223 |
"""Update API status based on token"""
|
config.py
CHANGED
|
@@ -17,12 +17,12 @@ class Config:
|
|
| 17 |
self.wildberries_api_token = os.getenv("WILDBERRIES_API_TOKEN")
|
| 18 |
self.wildberries_base_url = "https://statistics-api.wildberries.ru"
|
| 19 |
self.wildberries_content_url = "https://content-api.wildberries.ru"
|
| 20 |
-
self.wildberries_analytics_url = "https://analytics-api.wildberries.ru"
|
|
|
|
| 21 |
|
| 22 |
-
# Rate limiting settings (
|
| 23 |
self.rate_limit_requests = 300 # requests per minute
|
| 24 |
self.rate_limit_window = 60 # seconds
|
| 25 |
-
self.rate_limit_burst = 10 # burst allowance
|
| 26 |
|
| 27 |
# Request timeout settings
|
| 28 |
self.request_timeout = 30 # seconds
|
|
@@ -75,7 +75,13 @@ class Config:
|
|
| 75 |
"incomes": f"{self.wildberries_base_url}/api/v1/supplier/incomes",
|
| 76 |
"reportDetailByPeriod": f"{self.wildberries_base_url}/api/v1/supplier/reportDetailByPeriod",
|
| 77 |
"analytics": f"{self.wildberries_analytics_url}/api/v2/nm-report/detail",
|
| 78 |
-
"content": f"{self.wildberries_content_url}/content/v1/cards/cursor/list"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
def validate_token(self, token: str) -> bool:
|
|
|
|
| 17 |
self.wildberries_api_token = os.getenv("WILDBERRIES_API_TOKEN")
|
| 18 |
self.wildberries_base_url = "https://statistics-api.wildberries.ru"
|
| 19 |
self.wildberries_content_url = "https://content-api.wildberries.ru"
|
| 20 |
+
self.wildberries_analytics_url = "https://seller-analytics-api.wildberries.ru"
|
| 21 |
+
self.wildberries_common_url = "https://common-api.wildberries.ru"
|
| 22 |
|
| 23 |
+
# Rate limiting settings (matches official documentation)
|
| 24 |
self.rate_limit_requests = 300 # requests per minute
|
| 25 |
self.rate_limit_window = 60 # seconds
|
|
|
|
| 26 |
|
| 27 |
# Request timeout settings
|
| 28 |
self.request_timeout = 30 # seconds
|
|
|
|
| 75 |
"incomes": f"{self.wildberries_base_url}/api/v1/supplier/incomes",
|
| 76 |
"reportDetailByPeriod": f"{self.wildberries_base_url}/api/v1/supplier/reportDetailByPeriod",
|
| 77 |
"analytics": f"{self.wildberries_analytics_url}/api/v2/nm-report/detail",
|
| 78 |
+
"content": f"{self.wildberries_content_url}/content/v1/cards/cursor/list",
|
| 79 |
+
"ping_statistics": f"{self.wildberries_base_url}/ping",
|
| 80 |
+
"ping_content": f"{self.wildberries_content_url}/ping",
|
| 81 |
+
"ping_analytics": f"{self.wildberries_analytics_url}/ping",
|
| 82 |
+
"ping_common": f"{self.wildberries_common_url}/ping",
|
| 83 |
+
"news": f"{self.wildberries_common_url}/api/communications/v2/news",
|
| 84 |
+
"seller_info": f"{self.wildberries_common_url}/api/v1/seller-info"
|
| 85 |
}
|
| 86 |
|
| 87 |
def validate_token(self, token: str) -> bool:
|