Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -77,10 +77,15 @@ def process(input_file):
|
|
77 |
col_pov = header_map["product option value"]
|
78 |
col_qty = header_map["quantity"]
|
79 |
|
80 |
-
#
|
81 |
-
|
|
|
|
|
|
|
|
|
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
|
85 |
pov = ws_in.cell(row=r, column=col_pov).value
|
86 |
qty = ws_in.cell(row=r, column=col_qty).value
|
@@ -90,6 +95,7 @@ def process(input_file):
|
|
90 |
rows_scanned += 1
|
91 |
|
92 |
power = _normalize_power(pov)
|
|
|
93 |
try:
|
94 |
q = int(qty) if qty is not None and str(qty).strip() != "" else 0
|
95 |
except Exception:
|
@@ -98,8 +104,12 @@ def process(input_file):
|
|
98 |
except Exception:
|
99 |
q = 0
|
100 |
|
101 |
-
|
102 |
-
|
|
|
|
|
|
|
|
|
103 |
|
104 |
# --- OUTPUT: load template ---
|
105 |
template_path = _download_template()
|
@@ -117,18 +127,16 @@ def process(input_file):
|
|
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
|
132 |
labels = {}
|
133 |
for c, v in enumerate(row_vals, start=1):
|
134 |
if isinstance(v, str):
|
@@ -139,7 +147,7 @@ def process(input_file):
|
|
139 |
power_label_row = r
|
140 |
power_col_map = labels
|
141 |
|
142 |
-
# (C) numeric triplets
|
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()):
|
@@ -161,13 +169,11 @@ def process(input_file):
|
|
161 |
continue
|
162 |
sku_to_row[str(val).strip()] = r
|
163 |
|
164 |
-
# Optional top-area "MY SKU"
|
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
|
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":
|
@@ -175,30 +181,52 @@ def process(input_file):
|
|
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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
183 |
missing_skus = set()
|
184 |
missing_powers = set()
|
185 |
-
written_count = 0
|
186 |
|
187 |
for (sku, power), qty in agg.items():
|
188 |
row_idx = sku_to_row.get(sku)
|
189 |
if row_idx is None:
|
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:
|
199 |
missing_powers.add(power)
|
200 |
continue
|
201 |
-
|
202 |
current = ws_out.cell(row=row_idx, column=col_idx).value
|
203 |
try:
|
204 |
current_val = int(current) if current is not None and str(current).strip() != "" else 0
|
@@ -210,6 +238,39 @@ def process(input_file):
|
|
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")
|
@@ -217,13 +278,14 @@ def process(input_file):
|
|
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
|
225 |
if missing_powers:
|
226 |
-
log_lines.append(f"⚠️ Powers
|
227 |
log = "\n".join(log_lines)
|
228 |
|
229 |
return out_path, log
|
@@ -232,14 +294,20 @@ def process(input_file):
|
|
232 |
return None, f"Error: {e}"
|
233 |
|
234 |
with gr.Blocks(title="AMMU Order Form Filler") as demo:
|
235 |
-
gr.Markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
236 |
with gr.Row():
|
237 |
in_file = gr.File(label="Upload input Excel (.xlsx)", file_types=[".xlsx"])
|
238 |
with gr.Row():
|
239 |
run_btn = gr.Button("Process")
|
240 |
with gr.Row():
|
241 |
out_file = gr.File(label="Download filled template (.xlsx)")
|
242 |
-
log_box = gr.Textbox(label="Log", lines=
|
243 |
|
244 |
run_btn.click(fn=process, inputs=in_file, outputs=[out_file, log_box])
|
245 |
|
|
|
77 |
col_pov = header_map["product option value"]
|
78 |
col_qty = header_map["quantity"]
|
79 |
|
80 |
+
# Capture the entire header row & all row values so unmatched lines can be copied verbatim
|
81 |
+
in_max_col = ws_in.max_column
|
82 |
+
header_values = [ws_in.cell(row=header_row_idx, column=c).value for c in range(1, in_max_col + 1)]
|
83 |
+
|
84 |
+
# Pre-collect all entries (we'll decide matched vs unmatched after loading the template)
|
85 |
+
entries = [] # list of dicts: {sku, power_norm, qty, row_values}
|
86 |
rows_scanned = 0
|
87 |
for r in range(header_row_idx + 1, ws_in.max_row + 1):
|
88 |
+
row_values = [ws_in.cell(row=r, column=c).value for c in range(1, in_max_col + 1)]
|
89 |
sku = ws_in.cell(row=r, column=col_sku).value
|
90 |
pov = ws_in.cell(row=r, column=col_pov).value
|
91 |
qty = ws_in.cell(row=r, column=col_qty).value
|
|
|
95 |
rows_scanned += 1
|
96 |
|
97 |
power = _normalize_power(pov)
|
98 |
+
# robust int conversion
|
99 |
try:
|
100 |
q = int(qty) if qty is not None and str(qty).strip() != "" else 0
|
101 |
except Exception:
|
|
|
104 |
except Exception:
|
105 |
q = 0
|
106 |
|
107 |
+
entries.append({
|
108 |
+
"sku": (str(sku).strip() if sku is not None else None),
|
109 |
+
"power": power,
|
110 |
+
"qty": q,
|
111 |
+
"row_values": row_values
|
112 |
+
})
|
113 |
|
114 |
# --- OUTPUT: load template ---
|
115 |
template_path = _download_template()
|
|
|
127 |
triplet_row = None
|
128 |
triplet_col_map = {}
|
129 |
|
|
|
130 |
for r in range(1, 11):
|
131 |
row_vals = [ws_out.cell(row=r, column=c).value for c in range(1, ws_out.max_column + 1)]
|
132 |
|
133 |
# (A) "MY SKU" detection
|
134 |
for c, v in enumerate(row_vals, start=1):
|
135 |
if isinstance(v, str) and v.strip().lower() == "my sku":
|
|
|
136 |
mysku_header_row = r
|
137 |
mysku_col_idx = c
|
138 |
|
139 |
+
# (B) textual power labels
|
140 |
labels = {}
|
141 |
for c, v in enumerate(row_vals, start=1):
|
142 |
if isinstance(v, str):
|
|
|
147 |
power_label_row = r
|
148 |
power_col_map = labels
|
149 |
|
150 |
+
# (C) numeric triplets
|
151 |
trip = {}
|
152 |
for c, v in enumerate(row_vals, start=1):
|
153 |
if isinstance(v, str) and re.fullmatch(r"\d{2,3}", v.strip()):
|
|
|
169 |
continue
|
170 |
sku_to_row[str(val).strip()] = r
|
171 |
|
172 |
+
# Optional top-area "MY SKU" summary (distinct from the table header row)
|
|
|
|
|
173 |
summary_cell = None
|
174 |
for r in range(1, 11):
|
175 |
if r == mysku_header_row:
|
176 |
+
continue
|
177 |
for c in range(1, ws_out.max_column + 1):
|
178 |
v = ws_out.cell(row=r, column=c).value
|
179 |
if isinstance(v, str) and v.strip().lower() == "my sku":
|
|
|
181 |
break
|
182 |
if summary_cell:
|
183 |
break
|
|
|
|
|
|
|
184 |
|
185 |
+
# Classify entries: matched vs unmatched (line-by-line), and aggregate matched ones
|
186 |
+
agg = defaultdict(int) # (sku, power) -> summed qty
|
187 |
+
unmatched_rows = [] # list of row_values (verbatim from input)
|
188 |
+
|
189 |
+
for rec in entries:
|
190 |
+
sku, power, qty = rec["sku"], rec["power"], rec["qty"]
|
191 |
+
# Invalid minimal fields => treat as unmatched copy-through
|
192 |
+
if not sku or qty <= 0 or power is None:
|
193 |
+
unmatched_rows.append(rec["row_values"])
|
194 |
+
continue
|
195 |
+
|
196 |
+
row_idx = sku_to_row.get(sku)
|
197 |
+
if row_idx is None:
|
198 |
+
unmatched_rows.append(rec["row_values"])
|
199 |
+
continue
|
200 |
+
|
201 |
+
col_idx = power_col_map.get(power) if power_col_map else None
|
202 |
+
if col_idx is None and triplet_col_map:
|
203 |
+
key = _power_to_triplet_digits(power)
|
204 |
+
col_idx = triplet_col_map.get(key)
|
205 |
+
|
206 |
+
if col_idx is None:
|
207 |
+
unmatched_rows.append(rec["row_values"])
|
208 |
+
continue
|
209 |
+
|
210 |
+
# It's a match — add to aggregation
|
211 |
+
agg[(sku, power)] += qty
|
212 |
+
|
213 |
+
# Write aggregated matches to the template grid
|
214 |
+
written_count = 0
|
215 |
missing_skus = set()
|
216 |
missing_powers = set()
|
|
|
217 |
|
218 |
for (sku, power), qty in agg.items():
|
219 |
row_idx = sku_to_row.get(sku)
|
220 |
if row_idx is None:
|
221 |
missing_skus.add(sku)
|
222 |
continue
|
|
|
223 |
col_idx = power_col_map.get(power) if power_col_map else None
|
224 |
if col_idx is None and triplet_col_map:
|
225 |
key = _power_to_triplet_digits(power)
|
226 |
col_idx = triplet_col_map.get(key)
|
|
|
227 |
if col_idx is None:
|
228 |
missing_powers.add(power)
|
229 |
continue
|
|
|
230 |
current = ws_out.cell(row=row_idx, column=col_idx).value
|
231 |
try:
|
232 |
current_val = int(current) if current is not None and str(current).strip() != "" else 0
|
|
|
238 |
ws_out.cell(row=row_idx, column=col_idx).value = current_val + int(qty)
|
239 |
written_count += 1
|
240 |
|
241 |
+
# Fill the optional top-area summary of unique SKUs
|
242 |
+
if summary_cell and (agg or unmatched_rows):
|
243 |
+
unique_skus = sorted({sku for (sku, _) in agg.keys()})
|
244 |
+
# Also include SKUs from unmatched rows where available
|
245 |
+
try:
|
246 |
+
# Find the index of "SKU" in the input header to extract from unmatched rows
|
247 |
+
sku_header_idx = next((i for i, v in enumerate(header_values) if isinstance(v, str) and v.strip().lower() == "sku"), None)
|
248 |
+
if sku_header_idx is not None:
|
249 |
+
for rv in unmatched_rows:
|
250 |
+
if sku_header_idx < len(rv) and rv[sku_header_idx]:
|
251 |
+
unique_skus.append(str(rv[sku_header_idx]).strip())
|
252 |
+
except Exception:
|
253 |
+
pass
|
254 |
+
if unique_skus:
|
255 |
+
ws_out.cell(row=summary_cell[0], column=summary_cell[1]).value = ", ".join(sorted(set(unique_skus)))
|
256 |
+
|
257 |
+
# --- Create/replace the "additional order" sheet with unmatched rows copied verbatim ---
|
258 |
+
sheet_name = "additional order"
|
259 |
+
if sheet_name in wb_out.sheetnames:
|
260 |
+
# remove and recreate to avoid remnants
|
261 |
+
ws_old = wb_out[sheet_name]
|
262 |
+
wb_out.remove(ws_old)
|
263 |
+
ws_extra = wb_out.create_sheet(title=sheet_name)
|
264 |
+
|
265 |
+
# Write header
|
266 |
+
for c, val in enumerate(header_values, start=1):
|
267 |
+
ws_extra.cell(row=1, column=c).value = val
|
268 |
+
|
269 |
+
# Write unmatched lines
|
270 |
+
for i, row_vals in enumerate(unmatched_rows, start=2):
|
271 |
+
for c, val in enumerate(row_vals, start=1):
|
272 |
+
ws_extra.cell(row=i, column=c).value = val
|
273 |
+
|
274 |
# Save output
|
275 |
tmpdir = tempfile.mkdtemp()
|
276 |
out_path = os.path.join(tmpdir, "AMMU-order-form-FILLED.xlsx")
|
|
|
278 |
|
279 |
log_lines = [
|
280 |
f"Rows scanned in input: {rows_scanned}",
|
281 |
+
f"Unique matched (SKU, power) pairs aggregated: {len(agg)}",
|
282 |
f"Entries written into template: {written_count}",
|
283 |
+
f"Unmatched rows copied to '{sheet_name}': {len(unmatched_rows)}",
|
284 |
]
|
285 |
if missing_skus:
|
286 |
+
log_lines.append(f"⚠️ SKUs missing during aggregate write ({len(missing_skus)}): {', '.join(sorted(missing_skus))}")
|
287 |
if missing_powers:
|
288 |
+
log_lines.append(f"⚠️ Powers missing during aggregate write ({len(missing_powers)}): {', '.join(sorted(missing_powers))}")
|
289 |
log = "\n".join(log_lines)
|
290 |
|
291 |
return out_path, log
|
|
|
294 |
return None, f"Error: {e}"
|
295 |
|
296 |
with gr.Blocks(title="AMMU Order Form Filler") as demo:
|
297 |
+
gr.Markdown(
|
298 |
+
"### AMMU Order Form Filler\n"
|
299 |
+
"• Uses **MY SKU** column to map rows\n"
|
300 |
+
"• Matches power columns (text like `-1.25` or fallback triplets like `125`)\n"
|
301 |
+
"• Aggregates quantities for matched lines\n"
|
302 |
+
"• Copies **unmatched lines** to a new sheet **`additional order`** with headers"
|
303 |
+
)
|
304 |
with gr.Row():
|
305 |
in_file = gr.File(label="Upload input Excel (.xlsx)", file_types=[".xlsx"])
|
306 |
with gr.Row():
|
307 |
run_btn = gr.Button("Process")
|
308 |
with gr.Row():
|
309 |
out_file = gr.File(label="Download filled template (.xlsx)")
|
310 |
+
log_box = gr.Textbox(label="Log", lines=10)
|
311 |
|
312 |
run_btn.click(fn=process, inputs=in_file, outputs=[out_file, log_box])
|
313 |
|