MaheshP98 commited on
Commit
187f3a8
·
verified ·
1 Parent(s): 5f4249f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +327 -251
app.py CHANGED
@@ -1,4 +1,3 @@
1
- import gradio as gr
2
  import os
3
  import logging
4
  import matplotlib.pyplot as plt
@@ -12,51 +11,64 @@ from reportlab.pdfgen import canvas
12
  from reportlab.lib.utils import ImageReader
13
  from reportlab.lib.colors import red, black
14
  import requests
15
- from simple_salesforce import Salesforce # Added for Salesforce integration
16
 
17
  # Set up logging
18
- logging.basicConfig(level=logging.DEBUG)
19
  logger = logging.getLogger(__name__)
20
 
21
  # Load environment variables
22
  load_dotenv()
23
- HF_TOKEN = os.getenv("HF_TOKEN", "not_set")
24
  SF_USERNAME = os.getenv("SF_USERNAME")
25
  SF_PASSWORD = os.getenv("SF_PASSWORD")
26
  SF_SECURITY_TOKEN = os.getenv("SF_SECURITY_TOKEN")
 
27
 
28
  # Validate environment variables
29
- if HF_TOKEN == "not_set":
30
- logger.info("Hugging Face token not set. Using heuristic formula until HF_TOKEN is provided in .env.")
31
  else:
32
  logger.info("Hugging Face token loaded successfully.")
33
 
34
- # Initialize Salesforce connection
35
- try:
36
- sf = Salesforce(
37
- username=SF_USERNAME,
38
- password=SF_PASSWORD,
39
- security_token=SF_SECURITY_TOKEN
40
- )
41
- logger.info("Salesforce connection established successfully.")
42
- except Exception as e:
43
- logger.error(f"Failed to connect to Salesforce: {str(e)}")
44
  sf = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  # Custom function to format numbers in Indian style (e.g., 100000000 as 1,00,00,000.00)
47
  def format_indian_number(number):
48
- number = float(number)
49
- integer_part, decimal_part = f"{number:.2f}".split(".")
50
- integer_part = integer_part[::-1]
51
- formatted = ""
52
- for i, digit in enumerate(integer_part):
53
- if i == 3:
54
- formatted += ","
55
- elif i > 3 and (i - 3) % 2 == 0:
56
- formatted += ","
57
- formatted += digit
58
- integer_part = formatted[::-1]
59
- return f"₹{integer_part}.{decimal_part}"
 
 
 
 
60
 
61
  # Function to fetch budget data from Salesforce
62
  def fetch_budget_from_salesforce(project_id):
@@ -75,228 +87,272 @@ def fetch_budget_from_salesforce(project_id):
75
  if not records:
76
  return None, "Error: No budget data found for the given project ID."
77
 
78
- # Convert records to DataFrame for consistency with CSV processing
79
  data = []
80
  for record in records:
81
  data.append({
82
- 'Planned_Cost': record['Planned_Cost__c'],
83
- 'Actual_Spend': record['Actual_Spend_To_Date__c']
84
  })
85
  df = pd.DataFrame(data)
86
  return df, None
87
  except Exception as e:
 
88
  return None, f"Error fetching data from Salesforce: {str(e)}"
89
 
90
  # Function to process uploaded file for line items
91
  def process_uploaded_file(file):
92
  if file is None:
93
  return 0, 0, []
94
- df = pd.read_csv(file)
95
- if len(df) > 200:
96
- raise ValueError("File exceeds 200 line items. Please upload a file with 200 or fewer line items.")
97
- planned_cost = df['Planned_Cost'].sum()
98
- actual_spend = df['Actual_Spend'].sum()
99
- line_items = df.to_dict('records')
100
- return planned_cost, actual_spend, line_items
 
 
 
 
101
 
102
  # Function to cross-check indices with 3rd-party sources
103
  def cross_check_indices(material_cost_index, labor_index):
104
- if not (0 <= material_cost_index <= 300 and 0 <= labor_index <= 300):
105
- return "Warning: Material Cost Index or Labor Index out of expected range (0-300)."
106
- return "Indices within expected range (placeholder validation)."
 
 
 
 
 
 
107
 
108
  # Function to generate a bar chart
109
  def generate_bar_plot(planned_cost_inr, actual_spend_inr, forecast_cost_inr):
110
- fig, ax = plt.subplots(figsize=(8, 6))
111
- categories = ['Planned Cost', 'Actual Spend', 'Forecasted Cost']
112
- values = [planned_cost_inr, actual_spend_inr, forecast_cost_inr]
113
- bars = ax.bar(categories, values, color=['#1f77b4', '#ff7f0e', '#2ca02c'])
114
- ax.set_title("Budget Overview", fontsize=14, pad=15)
115
- ax.set_ylabel("Amount (₹)", fontsize=12)
116
- ax.tick_params(axis='x', rotation=45)
117
- ax.grid(True, axis='y', linestyle='--', alpha=0.7)
118
-
119
- for bar in bars:
120
- height = bar.get_height()
121
- ax.text(
122
- bar.get_x() + bar.get_width() / 2, height,
123
- format_indian_number(height), ha='center', va='bottom', fontsize=10
124
- )
125
-
126
- buf_gradio = io.BytesIO()
127
- plt.savefig(buf_gradio, format='png', bbox_inches='tight', dpi=100)
128
- buf_gradio.seek(0)
129
- gradio_image = Image.open(buf_gradio)
130
-
131
- buf_pdf = io.BytesIO()
132
- plt.savefig(buf_pdf, format='png', bbox_inches='tight', dpi=100)
133
- buf_pdf.seek(0)
134
-
135
- plt.close()
136
- return gradio_image, buf_pdf
 
 
 
 
137
 
138
  # Function to generate a pie chart for risk distribution
139
  def generate_pie_chart_data(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor):
140
- labels = ['Cost Deviation', 'Material Cost', 'Labor Cost', 'Scope Change']
141
- values = [
142
- max(cost_deviation_factor * 100, 0),
143
- max(material_cost_factor * 100, 0),
144
- max(labor_cost_factor * 100, 0),
145
- max(scope_change_factor * 100, 0)
146
- ]
147
- total = sum(values)
148
- if total == 0:
149
- values = [25, 25, 25, 25]
150
- return {
151
- "type": "pie",
152
- "data": {
153
- "labels": labels,
154
- "datasets": [{
155
- "label": "Risk Distribution",
156
- "data": values,
157
- "backgroundColor": ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"],
158
- "borderColor": ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"],
159
- "borderWidth": 1
160
- }]
161
- },
162
- "options": {
163
- "responsive": true,
164
- "plugins": {
165
- "legend": {
166
- "position": "top"
167
- },
168
- "title": {
169
- "display": true,
170
- "text": "Risk Factor Distribution"
 
 
171
  }
172
  }
173
  }
174
- }
 
 
 
 
 
 
 
 
175
 
176
  # Function to generate a gauge chart
177
  def generate_gauge_chart(risk_percentage, category):
178
- return {
179
- "type": "radar",
180
- "data": {
181
- "labels": ["Risk Level"],
182
- "datasets": [{
183
- "label": f"Risk for {category} (%)",
184
- "data": [risk_percentage],
185
- "backgroundColor": "rgba(255, 99, 132, 0.2)",
186
- "borderColor": "rgba(255, 99, 132, 1)",
187
- "borderWidth": 1,
188
- "pointBackgroundColor": "rgba(255, 99, 132, 1)"
189
- }]
190
- },
191
- "options": {
192
- "responsive": true,
193
- "scales": {
194
- "r": {
195
- "min": 0,
196
- "max": 100,
197
- "ticks": {
198
- "stepSize": 20
199
- }
200
- }
201
  },
202
- "plugins": {
203
- "legend": {
204
- "position": "top"
 
 
 
 
 
 
 
205
  },
206
- "title": {
207
- "display": true,
208
- "text": f"Risk Level Dashboard for {category}"
 
 
 
 
 
209
  }
210
  }
211
  }
212
- }
 
 
 
 
 
 
 
 
213
 
214
  # Function to generate a PDF report
215
  def generate_pdf(planned_cost_inr, actual_spend_inr, forecast_cost_inr, total_risk, risk_percentage, insights, status, top_causes, category, project_phase, material_cost_index, labor_index, scope_change_impact, alert_message, indices_validation, bar_chart_image):
216
- pdf_path = f"budget_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
217
- c = canvas.Canvas(pdf_path, pagesize=letter)
218
- width, height = letter
219
-
220
- c.setFont("Helvetica-Bold", 16)
221
- c.drawString(50, height - 50, "Budget Overrun Risk Report")
222
-
223
- c.setFont("Helvetica", 12)
224
- y_position = height - 100
225
-
226
- text_color = red if status == "Critical" else black
227
- c.setFillColor(text_color)
228
-
229
- c.drawString(50, y_position, f"Category: {category}")
230
- y_position -= 20
231
- c.drawString(50, y_position, f"Project Phase: {project_phase}")
232
- y_position -= 20
233
- c.drawString(50, y_position, f"Material Cost Index: {material_cost_index}")
234
- y_position -= 20
235
- c.drawString(50, y_position, f"Labor Index: {labor_index}")
236
- y_position -= 20
237
- c.drawString(50, y_position, f"Indices Validation: {indices_validation}")
238
- y_position -= 20
239
- c.drawString(50, y_position, f"Scope Change Impact: {scope_change_impact}%")
240
- y_position -= 20
241
- c.drawString(50, y_position, f"Planned Cost: {format_indian_number(planned_cost_inr)}")
242
- y_position -= 20
243
- c.drawString(50, y_position, f"Actual Spend: {format_indian_number(actual_spend_inr)}")
244
- y_position -= 20
245
- c.drawString(50, y_position, f"Forecasted Cost: {format_indian_number(forecast_cost_inr)}")
246
- y_position -= 20
247
- c.drawString(50, y_position, f"Total Risk: {total_risk}")
248
- y_position -= 20
249
- c.drawString(50, y_position, f"Risk Percentage: {risk_percentage}%")
250
- y_position -= 20
251
- c.drawString(50, y_position, f"Status: {status}")
252
- y_position -= 20
253
- c.drawString(50, y_position, f"Insights: {insights}")
254
- y_position -= 20
255
- c.drawString(50, y_position, f"Top Causes: {top_causes}")
256
- y_position -= 20
257
- c.drawString(50, y_position, f"Alert: {alert_message}")
258
- y_position -= 40
259
-
260
- chart_reader = ImageReader(bar_chart_image)
261
- c.drawImage(chart_reader, 50, y_position - 300, width=500, height=300)
262
-
263
- c.showPage()
264
- c.save()
265
- return pdf_path
 
 
 
 
 
266
 
267
  # Function to generate an Excel file
268
  def generate_excel(planned_cost_inr, actual_spend_inr, forecast_cost_inr, total_risk, risk_percentage, insights, status, top_causes, category, project_phase, material_cost_index, labor_index, scope_change_impact, alert_message, indices_validation):
269
- data = {
270
- "Category": [category],
271
- "Project Phase": [project_phase],
272
- "Material Cost Index": [material_cost_index],
273
- "Labor Index": [labor_index],
274
- "Indices Validation": [indices_validation],
275
- "Scope Change Impact (%)": [scope_change_impact],
276
- "Planned Cost (INR)": [planned_cost_inr],
277
- "Actual Spend (INR)": [actual_spend_inr],
278
- "Forecasted Cost (INR)": [forecast_cost_inr],
279
- "Total Risk": [total_risk],
280
- "Risk Percentage (%)": [risk_percentage],
281
- "Insights": [insights],
282
- "Status": [status],
283
- "Top Causes": [top_causes],
284
- "Alert": [alert_message]
285
- }
286
- df = pd.DataFrame(data)
287
-
288
- excel_path = f"prediction_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
289
- with pd.ExcelWriter(excel_path, engine='xlsxwriter') as writer:
290
- df.to_excel(writer, index=False, sheet_name='Results')
291
- workbook = writer.book
292
- worksheet = writer.sheets['Results']
293
 
294
- number_format = workbook.add_format({'num_format': '[₹]#,##,##,##0.00'})
295
- worksheet.set_column('G:G', None, number_format)
296
- worksheet.set_column('H:H', None, number_format)
297
- worksheet.set_column('I:I', None, number_format)
298
-
299
- return excel_path
 
 
 
 
 
 
 
 
 
300
 
301
  # Function to store results in Salesforce
302
  def store_results_in_salesforce(project_id, planned_cost_inr, actual_spend_inr, forecast_cost_inr, risk_percentage, insights, status, top_causes, category, project_phase, pdf_path):
@@ -304,7 +360,6 @@ def store_results_in_salesforce(project_id, planned_cost_inr, actual_spend_inr,
304
  return "Error: Salesforce connection not available."
305
 
306
  try:
307
- # Update or create record in Project_Budget_Risk__c
308
  record = {
309
  'Project_Name__c': project_id,
310
  'Budget_Category__c': category,
@@ -318,7 +373,6 @@ def store_results_in_salesforce(project_id, planned_cost_inr, actual_spend_inr,
318
  'Project_Phase__c': project_phase
319
  }
320
 
321
- # Check if record exists
322
  query = f"SELECT Id FROM Project_Budget_Risk__c WHERE Project_Name__c = '{project_id}'"
323
  result = sf.query(query)
324
  if result['records']:
@@ -327,28 +381,33 @@ def store_results_in_salesforce(project_id, planned_cost_inr, actual_spend_inr,
327
  else:
328
  sf.Project_Budget_Risk__c.create(record)
329
 
330
- # Upload PDF to Salesforce Files
331
- with open(pdf_path, 'rb') as pdf_file:
332
- sf_file = sf.ContentVersion.create({
333
- 'Title': f"Budget Report {project_id} {datetime.now().strftime('%Y%m%d_%H%M%S')}",
334
- 'PathOnClient': pdf_path,
335
- 'VersionData': pdf_file.read().hex()
336
- })
337
- file_id = sf_file['id']
338
-
339
- # Link PDF to the record
340
- sf.ContentDocumentLink.create({
341
- 'ContentDocumentId': file_id,
342
- 'LinkedEntityId': record_id,
343
- 'ShareType': 'V'
344
- })
345
-
346
- return f"Results stored in Salesforce with PDF ID: {file_id}"
347
  except Exception as e:
 
348
  return f"Error storing results in Salesforce: {str(e)}"
349
 
350
  # Prediction function
351
  def predict_risk(username, file, project_id, category, material_cost_index, labor_index, scope_change_impact, project_phase):
 
 
 
 
 
352
  # Validate user role via Salesforce
353
  if not sf:
354
  return "Error: Salesforce connection not available.", None, None, None, None, None, None
@@ -357,8 +416,10 @@ def predict_risk(username, file, project_id, category, material_cost_index, labo
357
  user_query = f"SELECT Profile.Name FROM User WHERE Username = '{username}'"
358
  user_result = sf.query(user_query)
359
  if not user_result['records'] or user_result['records'][0]['Profile']['Name'] != 'Finance':
 
360
  return "Access Denied: This app is restricted to finance roles only.", None, None, None, None, None, None
361
  except Exception as e:
 
362
  return f"Error validating user: {str(e)}", None, None, None, None, None, None
363
 
364
  # Fetch data from Salesforce if no file is uploaded
@@ -378,6 +439,7 @@ def predict_risk(username, file, project_id, category, material_cost_index, labo
378
  else:
379
  planned_cost_inr, actual_spend_inr, line_items = process_uploaded_file(file)
380
  except Exception as e:
 
381
  return f"Error processing data: {str(e)}", None, None, None, None, None, None
382
 
383
  # Validate numeric inputs
@@ -386,7 +448,7 @@ def predict_risk(username, file, project_id, category, material_cost_index, labo
386
  labor_index = float(labor_index) if labor_index else 0
387
  scope_change_impact = float(scope_change_impact) if scope_change_impact else 0
388
  except ValueError:
389
- logger.error("Invalid input: Inputs must be numeric.")
390
  return "Error: All numeric inputs must be valid numbers.", None, None, None, None, None, None
391
 
392
  logger.debug(f"Starting prediction: planned_cost_inr={planned_cost_inr}, actual_spend_inr={actual_spend_inr}, "
@@ -397,7 +459,7 @@ def predict_risk(username, file, project_id, category, material_cost_index, labo
397
  indices_validation = cross_check_indices(material_cost_index, labor_index)
398
 
399
  # Risk calculation with Hugging Face API
400
- if HF_TOKEN != "not_set":
401
  try:
402
  api_url = "https://api.huggingface.co/models/budget-overrun-risk"
403
  headers = {"Authorization": f"Bearer {HF_TOKEN}"}
@@ -417,23 +479,19 @@ def predict_risk(username, file, project_id, category, material_cost_index, labo
417
  labor_cost_factor = result.get('labor_cost_factor', 0)
418
  scope_change_factor = result.get('scope_change_factor', 0)
419
  except Exception as e:
420
- logger.error(f"Hugging Face API call failed: {str(e)}")
421
- return f"Error with Hugging Face API: {str(e)}", None, None, None, None, None, None
 
 
 
 
422
  else:
423
- # Fallback to heuristic formula
424
  cost_deviation_factor = (actual_spend_inr - planned_cost_inr) / planned_cost_inr if planned_cost_inr > 0 else 0
425
  material_cost_factor = (material_cost_index - 100) / 100 if material_cost_index > 100 else 0
426
  labor_cost_factor = (labor_index - 100) / 100 if labor_index > 100 else 0
427
  scope_change_factor = scope_change_impact / 100
428
-
429
- weights = {'cost_deviation': 0.4, 'material_cost': 0.2, 'labor_cost': 0.2, 'scope_change': 0.2}
430
- risk_percentage = (
431
- weights['cost_deviation'] * min(cost_deviation_factor * 100, 100) +
432
- weights['material_cost'] * min(material_cost_factor * 100, 100) +
433
- weights['labor_cost'] * min(labor_cost_factor * 100, 100) +
434
- weights['scope_change'] * min(scope_change_factor * 100, 100)
435
- )
436
- risk_percentage = round(max(0, min(risk_percentage, 100)), 2)
437
 
438
  total_risk = 1 if risk_percentage > 50 else 0
439
  forecast_cost_inr = planned_cost_inr * (1 + risk_percentage / 100)
@@ -508,6 +566,21 @@ def predict_risk(username, file, project_id, category, material_cost_index, labo
508
 
509
  return output_text, bar_chart_image, pie_chart_data, gauge_chart_data, pdf_file, excel_file, f"<div style='{alert_style}'>{alert_message}</div>"
510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  # Function to update explanations
512
  def update_material_cost_explanation(category):
513
  material_examples = {
@@ -634,11 +707,14 @@ with gr.Blocks(title="Budget Overrun Risk Estimator", css=custom_css) as demo:
634
 
635
  # Launch the app
636
  if __name__ == "__main__":
637
- demo.launch(
638
- server_name="0.0.0.0",
639
- server_port=7860,
640
- share=False,
641
- auth_message="Please log in with your Salesforce credentials.",
642
- allowed_paths=["/home/user/app"],
643
- ssr_mode=False
644
- )
 
 
 
 
 
1
  import os
2
  import logging
3
  import matplotlib.pyplot as plt
 
11
  from reportlab.lib.utils import ImageReader
12
  from reportlab.lib.colors import red, black
13
  import requests
14
+ from simple_salesforce import Salesforce, SalesforceLoginError
15
 
16
  # Set up logging
17
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
18
  logger = logging.getLogger(__name__)
19
 
20
  # Load environment variables
21
  load_dotenv()
22
+ HF_TOKEN = os.getenv("HF_TOKEN")
23
  SF_USERNAME = os.getenv("SF_USERNAME")
24
  SF_PASSWORD = os.getenv("SF_PASSWORD")
25
  SF_SECURITY_TOKEN = os.getenv("SF_SECURITY_TOKEN")
26
+ SF_INSTANCE_URL = os.getenv("SF_INSTANCE_URL", "https://budgetoverrunriskestimator-dev-ed.develop.my.salesforce.com")
27
 
28
  # Validate environment variables
29
+ if not HF_TOKEN:
30
+ logger.error("Hugging Face token not set. Please add HF_TOKEN to .env file.")
31
  else:
32
  logger.info("Hugging Face token loaded successfully.")
33
 
34
+ if not all([SF_USERNAME, SF_PASSWORD, SF_SECURITY_TOKEN]):
35
+ logger.error("Salesforce credentials incomplete. Please set SF_USERNAME, SF_PASSWORD, and SF_SECURITY_TOKEN in .env.")
 
 
 
 
 
 
 
 
36
  sf = None
37
+ else:
38
+ # Initialize Salesforce connection
39
+ try:
40
+ sf = Salesforce(
41
+ username=SF_USERNAME,
42
+ password=SF_PASSWORD,
43
+ security_token=SF_SECURITY_TOKEN,
44
+ instance_url=SF_INSTANCE_URL
45
+ )
46
+ logger.info("Salesforce connection established successfully.")
47
+ except SalesforceLoginError as e:
48
+ logger.error(f"Failed to connect to Salesforce: {str(e)}")
49
+ sf = None
50
+ except Exception as e:
51
+ logger.error(f"Unexpected error connecting to Salesforce: {str(e)}")
52
+ sf = None
53
 
54
  # Custom function to format numbers in Indian style (e.g., 100000000 as 1,00,00,000.00)
55
  def format_indian_number(number):
56
+ try:
57
+ number = float(number)
58
+ integer_part, decimal_part = f"{number:.2f}".split(".")
59
+ integer_part = integer_part[::-1]
60
+ formatted = ""
61
+ for i, digit in enumerate(integer_part):
62
+ if i == 3:
63
+ formatted += ","
64
+ elif i > 3 and (i - 3) % 2 == 0:
65
+ formatted += ","
66
+ formatted += digit
67
+ integer_part = formatted[::-1]
68
+ return f"₹{integer_part}.{decimal_part}"
69
+ except (ValueError, TypeError) as e:
70
+ logger.error(f"Error formatting number {number}: {str(e)}")
71
+ return "₹0.00"
72
 
73
  # Function to fetch budget data from Salesforce
74
  def fetch_budget_from_salesforce(project_id):
 
87
  if not records:
88
  return None, "Error: No budget data found for the given project ID."
89
 
 
90
  data = []
91
  for record in records:
92
  data.append({
93
+ 'Planned_Cost': record['Planned_Cost__c'] or 0,
94
+ 'Actual_Spend': record['Actual_Spend_To_Date__c'] or 0
95
  })
96
  df = pd.DataFrame(data)
97
  return df, None
98
  except Exception as e:
99
+ logger.error(f"Error fetching data from Salesforce: {str(e)}")
100
  return None, f"Error fetching data from Salesforce: {str(e)}"
101
 
102
  # Function to process uploaded file for line items
103
  def process_uploaded_file(file):
104
  if file is None:
105
  return 0, 0, []
106
+ try:
107
+ df = pd.read_csv(file)
108
+ if len(df) > 200:
109
+ raise ValueError("File exceeds 200 line items. Please upload a file with 200 or fewer line items.")
110
+ planned_cost = df['Planned_Cost'].sum()
111
+ actual_spend = df['Actual_Spend'].sum()
112
+ line_items = df.to_dict('records')
113
+ return planned_cost, actual_spend, line_items
114
+ except Exception as e:
115
+ logger.error(f"Error processing uploaded file: {str(e)}")
116
+ return 0, 0, []
117
 
118
  # Function to cross-check indices with 3rd-party sources
119
  def cross_check_indices(material_cost_index, labor_index):
120
+ try:
121
+ material_cost_index = float(material_cost_index)
122
+ labor_index = float(labor_index)
123
+ if not (0 <= material_cost_index <= 300 and 0 <= labor_index <= 300):
124
+ return "Warning: Material Cost Index or Labor Index out of expected range (0-300)."
125
+ return "Indices within expected range."
126
+ except (ValueError, TypeError) as e:
127
+ logger.error(f"Error validating indices: {str(e)}")
128
+ return "Error: Invalid indices provided."
129
 
130
  # Function to generate a bar chart
131
  def generate_bar_plot(planned_cost_inr, actual_spend_inr, forecast_cost_inr):
132
+ try:
133
+ fig, ax = plt.subplots(figsize=(8, 6))
134
+ categories = ['Planned Cost', 'Actual Spend', 'Forecasted Cost']
135
+ values = [planned_cost_inr, actual_spend_inr, forecast_cost_inr]
136
+ bars = ax.bar(categories, values, color=['#1f77b4', '#ff7f0e', '#2ca02c'])
137
+ ax.set_title("Budget Overview", fontsize=14, pad=15)
138
+ ax.set_ylabel("Amount (₹)", fontsize=12)
139
+ ax.tick_params(axis='x', rotation=45)
140
+ ax.grid(True, axis='y', linestyle='--', alpha=0.7)
141
+
142
+ for bar in bars:
143
+ height = bar.get_height()
144
+ ax.text(
145
+ bar.get_x() + bar.get_width() / 2, height,
146
+ format_indian_number(height), ha='center', va='bottom', fontsize=10
147
+ )
148
+
149
+ buf_gradio = io.BytesIO()
150
+ plt.savefig(buf_gradio, format='png', bbox_inches='tight', dpi=100)
151
+ buf_gradio.seek(0)
152
+ gradio_image = Image.open(buf_gradio)
153
+
154
+ buf_pdf = io.BytesIO()
155
+ plt.savefig(buf_pdf, format='png', bbox_inches='tight', dpi=100)
156
+ buf_pdf.seek(0)
157
+
158
+ plt.close()
159
+ return gradio_image, buf_pdf
160
+ except Exception as e:
161
+ logger.error(f"Error generating bar plot: {str(e)}")
162
+ return None, None
163
 
164
  # Function to generate a pie chart for risk distribution
165
  def generate_pie_chart_data(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor):
166
+ try:
167
+ labels = ['Cost Deviation', 'Material Cost', 'Labor Cost', 'Scope Change']
168
+ values = [
169
+ max(float(cost_deviation_factor) * 100, 0),
170
+ max(float(material_cost_factor) * 100, 0),
171
+ max(float(labor_cost_factor) * 100, 0),
172
+ max(float(scope_change_factor) * 100, 0)
173
+ ]
174
+ total = sum(values)
175
+ if total == 0:
176
+ values = [25, 25, 25, 25]
177
+ return {
178
+ "type": "pie",
179
+ "data": {
180
+ "labels": labels,
181
+ "datasets": [{
182
+ "label": "Risk Distribution",
183
+ "data": values,
184
+ "backgroundColor": ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"],
185
+ "borderColor": ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"],
186
+ "borderWidth": 1
187
+ }]
188
+ },
189
+ "options": {
190
+ "responsive": true,
191
+ "plugins": {
192
+ "legend": {
193
+ "position": "top"
194
+ },
195
+ "title": {
196
+ "display": true,
197
+ "text": "Risk Factor Distribution"
198
+ }
199
  }
200
  }
201
  }
202
+ except (ValueError, TypeError) as e:
203
+ logger.error(f"Error generating pie chart data: {str(e)}")
204
+ return {
205
+ "type": "pie",
206
+ "data": {
207
+ "labels": ["Error"],
208
+ "datasets": [{"label": "Error", "data": [100], "backgroundColor": ["#FF0000"]}]
209
+ }
210
+ }
211
 
212
  # Function to generate a gauge chart
213
  def generate_gauge_chart(risk_percentage, category):
214
+ try:
215
+ risk_percentage = float(risk_percentage)
216
+ return {
217
+ "type": "radar",
218
+ "data": {
219
+ "labels": ["Risk Level"],
220
+ "datasets": [{
221
+ "label": f"Risk for {category} (%)",
222
+ "data": [risk_percentage],
223
+ "backgroundColor": "rgba(255, 99, 132, 0.2)",
224
+ "borderColor": "rgba(255, 99, 132, 1)",
225
+ "borderWidth": 1,
226
+ "pointBackgroundColor": "rgba(255, 99, 132, 1)"
227
+ }]
 
 
 
 
 
 
 
 
 
228
  },
229
+ "options": {
230
+ "responsive": true,
231
+ "scales": {
232
+ "r": {
233
+ "min": 0,
234
+ "max": 100,
235
+ "ticks": {
236
+ "stepSize": 20
237
+ }
238
+ }
239
  },
240
+ "plugins": {
241
+ "legend": {
242
+ "position": "top"
243
+ },
244
+ "title": {
245
+ "display": true,
246
+ "text": f"Risk Level Dashboard for {category}"
247
+ }
248
  }
249
  }
250
  }
251
+ except (ValueError, TypeError) as e:
252
+ logger.error(f"Error generating gauge chart: {str(e)}")
253
+ return {
254
+ "type": "radar",
255
+ "data": {
256
+ "labels": ["Error"],
257
+ "datasets": [{"label": "Error", "data": [0], "backgroundColor": ["#FF0000"]}]
258
+ }
259
+ }
260
 
261
  # Function to generate a PDF report
262
  def generate_pdf(planned_cost_inr, actual_spend_inr, forecast_cost_inr, total_risk, risk_percentage, insights, status, top_causes, category, project_phase, material_cost_index, labor_index, scope_change_impact, alert_message, indices_validation, bar_chart_image):
263
+ try:
264
+ pdf_path = f"budget_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
265
+ c = canvas.Canvas(pdf_path, pagesize=letter)
266
+ width, height = letter
267
+
268
+ c.setFont("Helvetica-Bold", 16)
269
+ c.drawString(50, height - 50, "Budget Overrun Risk Report")
270
+
271
+ c.setFont("Helvetica", 12)
272
+ y_position = height - 100
273
+
274
+ text_color = red if status == "Critical" else black
275
+ c.setFillColor(text_color)
276
+
277
+ c.drawString(50, y_position, f"Category: {category}")
278
+ y_position -= 20
279
+ c.drawString(50, y_position, f"Project Phase: {project_phase}")
280
+ y_position -= 20
281
+ c.drawString(50, y_position, f"Material Cost Index: {material_cost_index}")
282
+ y_position -= 20
283
+ c.drawString(50, y_position, f"Labor Index: {labor_index}")
284
+ y_position -= 20
285
+ c.drawString(50, y_position, f"Indices Validation: {indices_validation}")
286
+ y_position -= 20
287
+ c.drawString(50, y_position, f"Scope Change Impact: {scope_change_impact}%")
288
+ y_position -= 20
289
+ c.drawString(50, y_position, f"Planned Cost: {format_indian_number(planned_cost_inr)}")
290
+ y_position -= 20
291
+ c.drawString(50, y_position, f"Actual Spend: {format_indian_number(actual_spend_inr)}")
292
+ y_position -= 20
293
+ c.drawString(50, y_position, f"Forecasted Cost: {format_indian_number(forecast_cost_inr)}")
294
+ y_position -= 20
295
+ c.drawString(50, y_position, f"Total Risk: {total_risk}")
296
+ y_position -= 20
297
+ c.drawString(50, y_position, f"Risk Percentage: {risk_percentage}%")
298
+ y_position -= 20
299
+ c.drawString(50, y_position, f"Status: {status}")
300
+ y_position -= 20
301
+ c.drawString(50, y_position, f"Insights: {insights}")
302
+ y_position -= 20
303
+ c.drawString(50, y_position, f"Top Causes: {top_causes}")
304
+ y_position -= 20
305
+ c.drawString(50, y_position, f"Alert: {alert_message}")
306
+ y_position -= 40
307
+
308
+ if bar_chart_image:
309
+ chart_reader = ImageReader(bar_chart_image)
310
+ c.drawImage(chart_reader, 50, y_position - 300, width=500, height=300)
311
+
312
+ c.showPage()
313
+ c.save()
314
+ return pdf_path
315
+ except Exception as e:
316
+ logger.error(f"Error generating PDF: {str(e)}")
317
+ return None
318
 
319
  # Function to generate an Excel file
320
  def generate_excel(planned_cost_inr, actual_spend_inr, forecast_cost_inr, total_risk, risk_percentage, insights, status, top_causes, category, project_phase, material_cost_index, labor_index, scope_change_impact, alert_message, indices_validation):
321
+ try:
322
+ data = {
323
+ "Category": [category],
324
+ "Project Phase": [project_phase],
325
+ "Material Cost Index": [material_cost_index],
326
+ "Labor Index": [labor_index],
327
+ "Indices Validation": [indices_validation],
328
+ "Scope Change Impact (%)": [scope_change_impact],
329
+ "Planned Cost (INR)": [planned_cost_inr],
330
+ "Actual Spend (INR)": [actual_spend_inr],
331
+ "Forecasted Cost (INR)": [forecast_cost_inr],
332
+ "Total Risk": [total_risk],
333
+ "Risk Percentage (%)": [risk_percentage],
334
+ "Insights": [insights],
335
+ "Status": [status],
336
+ "Top Causes": [top_causes],
337
+ "Alert": [alert_message]
338
+ }
339
+ df = pd.DataFrame(data)
 
 
 
 
 
340
 
341
+ excel_path = f"prediction_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
342
+ with pd.ExcelWriter(excel_path, engine='xlsxwriter') as writer:
343
+ df.to_excel(writer, index=False, sheet_name='Results')
344
+ workbook = writer.book
345
+ worksheet = writer.sheets['Results']
346
+
347
+ number_format = workbook.add_format({'num_format': '[₹]#,##,##,##0.00'})
348
+ worksheet.set_column('G:G', None, number_format)
349
+ worksheet.set_column('H:H', None, number_format)
350
+ worksheet.set_column('I:I', None, number_format)
351
+
352
+ return excel_path
353
+ except Exception as e:
354
+ logger.error(f"Error generating Excel: {str(e)}")
355
+ return None
356
 
357
  # Function to store results in Salesforce
358
  def store_results_in_salesforce(project_id, planned_cost_inr, actual_spend_inr, forecast_cost_inr, risk_percentage, insights, status, top_causes, category, project_phase, pdf_path):
 
360
  return "Error: Salesforce connection not available."
361
 
362
  try:
 
363
  record = {
364
  'Project_Name__c': project_id,
365
  'Budget_Category__c': category,
 
373
  'Project_Phase__c': project_phase
374
  }
375
 
 
376
  query = f"SELECT Id FROM Project_Budget_Risk__c WHERE Project_Name__c = '{project_id}'"
377
  result = sf.query(query)
378
  if result['records']:
 
381
  else:
382
  sf.Project_Budget_Risk__c.create(record)
383
 
384
+ if pdf_path and os.path.exists(pdf_path):
385
+ with open(pdf_path, 'rb') as pdf_file:
386
+ sf_file = sf.ContentVersion.create({
387
+ 'Title': f"Budget Report {project_id} {datetime.now().strftime('%Y%m%d_%H%M%S')}",
388
+ 'PathOnClient': pdf_path,
389
+ 'VersionData': pdf_file.read().hex()
390
+ })
391
+ file_id = sf_file['id']
392
+
393
+ sf.ContentDocumentLink.create({
394
+ 'ContentDocumentId': file_id,
395
+ 'LinkedEntityId': record_id,
396
+ 'ShareType': 'V'
397
+ })
398
+ return f"Results stored in Salesforce with PDF ID: {file_id}"
399
+ return "Results stored in Salesforce (no PDF uploaded)."
 
400
  except Exception as e:
401
+ logger.error(f"Error storing results in Salesforce: {str(e)}")
402
  return f"Error storing results in Salesforce: {str(e)}"
403
 
404
  # Prediction function
405
  def predict_risk(username, file, project_id, category, material_cost_index, labor_index, scope_change_impact, project_phase):
406
+ # Validate inputs
407
+ if not username:
408
+ logger.error("Username is empty.")
409
+ return "Error: Salesforce username is required.", None, None, None, None, None, None
410
+
411
  # Validate user role via Salesforce
412
  if not sf:
413
  return "Error: Salesforce connection not available.", None, None, None, None, None, None
 
416
  user_query = f"SELECT Profile.Name FROM User WHERE Username = '{username}'"
417
  user_result = sf.query(user_query)
418
  if not user_result['records'] or user_result['records'][0]['Profile']['Name'] != 'Finance':
419
+ logger.warning(f"Access denied for user {username}: Not a Finance role.")
420
  return "Access Denied: This app is restricted to finance roles only.", None, None, None, None, None, None
421
  except Exception as e:
422
+ logger.error(f"Error validating user {username}: {str(e)}")
423
  return f"Error validating user: {str(e)}", None, None, None, None, None, None
424
 
425
  # Fetch data from Salesforce if no file is uploaded
 
439
  else:
440
  planned_cost_inr, actual_spend_inr, line_items = process_uploaded_file(file)
441
  except Exception as e:
442
+ logger.error(f"Error processing data: {str(e)}")
443
  return f"Error processing data: {str(e)}", None, None, None, None, None, None
444
 
445
  # Validate numeric inputs
 
448
  labor_index = float(labor_index) if labor_index else 0
449
  scope_change_impact = float(scope_change_impact) if scope_change_impact else 0
450
  except ValueError:
451
+ logger.error("Invalid input: Material Cost Index, Labor Index, or Scope Change Impact must be numeric.")
452
  return "Error: All numeric inputs must be valid numbers.", None, None, None, None, None, None
453
 
454
  logger.debug(f"Starting prediction: planned_cost_inr={planned_cost_inr}, actual_spend_inr={actual_spend_inr}, "
 
459
  indices_validation = cross_check_indices(material_cost_index, labor_index)
460
 
461
  # Risk calculation with Hugging Face API
462
+ if HF_TOKEN:
463
  try:
464
  api_url = "https://api.huggingface.co/models/budget-overrun-risk"
465
  headers = {"Authorization": f"Bearer {HF_TOKEN}"}
 
479
  labor_cost_factor = result.get('labor_cost_factor', 0)
480
  scope_change_factor = result.get('scope_change_factor', 0)
481
  except Exception as e:
482
+ logger.error(f"Hugging Face API call failed: {str(e)}. Falling back to heuristic formula.")
483
+ cost_deviation_factor = (actual_spend_inr - planned_cost_inr) / planned_cost_inr if planned_cost_inr > 0 else 0
484
+ material_cost_factor = (material_cost_index - 100) / 100 if material_cost_index > 100 else 0
485
+ labor_cost_factor = (labor_index - 100) / 100 if labor_index > 100 else 0
486
+ scope_change_factor = scope_change_impact / 100
487
+ risk_percentage = calculate_heuristic_risk(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor)
488
  else:
489
+ logger.warning("No HF_TOKEN provided. Using heuristic formula for risk calculation.")
490
  cost_deviation_factor = (actual_spend_inr - planned_cost_inr) / planned_cost_inr if planned_cost_inr > 0 else 0
491
  material_cost_factor = (material_cost_index - 100) / 100 if material_cost_index > 100 else 0
492
  labor_cost_factor = (labor_index - 100) / 100 if labor_index > 100 else 0
493
  scope_change_factor = scope_change_impact / 100
494
+ risk_percentage = calculate_heuristic_risk(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor)
 
 
 
 
 
 
 
 
495
 
496
  total_risk = 1 if risk_percentage > 50 else 0
497
  forecast_cost_inr = planned_cost_inr * (1 + risk_percentage / 100)
 
566
 
567
  return output_text, bar_chart_image, pie_chart_data, gauge_chart_data, pdf_file, excel_file, f"<div style='{alert_style}'>{alert_message}</div>"
568
 
569
+ # Helper function for heuristic risk calculation
570
+ def calculate_heuristic_risk(cost_deviation_factor, material_cost_factor, labor_cost_factor, scope_change_factor):
571
+ try:
572
+ weights = {'cost_deviation': 0.4, 'material_cost': 0.2, 'labor_cost': 0.2, 'scope_change': 0.2}
573
+ risk_percentage = (
574
+ weights['cost_deviation'] * min(float(cost_deviation_factor) * 100, 100) +
575
+ weights['material_cost'] * min(float(material_cost_factor) * 100, 100) +
576
+ weights['labor_cost'] * min(float(labor_cost_factor) * 100, 100) +
577
+ weights['scope_change'] * min(float(scope_change_factor) * 100, 100)
578
+ )
579
+ return round(max(0, min(risk_percentage, 100)), 2)
580
+ except (ValueError, TypeError) as e:
581
+ logger.error(f"Error calculating heuristic risk: {str(e)}")
582
+ return 0
583
+
584
  # Function to update explanations
585
  def update_material_cost_explanation(category):
586
  material_examples = {
 
707
 
708
  # Launch the app
709
  if __name__ == "__main__":
710
+ try:
711
+ demo.launch(
712
+ server_name="0.0.0.0",
713
+ server_port=7860,
714
+ share=False,
715
+ auth_message="Please log in with your Salesforce credentials.",
716
+ allowed_paths=["/home/user/app"],
717
+ ssr_mode=False
718
+ )
719
+ except Exception as e:
720
+ logger.error(f"Failed to launch Gradio app: {str(e)}")