Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -8,23 +8,16 @@ from collections import defaultdict
|
|
8 |
|
9 |
HF_DATASET_REPO = "leadingbridge/ammu"
|
10 |
TEMPLATE_FILENAME = "AMMU-order-form-template.xlsx"
|
11 |
-
# If you commit the template file into the Space repo, this local fallback will be used.
|
12 |
LOCAL_TEMPLATE_FALLBACK = os.path.join(os.path.dirname(__file__), TEMPLATE_FILENAME)
|
13 |
|
14 |
def _normalize_power(val):
|
15 |
-
"""
|
16 |
-
Normalize input "Product Option Value" to match template's row-2 labels, e.g. '0.00', '-1.25'
|
17 |
-
Accepts numbers or strings such as 'PLANO', '0', '-1', '-1.0', '-1.00'.
|
18 |
-
"""
|
19 |
if val is None:
|
20 |
return None
|
21 |
s = str(val).strip()
|
22 |
if s == "":
|
23 |
return None
|
24 |
-
# Common synonyms for zero power
|
25 |
if s.lower() in {"plano", "piano", "0", "0.0", "0.00", "000"}:
|
26 |
return "0.00"
|
27 |
-
# Extract a signed/decimal number if present
|
28 |
m = re.search(r"(-?\d+(?:\.\d+)?)", s.replace(",", ""))
|
29 |
if not m:
|
30 |
return None
|
@@ -35,11 +28,9 @@ def _normalize_power(val):
|
|
35 |
return f"{num:.2f}"
|
36 |
|
37 |
def _power_to_triplet_digits(power_str: str) -> str:
|
38 |
-
"""'-1.25' -> '125', '0.00' -> '000', '-4.00' -> '400'"""
|
39 |
if power_str is None:
|
40 |
return None
|
41 |
-
s = power_str.strip().lstrip("+")
|
42 |
-
s = s.replace("-", "")
|
43 |
if "." in s:
|
44 |
whole, frac = s.split(".", 1)
|
45 |
frac = (frac + "00")[:2]
|
@@ -49,10 +40,6 @@ def _power_to_triplet_digits(power_str: str) -> str:
|
|
49 |
return digits.zfill(3)
|
50 |
|
51 |
def _find_header_row(ws: Worksheet, required_headers):
|
52 |
-
"""
|
53 |
-
Scan the top 10 rows to find a header row that includes all required headers (case-insensitive).
|
54 |
-
Returns (row_index, {header_lower: col_index})
|
55 |
-
"""
|
56 |
req = {h.lower() for h in required_headers}
|
57 |
for r in range(1, 11):
|
58 |
header_map = {}
|
@@ -69,36 +56,29 @@ def _find_header_row(ws: Worksheet, required_headers):
|
|
69 |
raise ValueError(f"Could not locate a header row containing: {required_headers}")
|
70 |
|
71 |
def _download_template():
|
72 |
-
# Prefer local copy if present (commit AMMU-order-form-template.xlsx to your Space repo for offline reliability).
|
73 |
if os.path.exists(LOCAL_TEMPLATE_FALLBACK):
|
74 |
return LOCAL_TEMPLATE_FALLBACK
|
75 |
-
|
76 |
-
|
|
|
77 |
|
78 |
def process(input_file):
|
79 |
-
"""
|
80 |
-
1) Read the uploaded input Excel.
|
81 |
-
2) Aggregate quantities by (SKU, Product Option Value).
|
82 |
-
3) Load the AMMU order form template.
|
83 |
-
4) For each (SKU, power), write quantity into the matching row (by SKU) and column:
|
84 |
-
- Prefer the row-2 labels like '0.00', '-1.00'
|
85 |
-
- Fallback to the row with numeric triplets '000', '125', etc.
|
86 |
-
5) Return a filled Excel file for download + a short log.
|
87 |
-
"""
|
88 |
try:
|
89 |
if input_file is None:
|
90 |
return None, "Please upload an Excel file first."
|
91 |
|
92 |
-
# ---
|
93 |
wb_in = load_workbook(input_file.name, data_only=True)
|
94 |
ws_in = wb_in.active
|
95 |
-
header_row_idx, header_map = _find_header_row(
|
|
|
|
|
96 |
col_sku = header_map["sku"]
|
97 |
col_pov = header_map["product option value"]
|
98 |
col_qty = header_map["quantity"]
|
99 |
|
100 |
-
# --- Aggregate quantities
|
101 |
-
agg = defaultdict(int)
|
102 |
rows_scanned = 0
|
103 |
for r in range(header_row_idx + 1, ws_in.max_row + 1):
|
104 |
sku = ws_in.cell(row=r, column=col_sku).value
|
@@ -121,30 +101,34 @@ def process(input_file):
|
|
121 |
if sku and power is not None and q:
|
122 |
agg[(str(sku).strip(), power)] += q
|
123 |
|
124 |
-
# ---
|
125 |
template_path = _download_template()
|
126 |
wb_out = load_workbook(template_path)
|
127 |
ws_out = wb_out.active
|
128 |
|
129 |
# Find:
|
130 |
-
#
|
131 |
-
#
|
132 |
-
#
|
133 |
-
|
134 |
-
|
135 |
power_label_row = None
|
136 |
power_col_map = {}
|
137 |
triplet_row = None
|
138 |
triplet_col_map = {}
|
139 |
|
|
|
140 |
for r in range(1, 11):
|
141 |
row_vals = [ws_out.cell(row=r, column=c).value for c in range(1, ws_out.max_column + 1)]
|
142 |
-
|
|
|
143 |
for c, v in enumerate(row_vals, start=1):
|
144 |
-
if isinstance(v, str) and v.strip().lower() == "sku":
|
145 |
-
|
146 |
-
|
147 |
-
|
|
|
|
|
148 |
labels = {}
|
149 |
for c, v in enumerate(row_vals, start=1):
|
150 |
if isinstance(v, str):
|
@@ -154,7 +138,8 @@ def process(input_file):
|
|
154 |
if len(labels) >= 5 and power_label_row is None:
|
155 |
power_label_row = r
|
156 |
power_col_map = labels
|
157 |
-
|
|
|
158 |
trip = {}
|
159 |
for c, v in enumerate(row_vals, start=1):
|
160 |
if isinstance(v, str) and re.fullmatch(r"\d{2,3}", v.strip()):
|
@@ -163,34 +148,38 @@ def process(input_file):
|
|
163 |
triplet_row = r
|
164 |
triplet_col_map = trip
|
165 |
|
166 |
-
if
|
167 |
-
raise ValueError("Could not find the 'SKU' header
|
168 |
if not (power_label_row or triplet_row):
|
169 |
-
raise ValueError("Could not find
|
170 |
|
171 |
-
# Build SKU -> row map
|
172 |
sku_to_row = {}
|
173 |
-
for r in range(
|
174 |
-
val = ws_out.cell(row=r, column=
|
175 |
if val is None:
|
176 |
continue
|
177 |
sku_to_row[str(val).strip()] = r
|
178 |
|
179 |
-
# Optional
|
180 |
-
|
|
|
|
|
181 |
for r in range(1, 11):
|
|
|
|
|
182 |
for c in range(1, ws_out.max_column + 1):
|
183 |
v = ws_out.cell(row=r, column=c).value
|
184 |
if isinstance(v, str) and v.strip().lower() == "my sku":
|
185 |
-
|
186 |
break
|
187 |
-
if
|
188 |
break
|
189 |
-
if
|
190 |
unique_skus = sorted({k[0] for k in agg.keys()})
|
191 |
-
ws_out.cell(row=
|
192 |
|
193 |
-
# Write aggregated quantities
|
194 |
missing_skus = set()
|
195 |
missing_powers = set()
|
196 |
written_count = 0
|
@@ -201,12 +190,9 @@ def process(input_file):
|
|
201 |
missing_skus.add(sku)
|
202 |
continue
|
203 |
|
204 |
-
# Prefer textual power labels row (e.g. '0.00', '-1.25')
|
205 |
col_idx = power_col_map.get(power) if power_col_map else None
|
206 |
-
|
207 |
-
# Fallback to numeric triplets (e.g. '000', '125')
|
208 |
if col_idx is None and triplet_col_map:
|
209 |
-
key = _power_to_triplet_digits(power)
|
210 |
col_idx = triplet_col_map.get(key)
|
211 |
|
212 |
if col_idx is None:
|
@@ -224,20 +210,21 @@ def process(input_file):
|
|
224 |
ws_out.cell(row=row_idx, column=col_idx).value = current_val + int(qty)
|
225 |
written_count += 1
|
226 |
|
227 |
-
# Save
|
228 |
tmpdir = tempfile.mkdtemp()
|
229 |
out_path = os.path.join(tmpdir, "AMMU-order-form-FILLED.xlsx")
|
230 |
wb_out.save(out_path)
|
231 |
|
232 |
-
log_lines = [
|
233 |
-
|
234 |
-
|
235 |
-
|
|
|
236 |
if missing_skus:
|
237 |
log_lines.append(f"⚠️ SKUs not found in template ({len(missing_skus)}): {', '.join(sorted(missing_skus))}")
|
238 |
if missing_powers:
|
239 |
log_lines.append(f"⚠️ Powers not found in template ({len(missing_powers)}): {', '.join(sorted(missing_powers))}")
|
240 |
-
log = "\n".join(log_lines)
|
241 |
|
242 |
return out_path, log
|
243 |
|
@@ -245,7 +232,7 @@ def process(input_file):
|
|
245 |
return None, f"Error: {e}"
|
246 |
|
247 |
with gr.Blocks(title="AMMU Order Form Filler") as demo:
|
248 |
-
gr.Markdown("### AMMU Order Form Filler\nUpload your input Excel. The app
|
249 |
with gr.Row():
|
250 |
in_file = gr.File(label="Upload input Excel (.xlsx)", file_types=[".xlsx"])
|
251 |
with gr.Row():
|
|
|
8 |
|
9 |
HF_DATASET_REPO = "leadingbridge/ammu"
|
10 |
TEMPLATE_FILENAME = "AMMU-order-form-template.xlsx"
|
|
|
11 |
LOCAL_TEMPLATE_FALLBACK = os.path.join(os.path.dirname(__file__), TEMPLATE_FILENAME)
|
12 |
|
13 |
def _normalize_power(val):
|
|
|
|
|
|
|
|
|
14 |
if val is None:
|
15 |
return None
|
16 |
s = str(val).strip()
|
17 |
if s == "":
|
18 |
return None
|
|
|
19 |
if s.lower() in {"plano", "piano", "0", "0.0", "0.00", "000"}:
|
20 |
return "0.00"
|
|
|
21 |
m = re.search(r"(-?\d+(?:\.\d+)?)", s.replace(",", ""))
|
22 |
if not m:
|
23 |
return None
|
|
|
28 |
return f"{num:.2f}"
|
29 |
|
30 |
def _power_to_triplet_digits(power_str: str) -> str:
|
|
|
31 |
if power_str is None:
|
32 |
return None
|
33 |
+
s = power_str.strip().lstrip("+").replace("-", "")
|
|
|
34 |
if "." in s:
|
35 |
whole, frac = s.split(".", 1)
|
36 |
frac = (frac + "00")[:2]
|
|
|
40 |
return digits.zfill(3)
|
41 |
|
42 |
def _find_header_row(ws: Worksheet, required_headers):
|
|
|
|
|
|
|
|
|
43 |
req = {h.lower() for h in required_headers}
|
44 |
for r in range(1, 11):
|
45 |
header_map = {}
|
|
|
56 |
raise ValueError(f"Could not locate a header row containing: {required_headers}")
|
57 |
|
58 |
def _download_template():
|
|
|
59 |
if os.path.exists(LOCAL_TEMPLATE_FALLBACK):
|
60 |
return LOCAL_TEMPLATE_FALLBACK
|
61 |
+
return hf_hub_download(
|
62 |
+
repo_id=HF_DATASET_REPO, filename=TEMPLATE_FILENAME, repo_type="dataset"
|
63 |
+
)
|
64 |
|
65 |
def process(input_file):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
try:
|
67 |
if input_file is None:
|
68 |
return None, "Please upload an Excel file first."
|
69 |
|
70 |
+
# --- INPUT: detect headers by name ---
|
71 |
wb_in = load_workbook(input_file.name, data_only=True)
|
72 |
ws_in = wb_in.active
|
73 |
+
header_row_idx, header_map = _find_header_row(
|
74 |
+
ws_in, {"SKU", "Product Option Value", "Quantity"}
|
75 |
+
)
|
76 |
col_sku = header_map["sku"]
|
77 |
col_pov = header_map["product option value"]
|
78 |
col_qty = header_map["quantity"]
|
79 |
|
80 |
+
# --- Aggregate quantities by (SKU, power) ---
|
81 |
+
agg = defaultdict(int)
|
82 |
rows_scanned = 0
|
83 |
for r in range(header_row_idx + 1, ws_in.max_row + 1):
|
84 |
sku = ws_in.cell(row=r, column=col_sku).value
|
|
|
101 |
if sku and power is not None and q:
|
102 |
agg[(str(sku).strip(), power)] += q
|
103 |
|
104 |
+
# --- OUTPUT: load template ---
|
105 |
template_path = _download_template()
|
106 |
wb_out = load_workbook(template_path)
|
107 |
ws_out = wb_out.active
|
108 |
|
109 |
# Find:
|
110 |
+
# (A) "MY SKU" column to build SKU->row map (instead of "SKU")
|
111 |
+
# (B) power label row (text like 0.00, -1.25)
|
112 |
+
# (C) triplet label row (000, 125, 400) as fallback
|
113 |
+
mysku_header_row = None
|
114 |
+
mysku_col_idx = None
|
115 |
power_label_row = None
|
116 |
power_col_map = {}
|
117 |
triplet_row = None
|
118 |
triplet_col_map = {}
|
119 |
|
120 |
+
# First pass over top 10 rows to find labels
|
121 |
for r in range(1, 11):
|
122 |
row_vals = [ws_out.cell(row=r, column=c).value for c in range(1, ws_out.max_column + 1)]
|
123 |
+
|
124 |
+
# (A) "MY SKU" detection
|
125 |
for c, v in enumerate(row_vals, start=1):
|
126 |
+
if isinstance(v, str) and v.strip().lower() == "my sku":
|
127 |
+
# Treat this as the table header for the SKU column
|
128 |
+
mysku_header_row = r
|
129 |
+
mysku_col_idx = c
|
130 |
+
|
131 |
+
# (B) textual power labels (prefer)
|
132 |
labels = {}
|
133 |
for c, v in enumerate(row_vals, start=1):
|
134 |
if isinstance(v, str):
|
|
|
138 |
if len(labels) >= 5 and power_label_row is None:
|
139 |
power_label_row = r
|
140 |
power_col_map = labels
|
141 |
+
|
142 |
+
# (C) numeric triplets (fallback)
|
143 |
trip = {}
|
144 |
for c, v in enumerate(row_vals, start=1):
|
145 |
if isinstance(v, str) and re.fullmatch(r"\d{2,3}", v.strip()):
|
|
|
148 |
triplet_row = r
|
149 |
triplet_col_map = trip
|
150 |
|
151 |
+
if mysku_header_row is None or mysku_col_idx is None:
|
152 |
+
raise ValueError("Could not find the 'MY SKU' header in the template (looked in rows 1–10).")
|
153 |
if not (power_label_row or triplet_row):
|
154 |
+
raise ValueError("Could not find power-column headers in the template (looked in rows 1–10).")
|
155 |
|
156 |
+
# Build SKU -> row map using the "MY SKU" column
|
157 |
sku_to_row = {}
|
158 |
+
for r in range(mysku_header_row + 1, ws_out.max_row + 1):
|
159 |
+
val = ws_out.cell(row=r, column=mysku_col_idx).value
|
160 |
if val is None:
|
161 |
continue
|
162 |
sku_to_row[str(val).strip()] = r
|
163 |
|
164 |
+
# Optional top-area "MY SKU" label for summary list:
|
165 |
+
# If there is ANOTHER "MY SKU" label in the top area (different from the table header row),
|
166 |
+
# write unique SKUs to the cell to its right.
|
167 |
+
summary_cell = None
|
168 |
for r in range(1, 11):
|
169 |
+
if r == mysku_header_row:
|
170 |
+
continue # skip the table header "MY SKU"
|
171 |
for c in range(1, ws_out.max_column + 1):
|
172 |
v = ws_out.cell(row=r, column=c).value
|
173 |
if isinstance(v, str) and v.strip().lower() == "my sku":
|
174 |
+
summary_cell = (r, c + 1)
|
175 |
break
|
176 |
+
if summary_cell:
|
177 |
break
|
178 |
+
if summary_cell and agg:
|
179 |
unique_skus = sorted({k[0] for k in agg.keys()})
|
180 |
+
ws_out.cell(row=summary_cell[0], column=summary_cell[1]).value = ", ".join(unique_skus)
|
181 |
|
182 |
+
# Write aggregated quantities
|
183 |
missing_skus = set()
|
184 |
missing_powers = set()
|
185 |
written_count = 0
|
|
|
190 |
missing_skus.add(sku)
|
191 |
continue
|
192 |
|
|
|
193 |
col_idx = power_col_map.get(power) if power_col_map else None
|
|
|
|
|
194 |
if col_idx is None and triplet_col_map:
|
195 |
+
key = _power_to_triplet_digits(power)
|
196 |
col_idx = triplet_col_map.get(key)
|
197 |
|
198 |
if col_idx is None:
|
|
|
210 |
ws_out.cell(row=row_idx, column=col_idx).value = current_val + int(qty)
|
211 |
written_count += 1
|
212 |
|
213 |
+
# Save output
|
214 |
tmpdir = tempfile.mkdtemp()
|
215 |
out_path = os.path.join(tmpdir, "AMMU-order-form-FILLED.xlsx")
|
216 |
wb_out.save(out_path)
|
217 |
|
218 |
+
log_lines = [
|
219 |
+
f"Rows scanned in input: {rows_scanned}",
|
220 |
+
f"Unique (SKU, power) pairs aggregated: {len(agg)}",
|
221 |
+
f"Entries written into template: {written_count}",
|
222 |
+
]
|
223 |
if missing_skus:
|
224 |
log_lines.append(f"⚠️ SKUs not found in template ({len(missing_skus)}): {', '.join(sorted(missing_skus))}")
|
225 |
if missing_powers:
|
226 |
log_lines.append(f"⚠️ Powers not found in template ({len(missing_powers)}): {', '.join(sorted(missing_powers))}")
|
227 |
+
log = "\n".join(log_lines)
|
228 |
|
229 |
return out_path, log
|
230 |
|
|
|
232 |
return None, f"Error: {e}"
|
233 |
|
234 |
with gr.Blocks(title="AMMU Order Form Filler") as demo:
|
235 |
+
gr.Markdown("### AMMU Order Form Filler\nUpload your input Excel. The app fills quantities into the AMMU template using **MY SKU** for row mapping and power columns for quantities.")
|
236 |
with gr.Row():
|
237 |
in_file = gr.File(label="Upload input Excel (.xlsx)", file_types=[".xlsx"])
|
238 |
with gr.Row():
|