Spaces:
Running
Running
Update app.py
Browse files
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()
|