Spaces:
Runtime error
Runtime error
| import pandas as pd | |
| import numpy as np | |
| import gradio as gr | |
| import os | |
| import tempfile | |
| import logging | |
| from simple_salesforce import Salesforce | |
| import plotly.express as px | |
| # Logging setup | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # Salesforce credentials | |
| SALESFORCE_USERNAME = "vijaypulmamidi.dev2025@sathkrutha.com" | |
| SALESFORCE_PASSWORD = "Vij@y9100754977" | |
| SALESFORCE_SECURITY_TOKEN = "CaZSEwVmB3EIAiV6G8ukdDp0" | |
| # Connect to Salesforce | |
| sf = Salesforce(username=SALESFORCE_USERNAME, password=SALESFORCE_PASSWORD, security_token=SALESFORCE_SECURITY_TOKEN) | |
| logger.info("Connected to Salesforce.") | |
| def find_salesforce_project(project_name, sf): | |
| try: | |
| query = f"SELECT Id FROM Project__c WHERE Name = '{project_name}' LIMIT 1" | |
| result = sf.query(query) | |
| if result['totalSize'] > 0: | |
| return result['records'][0]['Id'] | |
| except Exception as e: | |
| logger.warning(f"Salesforce project lookup failed for '{project_name}': {e}") | |
| return None | |
| def insert_reconciliation_to_salesforce(df, sf): | |
| inserted_count = 0 | |
| project_cache = {} | |
| for index, row in df.iterrows(): | |
| project_id = None | |
| if 'project_id' in df.columns and pd.notna(row['project_id']): | |
| project_name = row['project_id'] | |
| if project_name in project_cache: | |
| project_id = project_cache[project_name] | |
| else: | |
| project_id = find_salesforce_project(project_name, sf) | |
| if project_id: | |
| project_cache[project_name] = project_id | |
| else: | |
| logger.info(f"Project '{project_name}' not found in Salesforce, skipping project ID linkage.") | |
| record = { | |
| 'Material_Type__c': row['material_type'], | |
| 'Planned_Quantity__c': row['planned_quantity'], | |
| 'Received_Quantity__c': row['received_quantity'], | |
| 'Used_Quantity__c': row['used_quantity'], | |
| 'AI_Suggestion__c': row.get('ai_suggestion', ''), | |
| 'Reconciliation_Status__c': row.get('reconciliation_status', '') | |
| } | |
| if project_id: | |
| record['Project_ID__c'] = project_id | |
| try: | |
| sf.Material_Reconciliation_Record__c.create(record) | |
| inserted_count += 1 | |
| except Exception as e: | |
| logger.error(f"Failed to insert record for material {row['material_type']}: {e}") | |
| return f"Inserted {inserted_count} records into Salesforce" | |
| def generate_suggestion(row): | |
| if row['deviation'] > 5: | |
| excess = row['used_quantity'] - row['planned_quantity'] | |
| return f"Overuse Alert: Reduce future orders by {excess:.0f} units of {row['material_type']}." | |
| elif row['deviation'] < -5: | |
| surplus = abs(row['planned_quantity'] - row['used_quantity']) | |
| return f"Surplus Detected: {surplus:.0f} units unused. Consider reducing future orders." | |
| return "Usage as planned. No action needed." | |
| def reconcile_materials(csv_file): | |
| if isinstance(csv_file, str): | |
| df = pd.read_csv(csv_file) | |
| elif hasattr(csv_file, 'name'): | |
| df = pd.read_csv(csv_file.name) | |
| else: | |
| csv_file.seek(0) | |
| df = pd.read_csv(csv_file) | |
| df.columns = df.columns.str.strip().str.lower().str.replace(' ', '_') | |
| col_map = { | |
| 'project_id': 'project_id', | |
| 'material_type': 'material_type', | |
| 'planned_quantity': 'planned_quantity', | |
| 'received_quantity': 'received_quantity', | |
| 'used_quantity': 'used_quantity', | |
| } | |
| mapped_cols = {} | |
| for expected_col in col_map: | |
| for actual_col in df.columns: | |
| if actual_col == expected_col: | |
| mapped_cols[expected_col] = actual_col | |
| break | |
| df.rename(columns=mapped_cols, inplace=True) | |
| required = ['material_type', 'planned_quantity', 'received_quantity', 'used_quantity'] | |
| missing = [col for col in required if col not in df.columns] | |
| if missing: | |
| return None, f"Error: Missing required column(s): {', '.join(missing)}", None, None, None, None | |
| for col in ['planned_quantity', 'received_quantity', 'used_quantity']: | |
| df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0) | |
| df['used_quantity'] = df[['used_quantity', 'received_quantity']].min(axis=1) | |
| df['balance_quantity'] = df['received_quantity'] - df['used_quantity'] | |
| df['deviation'] = df.apply( | |
| lambda row: ((row['used_quantity'] - row['planned_quantity']) / row['planned_quantity']) * 100 | |
| if row['planned_quantity'] != 0 else 0, | |
| axis=1 | |
| ) | |
| df['anomaly'] = df['deviation'].apply(lambda d: -1 if abs(d) > 5 else 1) | |
| df['ai_suggestion'] = df.apply(generate_suggestion, axis=1) | |
| df['reconciliation_status'] = df['deviation'].apply(lambda d: 'Flagged' if abs(d) > 5 else 'Complete') | |
| salesforce_result = insert_reconciliation_to_salesforce(df, sf) | |
| output_text = f"Material Reconciliation Results\n=============================\n\n" | |
| output_text += f"{salesforce_result}\n\nDetailed Records:\n" | |
| for i, row in df.iterrows(): | |
| output_text += f"Record {i + 1}:\n" | |
| if 'project_id' in df.columns and pd.notna(row.get('project_id')): | |
| output_text += f" Project ID: {row['project_id']}\n" | |
| output_text += f" Material Type: {row['material_type']}\n" | |
| output_text += f" Planned Quantity: {row['planned_quantity']}\n" | |
| output_text += f" Received Quantity: {row['received_quantity']}\n" | |
| output_text += f" Used Quantity: {row['used_quantity']}\n" | |
| output_text += f" Balance Quantity: {row['balance_quantity']}\n" | |
| output_text += f" Deviation: {row['deviation']:.2f}%\n" | |
| output_text += f" Anomaly: {'Yes' if row['anomaly'] == -1 else 'No'}\n" | |
| output_text += f" AI Suggestion: {row['ai_suggestion']}\n" | |
| output_text += f" Reconciliation Status: {row['reconciliation_status']}\n" | |
| output_text += "-----------------------------\n" | |
| with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: | |
| output_file = tmp.name | |
| df.to_csv(output_file, index=False) | |
| bar_fig = px.bar( | |
| df, x='material_type', y='deviation', | |
| color='reconciliation_status', | |
| title='Deviation by Material Type', | |
| labels={'deviation': 'Deviation (%)'}, | |
| color_discrete_map={'Flagged': '#FF4B4B', 'Complete': '#36A2EB'} | |
| ) | |
| pie_data = df['reconciliation_status'].value_counts().reset_index() | |
| pie_data.columns = ['Reconciliation_Status', 'Count'] | |
| pie_fig = px.pie( | |
| pie_data, names='Reconciliation_Status', values='Count', | |
| title='Reconciliation Status Distribution', | |
| color_discrete_map={'Flagged': '#FF4B4B', 'Complete': '#36A2EB'} | |
| ) | |
| ai_summary = "\n".join([f"{row['material_type']}: {row['ai_suggestion']}" for _, row in df.iterrows()]) | |
| return output_file, output_text, df, bar_fig, pie_fig, ai_summary | |
| # Gradio UI | |
| with gr.Blocks(css='button:has(span:contains("Share via Link")) { display: none !important; }') as interface: | |
| gr.Markdown("# Material Reconciliation Dashboard") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| csv_input = gr.File(label="Upload CSV", file_types=[".csv"]) | |
| submit_button = gr.Button("Reconcile Materials") | |
| with gr.Column(scale=2): | |
| output_text = gr.Textbox(label="Detailed Results", lines=20) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| output_table = gr.Dataframe(label="Reconciled Data") | |
| with gr.Column(scale=1): | |
| ai_summary_output = gr.Textbox(label="AI Suggestions Summary") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| bar_plot = gr.Plot(label="Deviation Plot") | |
| with gr.Column(scale=1): | |
| pie_plot = gr.Plot(label="Status Distribution") | |
| output_file = gr.File(label="Download Reconciled CSV") | |
| submit_button.click( | |
| fn=reconcile_materials, | |
| inputs=csv_input, | |
| outputs=[output_file, output_text, output_table, bar_plot, pie_plot, ai_summary_output] | |
| ) | |
| interface.launch() | |