leadingbridge commited on
Commit
72933fe
·
verified ·
1 Parent(s): a0c3eb1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +53 -66
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
- # Otherwise download from the HF dataset (as you provided).
76
- return hf_hub_download(repo_id=HF_DATASET_REPO, filename=TEMPLATE_FILENAME, repo_type="dataset")
 
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
- # --- Read input & detect headers by name (not index) ---
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(ws_in, {"SKU", "Product Option Value", "Quantity"})
 
 
96
  col_sku = header_map["sku"]
97
  col_pov = header_map["product option value"]
98
  col_qty = header_map["quantity"]
99
 
100
- # --- Aggregate quantities across duplicate lines (same SKU + same power) ---
101
- agg = defaultdict(int) # (sku, power_str) -> qty sum
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
- # --- Load template ---
125
  template_path = _download_template()
126
  wb_out = load_workbook(template_path)
127
  ws_out = wb_out.active
128
 
129
  # Find:
130
- # a) header row containing "SKU"
131
- # b) row containing textual power labels ('0.00', '-1.00', ...)
132
- # c) (optional) row containing numeric triplets ('000', '125', ...)
133
- sku_header_row = None
134
- sku_col_idx = None
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
- # (a) 'SKU' header
 
143
  for c, v in enumerate(row_vals, start=1):
144
- if isinstance(v, str) and v.strip().lower() == "sku":
145
- sku_header_row = r
146
- sku_col_idx = c
147
- # (b) textual labels
 
 
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
- # (c) numeric triplets
 
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 sku_header_row is None or sku_col_idx is None:
167
- raise ValueError("Could not find the 'SKU' header row in the template (looked in rows 1–10).")
168
  if not (power_label_row or triplet_row):
169
- raise ValueError("Could not find the power-column headers in the template (looked in rows 1–10).")
170
 
171
- # Build SKU -> row map from the template
172
  sku_to_row = {}
173
- for r in range(sku_header_row + 1, ws_out.max_row + 1):
174
- val = ws_out.cell(row=r, column=sku_col_idx).value
175
  if val is None:
176
  continue
177
  sku_to_row[str(val).strip()] = r
178
 
179
- # Optional: write unique SKUs next to a "My SKU" label if it exists in the top area
180
- mysku_cell = None
 
 
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
- mysku_cell = (r, c + 1)
186
  break
187
- if mysku_cell:
188
  break
189
- if mysku_cell and agg:
190
  unique_skus = sorted({k[0] for k in agg.keys()})
191
- ws_out.cell(row=mysku_cell[0], column=mysku_cell[1]).value = ", ".join(unique_skus)
192
 
193
- # Write aggregated quantities into the correct cells
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) # e.g. '-1.25' -> '125'
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 to a temp file and return
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
- log_lines.append(f"Rows scanned in input: {rows_scanned}")
234
- log_lines.append(f"Unique (SKU, power) pairs aggregated: {len(agg)}")
235
- log_lines.append(f"Entries written into template: {written_count}")
 
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) if log_lines else "Done."
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 will fill quantities into the official AMMU template based on SKU and power.")
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():