VijayPulmamidi commited on
Commit
764ca7d
·
verified ·
1 Parent(s): b1a81e9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -285
app.py CHANGED
@@ -8,340 +8,242 @@ import logging
8
  from simple_salesforce import Salesforce
9
  import plotly.express as px
10
  import plotly.graph_objects as go
11
- import re
12
- from flask import Flask
13
- from http import HTTPStatus
14
 
15
  # Set up logging
16
  logging.basicConfig(level=logging.DEBUG)
17
  logger = logging.getLogger(__name__)
18
 
19
- # Initialize Flask for health check
20
- server = Flask(__name__)
 
 
21
 
22
- # Gradio app
23
- app = gr.Blocks()
24
-
25
- # Salesforce credentials from environment variables
26
- SALESFORCE_USERNAME = os.getenv("SALESFORCE_USERNAME")
27
- SALESFORCE_PASSWORD = os.getenv("SALESFORCE_PASSWORD")
28
- SALESFORCE_SECURITY_TOKEN = os.getenv("SALESFORCE_SECURITY_TOKEN")
29
-
30
- # Validate environment variables
31
- if not all([SALESFORCE_USERNAME, SALESFORCE_PASSWORD, SALESFORCE_SECURITY_TOKEN]):
32
- logger.error("Missing Salesforce credentials in environment variables")
33
- raise ValueError("Salesforce credentials must be set in environment variables")
34
-
35
- # Health check endpoint for Hugging Face Spaces
36
- @server.route('/health')
37
- def health_check():
38
- return {"status": "healthy"}, HTTPStatus.OK
39
-
40
- def sanitize_input(value):
41
- """Sanitize input to prevent SOQL injection."""
42
- if not value:
43
- return value
44
- sanitized = re.sub(r'[^a-zA-Z0-9\s_-]', '', str(value))[:100]
45
- return sanitized
46
-
47
- def connect_to_salesforce():
48
- """Connect to Salesforce with error handling."""
49
- try:
50
- sf = Salesforce(
51
- username=SALESFORCE_USERNAME,
52
- password=SALESFORCE_PASSWORD,
53
- security_token=SALESFORCE_SECURITY_TOKEN
54
- )
55
- logger.debug("Successfully connected to Salesforce")
56
- return sf
57
- except Exception as e:
58
- logger.error(f"Failed to connect to Salesforce: {str(e)}")
59
- raise Exception(f"Salesforce connection failed: {str(e)}")
60
 
61
  def find_salesforce_project(project_name, sf):
62
  """Find an existing Project__c record by name and return its ID."""
63
- try:
64
- sanitized_project_name = sanitize_input(project_name)
65
- query = f"SELECT Id FROM Project__c WHERE Name = '{sanitized_project_name}' LIMIT 1"
66
- result = sf.query(query)
67
- if result['totalSize'] > 0:
68
- project_id = result['records'][0]['Id']
69
- logger.debug(f"Found Project__c with Name: {sanitized_project_name}, ID: {project_id}")
70
- return project_id
71
- logger.debug(f"No Project__c found with Name: {sanitized_project_name}")
72
- return None
73
- except Exception as e:
74
- logger.error(f"Error finding project {sanitized_project_name}: {str(e)}")
75
- return None
76
 
77
  def insert_reconciliation_to_salesforce(df, sf):
78
- """Inserts reconciliation records into Salesforce in batches."""
79
  inserted_count = 0
80
  project_cache = {}
81
- batch_size = 200
82
 
83
- records = []
84
  for index, row in df.iterrows():
85
- try:
86
- project_id = None
87
- if 'Project_ID' in df.columns and pd.notna(row['Project_ID']):
88
- project_name = sanitize_input(row['Project_ID'])
89
- if project_name in project_cache:
90
- project_id = project_cache[project_name]
91
- else:
92
- project_id = find_salesforce_project(project_name, sf)
93
- if project_id:
94
- project_cache[project_name] = project_id
95
-
96
- reconciliation_record = {
97
- 'Material_Type__c': str(row['Material_Type'])[:255],
98
- 'Planned_Quantity__c': float(row['Planned_Quantity']),
99
- 'Received_Quantity__c': float(row['Received_Quantity']),
100
- 'Used_Quantity__c': float(row['Used_Quantity']),
101
- 'AI_Suggestion__c': str(row['AI_Suggestion'])[:1000],
102
- 'Reconciliation_Status__c': str(row['Reconciliation_Status'])
103
- }
104
- if project_id:
105
- reconciliation_record['Project_ID__c'] = project_id
106
-
107
- records.append(reconciliation_record)
108
-
109
- if len(records) >= batch_size:
110
- try:
111
- sf.bulk.Material_Reconciliation_Record__c.insert(records)
112
- inserted_count += len(records)
113
- logger.debug(f"Inserted batch of {len(records)} records")
114
- records = []
115
- except Exception as e:
116
- logger.error(f"Error inserting batch: {str(e)}")
117
- continue
118
 
119
- except Exception as e:
120
- logger.error(f"Error preparing record {index}: {str(e)}")
121
- continue
122
-
123
- if records:
124
- try:
125
- sf.bulk.Material_Reconciliation_Record__c.insert(records)
126
- inserted_count += len(records)
127
- logger.debug(f"Inserted final batch of {len(records)} records")
128
- except Exception as e:
129
- logger.error(f"Error inserting final batch: {str(e)}")
 
 
 
130
 
131
  logger.debug(f"Inserted {inserted_count} of {len(df)} records successfully")
132
  return f"Inserted {inserted_count} records into Salesforce"
133
 
134
  def generate_suggestion(row):
135
  """Generate AI suggestions based on reconciliation data."""
136
- try:
137
- if row['Anomaly'] == -1 or abs(row['Deviation']) > 50:
138
- if row['Balance_Quantity'] < 0:
139
- return f"Urgent: Reorder {abs(row['Balance_Quantity']):.0f} units of {row['Material_Type']}."
140
- elif row['Deviation'] > 50:
141
- return f"Excess: Reduce future orders of {row['Material_Type']} by {(row['Balance_Quantity'] - row['Planned_Quantity']):.0f} units."
142
- elif row['Deviation'] < -50:
143
- return f"Shortage: Order {abs(row['Used_Quantity'] - row['Planned_Quantity']):.0f} more units of {row['Material_Type']}."
144
- return "No action needed."
145
- except Exception as e:
146
- logger.error(f"Error generating suggestion for row: {str(e)}")
147
- return "Error generating suggestion"
148
 
149
  def reconcile_materials(csv_file):
150
  """Process CSV and reconcile materials, inserting results into Salesforce."""
151
  logger.debug("Starting reconcile_materials function")
152
  logger.debug(f"csv_file type: {type(csv_file)}")
153
- tmp_file_path = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- try:
156
- # Validate file input
157
- if csv_file is None:
158
- raise ValueError("No file uploaded")
159
-
160
- # Handle Gradio file input
161
- if isinstance(csv_file, str):
162
- logger.debug(f"Reading CSV from path: {csv_file}")
163
- if not csv_file.lower().endswith('.csv'):
164
- raise ValueError("Invalid file type. Please upload a CSV file.")
165
- if os.path.getsize(csv_file) / (1024 * 1024) > 10:
166
- raise ValueError("File size exceeds 10MB limit")
167
- df = pd.read_csv(csv_file)
168
  else:
169
- logger.debug("Reading CSV from file object")
170
- if not hasattr(csv_file, 'name') or not csv_file.name.lower().endswith('.csv'):
171
- raise ValueError("Invalid file type. Please upload a CSV file.")
172
- with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp_file:
173
- tmp_file.write(csv_file.read())
174
- tmp_file_path = tmp_file.name
175
- if os.path.getsize(tmp_file_path) / (1024 * 1024) > 10:
176
- raise ValueError("File size exceeds 10MB limit")
177
- df = pd.read_csv(tmp_file_path)
178
-
179
- logger.debug(f"CSV read successfully. Columns: {df.columns.tolist()}")
180
-
181
- # Validate CSV columns
182
- column_mapping = {
183
- 'Material_Type': 'Material_Type',
184
- 'Planned_Quantity': ['Planned_Quantity', 'Planned_Qty'],
185
- 'Received_Quantity': ['Received_Quantity', 'Received_Qty'],
186
- 'Used_Quantity': ['Used_Quantity', 'Used_Qty']
187
- }
188
-
189
- for internal_name, possible_names in column_mapping.items():
190
- if isinstance(possible_names, list):
191
- found = False
192
- for name in possible_names:
193
- if name in df.columns:
194
- df.rename(columns={name: internal_name}, inplace=True)
195
- found = True
196
- break
197
- if not found:
198
- raise ValueError(f"Missing required column: {internal_name}")
199
- else:
200
- if possible_names not in df.columns:
201
- raise ValueError(f"Missing required column: {possible_names}")
202
-
203
- # Validate data types
204
- for col in ['Planned_Quantity', 'Received_Quantity', 'Used_Quantity']:
205
- df[col] = pd.to_numeric(df[col], errors='coerce')
206
- if df[col].isna().any():
207
- raise ValueError(f"Column '{col}' contains non-numeric values or empty cells.")
208
-
209
- # Calculate balance and deviation
210
- df['Balance_Quantity'] = df['Received_Quantity'] - df['Used_Quantity']
211
- df['Deviation'] = df.apply(
212
- lambda row: ((row['Balance_Quantity'] - row['Planned_Quantity']) / row['Planned_Quantity'] * 100)
213
- if row['Planned_Quantity'] != 0 else 0, axis=1
214
- )
215
-
216
- # Anomaly detection
217
- iso_forest = IsolationForest(contamination=0.1, random_state=42)
218
- features = df[['Planned_Quantity', 'Received_Quantity', 'Used_Quantity', 'Deviation']]
219
- df['Anomaly'] = iso_forest.fit_predict(features)
220
- df.loc[df['Deviation'].abs() > 50, 'Anomaly'] = -1
221
-
222
- # Generate suggestions and status
223
- df['AI_Suggestion'] = df.apply(generate_suggestion, axis=1)
224
- df['Reconciliation_Status'] = df.apply(
225
- lambda row: 'Flagged' if row['Anomaly'] == -1 or abs(row['Deviation']) > 50 else 'Complete', axis=1
226
- )
227
-
228
- # Insert records into Salesforce
229
- sf = connect_to_salesforce()
230
- salesforce_result = insert_reconciliation_to_salesforce(df, sf)
231
-
232
- # Generate text output
233
- text_output = f"Material Reconciliation Results\n{'='*30}\n\n{salesforce_result}\n\nDetailed Records:\n"
234
- for index, row in df.iterrows():
235
- text_output += f"Record {index + 1}:\n"
236
- if 'Project_ID' in df.columns and pd.notna(row['Project_ID']):
237
- text_output += f" Project ID: {row['Project_ID']}\n"
238
- text_output += f" Material Type: {row['Material_Type']}\n"
239
- text_output += f" Planned Quantity: {row['Planned_Quantity']:.0f}\n"
240
- text_output += f" Received Quantity: {row['Received_Quantity']:.0f}\n"
241
- text_output += f" Used Quantity: {row['Used_Quantity']:.0f}\n"
242
- text_output += f" Balance Quantity: {row['Balance_Quantity']:.0f}\n"
243
- text_output += f" Deviation: {row['Deviation']:.2f}%\n"
244
- text_output += f" Anomaly: {'Yes' if row['Anomaly'] == -1 else 'No'}\n"
245
- text_output += f" AI Suggestion: {row['AI_Suggestion']}\n"
246
- text_output += f" Reconciliation Status: {row['Reconciliation_Status']}\n"
247
- text_output += f"{'-'*30}\n"
248
-
249
- # Save results to a temporary file
250
- with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp_file:
251
- output_file = tmp_file.name
252
- df.to_csv(output_file, index=False)
253
- logger.debug(f"Output CSV saved to: {output_file}")
254
-
255
- # Create visualizations
256
- bar_fig = px.bar(
257
- df,
258
- x='Material_Type',
259
- y='Deviation',
260
- color='Reconciliation_Status',
261
- title='Deviation by Material Type',
262
- labels={'Deviation': 'Deviation (%)'},
263
- color_discrete_map={'Flagged': '#FF4B4B', 'Complete': '#36A2EB'},
264
- hover_data=['Planned_Quantity', 'Received_Quantity', 'Used_Quantity']
265
- )
266
- bar_fig.update_layout(
267
- xaxis_title="Material Type",
268
- yaxis_title="Deviation (%)",
269
- xaxis_tickangle=45,
270
- hovermode='closest'
271
- )
272
 
273
- status_counts = df['Reconciliation_Status'].value_counts().reset_index()
274
- status_counts.columns = ['Reconciliation_Status', 'Count']
275
- pie_fig = px.pie(
276
- status_counts,
277
- names='Reconciliation_Status',
278
- values='Count',
279
- title='Reconciliation Status Distribution',
280
- color_discrete_map={'Flagged': '#FF4B4B', 'Complete': '#36A2EB'}
281
- )
282
- pie_fig.update_traces(textinfo='percent+label', hovertemplate='%{label}: %{value} (%{percent})')
283
 
284
- quantity_fig = px.bar(
285
- df,
286
- x='Material_Type',
287
- y=['Planned_Quantity', 'Received_Quantity', 'Used_Quantity'],
288
- title='Quantity Comparison by Material',
289
- labels={'value': 'Quantity', 'variable': 'Quantity Type'},
290
- barmode='stack'
291
- )
292
- quantity_fig.update_layout(xaxis_title="Material Type", yaxis_title="Quantity", xaxis_tickangle=45)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
- ai_summary = "\n".join([f"{row['Material_Type']}: {row['AI_Suggestion']}" for _, row in df.iterrows()])
 
295
 
296
- return output_file, text_output, df, bar_fig, pie_fig, quantity_fig, ai_summary
297
-
298
- except Exception as e:
299
- logger.error(f"Error in reconcile_materials: {str(e)}")
300
- return None, f"Error: {str(e)}", None, None, None, None, ""
301
- finally:
302
- if tmp_file_path and os.path.exists(tmp_file_path):
303
- try:
304
- os.unlink(tmp_file_path)
305
- except:
306
- pass
307
 
308
- # Gradio interface
309
- with app:
310
- gr.Markdown("# Material Reconciliation Dashboard", elem_classes="text-3xl font-bold mb-4 text-center")
 
311
  with gr.Row():
312
  with gr.Column(scale=1):
313
- gr.Markdown("## Upload CSV File", elem_classes="text-xl font-semibold mb-2")
314
- csv_input = gr.File(label="Upload CSV (max 10MB)", file_types=[".csv"])
315
- submit_button = gr.Button("Reconcile Materials", variant="primary")
316
  with gr.Column(scale=2):
317
- gr.Markdown("## Reconciliation Results", elem_classes="text-xl font-semibold mb-2")
318
- output_text = gr.Textbox(label="Detailed Results", lines=20, elem_classes="bg-gray-100 p-4 rounded")
319
  with gr.Row():
320
  with gr.Column(scale=2):
321
- gr.Markdown("## Data Table", elem_classes="text-xl font-semibold mb-2")
322
- output_table = gr.Dataframe(label="Reconciled Data", interactive=True)
323
  with gr.Column(scale=1):
324
- gr.Markdown("## AI Suggestions", elem_classes="text-xl font-semibold mb-2")
325
- ai_summary_output = gr.Textbox(label="AI Suggestions Summary", elem_classes="bg-gray-100 p-4 rounded")
326
  with gr.Row():
327
  with gr.Column(scale=1):
328
- gr.Markdown("## Deviation by Material", elem_classes="text-lg font-medium mb-2")
329
  bar_plot = gr.Plot(label="Deviation Plot")
330
  with gr.Column(scale=1):
331
- gr.Markdown("## Reconciliation Status Distribution", elem_classes="text-lg font-medium mb-2")
332
  pie_plot = gr.Plot(label="Status Distribution")
333
- with gr.Column(scale=1):
334
- gr.Markdown("## Quantity Comparison", elem_classes="text-lg font-medium mb-2")
335
- quantity_plot = gr.Plot(label="Quantity Plot")
336
  output_file = gr.File(label="Download Reconciled CSV")
337
 
338
  submit_button.click(
339
  fn=reconcile_materials,
340
  inputs=csv_input,
341
- outputs=[output_file, output_text, output_table, bar_plot, pie_plot, quantity_plot, ai_summary_output],
342
- _js="() => { return { show_progress: 'full' } }" # Enable loading indicator
343
  )
344
 
345
- if __name__ == '__main__':
346
- port = int(os.getenv("PORT", 7860))
347
- app.launch(server_name="0.0.0.0", server_port=port, debug=False)
 
8
  from simple_salesforce import Salesforce
9
  import plotly.express as px
10
  import plotly.graph_objects as go
11
+ from plotly.subplots import make_subplots
 
 
12
 
13
  # Set up logging
14
  logging.basicConfig(level=logging.DEBUG)
15
  logger = logging.getLogger(__name__)
16
 
17
+ # Salesforce credentials
18
+ SALESFORCE_USERNAME = "vijaypulmamidi.dev2025@sathkrutha.com"
19
+ SALESFORCE_PASSWORD = "Vij@y9100754977"
20
+ SALESFORCE_SECURITY_TOKEN = "CaZSEwVmB3EIAiV6G8ukdDp0"
21
 
22
+ # Connect to Salesforce
23
+ sf = Salesforce(username=SALESFORCE_USERNAME, password=SALESFORCE_PASSWORD, security_token=SALESFORCE_SECURITY_TOKEN)
24
+ logger.debug("Successfully connected to Salesforce.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  def find_salesforce_project(project_name, sf):
27
  """Find an existing Project__c record by name and return its ID."""
28
+ query = f"SELECT Id FROM Project__c WHERE Name = '{project_name}' LIMIT 1"
29
+ result = sf.query(query)
30
+ if result['totalSize'] > 0:
31
+ project_id = result['records'][0]['Id']
32
+ logger.debug(f"Found Project__c with Name: {project_name}, ID: {project_id}")
33
+ return project_id
34
+ logger.debug(f"No Project__c found with Name: {project_name}")
35
+ return None
 
 
 
 
 
36
 
37
  def insert_reconciliation_to_salesforce(df, sf):
38
+ """Inserts reconciliation records into Salesforce, linking to existing Project__c records if possible."""
39
  inserted_count = 0
40
  project_cache = {}
 
41
 
 
42
  for index, row in df.iterrows():
43
+ project_id = None
44
+ if 'Project_ID' in df.columns and pd.notna(row['Project_ID']):
45
+ project_name = row['Project_ID']
46
+ if project_name in project_cache:
47
+ project_id = project_cache[project_name]
48
+ else:
49
+ project_id = find_salesforce_project(project_name, sf)
50
+ if project_id:
51
+ project_cache[project_name] = project_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ reconciliation_record = {
54
+ 'Material_Type__c': row['Material_Type'],
55
+ 'Planned_Quantity__c': row['Planned_Quantity'],
56
+ 'Received_Quantity__c': row['Received_Quantity'],
57
+ 'Used_Quantity__c': row['Used_Quantity'],
58
+ 'AI_Suggestion__c': row['AI_Suggestion'],
59
+ 'Reconciliation_Status__c': row['Reconciliation_Status']
60
+ }
61
+ if project_id:
62
+ reconciliation_record['Project_ID__c'] = project_id
63
+
64
+ logger.debug(f"Inserting record: {reconciliation_record}")
65
+ sf.Material_Reconciliation_Record__c.create(reconciliation_record)
66
+ inserted_count += 1
67
 
68
  logger.debug(f"Inserted {inserted_count} of {len(df)} records successfully")
69
  return f"Inserted {inserted_count} records into Salesforce"
70
 
71
  def generate_suggestion(row):
72
  """Generate AI suggestions based on reconciliation data."""
73
+ if row['Anomaly'] == -1 or abs(row['Deviation']) > 50:
74
+ if row['Balance_Quantity'] < 0:
75
+ return f"Urgent: Reorder {abs(row['Balance_Quantity'])} units of {row['Material_Type']}."
76
+ elif row['Deviation'] > 50:
77
+ return f"Excess: Reduce future orders of {row['Material_Type']} by {row['Balance_Quantity'] - row['Planned_Quantity']} units."
78
+ elif row['Deviation'] < -50:
79
+ return f"Shortage: Order {abs(row['Used_Quantity'] - row['Planned_Quantity'])} more units of {row['Material_Type']}."
80
+ return "No action needed."
 
 
 
 
81
 
82
  def reconcile_materials(csv_file):
83
  """Process CSV and reconcile materials, inserting results into Salesforce."""
84
  logger.debug("Starting reconcile_materials function")
85
  logger.debug(f"csv_file type: {type(csv_file)}")
86
+
87
+ # Read CSV based on input type
88
+ if isinstance(csv_file, str):
89
+ logger.debug(f"Reading CSV from path: {csv_file}")
90
+ df = pd.read_csv(csv_file)
91
+ elif hasattr(csv_file, 'name'):
92
+ logger.debug(f"Reading CSV from file object with name: {csv_file.name}")
93
+ df = pd.read_csv(csv_file.name)
94
+ else:
95
+ logger.debug("Reading CSV from file object directly")
96
+ csv_file.seek(0)
97
+ df = pd.read_csv(csv_file)
98
+
99
+ logger.debug(f"CSV read successfully. Columns: {df.columns.tolist()}")
100
+
101
+ # Validate CSV columns (Project_ID is optional)
102
+ column_mapping = {
103
+ 'Material_Type': 'Material_Type',
104
+ 'Planned_Quantity': ['Planned_Quantity', 'Planned_Qty'],
105
+ 'Received_Quantity': ['Received_Quantity', 'Received_Qty'],
106
+ 'Used_Quantity': ['Used_Quantity', 'Used_Qty']
107
+ }
108
 
109
+ # Check for required columns and rename if necessary
110
+ for internal_name, possible_names in column_mapping.items():
111
+ if isinstance(possible_names, list):
112
+ found = False
113
+ for name in possible_names:
114
+ if name in df.columns:
115
+ df.rename(columns={name: internal_name}, inplace=True)
116
+ found = True
117
+ break
118
+ if not found:
119
+ raise ValueError(f"Missing required column: {internal_name}")
 
 
120
  else:
121
+ if possible_names not in df.columns:
122
+ raise ValueError(f"Missing required column: {possible_names}")
123
+
124
+ # Validate data types
125
+ for col in ['Planned_Quantity', 'Received_Quantity', 'Used_Quantity']:
126
+ df[col] = pd.to_numeric(df[col], errors='coerce')
127
+ if df[col].isna().any():
128
+ raise ValueError(f"Column '{col}' contains non-numeric values or empty cells.")
129
+
130
+ # Calculate balance
131
+ df['Balance_Quantity'] = df['Received_Quantity'] - df['Used_Quantity']
132
+ logger.debug("Balance_Quantity calculated")
133
+
134
+ # Calculate deviation
135
+ df['Deviation'] = df.apply(
136
+ lambda row: ((row['Balance_Quantity'] - row['Planned_Quantity']) / row['Planned_Quantity'])
137
+ if row['Planned_Quantity'] != 0 else 0, axis=1
138
+ )
139
+ logger.debug("Deviation calculated")
140
+
141
+ # Anomaly detection with forced anomaly for large deviations
142
+ iso_forest = IsolationForest(contamination=0.1, random_state=42)
143
+ features = df[['Planned_Quantity', 'Received_Quantity', 'Used_Quantity', 'Deviation']]
144
+ df['Anomaly'] = iso_forest.fit_predict(features)
145
+ df.loc[df['Deviation'].abs() > 50, 'Anomaly'] = -1
146
+ logger.debug("Anomaly detection completed")
147
+
148
+ # Generate suggestions
149
+ df['AI_Suggestion'] = df.apply(generate_suggestion, axis=1)
150
+ df['Reconciliation_Status'] = df.apply(
151
+ lambda row: 'Flagged' if row['Anomaly'] == -1 or abs(row['Deviation']) > 50 else 'Complete', axis=1
152
+ )
153
+ logger.debug("Suggestions and status generated")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
+ # Insert records into Salesforce
156
+ salesforce_result = insert_reconciliation_to_salesforce(df, sf)
157
+ logger.debug(f"Salesforce insert result: {salesforce_result}")
 
 
 
 
 
 
 
158
 
159
+ # Generate text output
160
+ text_output = "Material Reconciliation Results\n"
161
+ text_output += "=============================\n\n"
162
+ text_output += salesforce_result + "\n\n"
163
+ text_output += "Detailed Records:\n"
164
+ for index, row in df.iterrows():
165
+ text_output += f"Record {index + 1}:\n"
166
+ if 'Project_ID' in df.columns and pd.notna(row['Project_ID']):
167
+ text_output += f" Project ID: {row['Project_ID']}\n"
168
+ text_output += f" Material Type: {row['Material_Type']}\n"
169
+ text_output += f" Planned Quantity: {row['Planned_Quantity']}\n"
170
+ text_output += f" Received Quantity: {row['Received_Quantity']}\n"
171
+ text_output += f" Used Quantity: {row['Used_Quantity']}\n"
172
+ text_output += f" Balance Quantity: {row['Balance_Quantity']}\n"
173
+ text_output += f" Deviation: {row['Deviation']:.2f}%\n"
174
+ text_output += f" Anomaly: {'Yes' if row['Anomaly'] == -1 else 'No'}\n"
175
+ text_output += f" AI Suggestion: {row['AI_Suggestion']}\n"
176
+ text_output += f" Reconciliation Status: {row['Reconciliation_Status']}\n"
177
+ text_output += "-----------------------------\n"
178
+
179
+ # Save results to a temporary file
180
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp_file:
181
+ output_file = tmp_file.name
182
+ df.to_csv(output_file, index=False)
183
+ logger.debug(f"Output CSV saved to: {output_file}")
184
+
185
+ # Create visualizations
186
+ # Bar chart for Deviation by Material_Type
187
+ bar_fig = px.bar(
188
+ df,
189
+ x='Material_Type',
190
+ y='Deviation',
191
+ color='Reconciliation_Status',
192
+ title='Deviation by Material Type',
193
+ labels={'Deviation': 'Deviation (%)'},
194
+ color_discrete_map={'Flagged': '#FF4B4B', 'Complete': '#36A2EB'}
195
+ )
196
+ bar_fig.update_layout(xaxis_title="Material Type", yaxis_title="Deviation (%)")
197
+
198
+ # Pie chart for Reconciliation Status
199
+ status_counts = df['Reconciliation_Status'].value_counts().reset_index()
200
+ status_counts.columns = ['Reconciliation_Status', 'Count']
201
+ pie_fig = px.pie(
202
+ status_counts,
203
+ names='Reconciliation_Status',
204
+ values='Count',
205
+ title='Reconciliation Status Distribution',
206
+ color_discrete_map={'Flagged': '#FF4B4B', 'Complete': '#36A2EB'}
207
+ )
208
 
209
+ # AI Suggestions summary
210
+ ai_summary = "\n".join([f"{row['Material_Type']}: {row['AI_Suggestion']}" for _, row in df.iterrows()])
211
 
212
+ return output_file, text_output, df, bar_fig, pie_fig, ai_summary
 
 
 
 
 
 
 
 
 
 
213
 
214
+ # Gradio interface using Blocks
215
+ logger.debug("Setting up Gradio Blocks interface")
216
+ with gr.Blocks(css='button:has(span:contains("Share via Link")) { display: none !important; }') as interface:
217
+ gr.Markdown("# Material Reconciliation Dashboard")
218
  with gr.Row():
219
  with gr.Column(scale=1):
220
+ gr.Markdown("## Upload CSV File")
221
+ csv_input = gr.File(label="Upload CSV", file_types=[".csv"])
222
+ submit_button = gr.Button("Reconcile Materials")
223
  with gr.Column(scale=2):
224
+ gr.Markdown("## Reconciliation Results")
225
+ output_text = gr.Textbox(label="Detailed Results", lines=20)
226
  with gr.Row():
227
  with gr.Column(scale=2):
228
+ gr.Markdown("## Data Table")
229
+ output_table = gr.Dataframe(label="Reconciled Data")
230
  with gr.Column(scale=1):
231
+ gr.Markdown("## AI Suggestions")
232
+ ai_summary_output = gr.Textbox(label="AI Suggestions Summary")
233
  with gr.Row():
234
  with gr.Column(scale=1):
235
+ gr.Markdown("## Deviation by Material")
236
  bar_plot = gr.Plot(label="Deviation Plot")
237
  with gr.Column(scale=1):
238
+ gr.Markdown("## Reconciliation Status Distribution")
239
  pie_plot = gr.Plot(label="Status Distribution")
 
 
 
240
  output_file = gr.File(label="Download Reconciled CSV")
241
 
242
  submit_button.click(
243
  fn=reconcile_materials,
244
  inputs=csv_input,
245
+ outputs=[output_file, output_text, output_table, bar_plot, pie_plot, ai_summary_output]
 
246
  )
247
 
248
+ logger.debug("Launching Gradio Blocks interface")
249
+ interface.launch()