leadingbridge commited on
Commit
f806522
·
verified ·
1 Parent(s): f9ed421

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +172 -0
app.py CHANGED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import requests
5
+ from openpyxl import load_workbook
6
+
7
+ # Use your dataset file. We convert the "blob" URL to the raw "resolve" URL automatically.
8
+ HF_TEMPLATE_URL = "https://huggingface.co/datasets/leadingbridge/ammu/blob/main/AMMU-order-form-template.xlsx"
9
+ TEMPLATE_FILENAME = "AMMU-order-form-template.xlsx"
10
+
11
+ def _raw_url(url: str) -> str:
12
+ # Convert blob → resolve to download raw bytes
13
+ return url.replace("/blob/", "/resolve/")
14
+
15
+ def _read_input(file_obj):
16
+ """Read CSV or XLSX to DataFrame with normalized column names."""
17
+ name = getattr(file_obj, "name", None) or getattr(file_obj, "orig_name", None) or "upload"
18
+ if name.lower().endswith(".csv"):
19
+ df = pd.read_csv(file_obj)
20
+ else:
21
+ df = pd.read_excel(file_obj)
22
+ df.columns = [c.strip() for c in df.columns]
23
+ return df
24
+
25
+ def _aggregate(df: pd.DataFrame):
26
+ """Validate required cols and aggregate Quantity by SKU + Product Option Value."""
27
+ required = ["SKU", "Product Option Value", "Quantity"]
28
+ missing = [c for c in required if c not in df.columns]
29
+ if missing:
30
+ raise ValueError(f"Input file is missing required column(s): {', '.join(missing)}")
31
+
32
+ # Ensure numeric quantities
33
+ df["Quantity"] = pd.to_numeric(df["Quantity"], errors="coerce").fillna(0).astype(int)
34
+
35
+ # Group by SKU + Product Option Value
36
+ grouped = (
37
+ df.groupby(["SKU", "Product Option Value"], dropna=False)["Quantity"]
38
+ .sum()
39
+ .reset_index()
40
+ )
41
+ return grouped
42
+
43
+ def _find_cell(ws, text):
44
+ """Return (row, col) of the first cell equal to text."""
45
+ for r in ws.iter_rows(values_only=False):
46
+ for c in r:
47
+ if (c.value or "") == text:
48
+ return c.row, c.column
49
+ return None, None
50
+
51
+ def _match_row(ws, opt_val: str):
52
+ """
53
+ Match a row using:
54
+ 1) Exact match: Column A == opt_val
55
+ 2) Composite match: Column A == f"{opt_val} - <value in Column B of that row>"
56
+ Return row index or None.
57
+ """
58
+ opt_val = "" if opt_val is None else str(opt_val).strip()
59
+ max_row = ws.max_row
60
+ # 1) Exact match on Col A
61
+ for r in range(1, max_row + 1):
62
+ colA = ws.cell(row=r, column=1).value
63
+ if colA is not None and str(colA).strip() == opt_val:
64
+ return r
65
+
66
+ # 2) Composite: "Product Option Value - <Col B>"
67
+ for r in range(1, max_row + 1):
68
+ colA = ws.cell(row=r, column=1).value
69
+ colB = ws.cell(row=r, column=2).value
70
+ colA = "" if colA is None else str(colA).strip()
71
+ colB = "" if colB is None else str(colB).strip()
72
+ if colA == f"{opt_val} - {colB}":
73
+ return r
74
+
75
+ return None
76
+
77
+ def _choose_quantity_col(ws):
78
+ """
79
+ Choose the 'mapping column' to write quantities.
80
+ Heuristic: If we see a header cell that equals 'Qty' or 'Quantity' in early rows (2 or 3),
81
+ use that column; otherwise default to column B.
82
+ """
83
+ headers = {"qty", "quantity"}
84
+ for r in (2, 3):
85
+ for c in range(1, ws.max_column + 1):
86
+ val = ws.cell(row=r, column=c).value
87
+ if isinstance(val, str) and val.strip().lower() in headers:
88
+ return c
89
+ return 2 # default to column B if nothing explicit is found
90
+
91
+ def fill_template(input_file):
92
+ """
93
+ 1) Download template from HF dataset
94
+ 2) Read input (CSV/XLSX)
95
+ 3) Aggregate quantities by SKU + Product Option Value
96
+ 4) Write SKU → 'My SKU' (same row as label, next column)
97
+ 5) Write quantities into the mapping column per matched rows
98
+ 6) Return template bytes under the same filename for download
99
+ """
100
+ if input_file is None:
101
+ return None, "Please upload an input file."
102
+
103
+ # Fetch latest template from HF dataset
104
+ url = _raw_url(HF_TEMPLATE_URL)
105
+ resp = requests.get(url)
106
+ resp.raise_for_status()
107
+ tmpl_bytes = io.BytesIO(resp.content)
108
+
109
+ # Load template workbook (preserves formatting)
110
+ wb = load_workbook(tmpl_bytes)
111
+ ws = wb.active # assume first sheet is the order sheet
112
+
113
+ # Read & aggregate input
114
+ df = _read_input(input_file)
115
+ grouped = _aggregate(df)
116
+
117
+ # Decide which SKU to write (if multiple, choose the first)
118
+ skus = grouped["SKU"].dropna().astype(str).unique().tolist()
119
+ if not skus:
120
+ return None, "No SKU found in the input file."
121
+ chosen_sku = skus[0]
122
+ note = ""
123
+ if len(skus) > 1:
124
+ note = f"Multiple SKUs found {skus}. Using the first: {chosen_sku}."
125
+
126
+ # Map SKU → "My SKU" (same row, next column)
127
+ r, c = _find_cell(ws, "My SKU")
128
+ if r is not None and c is not None:
129
+ ws.cell(row=r, column=c + 1, value=str(chosen_sku))
130
+
131
+ # Determine mapping column for quantities
132
+ qty_col = _choose_quantity_col(ws)
133
+
134
+ # Filter to chosen SKU and aggregate again by option
135
+ block = (
136
+ grouped[grouped["SKU"].astype(str) == str(chosen_sku)]
137
+ .groupby("Product Option Value", dropna=False)["Quantity"]
138
+ .sum()
139
+ .reset_index()
140
+ )
141
+
142
+ # Fill quantities by row match
143
+ filled = 0
144
+ for _, rec in block.iterrows():
145
+ opt_val = rec["Product Option Value"]
146
+ qty = int(rec["Quantity"])
147
+ target_row = _match_row(ws, opt_val)
148
+ if target_row:
149
+ ws.cell(row=target_row, column=qty_col, value=qty)
150
+ filled += 1
151
+
152
+ # Save back into memory (keeping same filename)
153
+ out_buf = io.BytesIO()
154
+ wb.save(out_buf)
155
+ out_buf.seek(0)
156
+
157
+ status = f"Filled {filled} row(s). " + (note if note else "")
158
+ # Return a tuple (name, bytes-like) so Gradio downloads with the same template filename
159
+ return (TEMPLATE_FILENAME, out_buf), status
160
+
161
+ with gr.Blocks(title="AMMU Order Filler (Template Preserved)") as demo:
162
+ gr.Markdown("## AMMU Order Filler\nUpload your input file; we’ll fill your live AMMU template and return it unchanged in format.")
163
+
164
+ file_in = gr.File(label="Upload Input File (CSV or XLSX)", file_count="single", type="binary")
165
+ run_btn = gr.Button("Fill Template")
166
+ download = gr.File(label="Download Filled Template")
167
+ msg = gr.Markdown()
168
+
169
+ run_btn.click(fn=fill_template, inputs=file_in, outputs=[download, msg])
170
+
171
+ if __name__ == "__main__":
172
+ demo.launch()