AjaykumarPilla commited on
Commit
dc741c4
·
verified ·
1 Parent(s): c3082b3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +11 -31
app.py CHANGED
@@ -40,7 +40,7 @@ except Exception as e:
40
  sf = None
41
 
42
  # File to store forecast data
43
- DATA_FILE = "forecast_data.csv"
44
 
45
  def prepare_prophet_data(usage_series):
46
  end_date = datetime.now()
@@ -114,7 +114,6 @@ def validate_usage_series(usage_str):
114
  def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, alert_status: list, current_stock: int, forecast_7: int, forecast_14: int, forecast_30: int, fig_daily: go.Figure, fig_alerts: go.Figure, usage_series: str) -> BytesIO:
115
  try:
116
  logger.info("Starting PDF generation")
117
- # Validate inputs
118
  if not isinstance(forecast_data, dict) or not forecast_data:
119
  logger.error("Invalid forecast_data: Must be a non-empty dictionary")
120
  return None
@@ -139,7 +138,6 @@ def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, al
139
  y_position = 9.5 * inch
140
  logger.info("Initialized PDF canvas")
141
 
142
- # Basic Forecast Data
143
  logger.info("Writing forecast data")
144
  for key, value in forecast_data.items():
145
  display_key = key.replace('_', ' ').title()
@@ -147,7 +145,6 @@ def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, al
147
  c.drawString(1 * inch, y_position, f"{display_key}: {value_str}")
148
  y_position -= 0.3 * inch
149
 
150
- # Add Last 60 Days Usage
151
  y_position -= 0.3 * inch
152
  c.drawString(1 * inch, y_position, "Last 60 Days Usage (comma-separated):")
153
  y_position -= 0.3 * inch
@@ -160,7 +157,6 @@ def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, al
160
  c.drawText(text_object)
161
  logger.info("Added usage series")
162
 
163
- # Add Daily Forecast Values
164
  y_position -= 0.3 * inch
165
  c.drawString(1 * inch, y_position, "Daily Forecast Values (Next 30 Days):")
166
  y_position -= 0.3 * inch
@@ -174,7 +170,6 @@ def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, al
174
  c.drawText(text_object)
175
  logger.info("Added daily forecast values")
176
 
177
- # Add Threshold Alerts
178
  y_position -= 0.3 * inch
179
  c.drawString(1 * inch, y_position, "Threshold Alerts:")
180
  y_position -= 0.3 * inch
@@ -188,7 +183,6 @@ def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, al
188
  y_position -= 0.3 * inch
189
  logger.info("Added threshold alerts")
190
 
191
- # Add Daily Forecast Visualization Data
192
  y_position -= 0.3 * inch
193
  c.drawString(1 * inch, y_position, "Daily Forecast Visualization Data (Next 30 Days):")
194
  y_position -= 0.3 * inch
@@ -203,7 +197,6 @@ def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, al
203
  y_position = 10 * inch
204
  logger.info("Added daily forecast visualization data")
205
 
206
- # Add Daily Forecast Visualization Image
207
  y_position -= 0.3 * inch
208
  if y_position < 4 * inch:
209
  c.showPage()
@@ -222,7 +215,6 @@ def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, al
222
  c.drawString(1 * inch, y_position - 0.3 * inch, "Error: Could not include daily forecast visualization.")
223
  y_position -= 4.5 * inch
224
 
225
- # Add Threshold Alerts Visualization Data
226
  if y_position < 2 * inch:
227
  c.showPage()
228
  c.setFont("Helvetica", 10)
@@ -244,7 +236,6 @@ def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, al
244
  y_position = 10 * inch
245
  logger.info("Added threshold alerts visualization data")
246
 
247
- # Add Threshold Alerts Visualization Image
248
  y_position -= 0.3 * inch
249
  if y_position < 4 * inch:
250
  c.showPage()
@@ -298,11 +289,8 @@ def upload_pdf_to_salesforce(pdf_file: BytesIO, consumable_type: str, record_id:
298
  return None
299
 
300
  def save_forecast_data(consumable_type, usage_series, current_stock, daily_forecasts):
301
- """Save usage series, current stock, and daily forecasts to CSV."""
302
  try:
303
- # Convert usage series to string
304
  usage_str = ','.join(map(str, usage_series))
305
- # Prepare forecast data
306
  forecast_data = {
307
  'consumable_type': [consumable_type],
308
  'usage_series': [usage_str],
@@ -311,10 +299,9 @@ def save_forecast_data(consumable_type, usage_series, current_stock, daily_forec
311
  'forecast_yhat': [daily_forecasts['yhat'].tolist()]
312
  }
313
  df = pd.DataFrame(forecast_data)
314
- # Append to CSV or create new
315
  if os.path.exists(DATA_FILE):
316
  existing_df = pd.read_csv(DATA_FILE)
317
- existing_df = existing_df[existing_df['consumable_type'] != consumable_type] # Remove old data for this type
318
  df = pd.concat([existing_df, df], ignore_index=True)
319
  df.to_csv(DATA_FILE, index=False)
320
  logger.info(f"Saved forecast data for {consumable_type} to {DATA_FILE}")
@@ -322,7 +309,6 @@ def save_forecast_data(consumable_type, usage_series, current_stock, daily_forec
322
  logger.error(f"Error saving forecast data: {str(e)}")
323
 
324
  def load_forecast_data(consumable_type):
325
- """Load previous forecast data for a consumable type."""
326
  try:
327
  if not os.path.exists(DATA_FILE):
328
  logger.warning(f"No forecast data file found at {DATA_FILE}")
@@ -334,7 +320,6 @@ def load_forecast_data(consumable_type):
334
  return None, None, None
335
  usage_series = [float(x) for x in row['usage_series'].iloc[0].split(',')]
336
  current_stock = float(row['current_stock'].iloc[0])
337
- # Parse forecast data (stored as strings, need to eval safely)
338
  forecast_dates = eval(row['forecast_date'].iloc[0])
339
  forecast_yhat = eval(row['forecast_yhat'].iloc[0])
340
  daily_forecasts = pd.DataFrame({'ds': pd.to_datetime(forecast_dates), 'yhat': forecast_yhat})
@@ -344,7 +329,6 @@ def load_forecast_data(consumable_type):
344
  return None, None, None
345
 
346
  def process_forecast(consumable_type, usage_series, current_stock, is_automated=False):
347
- """Process forecast for a given consumable type."""
348
  usage_list, error = validate_usage_series(','.join(map(str, usage_series)))
349
  if error:
350
  logger.error(error)
@@ -424,7 +408,7 @@ def process_forecast(consumable_type, usage_series, current_stock, is_automated=
424
  plot_bgcolor='rgba(0,0,0,0)',
425
  paper_bgcolor='rgba(0,0,0,0)',
426
  margin=dict(l=50, r=50, t=50, b=100)
427
- )
428
  st.plotly_chart(fig_daily, use_container_width=True)
429
 
430
  st.header("Threshold Alerts Visualization")
@@ -446,7 +430,7 @@ def process_forecast(consumable_type, usage_series, current_stock, is_automated=
446
  xaxis_title='Category',
447
  yaxis_title='Units',
448
  template='plotly_white'
449
- )
450
  st.plotly_chart(fig_alerts)
451
  else:
452
  alert_status = [current_stock < forecast for forecast in [forecast_7, forecast_14, forecast_30]]
@@ -475,7 +459,7 @@ def process_forecast(consumable_type, usage_series, current_stock, is_automated=
475
  plot_bgcolor='rgba(0,0,0,0)',
476
  paper_bgcolor='rgba(0,0,0,0)',
477
  margin=dict(l=50, r=50, t=50, b=100)
478
- )
479
  alert_data = pd.DataFrame({
480
  'Category': ['Current Stock', '7-Day Forecast', '14-Day Forecast', '30-Day Forecast'],
481
  'Units': [current_stock, forecast_7, forecast_14, forecast_30],
@@ -494,9 +478,8 @@ def process_forecast(consumable_type, usage_series, current_stock, is_automated=
494
  xaxis_title='Category',
495
  yaxis_title='Units',
496
  template='plotly_white'
497
- )
498
 
499
- # Salesforce record creation with PDF upload
500
  if sf is not None:
501
  try:
502
  order_suggestions_text = f"7 Days: {max(0, forecast_7 - current_stock)} units, 14 Days: {max(0, forecast_14 - current_stock)} units, 30 Days: {max(0, forecast_30 - current_stock)} units"
@@ -547,33 +530,30 @@ def process_forecast(consumable_type, usage_series, current_stock, is_automated=
547
  logger.error(f"Error creating Salesforce record or uploading PDF: {e}", exc_info=True)
548
  if not is_automated:
549
  st.error(f"Error saving to Salesforce: {str(e)}")
 
550
 
551
  return daily_forecasts
552
 
553
  def automate_daily_forecast():
554
- """Run daily forecast automation for all consumable types."""
555
  consumable_types = ['Filters', 'Reagents', 'Vials']
556
  for consumable_type in consumable_types:
557
  logger.info(f"Processing automated forecast for {consumable_type}")
558
- # Load previous data
559
  usage_series, current_stock, prev_daily_forecasts = load_forecast_data(consumable_type)
560
 
561
  if usage_series is None or current_stock is None or prev_daily_forecasts is None:
562
  logger.warning(f"No previous data for {consumable_type}. Skipping automation.")
563
  continue
564
 
565
- # Get 61st day forecast (first day of previous 30-day forecast)
566
- next_day_usage = prev_daily_forecasts['yhat'].iloc[0]
567
- # Update usage series: remove oldest day, append new day
568
  usage_series = usage_series[1:] + [next_day_usage]
569
- # Update current stock: subtract yesterday's forecasted usage
570
  yesterday_usage = prev_daily_forecasts['yhat'].iloc[0]
571
  current_stock = max(0, current_stock - yesterday_usage)
572
 
573
- # Process forecast
574
  daily_forecasts = process_forecast(consumable_type, usage_series, current_stock, is_automated=True)
575
  if daily_forecasts is not None:
576
- # Save new data
577
  save_forecast_data(consumable_type, usage_series, current_stock, daily_forecasts)
578
  logger.info(f"Completed automated forecast for {consumable_type}")
579
  else:
 
40
  sf = None
41
 
42
  # File to store forecast data
43
+ DATA_FILE = "/public/forecast_data.csv"
44
 
45
  def prepare_prophet_data(usage_series):
46
  end_date = datetime.now()
 
114
  def generate_forecast_pdf(forecast_data: dict, daily_forecasts: pd.DataFrame, alert_status: list, current_stock: int, forecast_7: int, forecast_14: int, forecast_30: int, fig_daily: go.Figure, fig_alerts: go.Figure, usage_series: str) -> BytesIO:
115
  try:
116
  logger.info("Starting PDF generation")
 
117
  if not isinstance(forecast_data, dict) or not forecast_data:
118
  logger.error("Invalid forecast_data: Must be a non-empty dictionary")
119
  return None
 
138
  y_position = 9.5 * inch
139
  logger.info("Initialized PDF canvas")
140
 
 
141
  logger.info("Writing forecast data")
142
  for key, value in forecast_data.items():
143
  display_key = key.replace('_', ' ').title()
 
145
  c.drawString(1 * inch, y_position, f"{display_key}: {value_str}")
146
  y_position -= 0.3 * inch
147
 
 
148
  y_position -= 0.3 * inch
149
  c.drawString(1 * inch, y_position, "Last 60 Days Usage (comma-separated):")
150
  y_position -= 0.3 * inch
 
157
  c.drawText(text_object)
158
  logger.info("Added usage series")
159
 
 
160
  y_position -= 0.3 * inch
161
  c.drawString(1 * inch, y_position, "Daily Forecast Values (Next 30 Days):")
162
  y_position -= 0.3 * inch
 
170
  c.drawText(text_object)
171
  logger.info("Added daily forecast values")
172
 
 
173
  y_position -= 0.3 * inch
174
  c.drawString(1 * inch, y_position, "Threshold Alerts:")
175
  y_position -= 0.3 * inch
 
183
  y_position -= 0.3 * inch
184
  logger.info("Added threshold alerts")
185
 
 
186
  y_position -= 0.3 * inch
187
  c.drawString(1 * inch, y_position, "Daily Forecast Visualization Data (Next 30 Days):")
188
  y_position -= 0.3 * inch
 
197
  y_position = 10 * inch
198
  logger.info("Added daily forecast visualization data")
199
 
 
200
  y_position -= 0.3 * inch
201
  if y_position < 4 * inch:
202
  c.showPage()
 
215
  c.drawString(1 * inch, y_position - 0.3 * inch, "Error: Could not include daily forecast visualization.")
216
  y_position -= 4.5 * inch
217
 
 
218
  if y_position < 2 * inch:
219
  c.showPage()
220
  c.setFont("Helvetica", 10)
 
236
  y_position = 10 * inch
237
  logger.info("Added threshold alerts visualization data")
238
 
 
239
  y_position -= 0.3 * inch
240
  if y_position < 4 * inch:
241
  c.showPage()
 
289
  return None
290
 
291
  def save_forecast_data(consumable_type, usage_series, current_stock, daily_forecasts):
 
292
  try:
 
293
  usage_str = ','.join(map(str, usage_series))
 
294
  forecast_data = {
295
  'consumable_type': [consumable_type],
296
  'usage_series': [usage_str],
 
299
  'forecast_yhat': [daily_forecasts['yhat'].tolist()]
300
  }
301
  df = pd.DataFrame(forecast_data)
 
302
  if os.path.exists(DATA_FILE):
303
  existing_df = pd.read_csv(DATA_FILE)
304
+ existing_df = existing_df[existing_df['consumable_type'] != consumable_type]
305
  df = pd.concat([existing_df, df], ignore_index=True)
306
  df.to_csv(DATA_FILE, index=False)
307
  logger.info(f"Saved forecast data for {consumable_type} to {DATA_FILE}")
 
309
  logger.error(f"Error saving forecast data: {str(e)}")
310
 
311
  def load_forecast_data(consumable_type):
 
312
  try:
313
  if not os.path.exists(DATA_FILE):
314
  logger.warning(f"No forecast data file found at {DATA_FILE}")
 
320
  return None, None, None
321
  usage_series = [float(x) for x in row['usage_series'].iloc[0].split(',')]
322
  current_stock = float(row['current_stock'].iloc[0])
 
323
  forecast_dates = eval(row['forecast_date'].iloc[0])
324
  forecast_yhat = eval(row['forecast_yhat'].iloc[0])
325
  daily_forecasts = pd.DataFrame({'ds': pd.to_datetime(forecast_dates), 'yhat': forecast_yhat})
 
329
  return None, None, None
330
 
331
  def process_forecast(consumable_type, usage_series, current_stock, is_automated=False):
 
332
  usage_list, error = validate_usage_series(','.join(map(str, usage_series)))
333
  if error:
334
  logger.error(error)
 
408
  plot_bgcolor='rgba(0,0,0,0)',
409
  paper_bgcolor='rgba(0,0,0,0)',
410
  margin=dict(l=50, r=50, t=50, b=100)
411
+ ))
412
  st.plotly_chart(fig_daily, use_container_width=True)
413
 
414
  st.header("Threshold Alerts Visualization")
 
430
  xaxis_title='Category',
431
  yaxis_title='Units',
432
  template='plotly_white'
433
+ ))
434
  st.plotly_chart(fig_alerts)
435
  else:
436
  alert_status = [current_stock < forecast for forecast in [forecast_7, forecast_14, forecast_30]]
 
459
  plot_bgcolor='rgba(0,0,0,0)',
460
  paper_bgcolor='rgba(0,0,0,0)',
461
  margin=dict(l=50, r=50, t=50, b=100)
462
+ ))
463
  alert_data = pd.DataFrame({
464
  'Category': ['Current Stock', '7-Day Forecast', '14-Day Forecast', '30-Day Forecast'],
465
  'Units': [current_stock, forecast_7, forecast_14, forecast_30],
 
478
  xaxis_title='Category',
479
  yaxis_title='Units',
480
  template='plotly_white'
481
+ ))
482
 
 
483
  if sf is not None:
484
  try:
485
  order_suggestions_text = f"7 Days: {max(0, forecast_7 - current_stock)} units, 14 Days: {max(0, forecast_14 - current_stock)} units, 30 Days: {max(0, forecast_30 - current_stock)} units"
 
530
  logger.error(f"Error creating Salesforce record or uploading PDF: {e}", exc_info=True)
531
  if not is_automated:
532
  st.error(f"Error saving to Salesforce: {str(e)}")
533
+ return None
534
 
535
  return daily_forecasts
536
 
537
  def automate_daily_forecast():
 
538
  consumable_types = ['Filters', 'Reagents', 'Vials']
539
  for consumable_type in consumable_types:
540
  logger.info(f"Processing automated forecast for {consumable_type}")
 
541
  usage_series, current_stock, prev_daily_forecasts = load_forecast_data(consumable_type)
542
 
543
  if usage_series is None or current_stock is None or prev_daily_forecasts is None:
544
  logger.warning(f"No previous data for {consumable_type}. Skipping automation.")
545
  continue
546
 
547
+ # Shift usage series: Remove oldest day, append forecasted usage for today
548
+ next_day_usage = prev_daily_forecasts['yhat'].iloc[0] # Forecasted usage for today
 
549
  usage_series = usage_series[1:] + [next_day_usage]
550
+ # Update stock: Subtract yesterday's forecasted usage
551
  yesterday_usage = prev_daily_forecasts['yhat'].iloc[0]
552
  current_stock = max(0, current_stock - yesterday_usage)
553
 
554
+ # Generate new forecast with updated data
555
  daily_forecasts = process_forecast(consumable_type, usage_series, current_stock, is_automated=True)
556
  if daily_forecasts is not None:
 
557
  save_forecast_data(consumable_type, usage_series, current_stock, daily_forecasts)
558
  logger.info(f"Completed automated forecast for {consumable_type}")
559
  else: