VijayPulmamidi commited on
Commit
f51be9c
·
verified ·
1 Parent(s): 52cf177

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -125
app.py CHANGED
@@ -7,10 +7,8 @@ import tempfile
7
  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
 
@@ -21,24 +19,19 @@ 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']):
@@ -49,8 +42,8 @@ def insert_reconciliation_to_salesforce(df, sf):
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'],
@@ -59,191 +52,145 @@ def insert_reconciliation_to_salesforce(df, sf):
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()
 
7
  import logging
8
  from simple_salesforce import Salesforce
9
  import plotly.express as px
 
 
10
 
11
+ # Logging setup
12
  logging.basicConfig(level=logging.DEBUG)
13
  logger = logging.getLogger(__name__)
14
 
 
19
 
20
  # Connect to Salesforce
21
  sf = Salesforce(username=SALESFORCE_USERNAME, password=SALESFORCE_PASSWORD, security_token=SALESFORCE_SECURITY_TOKEN)
22
+ logger.debug("Connected to Salesforce.")
23
 
24
  def find_salesforce_project(project_name, sf):
 
25
  query = f"SELECT Id FROM Project__c WHERE Name = '{project_name}' LIMIT 1"
26
  result = sf.query(query)
27
  if result['totalSize'] > 0:
28
+ return result['records'][0]['Id']
 
 
 
29
  return None
30
 
31
  def insert_reconciliation_to_salesforce(df, sf):
 
32
  inserted_count = 0
33
  project_cache = {}
34
+
35
  for index, row in df.iterrows():
36
  project_id = None
37
  if 'Project_ID' in df.columns and pd.notna(row['Project_ID']):
 
42
  project_id = find_salesforce_project(project_name, sf)
43
  if project_id:
44
  project_cache[project_name] = project_id
45
+
46
+ record = {
47
  'Material_Type__c': row['Material_Type'],
48
  'Planned_Quantity__c': row['Planned_Quantity'],
49
  'Received_Quantity__c': row['Received_Quantity'],
 
52
  'Reconciliation_Status__c': row['Reconciliation_Status']
53
  }
54
  if project_id:
55
+ record['Project_ID__c'] = project_id
56
+
57
+ sf.Material_Reconciliation_Record__c.create(record)
 
58
  inserted_count += 1
59
+
 
60
  return f"Inserted {inserted_count} records into Salesforce"
61
 
62
  def generate_suggestion(row):
63
+ if row['Anomaly'] == -1 or abs(row['Deviation']) > 15:
64
+ if row['Deviation'] < -15:
65
+ shortage = abs(row['Planned_Quantity'] - row['Used_Quantity'])
66
+ return f"Shortage: Order {shortage:.0f} more units of {row['Material_Type']}."
67
+ elif row['Deviation'] > 15:
68
+ excess = row['Used_Quantity'] - row['Planned_Quantity']
69
+ return f"Excess: Reduce future orders by {excess:.0f} units of {row['Material_Type']}."
 
70
  return "No action needed."
71
 
72
  def reconcile_materials(csv_file):
 
 
 
 
 
73
  if isinstance(csv_file, str):
 
74
  df = pd.read_csv(csv_file)
75
  elif hasattr(csv_file, 'name'):
 
76
  df = pd.read_csv(csv_file.name)
77
  else:
 
78
  csv_file.seek(0)
79
  df = pd.read_csv(csv_file)
80
 
81
+ # Rename possible variant columns
82
+ col_map = {
83
+ 'Planned_Qty': 'Planned_Quantity',
84
+ 'Received_Qty': 'Received_Quantity',
85
+ 'Used_Qty': 'Used_Quantity'
 
 
 
86
  }
87
+ df.rename(columns=col_map, inplace=True)
88
+
89
+ # Check required columns
90
+ required = ['Material_Type', 'Planned_Quantity', 'Received_Quantity', 'Used_Quantity']
91
+ for col in required:
92
+ if col not in df.columns:
93
+ raise ValueError(f"Missing column: {col}")
94
+
 
 
 
 
 
 
 
 
 
95
  for col in ['Planned_Quantity', 'Received_Quantity', 'Used_Quantity']:
96
  df[col] = pd.to_numeric(df[col], errors='coerce')
97
  if df[col].isna().any():
98
+ raise ValueError(f"Non-numeric or missing values in '{col}'")
99
 
100
+ # Compute balance and deviation
101
  df['Balance_Quantity'] = df['Received_Quantity'] - df['Used_Quantity']
 
 
 
102
  df['Deviation'] = df.apply(
103
+ lambda row: ((row['Used_Quantity'] - row['Planned_Quantity']) / row['Planned_Quantity']) * 100
104
+ if row['Planned_Quantity'] != 0 else 0,
105
+ axis=1
106
  )
 
107
 
108
+ # Anomaly detection
 
109
  features = df[['Planned_Quantity', 'Received_Quantity', 'Used_Quantity', 'Deviation']]
110
+ iso_forest = IsolationForest(contamination=0.1, random_state=42)
111
  df['Anomaly'] = iso_forest.fit_predict(features)
 
 
112
 
113
+ # Enforce anomaly for deviation beyond 15%
114
+ df.loc[df['Deviation'].abs() > 15, 'Anomaly'] = -1
115
+
116
+ # AI Suggestions & Status
117
  df['AI_Suggestion'] = df.apply(generate_suggestion, axis=1)
118
  df['Reconciliation_Status'] = df.apply(
119
+ lambda row: 'Flagged' if row['Anomaly'] == -1 or abs(row['Deviation']) > 15 else 'Complete',
120
+ axis=1
121
  )
 
122
 
123
+ # Insert into Salesforce
124
  salesforce_result = insert_reconciliation_to_salesforce(df, sf)
 
125
 
126
+ # Output summary
127
+ output_text = f"Material Reconciliation Results\n=============================\n\n"
128
+ output_text += f"{salesforce_result}\n\nDetailed Records:\n"
129
+ for i, row in df.iterrows():
130
+ output_text += f"Record {i + 1}:\n"
 
 
131
  if 'Project_ID' in df.columns and pd.notna(row['Project_ID']):
132
+ output_text += f" Project ID: {row['Project_ID']}\n"
133
+ output_text += f" Material Type: {row['Material_Type']}\n"
134
+ output_text += f" Planned Quantity: {row['Planned_Quantity']}\n"
135
+ output_text += f" Received Quantity: {row['Received_Quantity']}\n"
136
+ output_text += f" Used Quantity: {row['Used_Quantity']}\n"
137
+ output_text += f" Balance Quantity: {row['Balance_Quantity']}\n"
138
+ output_text += f" Deviation: {row['Deviation']:.2f}%\n"
139
+ output_text += f" Anomaly: {'Yes' if row['Anomaly'] == -1 else 'No'}\n"
140
+ output_text += f" AI Suggestion: {row['AI_Suggestion']}\n"
141
+ output_text += f" Reconciliation Status: {row['Reconciliation_Status']}\n"
142
+ output_text += "-----------------------------\n"
143
+
144
+ # Save file
145
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp:
146
+ output_file = tmp.name
147
  df.to_csv(output_file, index=False)
 
148
 
149
+ # Charts
 
150
  bar_fig = px.bar(
151
+ df, x='Material_Type', y='Deviation',
 
 
152
  color='Reconciliation_Status',
153
  title='Deviation by Material Type',
154
  labels={'Deviation': 'Deviation (%)'},
155
  color_discrete_map={'Flagged': '#FF4B4B', 'Complete': '#36A2EB'}
156
  )
 
157
 
158
+ pie_data = df['Reconciliation_Status'].value_counts().reset_index()
159
+ pie_data.columns = ['Reconciliation_Status', 'Count']
 
160
  pie_fig = px.pie(
161
+ pie_data, names='Reconciliation_Status', values='Count',
 
 
162
  title='Reconciliation Status Distribution',
163
  color_discrete_map={'Flagged': '#FF4B4B', 'Complete': '#36A2EB'}
164
  )
165
 
 
166
  ai_summary = "\n".join([f"{row['Material_Type']}: {row['AI_Suggestion']}" for _, row in df.iterrows()])
167
+ return output_file, output_text, df, bar_fig, pie_fig, ai_summary
168
 
169
+ # Gradio UI
 
 
 
170
  with gr.Blocks(css='button:has(span:contains("Share via Link")) { display: none !important; }') as interface:
171
  gr.Markdown("# Material Reconciliation Dashboard")
172
  with gr.Row():
173
  with gr.Column(scale=1):
 
174
  csv_input = gr.File(label="Upload CSV", file_types=[".csv"])
175
  submit_button = gr.Button("Reconcile Materials")
176
  with gr.Column(scale=2):
 
177
  output_text = gr.Textbox(label="Detailed Results", lines=20)
178
  with gr.Row():
179
  with gr.Column(scale=2):
 
180
  output_table = gr.Dataframe(label="Reconciled Data")
181
  with gr.Column(scale=1):
 
182
  ai_summary_output = gr.Textbox(label="AI Suggestions Summary")
183
  with gr.Row():
184
  with gr.Column(scale=1):
 
185
  bar_plot = gr.Plot(label="Deviation Plot")
186
  with gr.Column(scale=1):
 
187
  pie_plot = gr.Plot(label="Status Distribution")
188
  output_file = gr.File(label="Download Reconciled CSV")
189
+
190
  submit_button.click(
191
  fn=reconcile_materials,
192
  inputs=csv_input,
193
  outputs=[output_file, output_text, output_table, bar_plot, pie_plot, ai_summary_output]
194
  )
195
 
 
196
  interface.launch()