Spaces:
No application file
No application file
import gradio as gr | |
import pandas as pd | |
import numpy as np | |
import os | |
from datetime import datetime | |
import tempfile | |
from collections import defaultdict | |
# Required columns for dyeing priority calculation | |
REQUIRED_COLS = [ | |
"Account", | |
"Order #", | |
"DESIGN", | |
"Labels", | |
"Colours", | |
"Kgs", | |
"Pending" | |
] | |
# Additional columns that might be present | |
OPTIONAL_COLS = ["Sqm", "Unnamed: 0"] | |
def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame: | |
"""Normalize column names by stripping whitespace""" | |
df = df.copy() | |
df.columns = [str(c).strip() for c in df.columns] | |
return df | |
def _parse_colours(colour_str): | |
"""Parse colour string into list of individual colours""" | |
if pd.isna(colour_str): | |
return [] | |
# Handle various separators (comma, semicolon, pipe, etc.) | |
colour_str = str(colour_str).strip() | |
# Try different separators | |
for sep in [',', ';', '|', '/', '+', '&']: | |
if sep in colour_str: | |
colours = [c.strip().upper() for c in colour_str.split(sep) if c.strip()] | |
return colours | |
# If no separators found, treat as single colour | |
return [colour_str.upper()] if colour_str else [] | |
def calculate_colour_totals(df: pd.DataFrame) -> pd.DataFrame: | |
"""Calculate total quantity required for each colour across all designs""" | |
colour_totals = defaultdict(float) | |
colour_details = defaultdict(list) # Track which designs use each colour | |
for _, row in df.iterrows(): | |
colours = _parse_colours(row['Colours']) | |
kgs = pd.to_numeric(row['Kgs'], errors='coerce') or 0 | |
design = str(row.get('DESIGN', 'Unknown')) | |
order_num = str(row.get('Order #', 'Unknown')) | |
if colours and kgs > 0: | |
# Distribute weight equally among colours if multiple colours | |
kgs_per_colour = kgs / len(colours) | |
for colour in colours: | |
colour_totals[colour] += kgs_per_colour | |
colour_details[colour].append({ | |
'Design': design, | |
'Order': order_num, | |
'Kgs_Contribution': kgs_per_colour, | |
'Total_Order_Kgs': kgs | |
}) | |
# Convert to DataFrame with detailed breakdown | |
colour_rows = [] | |
for colour, total_kgs in sorted(colour_totals.items(), key=lambda x: x[1], reverse=True): | |
designs_using = list(set([detail['Design'] for detail in colour_details[colour]])) | |
orders_count = len(colour_details[colour]) | |
colour_rows.append({ | |
'Colour': colour, | |
'Total_Kgs_Required': round(total_kgs, 2), | |
'Designs_Using_This_Colour': ', '.join(sorted(designs_using)), | |
'Number_of_Orders': orders_count, | |
'Priority_Rank': len(colour_rows) + 1 | |
}) | |
colour_df = pd.DataFrame(colour_rows) | |
return colour_df, colour_details | |
def create_detailed_colour_breakdown(colour_details: dict) -> pd.DataFrame: | |
"""Create detailed breakdown showing which orders contribute to each colour""" | |
breakdown_rows = [] | |
for colour, details in colour_details.items(): | |
for detail in details: | |
breakdown_rows.append({ | |
'Colour': colour, | |
'Design': detail['Design'], | |
'Order_Number': detail['Order'], | |
'Kgs_for_This_Colour': round(detail['Kgs_Contribution'], 2), | |
'Total_Order_Kgs': detail['Total_Order_Kgs'] | |
}) | |
breakdown_df = pd.DataFrame(breakdown_rows) | |
# Sort by colour, then by kgs contribution (descending) | |
breakdown_df = breakdown_df.sort_values(['Colour', 'Kgs_for_This_Colour'], ascending=[True, False]) | |
return breakdown_df | |
def detect_date_columns(df: pd.DataFrame) -> list: | |
"""Detect date columns in the dataframe""" | |
date_columns = [] | |
for col in df.columns: | |
col_str = str(col).strip() | |
# Try to parse as datetime | |
try: | |
pd.to_datetime(col_str) | |
date_columns.append(col) | |
except: | |
# Check for date patterns like "13/8", "14/8" | |
if '/' in col_str and len(col_str.split('/')) == 2: | |
try: | |
parts = col_str.split('/') | |
if all(part.isdigit() for part in parts): | |
date_columns.append(col) | |
except: | |
pass | |
return date_columns | |
def find_earliest_order_date(df: pd.DataFrame) -> pd.Series: | |
"""Find the earliest date for each order from date columns""" | |
date_columns = detect_date_columns(df) | |
if not date_columns: | |
# No date columns found, assign all orders as very old (high priority) | |
return pd.Series([365] * len(df), index=df.index) # 365 days old | |
earliest_dates = [] | |
for idx, row in df.iterrows(): | |
order_dates = [] | |
for date_col in date_columns: | |
cell_value = row[date_col] | |
# Skip if cell is empty or contains non-date data | |
if pd.isna(cell_value) or cell_value == 0 or cell_value == "": | |
continue | |
# Try to parse date from column name | |
try: | |
if '/' in str(date_col): | |
# Handle formats like "13/8" (day/month) | |
day, month = str(date_col).split('/') | |
# Assume current year | |
date_obj = pd.to_datetime(f"2025-{month.zfill(2)}-{day.zfill(2)}") | |
else: | |
# Handle datetime column names | |
date_obj = pd.to_datetime(str(date_col)) | |
# If there's actual data in this cell (not empty/zero), consider this date | |
if not pd.isna(cell_value) and str(cell_value).strip() != "" and str(cell_value) != "0": | |
order_dates.append(date_obj) | |
except: | |
continue | |
# Find earliest date for this order | |
if order_dates: | |
earliest_date = min(order_dates) | |
else: | |
# No valid dates found, assign a default old date | |
earliest_date = pd.to_datetime("2024-01-01") | |
earliest_dates.append(earliest_date) | |
return pd.Series(earliest_dates, index=df.index) | |
def compute_dyeing_priority(df: pd.DataFrame, min_kgs: int = 100, weights: dict = None) -> tuple: | |
""" | |
Compute dyeing priority based on: | |
1. Oldest orders with minimum kgs per design | |
2. Designs with fewest colours | |
3. Order age | |
""" | |
# Default weights if not provided | |
if weights is None: | |
weights = {"AGE_WEIGHT": 50, "COLOUR_SIMPLICITY_WEIGHT": 30, "DESIGN_WEIGHT": 20} | |
df = _normalize_columns(df) | |
# Check for required columns (excluding Date which is now optional) | |
missing = [c for c in REQUIRED_COLS if c not in df.columns] | |
if missing: | |
raise ValueError(f"Missing required columns: {missing}. Found columns: {list(df.columns)}") | |
# Create working copy | |
out = df.copy() | |
# Find earliest order dates from date columns | |
out["OrderDate"] = find_earliest_order_date(out) | |
# Calculate age in days | |
today = pd.Timestamp.now().normalize() | |
out["OrderAgeDays"] = (today - out["OrderDate"]).dt.days | |
out["OrderAgeDays"] = out["OrderAgeDays"].fillna(0).clip(lower=0) | |
# Convert Kgs to numeric | |
out["Kgs"] = pd.to_numeric(out["Kgs"], errors="coerce").fillna(0) | |
# Parse colours and count them | |
out["ColourList"] = out["Colours"].apply(_parse_colours) | |
out["ColourCount"] = out["ColourList"].apply(len) | |
# Group by design to calculate design-level metrics | |
design_groups = out.groupby("DESIGN").agg({ | |
"Kgs": "sum", | |
"OrderDate": "min", # Oldest date for this design | |
"OrderAgeDays": "max", # Maximum age for this design | |
"ColourCount": "first", # Colour count should be same for same design | |
"Order #": "count" # Number of orders for this design | |
}).reset_index() | |
design_groups.columns = ["DESIGN", "Total_Kgs", "Oldest_Date", "Max_Age_Days", "ColourCount", "Order_Count"] | |
# Filter designs that meet minimum kg requirement | |
design_groups["MeetsMinKgs"] = design_groups["Total_Kgs"] >= min_kgs | |
# Calculate scores for designs that meet criteria | |
eligible_designs = design_groups[design_groups["MeetsMinKgs"]].copy() | |
if len(eligible_designs) == 0: | |
# If no designs meet criteria, include all for ranking | |
eligible_designs = design_groups.copy() | |
eligible_designs["MeetsMinKgs"] = False | |
# Age Score (0-1, older = higher) | |
if eligible_designs["Max_Age_Days"].max() > 0: | |
eligible_designs["AgeScore_01"] = eligible_designs["Max_Age_Days"] / eligible_designs["Max_Age_Days"].max() | |
else: | |
eligible_designs["AgeScore_01"] = 0 | |
# Colour Simplicity Score (0-1, fewer colours = higher) | |
if eligible_designs["ColourCount"].max() > 0: | |
eligible_designs["ColourSimplicityScore_01"] = 1 - (eligible_designs["ColourCount"] / eligible_designs["ColourCount"].max()) | |
else: | |
eligible_designs["ColourSimplicityScore_01"] = 0 | |
# Design Volume Score (0-1, more kgs = higher priority for production efficiency) | |
if eligible_designs["Total_Kgs"].max() > 0: | |
eligible_designs["VolumeScore_01"] = eligible_designs["Total_Kgs"] / eligible_designs["Total_Kgs"].max() | |
else: | |
eligible_designs["VolumeScore_01"] = 0 | |
# Calculate weighted priority scores | |
w_age = weights["AGE_WEIGHT"] / 100.0 | |
w_colour = weights["COLOUR_SIMPLICITY_WEIGHT"] / 100.0 | |
w_design = weights["DESIGN_WEIGHT"] / 100.0 | |
eligible_designs["AgeScore"] = eligible_designs["AgeScore_01"] * w_age | |
eligible_designs["ColourSimplicityScore"] = eligible_designs["ColourSimplicityScore_01"] * w_colour | |
eligible_designs["VolumeScore"] = eligible_designs["VolumeScore_01"] * w_design | |
eligible_designs["PriorityScore"] = ( | |
eligible_designs["AgeScore"] + | |
eligible_designs["ColourSimplicityScore"] + | |
eligible_designs["VolumeScore"] | |
) | |
# Sort by priority | |
eligible_designs = eligible_designs.sort_values( | |
["MeetsMinKgs", "PriorityScore", "Max_Age_Days"], | |
ascending=[False, False, False] | |
) | |
# Join back to original data to get detailed view | |
detailed_results = out.merge( | |
eligible_designs[["DESIGN", "Total_Kgs", "Max_Age_Days", "MeetsMinKgs", | |
"AgeScore", "ColourSimplicityScore", "VolumeScore", "PriorityScore"]], | |
on="DESIGN", | |
how="left" | |
) | |
# Sort detailed results by priority | |
detailed_results = detailed_results.sort_values( | |
["MeetsMinKgs", "PriorityScore", "OrderAgeDays"], | |
ascending=[False, False, False] | |
) | |
# Calculate colour totals with detailed breakdown | |
colour_totals, colour_details = calculate_colour_totals(out) | |
colour_breakdown = create_detailed_colour_breakdown(colour_details) | |
return detailed_results, eligible_designs, colour_totals, colour_breakdown | |
def save_dyeing_results(detailed_df, design_summary, colour_totals, colour_breakdown, output_path, min_kgs, weights): | |
"""Save all results with multiple sheets""" | |
with pd.ExcelWriter(output_path, engine='openpyxl') as writer: | |
# Sheet 1: Colour Requirements Summary (MAIN PRIORITY - what you need most!) | |
colour_totals.to_excel(writer, sheet_name='COLOUR_REQUIREMENTS', index=False) | |
# Sheet 2: Detailed Colour Breakdown (which orders contribute to each colour) | |
colour_breakdown.to_excel(writer, sheet_name='Colour_Order_Breakdown', index=False) | |
# Sheet 3: Design Summary (design-level priority ranking) | |
design_summary.to_excel(writer, sheet_name='Design_Priority_Summary', index=False) | |
# Sheet 4: Detailed Order Priority | |
detailed_df.to_excel(writer, sheet_name='Order_Priority_Detail', index=False) | |
# Sheet 5: Instructions | |
instructions_data = [ | |
['π¨ DYEING PRIORITY & COLOUR REQUIREMENTS ANALYSIS'], | |
[''], | |
['π SHEET EXPLANATIONS:'], | |
[''], | |
['1. COLOUR_REQUIREMENTS - π― MAIN OUTPUT YOU NEED'], | |
[' β’ Total kgs needed for each colour (consolidated across all designs)'], | |
[' β’ No colour repetition - each colour listed once with total quantity'], | |
[' β’ Sorted by quantity (highest first) for production planning'], | |
[' β’ Shows which designs use each colour and order count'], | |
[''], | |
['2. Colour_Order_Breakdown - Detailed breakdown'], | |
[' β’ Shows exactly which orders contribute to each colour total'], | |
[' β’ Useful for tracking and verification'], | |
[''], | |
['3. Design_Priority_Summary - Design-level priorities'], | |
[' β’ Ranked by priority score for production sequence'], | |
[''], | |
['4. Order_Priority_Detail - Individual order details'], | |
[' β’ All orders with calculated priority scores'], | |
[''], | |
['π― PRIORITY METHODOLOGY:'], | |
[f'β’ Age Weight: {weights["AGE_WEIGHT"]}% - Prioritizes older orders'], | |
[f'β’ Colour Simplicity Weight: {weights["COLOUR_SIMPLICITY_WEIGHT"]}% - Fewer colours = higher priority'], | |
[f'β’ Design Volume Weight: {weights["DESIGN_WEIGHT"]}% - Larger quantities get priority'], | |
[f'β’ Minimum Kgs Threshold: {min_kgs} - Only designs with total kgs >= this value are prioritized'], | |
[''], | |
['π¨ COLOUR CONSOLIDATION LOGIC:'], | |
['β’ If RED is used in Design-A (100kg) and Design-B (50kg)'], | |
['β’ Output shows: RED = 150kg total (no repetition)'], | |
['β’ Helps plan exact dye batch quantities needed'], | |
['β’ Multi-colour orders split proportionally (e.g., "Red,Blue" 100kg = 50kg each)'], | |
[''], | |
['π USAGE RECOMMENDATIONS:'], | |
['β’ Use COLOUR_REQUIREMENTS sheet for dye purchasing/batching'], | |
['β’ Use Design_Priority_Summary for production sequence planning'], | |
['β’ Check Colour_Order_Breakdown for detailed verification'], | |
[''], | |
[f'Generated on: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'] | |
] | |
instructions_df = pd.DataFrame(instructions_data, columns=['Instructions']) | |
instructions_df.to_excel(writer, sheet_name='Instructions', index=False) | |
# Gradio Interface Functions | |
def load_excel(file): | |
"""Load Excel file and return available sheet names""" | |
if file is None: | |
return gr.Dropdown(choices=[]), "Please upload a file first." | |
try: | |
xls = pd.ExcelFile(file.name) | |
return gr.Dropdown(choices=xls.sheet_names, value=xls.sheet_names[0]), "β File loaded successfully!" | |
except Exception as e: | |
return gr.Dropdown(choices=[]), f"β Error loading file: {str(e)}" | |
def validate_weights(age_weight, colour_weight, design_weight): | |
"""Validate that weights sum to 100%""" | |
total = age_weight + colour_weight + design_weight | |
if total == 100: | |
return "β Weights are valid (sum = 100%)" | |
else: | |
return f"β οΈ Weights sum to {total}%. Please adjust to equal 100%." | |
def preview_dyeing_data(file, sheet_name): | |
"""Preview the selected sheet data for dyeing analysis""" | |
if file is None or not sheet_name: | |
return "Please upload a file and select a sheet first.", pd.DataFrame() | |
try: | |
df = pd.read_excel(file.name, sheet_name=sheet_name) | |
# Show basic info | |
preview_info = f"π **Sheet: {sheet_name}**\n" | |
preview_info += f"- Rows: {len(df)}\n" | |
preview_info += f"- Columns: {len(df.columns)}\n\n" | |
# Check for required columns | |
df_norm = df.copy() | |
df_norm.columns = [str(c).strip() for c in df_norm.columns] | |
missing = [c for c in REQUIRED_COLS if c not in df_norm.columns] | |
if missing: | |
preview_info += f"β **Missing required columns:** {missing}\n\n" | |
else: | |
preview_info += "β **All required columns found!**\n\n" | |
# Detect date columns | |
date_columns = detect_date_columns(df_norm) | |
if date_columns: | |
preview_info += f"π **Date columns detected:** {len(date_columns)} columns\n" | |
preview_info += f" Sample dates: {date_columns[:5]}\n\n" | |
else: | |
preview_info += "β οΈ **No date columns detected** - will use default prioritization\n\n" | |
# Show some statistics | |
if 'Kgs' in df_norm.columns: | |
total_kgs = pd.to_numeric(df_norm['Kgs'], errors='coerce').sum() | |
preview_info += f"**Total Kgs:** {total_kgs:,.1f}\n" | |
if 'DESIGN' in df_norm.columns: | |
unique_designs = df_norm['DESIGN'].nunique() | |
preview_info += f"**Unique Designs:** {unique_designs}\n" | |
preview_info += f"\n**Available columns:**\n" | |
for i, col in enumerate(df.columns, 1): | |
marker = "π " if col in date_columns else "" | |
preview_info += f"{i}. {col} {marker}\n" | |
# Show first few rows | |
preview_df = df.head(5) | |
return preview_info, preview_df | |
except Exception as e: | |
return f"β Error previewing data: {str(e)}", pd.DataFrame() | |
def process_dyeing_priority(file, sheet_name, age_weight, colour_weight, design_weight, min_kgs): | |
"""Main processing function for dyeing priorities""" | |
if file is None: | |
return None, None, None, "β Please upload a file first." | |
if not sheet_name: | |
return None, None, None, "β Please select a sheet." | |
# Validate weights | |
total_weight = age_weight + colour_weight + design_weight | |
if total_weight != 100: | |
return None, None, None, f"β Error: Total weight must equal 100% (currently {total_weight}%)" | |
try: | |
# Load data | |
df = pd.read_excel(file.name, sheet_name=sheet_name) | |
if df.empty: | |
return None, None, None, "β The selected sheet is empty." | |
# Prepare weights | |
weights = { | |
"AGE_WEIGHT": age_weight, | |
"COLOUR_SIMPLICITY_WEIGHT": colour_weight, | |
"DESIGN_WEIGHT": design_weight | |
} | |
# Compute priorities | |
detailed_results, design_summary, colour_totals, colour_breakdown = compute_dyeing_priority( | |
df, min_kgs=min_kgs, weights=weights | |
) | |
# Create temporary output file | |
output_path = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx').name | |
save_dyeing_results(detailed_results, design_summary, colour_totals, colour_breakdown, output_path, min_kgs, weights) | |
# Create success message | |
total_designs = len(design_summary) | |
eligible_designs = sum(design_summary['MeetsMinKgs']) | |
total_colours = len(colour_totals) | |
top_colours = colour_totals.head(3)['Colour'].tolist() if len(colour_totals) > 0 else [] | |
success_msg = f"β Dyeing Priority Analysis Complete!\n" | |
success_msg += f"π SUMMARY:\n" | |
success_msg += f"- Total Designs Analyzed: {total_designs}\n" | |
success_msg += f"- Designs Meeting {min_kgs}kg Threshold: {eligible_designs}\n" | |
success_msg += f"- Unique Colours Required: {total_colours}\n" | |
if top_colours: | |
success_msg += f"- Top 3 Colours by Volume: {', '.join(top_colours)}\n" | |
success_msg += f"- Highest Priority Score: {design_summary['PriorityScore'].max():.3f}\n\n" | |
success_msg += f"π¨ COLOUR REQUIREMENTS sheet contains consolidated totals!\n" | |
success_msg += f"π₯ Download complete analysis below" | |
return output_path, design_summary.head(10), colour_totals.head(15), success_msg | |
except Exception as e: | |
return None, None, None, f"β Error processing data: {str(e)}" | |
# Create Gradio Interface | |
def create_dyeing_interface(): | |
with gr.Blocks(title="Dyeing Urgency Priority Calculator", theme=gr.themes.Soft()) as demo: | |
gr.Markdown(""" | |
# π¨ Dyeing Urgency Priority Calculator | |
Upload your Excel file with dyeing/textile manufacturing data to calculate production priorities based on: | |
- **Order Age**: Prioritize older orders first (detects dates from column headers) | |
- **Colour Simplicity**: Fewer colours = easier production | |
- **Design Volume**: Larger quantities for efficiency | |
**Expected Columns**: Account, Order #, DESIGN, Labels, Colours, Kgs, Pending | |
**Date Detection**: Automatically detects date columns (like 2025-01-08, 13/8, etc.) | |
""") | |
with gr.Row(): | |
with gr.Column(scale=1): | |
gr.Markdown("## π File Upload & Selection") | |
file_input = gr.File( | |
label="Upload Excel File", | |
file_types=[".xlsx", ".xls"], | |
type="filepath" | |
) | |
sheet_dropdown = gr.Dropdown( | |
label="Select Sheet", | |
choices=[], | |
interactive=True | |
) | |
file_status = gr.Textbox(label="File Status", interactive=False) | |
with gr.Column(scale=1): | |
gr.Markdown("## βοΈ Priority Weights (must sum to 100%)") | |
age_weight = gr.Slider( | |
minimum=0, maximum=100, value=50, step=1, | |
label="Age Weight (%)", | |
info="Higher = prioritize older orders more" | |
) | |
colour_weight = gr.Slider( | |
minimum=0, maximum=100, value=30, step=1, | |
label="Colour Simplicity Weight (%)", | |
info="Higher = prioritize designs with fewer colours" | |
) | |
design_weight = gr.Slider( | |
minimum=0, maximum=100, value=20, step=1, | |
label="Design Volume Weight (%)", | |
info="Higher = prioritize larger quantity designs" | |
) | |
weight_status = gr.Textbox(label="Weight Validation", interactive=False) | |
min_kgs = gr.Number( | |
label="Minimum Kgs Threshold per Design", | |
value=100, | |
info="Only designs with total kgs >= this value get priority" | |
) | |
with gr.Row(): | |
preview_btn = gr.Button("ποΈ Preview Data", variant="secondary") | |
process_btn = gr.Button("π¨ Calculate Dyeing Priorities", variant="primary", size="lg") | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("## π Data Preview") | |
preview_info = gr.Textbox(label="Data Information", lines=10, interactive=False) | |
preview_table = gr.Dataframe(label="Sample Data") | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("## π Priority Results") | |
results_info = gr.Textbox(label="Processing Status", interactive=False) | |
with gr.Column(): | |
download_file = gr.File(label="π₯ Download Complete Analysis") | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("## π Top Design Priorities") | |
design_results = gr.Dataframe(label="Design Priority Summary") | |
with gr.Column(): | |
gr.Markdown("## π¨ Colour Requirements (Consolidated)") | |
colour_results = gr.Dataframe( | |
label="Total Kgs Required Per Colour", | |
headers=["Colour", "Total Kgs", "Used in Designs", "Orders Count"], | |
interactive=False | |
) | |
# Event handlers | |
file_input.change( | |
fn=load_excel, | |
inputs=[file_input], | |
outputs=[sheet_dropdown, file_status] | |
) | |
for weight_input in [age_weight, colour_weight, design_weight]: | |
weight_input.change( | |
fn=validate_weights, | |
inputs=[age_weight, colour_weight, design_weight], | |
outputs=[weight_status] | |
) | |
preview_btn.click( | |
fn=preview_dyeing_data, | |
inputs=[file_input, sheet_dropdown], | |
outputs=[preview_info, preview_table] | |
) | |
process_btn.click( | |
fn=process_dyeing_priority, | |
inputs=[file_input, sheet_dropdown, age_weight, colour_weight, design_weight, min_kgs], | |
outputs=[download_file, design_results, colour_results, results_info] | |
) | |
# Initialize weight validation | |
demo.load( | |
fn=validate_weights, | |
inputs=[age_weight, colour_weight, design_weight], | |
outputs=[weight_status] | |
) | |
return demo | |
# Launch the app | |
if __name__ == "__main__": | |
demo = create_dyeing_interface() | |
demo.launch( | |
#server_name="0.0.0.0", | |
#server_port=7860, | |
share=True, | |
debug=True | |
) |