DPPATRA commited on
Commit
78f194c
·
verified ·
1 Parent(s): f2ffeeb

Upload 11 files

Browse files
Files changed (11) hide show
  1. Docker +33 -0
  2. README.md +2 -9
  3. app.py +627 -0
  4. config.py +11 -0
  5. main.py +138 -0
  6. output_writer.py +40 -0
  7. priority_logic.py +85 -0
  8. processor.py +277 -0
  9. requirements.txt +4 -0
  10. sheet_reader.py +9 -0
  11. utils.py +30 -0
Docker ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image with Python
2
+ FROM python:3.12
3
+
4
+ #Create a user to avoid running as root
5
+ RUN useradd -m -u 1000 user
6
+ USER user
7
+
8
+ # Set environment variables
9
+ ENV PATH="/home/user/.local/bin:$PATH"
10
+
11
+ # Set working directory
12
+ WORKDIR /app
13
+
14
+ # Install system dependencies if needed
15
+ RUN apt-get update && apt-get install -y \
16
+ build-essential \
17
+ python3-dev \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # Copy requirements
21
+ COPY requirements.txt .
22
+
23
+ # Install Python dependencies
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ # Copy app files
27
+ COPY . .
28
+
29
+ # Expose Hugging Face Spaces port
30
+ EXPOSE 7860
31
+
32
+ # Run the Gradio app
33
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,13 +1,6 @@
1
  ---
2
- title: Carpet Efficiency
3
- emoji: 🏆
4
- colorFrom: green
5
- colorTo: green
6
  sdk: gradio
7
  sdk_version: 5.43.1
8
- app_file: app.py
9
- pinned: false
10
- short_description: Boosts efficiency of carpet manufacturing
11
  ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Dyeing_Urgency_Priority_App_
3
+ app_file: gradio_priority_app.py
 
 
4
  sdk: gradio
5
  sdk_version: 5.43.1
 
 
 
6
  ---
 
 
app.py ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ import os
5
+ from datetime import datetime
6
+ import tempfile
7
+ from collections import defaultdict
8
+
9
+ # Required columns for dyeing priority calculation
10
+ REQUIRED_COLS = [
11
+ "Account",
12
+ "Order #",
13
+ "DESIGN",
14
+ "Labels",
15
+ "Colours",
16
+ "Kgs",
17
+ "Pending"
18
+ ]
19
+
20
+ # Additional columns that might be present
21
+ OPTIONAL_COLS = ["Sqm", "Unnamed: 0"]
22
+
23
+ def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
24
+ """Normalize column names by stripping whitespace"""
25
+ df = df.copy()
26
+ df.columns = [str(c).strip() for c in df.columns]
27
+ return df
28
+
29
+ def _parse_colours(colour_str):
30
+ """Parse colour string into list of individual colours"""
31
+ if pd.isna(colour_str):
32
+ return []
33
+
34
+ # Handle various separators (comma, semicolon, pipe, etc.)
35
+ colour_str = str(colour_str).strip()
36
+
37
+ # Try different separators
38
+ for sep in [',', ';', '|', '/', '+', '&']:
39
+ if sep in colour_str:
40
+ colours = [c.strip().upper() for c in colour_str.split(sep) if c.strip()]
41
+ return colours
42
+
43
+ # If no separators found, treat as single colour
44
+ return [colour_str.upper()] if colour_str else []
45
+
46
+ def calculate_colour_totals(df: pd.DataFrame) -> pd.DataFrame:
47
+ """Calculate total quantity required for each colour across all designs"""
48
+ colour_totals = defaultdict(float)
49
+ colour_details = defaultdict(list) # Track which designs use each colour
50
+
51
+ for _, row in df.iterrows():
52
+ colours = _parse_colours(row['Colours'])
53
+ kgs = pd.to_numeric(row['Kgs'], errors='coerce') or 0
54
+ design = str(row.get('DESIGN', 'Unknown'))
55
+ order_num = str(row.get('Order #', 'Unknown'))
56
+
57
+ if colours and kgs > 0:
58
+ # Distribute weight equally among colours if multiple colours
59
+ kgs_per_colour = kgs / len(colours)
60
+ for colour in colours:
61
+ colour_totals[colour] += kgs_per_colour
62
+ colour_details[colour].append({
63
+ 'Design': design,
64
+ 'Order': order_num,
65
+ 'Kgs_Contribution': kgs_per_colour,
66
+ 'Total_Order_Kgs': kgs
67
+ })
68
+
69
+ # Convert to DataFrame with detailed breakdown
70
+ colour_rows = []
71
+ for colour, total_kgs in sorted(colour_totals.items(), key=lambda x: x[1], reverse=True):
72
+ designs_using = list(set([detail['Design'] for detail in colour_details[colour]]))
73
+ orders_count = len(colour_details[colour])
74
+
75
+ colour_rows.append({
76
+ 'Colour': colour,
77
+ 'Total_Kgs_Required': round(total_kgs, 2),
78
+ 'Designs_Using_This_Colour': ', '.join(sorted(designs_using)),
79
+ 'Number_of_Orders': orders_count,
80
+ 'Priority_Rank': len(colour_rows) + 1
81
+ })
82
+
83
+ colour_df = pd.DataFrame(colour_rows)
84
+ return colour_df, colour_details
85
+
86
+ def create_detailed_colour_breakdown(colour_details: dict) -> pd.DataFrame:
87
+ """Create detailed breakdown showing which orders contribute to each colour"""
88
+ breakdown_rows = []
89
+
90
+ for colour, details in colour_details.items():
91
+ for detail in details:
92
+ breakdown_rows.append({
93
+ 'Colour': colour,
94
+ 'Design': detail['Design'],
95
+ 'Order_Number': detail['Order'],
96
+ 'Kgs_for_This_Colour': round(detail['Kgs_Contribution'], 2),
97
+ 'Total_Order_Kgs': detail['Total_Order_Kgs']
98
+ })
99
+
100
+ breakdown_df = pd.DataFrame(breakdown_rows)
101
+ # Sort by colour, then by kgs contribution (descending)
102
+ breakdown_df = breakdown_df.sort_values(['Colour', 'Kgs_for_This_Colour'], ascending=[True, False])
103
+
104
+ return breakdown_df
105
+
106
+ def detect_date_columns(df: pd.DataFrame) -> list:
107
+ """Detect date columns in the dataframe"""
108
+ date_columns = []
109
+
110
+ for col in df.columns:
111
+ col_str = str(col).strip()
112
+
113
+ # Try to parse as datetime
114
+ try:
115
+ pd.to_datetime(col_str)
116
+ date_columns.append(col)
117
+ except:
118
+ # Check for date patterns like "13/8", "14/8"
119
+ if '/' in col_str and len(col_str.split('/')) == 2:
120
+ try:
121
+ parts = col_str.split('/')
122
+ if all(part.isdigit() for part in parts):
123
+ date_columns.append(col)
124
+ except:
125
+ pass
126
+
127
+ return date_columns
128
+
129
+ def find_earliest_order_date(df: pd.DataFrame) -> pd.Series:
130
+ """Find the earliest date for each order from date columns"""
131
+ date_columns = detect_date_columns(df)
132
+
133
+ if not date_columns:
134
+ # No date columns found, assign all orders as very old (high priority)
135
+ return pd.Series([365] * len(df), index=df.index) # 365 days old
136
+
137
+ earliest_dates = []
138
+
139
+ for idx, row in df.iterrows():
140
+ order_dates = []
141
+
142
+ for date_col in date_columns:
143
+ cell_value = row[date_col]
144
+
145
+ # Skip if cell is empty or contains non-date data
146
+ if pd.isna(cell_value) or cell_value == 0 or cell_value == "":
147
+ continue
148
+
149
+ # Try to parse date from column name
150
+ try:
151
+ if '/' in str(date_col):
152
+ # Handle formats like "13/8" (day/month)
153
+ day, month = str(date_col).split('/')
154
+ # Assume current year
155
+ date_obj = pd.to_datetime(f"2025-{month.zfill(2)}-{day.zfill(2)}")
156
+ else:
157
+ # Handle datetime column names
158
+ date_obj = pd.to_datetime(str(date_col))
159
+
160
+ # If there's actual data in this cell (not empty/zero), consider this date
161
+ if not pd.isna(cell_value) and str(cell_value).strip() != "" and str(cell_value) != "0":
162
+ order_dates.append(date_obj)
163
+
164
+ except:
165
+ continue
166
+
167
+ # Find earliest date for this order
168
+ if order_dates:
169
+ earliest_date = min(order_dates)
170
+ else:
171
+ # No valid dates found, assign a default old date
172
+ earliest_date = pd.to_datetime("2024-01-01")
173
+
174
+ earliest_dates.append(earliest_date)
175
+
176
+ return pd.Series(earliest_dates, index=df.index)
177
+
178
+ def compute_dyeing_priority(df: pd.DataFrame, min_kgs: int = 100, weights: dict = None) -> tuple:
179
+ """
180
+ Compute dyeing priority based on:
181
+ 1. Oldest orders with minimum kgs per design
182
+ 2. Designs with fewest colours
183
+ 3. Order age
184
+ """
185
+
186
+ # Default weights if not provided
187
+ if weights is None:
188
+ weights = {"AGE_WEIGHT": 50, "COLOUR_SIMPLICITY_WEIGHT": 30, "DESIGN_WEIGHT": 20}
189
+
190
+ df = _normalize_columns(df)
191
+
192
+ # Check for required columns (excluding Date which is now optional)
193
+ missing = [c for c in REQUIRED_COLS if c not in df.columns]
194
+ if missing:
195
+ raise ValueError(f"Missing required columns: {missing}. Found columns: {list(df.columns)}")
196
+
197
+ # Create working copy
198
+ out = df.copy()
199
+
200
+ # Find earliest order dates from date columns
201
+ out["OrderDate"] = find_earliest_order_date(out)
202
+
203
+ # Calculate age in days
204
+ today = pd.Timestamp.now().normalize()
205
+ out["OrderAgeDays"] = (today - out["OrderDate"]).dt.days
206
+ out["OrderAgeDays"] = out["OrderAgeDays"].fillna(0).clip(lower=0)
207
+
208
+ # Convert Kgs to numeric
209
+ out["Kgs"] = pd.to_numeric(out["Kgs"], errors="coerce").fillna(0)
210
+
211
+ # Parse colours and count them
212
+ out["ColourList"] = out["Colours"].apply(_parse_colours)
213
+ out["ColourCount"] = out["ColourList"].apply(len)
214
+
215
+ # Group by design to calculate design-level metrics
216
+ design_groups = out.groupby("DESIGN").agg({
217
+ "Kgs": "sum",
218
+ "OrderDate": "min", # Oldest date for this design
219
+ "OrderAgeDays": "max", # Maximum age for this design
220
+ "ColourCount": "first", # Colour count should be same for same design
221
+ "Order #": "count" # Number of orders for this design
222
+ }).reset_index()
223
+
224
+ design_groups.columns = ["DESIGN", "Total_Kgs", "Oldest_Date", "Max_Age_Days", "ColourCount", "Order_Count"]
225
+
226
+ # Filter designs that meet minimum kg requirement
227
+ design_groups["MeetsMinKgs"] = design_groups["Total_Kgs"] >= min_kgs
228
+
229
+ # Calculate scores for designs that meet criteria
230
+ eligible_designs = design_groups[design_groups["MeetsMinKgs"]].copy()
231
+
232
+ if len(eligible_designs) == 0:
233
+ # If no designs meet criteria, include all for ranking
234
+ eligible_designs = design_groups.copy()
235
+ eligible_designs["MeetsMinKgs"] = False
236
+
237
+ # Age Score (0-1, older = higher)
238
+ if eligible_designs["Max_Age_Days"].max() > 0:
239
+ eligible_designs["AgeScore_01"] = eligible_designs["Max_Age_Days"] / eligible_designs["Max_Age_Days"].max()
240
+ else:
241
+ eligible_designs["AgeScore_01"] = 0
242
+
243
+ # Colour Simplicity Score (0-1, fewer colours = higher)
244
+ if eligible_designs["ColourCount"].max() > 0:
245
+ eligible_designs["ColourSimplicityScore_01"] = 1 - (eligible_designs["ColourCount"] / eligible_designs["ColourCount"].max())
246
+ else:
247
+ eligible_designs["ColourSimplicityScore_01"] = 0
248
+
249
+ # Design Volume Score (0-1, more kgs = higher priority for production efficiency)
250
+ if eligible_designs["Total_Kgs"].max() > 0:
251
+ eligible_designs["VolumeScore_01"] = eligible_designs["Total_Kgs"] / eligible_designs["Total_Kgs"].max()
252
+ else:
253
+ eligible_designs["VolumeScore_01"] = 0
254
+
255
+ # Calculate weighted priority scores
256
+ w_age = weights["AGE_WEIGHT"] / 100.0
257
+ w_colour = weights["COLOUR_SIMPLICITY_WEIGHT"] / 100.0
258
+ w_design = weights["DESIGN_WEIGHT"] / 100.0
259
+
260
+ eligible_designs["AgeScore"] = eligible_designs["AgeScore_01"] * w_age
261
+ eligible_designs["ColourSimplicityScore"] = eligible_designs["ColourSimplicityScore_01"] * w_colour
262
+ eligible_designs["VolumeScore"] = eligible_designs["VolumeScore_01"] * w_design
263
+
264
+ eligible_designs["PriorityScore"] = (
265
+ eligible_designs["AgeScore"] +
266
+ eligible_designs["ColourSimplicityScore"] +
267
+ eligible_designs["VolumeScore"]
268
+ )
269
+
270
+ # Sort by priority
271
+ eligible_designs = eligible_designs.sort_values(
272
+ ["MeetsMinKgs", "PriorityScore", "Max_Age_Days"],
273
+ ascending=[False, False, False]
274
+ )
275
+
276
+ # Join back to original data to get detailed view
277
+ detailed_results = out.merge(
278
+ eligible_designs[["DESIGN", "Total_Kgs", "Max_Age_Days", "MeetsMinKgs",
279
+ "AgeScore", "ColourSimplicityScore", "VolumeScore", "PriorityScore"]],
280
+ on="DESIGN",
281
+ how="left"
282
+ )
283
+
284
+ # Sort detailed results by priority
285
+ detailed_results = detailed_results.sort_values(
286
+ ["MeetsMinKgs", "PriorityScore", "OrderAgeDays"],
287
+ ascending=[False, False, False]
288
+ )
289
+
290
+ # Calculate colour totals with detailed breakdown
291
+ colour_totals, colour_details = calculate_colour_totals(out)
292
+ colour_breakdown = create_detailed_colour_breakdown(colour_details)
293
+
294
+ return detailed_results, eligible_designs, colour_totals, colour_breakdown
295
+
296
+ def save_dyeing_results(detailed_df, design_summary, colour_totals, colour_breakdown, output_path, min_kgs, weights):
297
+ """Save all results with multiple sheets"""
298
+
299
+ with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
300
+
301
+ # Sheet 1: Colour Requirements Summary (MAIN PRIORITY - what you need most!)
302
+ colour_totals.to_excel(writer, sheet_name='COLOUR_REQUIREMENTS', index=False)
303
+
304
+ # Sheet 2: Detailed Colour Breakdown (which orders contribute to each colour)
305
+ colour_breakdown.to_excel(writer, sheet_name='Colour_Order_Breakdown', index=False)
306
+
307
+ # Sheet 3: Design Summary (design-level priority ranking)
308
+ design_summary.to_excel(writer, sheet_name='Design_Priority_Summary', index=False)
309
+
310
+ # Sheet 4: Detailed Order Priority
311
+ detailed_df.to_excel(writer, sheet_name='Order_Priority_Detail', index=False)
312
+
313
+ # Sheet 5: Instructions
314
+ instructions_data = [
315
+ ['🎨 DYEING PRIORITY & COLOUR REQUIREMENTS ANALYSIS'],
316
+ [''],
317
+ ['📋 SHEET EXPLANATIONS:'],
318
+ [''],
319
+ ['1. COLOUR_REQUIREMENTS - 🎯 MAIN OUTPUT YOU NEED'],
320
+ [' • Total kgs needed for each colour (consolidated across all designs)'],
321
+ [' • No colour repetition - each colour listed once with total quantity'],
322
+ [' • Sorted by quantity (highest first) for production planning'],
323
+ [' • Shows which designs use each colour and order count'],
324
+ [''],
325
+ ['2. Colour_Order_Breakdown - Detailed breakdown'],
326
+ [' • Shows exactly which orders contribute to each colour total'],
327
+ [' • Useful for tracking and verification'],
328
+ [''],
329
+ ['3. Design_Priority_Summary - Design-level priorities'],
330
+ [' • Ranked by priority score for production sequence'],
331
+ [''],
332
+ ['4. Order_Priority_Detail - Individual order details'],
333
+ [' • All orders with calculated priority scores'],
334
+ [''],
335
+ ['🎯 PRIORITY METHODOLOGY:'],
336
+ [f'• Age Weight: {weights["AGE_WEIGHT"]}% - Prioritizes older orders'],
337
+ [f'• Colour Simplicity Weight: {weights["COLOUR_SIMPLICITY_WEIGHT"]}% - Fewer colours = higher priority'],
338
+ [f'• Design Volume Weight: {weights["DESIGN_WEIGHT"]}% - Larger quantities get priority'],
339
+ [f'• Minimum Kgs Threshold: {min_kgs} - Only designs with total kgs >= this value are prioritized'],
340
+ [''],
341
+ ['🎨 COLOUR CONSOLIDATION LOGIC:'],
342
+ ['• If RED is used in Design-A (100kg) and Design-B (50kg)'],
343
+ ['• Output shows: RED = 150kg total (no repetition)'],
344
+ ['• Helps plan exact dye batch quantities needed'],
345
+ ['• Multi-colour orders split proportionally (e.g., "Red,Blue" 100kg = 50kg each)'],
346
+ [''],
347
+ ['📊 USAGE RECOMMENDATIONS:'],
348
+ ['• Use COLOUR_REQUIREMENTS sheet for dye purchasing/batching'],
349
+ ['• Use Design_Priority_Summary for production sequence planning'],
350
+ ['• Check Colour_Order_Breakdown for detailed verification'],
351
+ [''],
352
+ [f'Generated on: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}']
353
+ ]
354
+
355
+ instructions_df = pd.DataFrame(instructions_data, columns=['Instructions'])
356
+ instructions_df.to_excel(writer, sheet_name='Instructions', index=False)
357
+
358
+ # Gradio Interface Functions
359
+ def load_excel(file):
360
+ """Load Excel file and return available sheet names"""
361
+ if file is None:
362
+ return gr.Dropdown(choices=[]), "Please upload a file first."
363
+
364
+ try:
365
+ xls = pd.ExcelFile(file.name)
366
+ return gr.Dropdown(choices=xls.sheet_names, value=xls.sheet_names[0]), "✅ File loaded successfully!"
367
+ except Exception as e:
368
+ return gr.Dropdown(choices=[]), f"❌ Error loading file: {str(e)}"
369
+
370
+ def validate_weights(age_weight, colour_weight, design_weight):
371
+ """Validate that weights sum to 100%"""
372
+ total = age_weight + colour_weight + design_weight
373
+ if total == 100:
374
+ return "✅ Weights are valid (sum = 100%)"
375
+ else:
376
+ return f"⚠️ Weights sum to {total}%. Please adjust to equal 100%."
377
+
378
+ def preview_dyeing_data(file, sheet_name):
379
+ """Preview the selected sheet data for dyeing analysis"""
380
+ if file is None or not sheet_name:
381
+ return "Please upload a file and select a sheet first.", pd.DataFrame()
382
+
383
+ try:
384
+ df = pd.read_excel(file.name, sheet_name=sheet_name)
385
+
386
+ # Show basic info
387
+ preview_info = f"📊 **Sheet: {sheet_name}**\n"
388
+ preview_info += f"- Rows: {len(df)}\n"
389
+ preview_info += f"- Columns: {len(df.columns)}\n\n"
390
+
391
+ # Check for required columns
392
+ df_norm = df.copy()
393
+ df_norm.columns = [str(c).strip() for c in df_norm.columns]
394
+ missing = [c for c in REQUIRED_COLS if c not in df_norm.columns]
395
+
396
+ if missing:
397
+ preview_info += f"❌ **Missing required columns:** {missing}\n\n"
398
+ else:
399
+ preview_info += "✅ **All required columns found!**\n\n"
400
+
401
+ # Detect date columns
402
+ date_columns = detect_date_columns(df_norm)
403
+ if date_columns:
404
+ preview_info += f"📅 **Date columns detected:** {len(date_columns)} columns\n"
405
+ preview_info += f" Sample dates: {date_columns[:5]}\n\n"
406
+ else:
407
+ preview_info += "⚠️ **No date columns detected** - will use default prioritization\n\n"
408
+
409
+ # Show some statistics
410
+ if 'Kgs' in df_norm.columns:
411
+ total_kgs = pd.to_numeric(df_norm['Kgs'], errors='coerce').sum()
412
+ preview_info += f"**Total Kgs:** {total_kgs:,.1f}\n"
413
+
414
+ if 'DESIGN' in df_norm.columns:
415
+ unique_designs = df_norm['DESIGN'].nunique()
416
+ preview_info += f"**Unique Designs:** {unique_designs}\n"
417
+
418
+ preview_info += f"\n**Available columns:**\n"
419
+ for i, col in enumerate(df.columns, 1):
420
+ marker = "📅" if col in date_columns else ""
421
+ preview_info += f"{i}. {col} {marker}\n"
422
+
423
+ # Show first few rows
424
+ preview_df = df.head(5)
425
+
426
+ return preview_info, preview_df
427
+
428
+ except Exception as e:
429
+ return f"❌ Error previewing data: {str(e)}", pd.DataFrame()
430
+
431
+ def process_dyeing_priority(file, sheet_name, age_weight, colour_weight, design_weight, min_kgs):
432
+ """Main processing function for dyeing priorities"""
433
+
434
+ if file is None:
435
+ return None, None, None, "❌ Please upload a file first."
436
+
437
+ if not sheet_name:
438
+ return None, None, None, "❌ Please select a sheet."
439
+
440
+ # Validate weights
441
+ total_weight = age_weight + colour_weight + design_weight
442
+ if total_weight != 100:
443
+ return None, None, None, f"❌ Error: Total weight must equal 100% (currently {total_weight}%)"
444
+
445
+ try:
446
+ # Load data
447
+ df = pd.read_excel(file.name, sheet_name=sheet_name)
448
+
449
+ if df.empty:
450
+ return None, None, None, "❌ The selected sheet is empty."
451
+
452
+ # Prepare weights
453
+ weights = {
454
+ "AGE_WEIGHT": age_weight,
455
+ "COLOUR_SIMPLICITY_WEIGHT": colour_weight,
456
+ "DESIGN_WEIGHT": design_weight
457
+ }
458
+
459
+ # Compute priorities
460
+ detailed_results, design_summary, colour_totals, colour_breakdown = compute_dyeing_priority(
461
+ df, min_kgs=min_kgs, weights=weights
462
+ )
463
+
464
+ # Create temporary output file
465
+ output_path = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx').name
466
+ save_dyeing_results(detailed_results, design_summary, colour_totals, colour_breakdown, output_path, min_kgs, weights)
467
+
468
+ # Create success message
469
+ total_designs = len(design_summary)
470
+ eligible_designs = sum(design_summary['MeetsMinKgs'])
471
+ total_colours = len(colour_totals)
472
+ top_colours = colour_totals.head(3)['Colour'].tolist() if len(colour_totals) > 0 else []
473
+
474
+ success_msg = f"✅ Dyeing Priority Analysis Complete!\n"
475
+ success_msg += f"📊 SUMMARY:\n"
476
+ success_msg += f"- Total Designs Analyzed: {total_designs}\n"
477
+ success_msg += f"- Designs Meeting {min_kgs}kg Threshold: {eligible_designs}\n"
478
+ success_msg += f"- Unique Colours Required: {total_colours}\n"
479
+ if top_colours:
480
+ success_msg += f"- Top 3 Colours by Volume: {', '.join(top_colours)}\n"
481
+ success_msg += f"- Highest Priority Score: {design_summary['PriorityScore'].max():.3f}\n\n"
482
+ success_msg += f"🎨 COLOUR REQUIREMENTS sheet contains consolidated totals!\n"
483
+ success_msg += f"📥 Download complete analysis below"
484
+
485
+ return output_path, design_summary.head(10), colour_totals.head(15), success_msg
486
+
487
+ except Exception as e:
488
+ return None, None, None, f"❌ Error processing data: {str(e)}"
489
+
490
+ # Create Gradio Interface
491
+ def create_dyeing_interface():
492
+ with gr.Blocks(title="Dyeing Urgency Priority Calculator", theme=gr.themes.Soft()) as demo:
493
+
494
+ gr.Markdown("""
495
+ # 🎨 Dyeing Urgency Priority Calculator
496
+
497
+ Upload your Excel file with dyeing/textile manufacturing data to calculate production priorities based on:
498
+ - **Order Age**: Prioritize older orders first (detects dates from column headers)
499
+ - **Colour Simplicity**: Fewer colours = easier production
500
+ - **Design Volume**: Larger quantities for efficiency
501
+
502
+ **Expected Columns**: Account, Order #, DESIGN, Labels, Colours, Kgs, Pending
503
+ **Date Detection**: Automatically detects date columns (like 2025-01-08, 13/8, etc.)
504
+ """)
505
+
506
+ with gr.Row():
507
+ with gr.Column(scale=1):
508
+ gr.Markdown("## 📁 File Upload & Selection")
509
+
510
+ file_input = gr.File(
511
+ label="Upload Excel File",
512
+ file_types=[".xlsx", ".xls"],
513
+ type="filepath"
514
+ )
515
+
516
+ sheet_dropdown = gr.Dropdown(
517
+ label="Select Sheet",
518
+ choices=[],
519
+ interactive=True
520
+ )
521
+
522
+ file_status = gr.Textbox(label="File Status", interactive=False)
523
+
524
+ with gr.Column(scale=1):
525
+ gr.Markdown("## ⚖️ Priority Weights (must sum to 100%)")
526
+
527
+ age_weight = gr.Slider(
528
+ minimum=0, maximum=100, value=50, step=1,
529
+ label="Age Weight (%)",
530
+ info="Higher = prioritize older orders more"
531
+ )
532
+
533
+ colour_weight = gr.Slider(
534
+ minimum=0, maximum=100, value=30, step=1,
535
+ label="Colour Simplicity Weight (%)",
536
+ info="Higher = prioritize designs with fewer colours"
537
+ )
538
+
539
+ design_weight = gr.Slider(
540
+ minimum=0, maximum=100, value=20, step=1,
541
+ label="Design Volume Weight (%)",
542
+ info="Higher = prioritize larger quantity designs"
543
+ )
544
+
545
+ weight_status = gr.Textbox(label="Weight Validation", interactive=False)
546
+
547
+ min_kgs = gr.Number(
548
+ label="Minimum Kgs Threshold per Design",
549
+ value=100,
550
+ info="Only designs with total kgs >= this value get priority"
551
+ )
552
+
553
+ with gr.Row():
554
+ preview_btn = gr.Button("👁️ Preview Data", variant="secondary")
555
+ process_btn = gr.Button("🎨 Calculate Dyeing Priorities", variant="primary", size="lg")
556
+
557
+ with gr.Row():
558
+ with gr.Column():
559
+ gr.Markdown("## 📊 Data Preview")
560
+ preview_info = gr.Textbox(label="Data Information", lines=10, interactive=False)
561
+ preview_table = gr.Dataframe(label="Sample Data")
562
+
563
+ with gr.Row():
564
+ with gr.Column():
565
+ gr.Markdown("## 🏆 Priority Results")
566
+ results_info = gr.Textbox(label="Processing Status", interactive=False)
567
+
568
+ with gr.Column():
569
+ download_file = gr.File(label="📥 Download Complete Analysis")
570
+
571
+ with gr.Row():
572
+ with gr.Column():
573
+ gr.Markdown("## 📋 Top Design Priorities")
574
+ design_results = gr.Dataframe(label="Design Priority Summary")
575
+
576
+ with gr.Column():
577
+ gr.Markdown("## 🎨 Colour Requirements (Consolidated)")
578
+ colour_results = gr.Dataframe(
579
+ label="Total Kgs Required Per Colour",
580
+ headers=["Colour", "Total Kgs", "Used in Designs", "Orders Count"],
581
+ interactive=False
582
+ )
583
+
584
+ # Event handlers
585
+ file_input.change(
586
+ fn=load_excel,
587
+ inputs=[file_input],
588
+ outputs=[sheet_dropdown, file_status]
589
+ )
590
+
591
+ for weight_input in [age_weight, colour_weight, design_weight]:
592
+ weight_input.change(
593
+ fn=validate_weights,
594
+ inputs=[age_weight, colour_weight, design_weight],
595
+ outputs=[weight_status]
596
+ )
597
+
598
+ preview_btn.click(
599
+ fn=preview_dyeing_data,
600
+ inputs=[file_input, sheet_dropdown],
601
+ outputs=[preview_info, preview_table]
602
+ )
603
+
604
+ process_btn.click(
605
+ fn=process_dyeing_priority,
606
+ inputs=[file_input, sheet_dropdown, age_weight, colour_weight, design_weight, min_kgs],
607
+ outputs=[download_file, design_results, colour_results, results_info]
608
+ )
609
+
610
+ # Initialize weight validation
611
+ demo.load(
612
+ fn=validate_weights,
613
+ inputs=[age_weight, colour_weight, design_weight],
614
+ outputs=[weight_status]
615
+ )
616
+
617
+ return demo
618
+
619
+ # Launch the app
620
+ if __name__ == "__main__":
621
+ demo = create_dyeing_interface()
622
+ demo.launch(
623
+ #server_name="0.0.0.0",
624
+ #server_port=7860,
625
+ share=True,
626
+ debug=True
627
+ )
config.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+ # User-adjustable weights for the priority scoring (must sum to 100)
3
+ WEIGHTS = {
4
+ "AGE_WEIGHT": 60, # % weight for Order Age
5
+ "COMPONENT_WEIGHT": 30, # % weight for Simplicity (fewer components)
6
+ "MANUAL_WEIGHT": 10 # % weight for Manual override
7
+ }
8
+
9
+ # Validate
10
+ if sum(WEIGHTS.values()) != 100:
11
+ raise ValueError("The weights in config.py must sum to 100.")
main.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from processor import ManufacturingProcessor, get_file_preview
3
+ from utils import prompt_weights
4
+
5
+ def main():
6
+ print("🏭 Manufacturing Priority Decision Helper")
7
+
8
+ # Get file path
9
+ file_path = input("Enter the full path to your Excel file: ").strip()
10
+ if not os.path.exists(file_path):
11
+ print("❌ File not found.")
12
+ return
13
+
14
+ # Initialize processor
15
+ try:
16
+ processor = ManufacturingProcessor()
17
+ file_info = processor.get_file_info(file_path)
18
+ except Exception as e:
19
+ print(f"❌ Unable to read Excel file: {e}")
20
+ return
21
+
22
+ # Show available sheets
23
+ print(f"\n📑 Available sheets in '{file_info['file_name']}':")
24
+ for i, sheet_name in enumerate(file_info['sheets'], start=1):
25
+ print(f" {i}. {sheet_name}")
26
+
27
+ # Sheet selection
28
+ while True:
29
+ try:
30
+ idx = int(input("Select a sheet by number: "))
31
+ if 1 <= idx <= len(file_info['sheets']):
32
+ selected_sheet = file_info['sheets'][idx-1]
33
+ break
34
+ except ValueError:
35
+ pass
36
+ print("⚠️ Invalid selection, try again.")
37
+
38
+ print(f"✅ Selected sheet: {selected_sheet}")
39
+
40
+ # Preview data and validate
41
+ print("\n🔍 Analyzing data...")
42
+ try:
43
+ preview = get_file_preview(file_path, selected_sheet)
44
+ validation = preview['validation']
45
+
46
+ print(f"📊 Data Summary:")
47
+ print(f" - Rows: {validation['row_count']}")
48
+ print(f" - Available columns: {len(validation['available_columns'])}")
49
+
50
+ if not validation['valid']:
51
+ print(f"\n❌ Data validation failed:")
52
+ print(f" Missing required columns: {validation['missing_columns']}")
53
+ return
54
+
55
+ if validation['data_issues']:
56
+ print(f"\n⚠️ Data quality issues found:")
57
+ for issue in validation['data_issues']:
58
+ print(f" - {issue}")
59
+
60
+ continue_anyway = input("\nContinue processing anyway? (y/N): ").strip().lower()
61
+ if continue_anyway != 'y':
62
+ return
63
+
64
+ print("✅ Data validation passed!")
65
+
66
+ except Exception as e:
67
+ print(f"❌ Error analyzing data: {e}")
68
+ return
69
+
70
+ # Weight adjustment (optional)
71
+ print(f"\n⚖️ Current weights: Age={processor.weights['AGE_WEIGHT']}%, "
72
+ f"Component={processor.weights['COMPONENT_WEIGHT']}%, "
73
+ f"Manual={processor.weights['MANUAL_WEIGHT']}%")
74
+
75
+ adjust_weights = input("Would you like to adjust weights? (y/N): ").strip().lower()
76
+ if adjust_weights == 'y':
77
+ try:
78
+ new_weights = prompt_weights(processor.weights.copy())
79
+ processor.weights = new_weights
80
+ print(f"✅ Updated weights: {new_weights}")
81
+ except Exception as e:
82
+ print(f"⚠️ Error setting weights, using defaults: {e}")
83
+
84
+ # Quantity threshold
85
+ try:
86
+ min_qty_input = input("Enter minimum quantity threshold for FIFO (default 50): ").strip()
87
+ min_qty = int(min_qty_input) if min_qty_input else 50
88
+ except ValueError:
89
+ min_qty = 50
90
+ print("⚠️ Invalid input, using default threshold of 50")
91
+
92
+ # Process the data
93
+ print(f"\n🔄 Processing data with minimum quantity threshold: {min_qty}")
94
+ try:
95
+ processed_df, processing_info = processor.process_file(
96
+ file_path, selected_sheet, min_qty
97
+ )
98
+
99
+ print("✅ Priority calculation completed!")
100
+ print(f"📈 Results summary:")
101
+ print(f" - Total products: {processing_info['total_products']}")
102
+ print(f" - Products above threshold: {processing_info['products_above_threshold']}")
103
+ print(f" - Highest priority score: {processing_info['highest_priority_score']:.4f}")
104
+
105
+ except Exception as e:
106
+ print(f"❌ Error processing data: {e}")
107
+ return
108
+
109
+ # Show preview of results
110
+ print(f"\n🏆 Top 10 Priority Results:")
111
+ display_cols = [c for c in ["Name of Product", "Components Used", "Quantity of Each Component",
112
+ "Oldest Product Required First", "Priority Assigned",
113
+ "OrderAgeDays", "ComponentCount", "QtyThresholdOK", "PriorityScore"]
114
+ if c in processed_df.columns]
115
+
116
+ print(processed_df[display_cols].head(10).to_string(index=False, max_colwidth=20))
117
+
118
+ # Save results
119
+ base_name = os.path.splitext(os.path.basename(file_path))[0]
120
+ output_dir = os.path.dirname(file_path)
121
+ output_path = os.path.join(output_dir, f"{base_name}_PRIORITY.xlsx")
122
+
123
+ try:
124
+ final_output = processor.save_results(processed_df, output_path, processing_info)
125
+ print(f"\n💾 Results saved to: {final_output}")
126
+ print(f"\n📋 Output includes:")
127
+ print(f" - Priority_Results: Ranked manufacturing data")
128
+ print(f" - Instructions: Methodology and column explanations")
129
+ print(f" - Processing_Log: Detailed processing information")
130
+
131
+ except Exception as e:
132
+ print(f"❌ Failed to save results: {e}")
133
+ return
134
+
135
+ print(f"\n🎉 Processing complete! Check the output file for detailed results.")
136
+
137
+ if __name__ == "__main__":
138
+ main()
output_writer.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # output_writer.py
2
+ import pandas as pd
3
+ from datetime import datetime
4
+
5
+ def save_with_instructions(df: pd.DataFrame, output_path: str, min_qty: int = 50, weights: dict = None):
6
+ """
7
+ Save the priority results to Excel with an instructions sheet
8
+ """
9
+ if weights is None:
10
+ weights = {"AGE_WEIGHT": 60, "COMPONENT_WEIGHT": 30, "MANUAL_WEIGHT": 10}
11
+
12
+ with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
13
+ # Save main results
14
+ df.to_excel(writer, sheet_name='Priority_Results', index=False)
15
+
16
+ # Create instructions sheet
17
+ instructions_data = [
18
+ ['Manufacturing Priority Decision Results'],
19
+ [''],
20
+ ['METHODOLOGY:'],
21
+ [f'Age Weight: {weights["AGE_WEIGHT"]}%'],
22
+ [f'Component Simplicity Weight: {weights["COMPONENT_WEIGHT"]}%'],
23
+ [f'Manual Priority Weight: {weights["MANUAL_WEIGHT"]}%'],
24
+ [f'Minimum Quantity Threshold: {min_qty}'],
25
+ [''],
26
+ ['COLUMNS EXPLANATION:'],
27
+ ['OrderAgeDays: Days since oldest product required'],
28
+ ['ComponentCount: Number of unique components needed'],
29
+ ['QtyThresholdOK: Whether quantity meets minimum threshold'],
30
+ ['AgeScore: Weighted age contribution to priority'],
31
+ ['SimplicityScore: Weighted simplicity contribution to priority'],
32
+ ['ManualScore: Weighted manual priority contribution'],
33
+ ['PriorityScore: Final calculated priority (0-1 scale)'],
34
+ [''],
35
+ ['Higher PriorityScore = Higher Manufacturing Priority'],
36
+ [f'Generated on: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}']
37
+ ]
38
+
39
+ instructions_df = pd.DataFrame(instructions_data, columns=['Instructions'])
40
+ instructions_df.to_excel(writer, sheet_name='Instructions', index=False)
priority_logic.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from config import WEIGHTS
4
+
5
+ REQUIRED_COLS = [
6
+ "Name of Product",
7
+ "Components Used",
8
+ "Quantity of Each Component",
9
+ "Oldest Product Required First",
10
+ "Priority Assigned",
11
+ ]
12
+
13
+ def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
14
+ df = df.copy()
15
+ df.columns = [str(c).strip() for c in df.columns]
16
+ return df
17
+
18
+ def _component_count(series: pd.Series) -> pd.Series:
19
+ def _count(x):
20
+ if pd.isna(x):
21
+ return 0
22
+ parts = [p.strip() for p in str(x).split(",") if str(p).strip()]
23
+ return len(set(parts)) if parts else 0
24
+ return series.apply(_count)
25
+
26
+ def compute_priority(df: pd.DataFrame, min_qty: int = 50) -> pd.DataFrame:
27
+ df = _normalize_columns(df)
28
+
29
+ missing = [c for c in REQUIRED_COLS if c not in df.columns]
30
+ if missing:
31
+ raise ValueError(f"Missing required columns: {missing}")
32
+
33
+ out = df.copy()
34
+ out["Oldest Product Required First"] = pd.to_datetime(out["Oldest Product Required First"], errors="coerce")
35
+
36
+ today = pd.Timestamp.now().normalize()
37
+ out["OrderAgeDays"] = (today - pd.to_datetime(out["Oldest Product Required First"], errors="coerce")).dt.days
38
+ out["OrderAgeDays"] = out["OrderAgeDays"].fillna(0).clip(lower=0)
39
+
40
+ if out["OrderAgeDays"].max() > 0:
41
+ age_score_01 = out["OrderAgeDays"] / out["OrderAgeDays"].max()
42
+ else:
43
+ age_score_01 = 0
44
+
45
+ qty = pd.to_numeric(out["Quantity of Each Component"], errors="coerce").fillna(0)
46
+ age_score_01 = np.where(qty >= min_qty, age_score_01, 0)
47
+
48
+ comp_count = _component_count(out["Components Used"])
49
+ if comp_count.max() > 0:
50
+ comp_simplicity_01 = 1 - (comp_count / comp_count.max())
51
+ else:
52
+ comp_simplicity_01 = 0
53
+
54
+ def manual_to01(x):
55
+ if pd.isna(x):
56
+ return 0.0
57
+ s = str(x).strip().lower()
58
+ if s in {"high", "urgent", "yes", "y", "true"}:
59
+ return 1.0
60
+ try:
61
+ return 1.0 if float(s) > 0 else 0.0
62
+ except:
63
+ return 0.0
64
+
65
+ manual_01 = out["Priority Assigned"].apply(manual_to01)
66
+
67
+ w_age = WEIGHTS["AGE_WEIGHT"] / 100.0
68
+ w_comp = WEIGHTS["COMPONENT_WEIGHT"] / 100.0
69
+ w_man = WEIGHTS["MANUAL_WEIGHT"] / 100.0
70
+
71
+ out["AgeScore"] = age_score_01 * w_age
72
+ if isinstance(comp_simplicity_01, pd.Series):
73
+ out["SimplicityScore"] = comp_simplicity_01 * w_comp
74
+ else:
75
+ out["SimplicityScore"] = comp_simplicity_01
76
+
77
+ out["ManualScore"] = manual_01 * w_man
78
+
79
+ out["PriorityScore"] = out["AgeScore"] + out["SimplicityScore"] + out["ManualScore"]
80
+
81
+ out["ComponentCount"] = comp_count
82
+ out["QtyThresholdOK"] = qty >= min_qty
83
+
84
+ out = out.sort_values(["PriorityScore", "OrderAgeDays"], ascending=[False, False])
85
+ return out
processor.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # processor.py
2
+ """
3
+ Main processing orchestrator that ties together all the manufacturing priority logic.
4
+ This module provides high-level functions that both the CLI and Gradio interfaces can use.
5
+ """
6
+
7
+ import os
8
+ import pandas as pd
9
+ from typing import Dict, List, Tuple, Optional
10
+ from datetime import datetime
11
+
12
+ from config import WEIGHTS
13
+ from sheet_reader import list_sheets, read_sheet
14
+ from priority_logic import compute_priority
15
+ from output_writer import save_with_instructions
16
+ from utils import prompt_weights
17
+
18
+
19
+ class ManufacturingProcessor:
20
+ """
21
+ Main processor class for manufacturing priority calculations.
22
+ Encapsulates all the logic needed to process Excel files and generate priority rankings.
23
+ """
24
+
25
+ def __init__(self, weights: Optional[Dict[str, int]] = None):
26
+ """Initialize processor with weights"""
27
+ self.weights = weights or WEIGHTS.copy()
28
+ self.validate_weights()
29
+
30
+ def validate_weights(self) -> None:
31
+ """Ensure weights sum to 100"""
32
+ total = sum(self.weights.values())
33
+ if total != 100:
34
+ raise ValueError(f"Weights must sum to 100, got {total}")
35
+
36
+ def get_file_info(self, file_path: str) -> Dict:
37
+ """Get information about the Excel file"""
38
+ if not os.path.exists(file_path):
39
+ raise FileNotFoundError(f"File not found: {file_path}")
40
+
41
+ try:
42
+ sheets = list_sheets(file_path)
43
+ file_size = os.path.getsize(file_path)
44
+
45
+ return {
46
+ "file_path": file_path,
47
+ "file_name": os.path.basename(file_path),
48
+ "file_size": file_size,
49
+ "sheets": sheets,
50
+ "sheet_count": len(sheets)
51
+ }
52
+ except Exception as e:
53
+ raise Exception(f"Error reading file info: {e}")
54
+
55
+ def validate_sheet_data(self, df: pd.DataFrame) -> Dict:
56
+ """Validate that the sheet has required columns and data"""
57
+ from priority_logic import REQUIRED_COLS
58
+
59
+ # Normalize column names
60
+ df_norm = df.copy()
61
+ df_norm.columns = [str(c).strip() for c in df_norm.columns]
62
+
63
+ # Check required columns
64
+ missing_cols = [col for col in REQUIRED_COLS if col not in df_norm.columns]
65
+
66
+ # Basic data validation
67
+ validation_result = {
68
+ "valid": len(missing_cols) == 0,
69
+ "missing_columns": missing_cols,
70
+ "available_columns": list(df.columns),
71
+ "row_count": len(df),
72
+ "empty_rows": df.isnull().all(axis=1).sum(),
73
+ "data_issues": []
74
+ }
75
+
76
+ if validation_result["valid"]:
77
+ # Check for data quality issues
78
+ try:
79
+ # Check date column
80
+ date_col = "Oldest Product Required First"
81
+ date_issues = pd.to_datetime(df_norm[date_col], errors='coerce').isnull().sum()
82
+ if date_issues > 0:
83
+ validation_result["data_issues"].append(f"{date_issues} invalid dates in '{date_col}'")
84
+
85
+ # Check quantity column
86
+ qty_col = "Quantity of Each Component"
87
+ qty_numeric = pd.to_numeric(df_norm[qty_col], errors='coerce')
88
+ qty_issues = qty_numeric.isnull().sum()
89
+ if qty_issues > 0:
90
+ validation_result["data_issues"].append(f"{qty_issues} non-numeric values in '{qty_col}'")
91
+
92
+ # Check for completely empty required columns
93
+ for col in REQUIRED_COLS:
94
+ if col in df_norm.columns:
95
+ empty_count = df_norm[col].isnull().sum()
96
+ if empty_count == len(df_norm):
97
+ validation_result["data_issues"].append(f"Column '{col}' is completely empty")
98
+
99
+ except Exception as e:
100
+ validation_result["data_issues"].append(f"Data validation error: {e}")
101
+
102
+ return validation_result
103
+
104
+ def process_file(self,
105
+ file_path: str,
106
+ sheet_name: str,
107
+ min_qty: int = 50,
108
+ custom_weights: Dict[str, int] = None) -> Tuple[pd.DataFrame, Dict]:
109
+ """
110
+ Process a single sheet from an Excel file and return prioritized results.
111
+
112
+ Returns:
113
+ Tuple of (processed_dataframe, processing_info)
114
+ """
115
+
116
+ # Use custom weights if provided
117
+ weights = custom_weights or self.weights
118
+ if custom_weights:
119
+ temp_weights = custom_weights.copy()
120
+ if sum(temp_weights.values()) != 100:
121
+ raise ValueError("Custom weights must sum to 100")
122
+ else:
123
+ temp_weights = weights
124
+
125
+ # Read the data
126
+ df = read_sheet(file_path, sheet_name)
127
+ if df is None or df.empty:
128
+ raise ValueError("Sheet is empty or could not be read")
129
+
130
+ # Validate data
131
+ validation = self.validate_sheet_data(df)
132
+ if not validation["valid"]:
133
+ raise ValueError(f"Data validation failed: Missing columns {validation['missing_columns']}")
134
+
135
+ # Process priority calculation
136
+ try:
137
+ processed_df = compute_priority(df, min_qty=min_qty, weights=temp_weights)
138
+ except Exception as e:
139
+ raise Exception(f"Priority calculation failed: {e}")
140
+
141
+ # Generate processing info
142
+ processing_info = {
143
+ "timestamp": datetime.now().isoformat(),
144
+ "file_name": os.path.basename(file_path),
145
+ "sheet_name": sheet_name,
146
+ "weights_used": temp_weights,
147
+ "min_quantity": min_qty,
148
+ "total_products": len(df),
149
+ "products_above_threshold": sum(processed_df["QtyThresholdOK"]),
150
+ "highest_priority_score": processed_df["PriorityScore"].max(),
151
+ "lowest_priority_score": processed_df["PriorityScore"].min(),
152
+ "validation_info": validation
153
+ }
154
+
155
+ return processed_df, processing_info
156
+
157
+ def save_results(self,
158
+ processed_df: pd.DataFrame,
159
+ output_path: str,
160
+ processing_info: Dict) -> str:
161
+ """Save processed results with full documentation"""
162
+
163
+ try:
164
+ save_with_instructions(
165
+ processed_df,
166
+ output_path,
167
+ min_qty=processing_info["min_quantity"],
168
+ weights=processing_info["weights_used"]
169
+ )
170
+
171
+ # Add processing log sheet
172
+ self._add_processing_log(output_path, processing_info)
173
+
174
+ return output_path
175
+
176
+ except Exception as e:
177
+ raise Exception(f"Failed to save results: {e}")
178
+
179
+ def _add_processing_log(self, output_path: str, processing_info: Dict):
180
+ """Add a processing log sheet to the output file"""
181
+ try:
182
+ # Read existing file and add log sheet
183
+ with pd.ExcelWriter(output_path, mode='a', engine='openpyxl', if_sheet_exists='replace') as writer:
184
+ log_data = []
185
+ log_data.append(["PROCESSING LOG"])
186
+ log_data.append([""])
187
+ log_data.append(["Processing Timestamp", processing_info["timestamp"]])
188
+ log_data.append(["Source File", processing_info["file_name"]])
189
+ log_data.append(["Sheet Processed", processing_info["sheet_name"]])
190
+ log_data.append([""])
191
+ log_data.append(["SETTINGS USED"])
192
+ log_data.append(["Age Weight", f"{processing_info['weights_used']['AGE_WEIGHT']}%"])
193
+ log_data.append(["Component Weight", f"{processing_info['weights_used']['COMPONENT_WEIGHT']}%"])
194
+ log_data.append(["Manual Weight", f"{processing_info['weights_used']['MANUAL_WEIGHT']}%"])
195
+ log_data.append(["Minimum Quantity", processing_info["min_quantity"]])
196
+ log_data.append([""])
197
+ log_data.append(["RESULTS SUMMARY"])
198
+ log_data.append(["Total Products", processing_info["total_products"]])
199
+ log_data.append(["Above Threshold", processing_info["products_above_threshold"]])
200
+ log_data.append(["Highest Priority Score", f"{processing_info['highest_priority_score']:.4f}"])
201
+ log_data.append(["Lowest Priority Score", f"{processing_info['lowest_priority_score']:.4f}"])
202
+
203
+ if processing_info["validation_info"]["data_issues"]:
204
+ log_data.append([""])
205
+ log_data.append(["DATA ISSUES FOUND"])
206
+ for issue in processing_info["validation_info"]["data_issues"]:
207
+ log_data.append(["", issue])
208
+
209
+ log_df = pd.DataFrame(log_data, columns=["Parameter", "Value"])
210
+ log_df.to_excel(writer, sheet_name='Processing_Log', index=False)
211
+
212
+ except Exception as e:
213
+ # If adding log fails, don't fail the whole operation
214
+ print(f"Warning: Could not add processing log: {e}")
215
+
216
+
217
+ # Convenience functions for easy import
218
+ def quick_process(file_path: str,
219
+ sheet_name: str,
220
+ output_path: str = None,
221
+ min_qty: int = 50,
222
+ weights: Optional[Dict[str, int]] = None) -> str:
223
+ """
224
+ Quick processing function that handles the full workflow.
225
+
226
+ Args:
227
+ file_path: Path to Excel file
228
+ sheet_name: Name of sheet to process
229
+ output_path: Where to save results (optional, will auto-generate if not provided)
230
+ min_qty: Minimum quantity threshold
231
+ weights: Custom weights dict (optional)
232
+
233
+ Returns:
234
+ Path to generated output file
235
+ """
236
+ processor = ManufacturingProcessor(weights)
237
+
238
+ # Process the data
239
+ processed_df, processing_info = processor.process_file(
240
+ file_path, sheet_name, min_qty, weights
241
+ )
242
+
243
+ # Generate output path if not provided
244
+ if output_path is None:
245
+ base_name = os.path.splitext(os.path.basename(file_path))[0]
246
+ output_dir = os.path.dirname(file_path)
247
+ output_path = os.path.join(output_dir, f"{base_name}_PRIORITY.xlsx")
248
+
249
+ # Save results
250
+ return processor.save_results(processed_df, output_path, processing_info)
251
+
252
+
253
+ def get_file_preview(file_path: str, sheet_name: str, max_rows: int = 5) -> Dict:
254
+ """
255
+ Get a preview of the file data for validation purposes.
256
+
257
+ Returns:
258
+ Dict containing preview info and sample data
259
+ """
260
+ processor = ManufacturingProcessor()
261
+
262
+ # Get file info
263
+ file_info = processor.get_file_info(file_path)
264
+
265
+ # Read sample data
266
+ df = read_sheet(file_path, sheet_name)
267
+ sample_df = df.head(max_rows) if df is not None else pd.DataFrame()
268
+
269
+ # Validate data
270
+ validation = processor.validate_sheet_data(df) if df is not None else {"valid": False}
271
+
272
+ return {
273
+ "file_info": file_info,
274
+ "sample_data": sample_df,
275
+ "validation": validation,
276
+ "preview_rows": len(sample_df)
277
+ }
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ pandas>=1.5.0
3
+ openpyxl>=3.0.0
4
+ numpy>=1.20.0
sheet_reader.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # sheet_reader.py
2
+ import pandas as pd
3
+
4
+ def list_sheets(file_path: str):
5
+ xls = pd.ExcelFile(file_path)
6
+ return xls.sheet_names
7
+
8
+ def read_sheet(file_path: str, sheet_name: str) -> pd.DataFrame:
9
+ return pd.read_excel(file_path, sheet_name=sheet_name)
utils.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils.py
2
+ def prompt_weights(default_weights: dict) -> dict:
3
+ """
4
+ Interactively collect weights from the user (in %) and validate that they sum to 100.
5
+ Press Enter to keep defaults.
6
+ """
7
+ print("\\n⚖️ Set Weights (press Enter to keep default values)")
8
+ new_w = {}
9
+ for key, val in default_weights.items():
10
+ try:
11
+ raw = input(f" {key.replace('_WEIGHT','').title()} (%), default {val}: ").strip()
12
+ new_w[key] = val if raw == "" else int(raw)
13
+ except ValueError:
14
+ print(f" Invalid input for {key}, keeping default {val}.")
15
+ new_w[key] = val
16
+
17
+ total = sum(new_w.values())
18
+ if total != 100:
19
+ print(f" Weights sum to {total}, normalizing to 100 proportionally.")
20
+ factor = 100.0 / total if total else 0
21
+ for k in new_w:
22
+ new_w[k] = int(round(new_w[k] * factor))
23
+ # ensure exact 100 by adjusting the largest key if rounding drift
24
+ drift = 100 - sum(new_w.values())
25
+ if drift != 0:
26
+ largest_key = max(new_w.keys(), key=new_w.get)
27
+ new_w[largest_key] += drift
28
+
29
+ print(" Final Weights:", new_w)
30
+ return new_w