File size: 8,089 Bytes
d813e54
 
d75559e
 
 
 
48502df
d813e54
 
f51be9c
5052850
d813e54
 
764ca7d
 
 
 
d813e54
764ca7d
 
5052850
d813e54
 
5052850
 
 
 
 
 
 
764ca7d
d813e54
 
 
 
f51be9c
2a6abbc
764ca7d
5052850
 
764ca7d
 
 
 
 
 
5052850
 
f51be9c
 
5052850
 
 
 
 
 
764ca7d
 
f51be9c
 
5052850
 
 
 
 
f51be9c
48502df
d813e54
 
920f674
5052850
 
920f674
5052850
ddfdc56
 
d75559e
 
764ca7d
 
 
 
 
 
 
 
5052850
 
f51be9c
5052850
 
 
 
 
764ca7d
920f674
5052850
 
 
 
 
 
920f674
5052850
 
 
 
 
 
 
 
 
764ca7d
920f674
5052850
 
 
 
f51be9c
764ca7d
 
920f674
5052850
920f674
0785555
764ca7d
0785555
f51be9c
 
 
 
5052850
 
 
 
 
 
 
 
 
 
 
f51be9c
 
 
 
764ca7d
 
 
5052850
 
764ca7d
5052850
764ca7d
 
 
5052850
f51be9c
764ca7d
f51be9c
764ca7d
 
 
0785555
5052850
f51be9c
0785555
f51be9c
764ca7d
 
d75559e
 
764ca7d
 
d75559e
764ca7d
d75559e
 
764ca7d
d75559e
764ca7d
d75559e
 
 
 
 
 
f51be9c
d75559e
 
 
764ca7d
d75559e
d813e54
5052850
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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()