AjaykumarPilla commited on
Commit
298b241
·
verified ·
1 Parent(s): 1c6c152

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +362 -67
app.py CHANGED
@@ -4,21 +4,48 @@ from prophet import Prophet
4
  from datetime import datetime, timedelta
5
  import numpy as np
6
  import plotly.graph_objects as go
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- # Prepare data for Prophet
9
  def prepare_prophet_data(usage_series):
10
  end_date = datetime.now()
11
  start_date = end_date - timedelta(days=len(usage_series) - 1)
12
  dates = [start_date + timedelta(days=i) for i in range(len(usage_series))]
13
- prophet_df = pd.DataFrame({
14
- 'ds': dates,
15
- 'y': usage_series
16
- })
17
  prophet_df['cap'] = 60
18
  prophet_df['floor'] = 0
19
  return prophet_df
20
 
21
- # Train Prophet model
22
  def train_model_with_usage(usage_series):
23
  prophet_df = prepare_prophet_data(usage_series)
24
  model = Prophet(
@@ -31,114 +58,332 @@ def train_model_with_usage(usage_series):
31
  model.fit(prophet_df)
32
  return model
33
 
34
- # Forecast function
35
  def make_forecast(model, periods):
36
  future = model.make_future_dataframe(periods=periods)
37
  future['cap'] = 60
38
  future['floor'] = 0
39
  forecast = model.predict(future)
40
  daily_forecasts = forecast['yhat'].tail(periods).tolist()
41
- return round(sum(max(0, y) for y in daily_forecasts)) # Clip negatives
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- # Input validation
44
  def validate_usage_series(usage_str):
45
  try:
46
  usage_list = [float(x) for x in usage_str.split(',')]
 
47
  if len(usage_list) != 60:
48
- return None, "Usage series must contain exactly 60 values."
49
  if any(x < 0 for x in usage_list):
50
  return None, "Usage values must be non-negative."
51
  return usage_list, None
52
  except:
53
  return None, "Invalid usage series format. Please enter 60 comma-separated numbers."
54
 
55
- # Main Streamlit app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  def main():
57
  st.title("SmartLab Consumables Forecast")
58
-
59
  st.header("Input Parameters")
60
- consumable_type = st.selectbox("Consumable Type", ['Filters', 'Reagents', 'Vials'])
 
 
61
  usage_series = st.text_input("Last 60 Days Usage (comma-separated)", "")
62
  current_stock = st.number_input("Current Stock", min_value=0, value=0)
63
-
64
  if st.button("Generate Forecast"):
65
  usage_list, error = validate_usage_series(usage_series)
66
  if error:
67
  st.error(error)
68
  return
69
-
70
  try:
71
  model = train_model_with_usage(usage_list)
72
  except Exception as e:
73
  st.error(f"Error training model: {str(e)}")
74
  return
75
-
76
  forecast_7 = make_forecast(model, 7)
77
  forecast_14 = make_forecast(model, 14)
78
  forecast_30 = make_forecast(model, 30)
79
-
 
 
80
  st.header("Forecast Results")
81
- st.write(f"**7-Day Forecast**: {forecast_7} units")
82
- st.write(f"**14-Day Forecast**: {forecast_14} units")
83
- st.write(f"**30-Day Forecast**: {forecast_30} units")
84
-
 
 
 
 
 
85
  st.header("Threshold Alerts")
86
- if current_stock < forecast_7:
87
- st.warning(f"Alert: Current stock ({current_stock}) is below 7-day forecast ({forecast_7}). 🚩")
88
- else:
89
- st.write("No alert for 7-day forecast.")
90
- if current_stock < forecast_14:
91
- st.warning(f"Alert: Current stock ({current_stock}) is below 14-day forecast ({forecast_14}). 🚩")
92
- else:
93
- st.write("No alert for 14-day forecast.")
94
- if current_stock < forecast_30:
95
- st.warning(f"Alert: Current stock ({current_stock}) is below 30-day forecast ({forecast_30}). 🚩")
96
- else:
97
- st.write("No alert for 30-day forecast.")
98
-
99
  st.header("Order Suggestions")
100
- order_7 = max(0, round(forecast_7 - current_stock))
101
- order_14 = max(0, round(forecast_14 - current_stock))
102
- order_30 = max(0, round(forecast_30 - current_stock))
103
-
104
- st.write(f"**For 7 Days**: Order {order_7} additional units.")
105
- st.write(f"**For 14 Days**: Order {order_14} additional units.")
106
- st.write(f"**For 30 Days**: Order {order_30} additional units.")
107
-
108
- # Forecast visualization
109
- st.header("Forecast Visualization")
110
- forecast_data = pd.DataFrame({
111
- 'Period': ['7 Days', '14 Days', '30 Days'],
112
- 'Units': [forecast_7, forecast_14, forecast_30]
113
- })
114
- fig_forecast = go.Figure()
115
- fig_forecast.add_trace(go.Scatter(
116
- x=forecast_data['Period'],
117
- y=forecast_data['Units'],
118
  mode='lines+markers',
119
- name='Forecasted Units',
120
- line=dict(color='blue'),
121
- marker=dict(size=10)
 
 
122
  ))
123
- fig_forecast.update_layout(
124
- title='Consumable Usage Forecast',
125
- xaxis_title='Time Period',
 
126
  yaxis_title='Units',
127
- template='plotly_white'
 
 
 
 
 
 
 
 
128
  )
129
- st.plotly_chart(fig_forecast)
130
 
131
- # Threshold alerts bar chart
132
  st.header("Threshold Alerts Visualization")
133
  alert_data = pd.DataFrame({
134
  'Category': ['Current Stock', '7-Day Forecast', '14-Day Forecast', '30-Day Forecast'],
135
  'Units': [current_stock, forecast_7, forecast_14, forecast_30],
136
- 'Alert': [
137
- False,
138
- current_stock < forecast_7,
139
- current_stock < forecast_14,
140
- current_stock < forecast_30
141
- ]
142
  })
143
  fig_alerts = go.Figure()
144
  fig_alerts.add_trace(go.Bar(
@@ -156,5 +401,55 @@ def main():
156
  )
157
  st.plotly_chart(fig_alerts)
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  if __name__ == "__main__":
160
  main()
 
 
4
  from datetime import datetime, timedelta
5
  import numpy as np
6
  import plotly.graph_objects as go
7
+ import os
8
+ from dotenv import load_dotenv
9
+ from simple_salesforce import Salesforce
10
+ import logging
11
+ from reportlab.lib.pagesizes import letter
12
+ from reportlab.pdfgen import canvas
13
+ from reportlab.lib.units import inch
14
+ from io import BytesIO
15
+ import base64
16
+ from reportlab.platypus import Image
17
+ import plotly.io as pio
18
+
19
+ # Load environment variables from .env file
20
+ load_dotenv()
21
+
22
+ # Setup logging
23
+ logging.basicConfig(level=logging.INFO)
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Salesforce connection
27
+ try:
28
+ sf = Salesforce(
29
+ username=os.getenv("SF_USERNAME"),
30
+ password=os.getenv("SF_PASSWORD"),
31
+ security_token=os.getenv("SF_SECURITY_TOKEN"),
32
+ instance_url=os.getenv("SF_INSTANCE_URL")
33
+ )
34
+ logger.info("✅ Connected to Salesforce")
35
+ logger.info(f"Connected Salesforce user: {sf.username}")
36
+ except Exception as e:
37
+ logger.error(f"❌ Salesforce connection failed: {e}")
38
+ sf = None
39
 
 
40
  def prepare_prophet_data(usage_series):
41
  end_date = datetime.now()
42
  start_date = end_date - timedelta(days=len(usage_series) - 1)
43
  dates = [start_date + timedelta(days=i) for i in range(len(usage_series))]
44
+ prophet_df = pd.DataFrame({'ds': dates, 'y': usage_series})
 
 
 
45
  prophet_df['cap'] = 60
46
  prophet_df['floor'] = 0
47
  return prophet_df
48
 
 
49
  def train_model_with_usage(usage_series):
50
  prophet_df = prepare_prophet_data(usage_series)
51
  model = Prophet(
 
58
  model.fit(prophet_df)
59
  return model
60
 
 
61
  def make_forecast(model, periods):
62
  future = model.make_future_dataframe(periods=periods)
63
  future['cap'] = 60
64
  future['floor'] = 0
65
  forecast = model.predict(future)
66
  daily_forecasts = forecast['yhat'].tail(periods).tolist()
67
+ return round(sum(max(0, y) for y in daily_forecasts))
68
+
69
+ def get_daily_forecasts(model, periods=30):
70
+ future = model.make_future_dataframe(periods=periods)
71
+ future['cap'] = 60
72
+ future['floor'] = 0
73
+ forecast = model.predict(future)
74
+ daily_forecasts = forecast[['ds', 'yhat']].tail(periods)
75
+ daily_forecasts['yhat'] = daily_forecasts['yhat'].apply(lambda x: max(0, round(x)))
76
+ return daily_forecasts
77
+
78
+ def calculate_reorder_date(model, current_stock, lead_time_days=3, safety_threshold=0):
79
+ future = model.make_future_dataframe(periods=30)
80
+ future['cap'] = 60
81
+ future['floor'] = 0
82
+ forecast = model.predict(future)
83
+ daily_forecasts = forecast[['ds', 'yhat']].tail(30)
84
+
85
+ stock = current_stock
86
+ for _, row in daily_forecasts.iterrows():
87
+ daily_usage = max(0, round(row['yhat']))
88
+ stock -= daily_usage
89
+ if stock <= safety_threshold:
90
+ stockout_date = row['ds']
91
+ reorder_date = stockout_date - timedelta(days=lead_time_days)
92
+ if reorder_date < datetime.now():
93
+ reorder_date = datetime.now().date()
94
+ return reorder_date.strftime('%Y-%m-%d')
95
+ return None
96
 
 
97
  def validate_usage_series(usage_str):
98
  try:
99
  usage_list = [float(x) for x in usage_str.split(',')]
100
+ logger.info(f"Input usage series length: {len(usage_list)}")
101
  if len(usage_list) != 60:
102
+ return None, f"Usage series must contain exactly 60 values. Found {len(usage_list)} values."
103
  if any(x < 0 for x in usage_list):
104
  return None, "Usage values must be non-negative."
105
  return usage_list, None
106
  except:
107
  return None, "Invalid usage series format. Please enter 60 comma-separated numbers."
108
 
109
+ 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:
110
+ try:
111
+ logger.info("Starting PDF generation")
112
+ # Validate inputs
113
+ if not isinstance(forecast_data, dict) or not forecast_data:
114
+ logger.error("Invalid forecast_data: Must be a non-empty dictionary")
115
+ return None
116
+ if not isinstance(daily_forecasts, pd.DataFrame) or daily_forecasts.empty:
117
+ logger.error("Invalid daily_forecasts: Must be a non-empty DataFrame")
118
+ return None
119
+ if not isinstance(alert_status, list) or len(alert_status) != 3:
120
+ logger.error("Invalid alert_status: Must be a list of 3 booleans")
121
+ return None
122
+ if not isinstance(usage_series, str) or not usage_series:
123
+ logger.error("Invalid usage_series: Must be a non-empty string")
124
+ return None
125
+ if not isinstance(fig_daily, go.Figure) or not isinstance(fig_alerts, go.Figure):
126
+ logger.error("Invalid Plotly figures: fig_daily and fig_alerts must be valid go.Figure objects")
127
+ return None
128
+
129
+ pdf_file = BytesIO()
130
+ c = canvas.Canvas(pdf_file, pagesize=letter)
131
+ c.setFont("Helvetica", 12)
132
+ c.drawString(1 * inch, 10 * inch, "Consumables Forecast Report")
133
+ c.setFont("Helvetica", 10)
134
+ y_position = 9.5 * inch
135
+ logger.info("Initialized PDF canvas")
136
+
137
+ # Basic Forecast Data
138
+ logger.info("Writing forecast data")
139
+ for key, value in forecast_data.items():
140
+ display_key = key.replace('_', ' ').title()
141
+ value_str = str(value)
142
+ c.drawString(1 * inch, y_position, f"{display_key}: {value_str}")
143
+ y_position -= 0.3 * inch
144
+
145
+ # Add Last 60 Days Usage
146
+ y_position -= 0.3 * inch
147
+ c.drawString(1 * inch, y_position, "Last 60 Days Usage (comma-separated):")
148
+ y_position -= 0.3 * inch
149
+ text_object = c.beginText(1 * inch, y_position)
150
+ text_object.setFont("Helvetica", 10)
151
+ text_lines = [usage_series[i:i+50] for i in range(0, len(usage_series), 50)]
152
+ for line in text_lines:
153
+ text_object.textLine(line)
154
+ y_position -= 0.3 * inch
155
+ c.drawText(text_object)
156
+ logger.info("Added usage series")
157
+
158
+ # Add Daily Forecast Values
159
+ y_position -= 0.3 * inch
160
+ c.drawString(1 * inch, y_position, "Daily Forecast Values (Next 30 Days):")
161
+ y_position -= 0.3 * inch
162
+ daily_values = ", ".join([str(int(x)) for x in daily_forecasts['yhat'].tolist()])
163
+ text_object = c.beginText(1 * inch, y_position)
164
+ text_object.setFont("Helvetica", 10)
165
+ text_lines = [daily_values[i:i+50] for i in range(0, len(daily_values), 50)]
166
+ for line in text_lines:
167
+ text_object.textLine(line)
168
+ y_position -= 0.3 * inch
169
+ c.drawText(text_object)
170
+ logger.info("Added daily forecast values")
171
+
172
+ # Add Threshold Alerts
173
+ y_position -= 0.3 * inch
174
+ c.drawString(1 * inch, y_position, "Threshold Alerts:")
175
+ y_position -= 0.3 * inch
176
+ for forecast, period, alert in zip([forecast_7, forecast_14, forecast_30], ['7-day', '14-day', '30-day'], alert_status):
177
+ flag_indicator = "[Flag] " if alert else ""
178
+ if alert:
179
+ alert_text = f"{flag_indicator}Alert: Current stock ({current_stock}) is below {period} forecast ({forecast})."
180
+ else:
181
+ alert_text = f"No alert for {period} forecast."
182
+ c.drawString(1 * inch, y_position, alert_text)
183
+ y_position -= 0.3 * inch
184
+ logger.info("Added threshold alerts")
185
+
186
+ # Add Daily Forecast Visualization Data
187
+ y_position -= 0.3 * inch
188
+ c.drawString(1 * inch, y_position, "Daily Forecast Visualization Data (Next 30 Days):")
189
+ y_position -= 0.3 * inch
190
+ for index, row in daily_forecasts.iterrows():
191
+ date_str = row['ds'].strftime('%Y-%m-%d')
192
+ forecast_value = int(row['yhat'])
193
+ c.drawString(1 * inch, y_position, f"Date: {date_str}, Forecast: {forecast_value} units")
194
+ y_position -= 0.3 * inch
195
+ if y_position < 1 * inch:
196
+ c.showPage()
197
+ c.setFont("Helvetica", 10)
198
+ y_position = 10 * inch
199
+ logger.info("Added daily forecast visualization data")
200
+
201
+ # Add Daily Forecast Visualization Image
202
+ y_position -= 0.3 * inch
203
+ if y_position < 4 * inch:
204
+ c.showPage()
205
+ y_position = 10 * inch
206
+ c.drawString(1 * inch, y_position, "Daily Forecast Visualization (Next 30 Days):")
207
+ y_position -= 0.3 * inch
208
+ daily_chart_img = BytesIO()
209
+ try:
210
+ pio.write_image(fig_daily, daily_chart_img, format='png', width=600, height=400)
211
+ daily_chart_img.seek(0)
212
+ img = Image(daily_chart_img, width=6 * inch, height=4 * inch)
213
+ img.drawOn(c, 1 * inch, y_position - 4 * inch)
214
+ logger.info("Added daily forecast visualization image")
215
+ except Exception as e:
216
+ logger.error(f"Failed to export daily forecast image: {str(e)}")
217
+ c.drawString(1 * inch, y_position - 0.3 * inch, "Error: Could not include daily forecast visualization.")
218
+ y_position -= 4.5 * inch
219
+
220
+ # Add Threshold Alerts Visualization Data
221
+ if y_position < 2 * inch:
222
+ c.showPage()
223
+ c.setFont("Helvetica", 10)
224
+ y_position = 10 * inch
225
+ c.drawString(1 * inch, y_position, "Threshold Alerts Visualization Data:")
226
+ y_position -= 0.3 * inch
227
+ alert_data = pd.DataFrame({
228
+ 'Category': ['Current Stock', '7-Day Forecast', '14-Day Forecast', '30-Day Forecast'],
229
+ 'Units': [current_stock, forecast_7, forecast_14, forecast_30],
230
+ 'Alert': [False] + alert_status
231
+ })
232
+ for _, row in alert_data.iterrows():
233
+ alert_text = f"Category: {row['Category']}, Units: {row['Units']}, Alert: {'Yes' if row['Alert'] else 'No'}"
234
+ c.drawString(1 * inch, y_position, alert_text)
235
+ y_position -= 0.3 * inch
236
+ if y_position < 1 * inch:
237
+ c.showPage()
238
+ c.setFont("Helvetica", 10)
239
+ y_position = 10 * inch
240
+ logger.info("Added threshold alerts visualization data")
241
+
242
+ # Add Threshold Alerts Visualization Image
243
+ y_position -= 0.3 * inch
244
+ if y_position < 4 * inch:
245
+ c.showPage()
246
+ y_position = 10 * inch
247
+ c.drawString(1 * inch, y_position, "Threshold Alerts Visualization:")
248
+ y_position -= 0.3 * inch
249
+ alerts_chart_img = BytesIO()
250
+ try:
251
+ pio.write_image(fig_alerts, alerts_chart_img, format='png', width=600, height=400)
252
+ alerts_chart_img.seek(0)
253
+ img = Image(alerts_chart_img, width=6 * inch, height=4 * inch)
254
+ img.drawOn(c, 1 * inch, y_position - 4 * inch)
255
+ logger.info("Added threshold alerts visualization image")
256
+ except Exception as e:
257
+ logger.error(f"Failed to export alerts visualization image: {str(e)}")
258
+ c.drawString(1 * inch, y_position - 0.3 * inch, "Error: Could not include threshold alerts visualization.")
259
+
260
+ c.showPage()
261
+ c.save()
262
+ pdf_file.seek(0)
263
+ logger.info("PDF generation completed successfully")
264
+ return pdf_file
265
+ except Exception as e:
266
+ logger.error(f"Error generating PDF: {str(e)}")
267
+ return None
268
+
269
+ def upload_pdf_to_salesforce(pdf_file: BytesIO, consumable_type: str, record_id: str) -> str:
270
+ try:
271
+ if not sf:
272
+ return None
273
+
274
+ encoded_pdf_data = base64.b64encode(pdf_file.getvalue()).decode('utf-8')
275
+ content_version_data = {
276
+ "Title": f"{consumable_type} - Consumables Forecast PDF",
277
+ "PathOnClient": f"{consumable_type}_Consumables_Forecast.pdf",
278
+ "VersionData": encoded_pdf_data,
279
+ "FirstPublishLocationId": record_id
280
+ }
281
+
282
+ content_version = sf.ContentVersion.create(content_version_data)
283
+ content_version_id = content_version["id"]
284
+
285
+ result = sf.query(f"SELECT Id, ContentDocumentId FROM ContentVersion WHERE Id = '{content_version_id}'")
286
+ if not result['records']:
287
+ return None
288
+
289
+ file_url = f"https://{sf.sf_instance}/sfc/servlet.shepherd/version/download/{content_version_id}"
290
+ return file_url
291
+ except Exception as e:
292
+ logger.error(f"Error uploading PDF to Salesforce: {str(e)}")
293
+ return None
294
+
295
  def main():
296
  st.title("SmartLab Consumables Forecast")
 
297
  st.header("Input Parameters")
298
+
299
+ consumable_type_label = st.selectbox("Consumable Type", ['Filters', 'Reagents', 'Vials'])
300
+ consumable_type = consumable_type_label
301
  usage_series = st.text_input("Last 60 Days Usage (comma-separated)", "")
302
  current_stock = st.number_input("Current Stock", min_value=0, value=0)
303
+
304
  if st.button("Generate Forecast"):
305
  usage_list, error = validate_usage_series(usage_series)
306
  if error:
307
  st.error(error)
308
  return
309
+
310
  try:
311
  model = train_model_with_usage(usage_list)
312
  except Exception as e:
313
  st.error(f"Error training model: {str(e)}")
314
  return
315
+
316
  forecast_7 = make_forecast(model, 7)
317
  forecast_14 = make_forecast(model, 14)
318
  forecast_30 = make_forecast(model, 30)
319
+ daily_forecasts = get_daily_forecasts(model, 30)
320
+ reorder_date = calculate_reorder_date(model, current_stock)
321
+
322
  st.header("Forecast Results")
323
+ col1, col2, col3 = st.columns(3)
324
+ col1.metric("7-Day Forecast", f"{forecast_7} units")
325
+ col2.metric("14-Day Forecast", f"{forecast_14} units")
326
+ col3.metric("30-Day Forecast", f"{forecast_30} units")
327
+
328
+ st.header("Daily Forecast Values (Next 30 Days)")
329
+ daily_values = ", ".join([str(int(x)) for x in daily_forecasts['yhat'].tolist()])
330
+ st.text_area("Comma-separated daily forecasts", daily_values, height=100)
331
+
332
  st.header("Threshold Alerts")
333
+ alert_status = []
334
+ for forecast, period in zip([forecast_7, forecast_14, forecast_30], ['7-day', '14-day', '30-day']):
335
+ if current_stock < forecast:
336
+ st.warning(f"Alert: Current stock ({current_stock}) is below {period} forecast ({forecast}). 🚩")
337
+ alert_status.append(True)
338
+ else:
339
+ st.info(f"No alert for {period} forecast.")
340
+ alert_status.append(False)
341
+
 
 
 
 
342
  st.header("Order Suggestions")
343
+ st.write(f"**For 7 Days**: Order {max(0, forecast_7 - current_stock)} additional units.")
344
+ st.write(f"**For 14 Days**: Order {max(0, forecast_14 - current_stock)} additional units.")
345
+ st.write(f"**For 30 Days**: Order {max(0, forecast_30 - current_stock)} additional units.")
346
+
347
+ st.header("Reorder Information")
348
+ if any(alert_status):
349
+ st.warning(f"Reorder recommended. Suggested reorder date: {reorder_date if reorder_date else 'Not within 30 days'}")
350
+ else:
351
+ st.info("No reorder required within 30 days.")
352
+
353
+ st.header("Daily Forecast Visualization (Next 30 Days)")
354
+ fig_daily = go.Figure()
355
+ fig_daily.add_trace(go.Scatter(
356
+ x=daily_forecasts['ds'],
357
+ y=daily_forecasts['yhat'],
 
 
 
358
  mode='lines+markers',
359
+ name='Daily Forecast',
360
+ line=dict(color='royalblue', width=2),
361
+ marker=dict(size=8, color='darkorange', line=dict(width=2, color='black')),
362
+ fill='tozeroy',
363
+ fillcolor='rgba(0, 176, 246, 0.2)'
364
  ))
365
+ y_values = daily_forecasts['yhat'].tolist()
366
+ fig_daily.update_layout(
367
+ title='Daily Consumable Usage Forecast (30 Days)',
368
+ xaxis_title='Date',
369
  yaxis_title='Units',
370
+ template='plotly_white',
371
+ xaxis=dict(tickformat="%Y-%m-%d", tickangle=45, tickmode='auto', nticks=10),
372
+ yaxis=dict(range=[max(0, min(y_values) - 5), max(y_values) + 5], tickmode='linear', dtick=2),
373
+ showlegend=True,
374
+ legend=dict(x=0.01, y=0.99),
375
+ hovermode='x unified',
376
+ plot_bgcolor='rgba(0,0,0,0)',
377
+ paper_bgcolor='rgba(0,0,0,0)',
378
+ margin=dict(l=50, r=50, t=50, b=100)
379
  )
380
+ st.plotly_chart(fig_daily, use_container_width=True)
381
 
 
382
  st.header("Threshold Alerts Visualization")
383
  alert_data = pd.DataFrame({
384
  'Category': ['Current Stock', '7-Day Forecast', '14-Day Forecast', '30-Day Forecast'],
385
  'Units': [current_stock, forecast_7, forecast_14, forecast_30],
386
+ 'Alert': [False] + alert_status
 
 
 
 
 
387
  })
388
  fig_alerts = go.Figure()
389
  fig_alerts.add_trace(go.Bar(
 
401
  )
402
  st.plotly_chart(fig_alerts)
403
 
404
+ # Salesforce record creation with PDF upload
405
+ if sf is not None:
406
+ try:
407
+ 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"
408
+ forecast_data = {
409
+ "Consumable Type": consumable_type,
410
+ "Current Stock": current_stock,
411
+ "7-Day Forecast": f"{forecast_7} units",
412
+ "14-Day Forecast": f"{forecast_14} units",
413
+ "30-Day Forecast": f"{forecast_30} units",
414
+ "Order Suggestions": order_suggestions_text,
415
+ "Reorder Recommendation": "Yes" if any(alert_status) else "No",
416
+ "Reorder Date": reorder_date if reorder_date else "Not within 30 days"
417
+ }
418
+ pdf_file = generate_forecast_pdf(forecast_data, daily_forecasts, alert_status, current_stock, forecast_7, forecast_14, forecast_30, fig_daily, fig_alerts, usage_series)
419
+ sf_data = {
420
+ 'Consumable_Type__c': consumable_type,
421
+ 'Forecast_Period__c': '7days',
422
+ 'ForeCasted_Quantity__c': float(forecast_7),
423
+ 'ForeCasted_Quantity_14days__c': float(forecast_14),
424
+ 'ForeCasted_Quantity_30days__c': float(forecast_30),
425
+ 'Current_Stock__c': float(current_stock),
426
+ 'Order_Suggestions__c': order_suggestions_text,
427
+ 'Reorder_Recommendation__c': any(alert_status),
428
+ 'Reorder_Date__c': reorder_date,
429
+ 'Pdf_report__c': '' # Placeholder for PDF URL
430
+ }
431
+ result = sf.Consumables_Forecaste__c.create(sf_data)
432
+ logger.info(f"Salesforce record created: {result}")
433
+
434
+ if pdf_file:
435
+ pdf_url = upload_pdf_to_salesforce(pdf_file, consumable_type, result['id'])
436
+ if pdf_url:
437
+ sf.Consumables_Forecaste__c.update(
438
+ result['id'],
439
+ {"Pdf_report__c": pdf_url}
440
+ )
441
+ logger.info(f"PDF uploaded to Salesforce: {pdf_url}")
442
+
443
+ else:
444
+ logger.error("Failed to upload PDF to Salesforce")
445
+ st.error("Failed to upload PDF to Salesforce")
446
+ else:
447
+ logger.error("Failed to generate PDF")
448
+ st.error("Failed to generate PDF")
449
+ except Exception as e:
450
+ logger.error(f"Error creating Salesforce record or uploading PDF: {e}", exc_info=True)
451
+ st.error(f"Error saving to Salesforce: {str(e)}")
452
+
453
  if __name__ == "__main__":
454
  main()
455
+ sf = None