VijayPulmamidi's picture
Update app.py
92c69d2 verified
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@y910075"
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):
# Read CSV
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)
# Normalize column names
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['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
)
# Rule-based anomaly detection based on ±5% threshold
df['anomaly'] = df['deviation'].apply(lambda d: -1 if abs(d) > 5 else 1)
# AI Suggestions
df['ai_suggestion'] = df.apply(generate_suggestion, axis=1)
# Reconciliation Status
df['reconciliation_status'] = df['deviation'].apply(lambda d: 'Flagged' if abs(d) > 5 else 'Complete')
# Insert into Salesforce
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 (%)', 'material_type': 'Material Type'},
color_discrete_map={'Flagged': '#FF4B4B', 'Complete': '#36A2EB'},
height=400,
template='plotly_white'
)
bar_fig.update_layout(
title={'x': 0.5, 'xanchor': 'center', 'font': {'size': 20}},
xaxis_title="Material Type",
yaxis_title="Deviation (%)",
font=dict(size=12),
showlegend=True,
margin=dict(l=50, r=50, t=80, b=50)
)
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'},
height=400,
template='plotly_white'
)
pie_fig.update_layout(
title={'x': 0.5, 'xanchor': 'center', 'font': {'size': 20}},
font=dict(size=12),
margin=dict(l=50, r=50, t=80, b=50)
)
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
# Custom CSS for enhanced styling
custom_css = """
body {
font-family: 'Arial', sans-serif;
background-color: #f4f7fa;
}
.gradio-container {
max-width: 1200px;
margin: auto;
padding: 20px;
}
h1 {
color: #1a3c6e;
text-align: center;
font-size: 2.2em;
margin-bottom: 20px;
}
.gr-button {
background-color: #1a3c6e !important;
color: white !important;
border-radius: 8px !important;
padding: 10px 20px !important;
font-weight: bold !important;
transition: background-color 0.3s ease;
}
.gr-button:hover {
background-color: #2a5b9e !important;
}
.gr-file, .gr-textbox, .gr-dataframe {
border-radius: 8px !important;
border: 1px solid #d1d5db !important;
}
.gr-file label, .gr-textbox label, .gr-dataframe label, .gr-plot label {
font-weight: bold;
color: #1a3c6e;
margin-bottom: 8px;
}
.gr-row {
margin-bottom: 20px;
}
.gr-column {
padding: 10px;
}
.gr-dataframe table {
border-collapse: collapse;
width: 100%;
}
.gr-dataframe th, .gr-dataframe td {
border: 1px solid #e5e7eb;
padding: 8px;
text-align: left;
}
.gr-dataframe th {
background-color: #e6eef8;
color: #1a3c6e;
}
.status-message {
color: #1a3c6e;
font-style: italic;
margin-top: 10px;
}
"""
# Gradio UI
with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as interface:
gr.Markdown(
"""
# Material Reconciliation Dashboard
Upload a CSV file containing material data (columns: `project_id`, `material_type`, `planned_quantity`, `received_quantity`, `used_quantity`) to reconcile materials, view AI suggestions, and sync with Salesforce. Results include detailed reports, visualizations, and a downloadable CSV.
"""
)
with gr.Row():
with gr.Column(scale=1):
csv_input = gr.File(
label="Upload CSV",
file_types=[".csv"],
elem_id="csv-input",
tooltip="Upload a CSV file with columns: project_id, material_type, planned_quantity, received_quantity, used_quantity"
)
submit_button = gr.Button(
"Reconcile Materials",
variant="primary",
elem_id="submit-button",
loading_text="Processing..."
)
status_message = gr.Markdown(
"", elem_classes=["status-message"], visible=False
)
with gr.Column(scale=2):
output_text = gr.Textbox(
label="Detailed Results",
lines=20,
placeholder="Reconciliation results will appear here...",
show_copy_button=True
)
with gr.Row():
with gr.Column(scale=2):
output_table = gr.Dataframe(
label="Reconciled Data",
headers=[
'project_id', 'material_type', 'planned_quantity', 'received_quantity',
'used_quantity', 'balance_quantity', 'deviation', 'anomaly',
'ai_suggestion', 'reconciliation_status'
],
wrap=True,
interactive=False
)
with gr.Column(scale=1):
ai_summary_output = gr.Textbox(
label="AI Suggestions Summary",
placeholder="AI suggestions will appear here...",
show_copy_button=True
)
with gr.Row():
with gr.Column(scale=1):
bar_plot = gr.Plot(
label="Deviation by Material Type",
tooltip="Bar chart showing deviation percentages by material type"
)
with gr.Column(scale=1):
pie_plot = gr.Plot(
label="Reconciliation Status Distribution",
tooltip="Pie chart showing the distribution of reconciliation statuses"
)
output_file = gr.File(
label="Download Reconciled CSV",
tooltip="Download the reconciled data as a CSV file"
)
def update_status_message():
return gr.Markdown.update(
value="Processing complete! Review the results below.",
visible=True
)
submit_button.click(
fn=reconcile_materials,
inputs=csv_input,
outputs=[output_file, output_text, output_table, bar_plot, pie_plot, ai_summary_output],
_js="() => { document.querySelector('.status-message').innerText = 'Processing...'; return true; }"
).then(
fn=update_status_message,
inputs=None,
outputs=status_message
)
interface.launch()