VijayPulmamidi commited on
Commit
0785555
·
verified ·
1 Parent(s): cd29e7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +285 -187
app.py CHANGED
@@ -8,242 +8,340 @@ import logging
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
 
 
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)