Bhuvi13 commited on
Commit
3a2d97b
·
verified ·
1 Parent(s): 82f069e

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +376 -220
src/streamlit_app.py CHANGED
@@ -207,25 +207,44 @@ def run_inference_on_image(image: Image.Image, processor, model, device, decoder
207
  # ---------------------------
208
  def map_prediction_to_ui(pred):
209
  import json, re
210
- from datetime import datetime
211
 
 
212
  def safe_json_load(s):
213
  if s is None:
214
  return None
215
  if isinstance(s, (dict, list)):
216
  return s
217
  if isinstance(s, str):
 
 
 
218
  try:
219
  return json.loads(s)
220
  except Exception:
221
- try:
222
- t = s.strip()
223
- t = t.replace("\\'", "'").replace('\"{', '{').replace('}\"', '}')
224
- return json.loads(t)
225
- except Exception:
226
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  return None
228
 
 
229
  def clean_number(x):
230
  if x is None:
231
  return 0.0
@@ -234,6 +253,7 @@ def map_prediction_to_ui(pred):
234
  s = str(x).strip()
235
  if s == "":
236
  return 0.0
 
237
  s = re.sub(r"[,\s]", "", s)
238
  s = re.sub(r"[^\d\.\-]", "", s)
239
  if s in ("", ".", "-", "-."):
@@ -243,28 +263,69 @@ def map_prediction_to_ui(pred):
243
  except Exception:
244
  return 0.0
245
 
246
- def parse_date(s):
247
- if not s:
248
- return ""
249
- s = str(s).strip()
250
- for fmt in ("%Y-%m-%d", "%d-%m-%Y", "%d/%m/%Y", "%m/%d/%Y", "%d.%m.%Y"):
251
- try:
252
- return datetime.strptime(s, fmt).strftime("%Y-%m-%d")
253
- except Exception:
254
- pass
255
- m = re.match(r"^(\d{1,2})/(\d{1,2})/(\d{4})$", s)
256
- if m:
257
- a, b, y = int(m.group(1)), int(m.group(2)), int(m.group(3))
258
- if a > 12:
259
- d, mo = a, b
260
- else:
261
- mo, d = a, b
262
- try:
263
- return datetime(year=y, month=mo, day=d).strftime("%Y-%m-%d")
264
- except Exception:
265
- return s
266
- return s
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
 
268
  ui = {
269
  "Invoice Number": "",
270
  "Invoice Date": "",
@@ -284,137 +345,135 @@ def map_prediction_to_ui(pred):
284
  "Itemized Data": []
285
  }
286
 
287
- if pred is None:
288
- return ui
289
-
290
- if isinstance(pred, str):
291
- parsed = safe_json_load(pred)
292
- if parsed is not None:
293
- pred = parsed
294
-
295
- gt = None
296
- if isinstance(pred, dict):
297
- if "gt_parse" in pred:
298
- gp = pred["gt_parse"]
299
- gp_parsed = safe_json_load(gp)
300
- gt = gp_parsed if gp_parsed is not None else (gp if isinstance(gp, dict) else {})
301
- else:
302
- gt = pred
303
- else:
304
- return ui
305
-
306
- header = gt.get("header") or {}
307
- items = gt.get("items") or []
308
- summary = gt.get("summary") or {}
309
-
310
- ui["Invoice Number"] = header.get("invoice_no") or header.get("invoice_number") or ui["Invoice Number"]
311
- ui["Invoice Date"] = str(header.get("invoice_date") or header.get("inv_date") or "")
312
- ui["Due Date"] = str(header.get("due_date") or header.get("due") or "")
313
-
314
- ui["Sender Name"] = header.get("sender_name") or header.get("seller_name") or header.get("from_name") or ui["Sender Name"]
315
- ui["Sender Address"] = header.get("sender_addr") or header.get("sender_address") or header.get("seller_addr") or ui["Sender Address"]
316
- ui["Recipient Name"] = header.get("rcpt_name") or header.get("recipient_name") or header.get("to_name") or ui["Recipient Name"]
317
- ui["Recipient Address"] = header.get("rcpt_addr") or header.get("rcpt_address") or header.get("recipient_address") or ui["Recipient Address"]
318
 
319
- ui["Sender"] = {"Name": ui["Sender Name"], "Address": ui["Sender Address"]}
320
- ui["Recipient"] = {"Name": ui["Recipient Name"], "Address": ui["Recipient Address"]}
 
 
 
 
 
 
321
 
 
322
  bank = {}
323
- if header.get("bank_name"):
324
- bank["bank_name"] = str(header.get("bank_name")).strip()
325
- if header.get("bank_acc_no"):
326
- bank["bank_account_number"] = str(header.get("bank_acc_no")).strip()
327
- if header.get("bank_account_number"):
328
- bank["bank_account_number"] = bank.get("bank_account_number") or str(header.get("bank_account_number")).strip()
329
- if header.get("bank_iban"):
330
- bank["bank_iban"] = str(header.get("bank_iban")).strip()
331
- if header.get("bank_routing"):
332
- bank["bank_routing"] = str(header.get("bank_routing")).strip()
333
- if header.get("bank_swift"):
334
- bank["bank_swift"] = str(header.get("bank_swift")).strip()
335
- if header.get("bank_branch"):
336
- bank["bank_branch"] = str(header.get("bank_branch")).strip()
337
- if header.get("bank_acc_name"):
338
- bank["bank_acc_name"] = str(header.get("bank_acc_name")).strip()
339
- hb = header.get("bank")
340
- if isinstance(hb, dict):
341
- for k, v in hb.items():
342
- if not v:
343
- continue
344
- lk = k.lower()
345
- if "iban" in lk:
346
- bank["bank_iban"] = bank.get("bank_iban") or str(v).strip()
347
- elif "swift" in lk:
348
- bank["bank_swift"] = bank.get("bank_swift") or str(v).strip()
349
- elif "acc" in lk or "account" in lk:
350
- bank["bank_account_number"] = bank.get("bank_account_number") or str(v).strip()
351
- elif "name" in lk and "bank" in lk:
352
- bank["bank_name"] = bank.get("bank_name") or str(v).strip()
353
- elif "branch" in lk:
354
- bank["bank_branch"] = bank.get("bank_branch") or str(v).strip()
355
- elif "acc_name" in lk or "account_name" in lk:
356
- bank["bank_acc_name"] = bank.get("bank_acc_name") or str(v).strip()
357
-
358
  ui["Bank Details"] = bank
359
 
360
- ui["Subtotal"] = clean_number(summary.get("subtotal") or summary.get("sub_total") or summary.get("subTotal"))
361
- ui["Tax Percentage"] = clean_number(summary.get("tax_rate") or summary.get("taxRate") or summary.get("tax_percentage"))
362
- ui["Total Tax"] = clean_number(summary.get("tax_amount") or summary.get("tax") or summary.get("taxAmount"))
363
- ui["Total Amount"] = clean_number(summary.get("total_amount") or summary.get("grand_total") or summary.get("total") or summary.get("amount_total"))
364
- ui["Currency"] = summary.get("currency") or header.get("currency") or ui["Currency"] or ""
365
-
366
- normalized_items = []
367
-
368
- if isinstance(items, str):
369
- parsed_items = safe_json_load(items)
370
- if parsed_items is not None:
371
- items = parsed_items
372
-
373
- if isinstance(items, dict):
374
- if any(isinstance(v, list) for v in items.values()):
375
- list_cols = {k: v for k, v in items.items() if isinstance(v, list)}
376
- max_len = max((len(v) for v in list_cols.values()), default=0)
377
- for i in range(max_len):
378
- row = {}
379
- for k, v in items.items():
380
- if isinstance(v, list):
381
- row[k] = v[i] if i < len(v) else ""
382
- else:
383
- row[k] = v
384
- normalized_items.append(row)
385
- else:
386
- normalized_items.append(items)
387
- elif isinstance(items, list):
388
- normalized_items = items
389
- else:
390
- normalized_items = []
391
-
392
- item_rows = []
393
- for it in normalized_items:
394
- if not isinstance(it, dict):
395
- item_rows.append({"Description": str(it), "Quantity": 1, "Unit Price": 0.0, "Amount": 0.0, "Tax": 0.0, "Line Total": 0.0})
396
- continue
397
- desc = it.get("descriptions") or it.get("description") or it.get("desc") or it.get("item") or it.get("name") or ""
398
- qty = it.get("quantity") or it.get("qty") or it.get("Quantity") or ""
399
- unit = it.get("unit_price") or it.get("unitPrice") or it.get("price") or ""
400
- amt = it.get("amount") or it.get("Line_total") or it.get("line_total") or it.get("total") or ""
401
-
402
- # Extract item-level tax if available under common keys
403
- tax_val = it.get("tax") or it.get("tax_amount") or it.get("line_tax") or it.get("item_tax") or it.get("taxAmount") or ""
404
-
405
- # Extract explicit line total if present; otherwise fall back to amount
406
- line_total_val = it.get("Line_total") or it.get("line_total") or it.get("lineTotal") or amt
407
-
408
- item_rows.append({
409
- "Description": str(desc).strip(),
410
- "Quantity": float(clean_number(qty)),
411
- "Unit Price": float(clean_number(unit)),
412
- "Amount": float(clean_number(amt)),
413
- "Tax": float(clean_number(tax_val)),
414
- "Line Total": float(clean_number(line_total_val))
415
- })
416
-
417
- ui["Itemized Data"] = item_rows
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
 
419
  return ui
420
 
@@ -425,13 +484,47 @@ def flatten_invoice_to_rows(invoice_data) -> list:
425
  """
426
  Converts nested invoice data into a flat list of rows (one per line item),
427
  with invoice-level and sender/recipient/bank fields repeated in each row.
 
 
 
 
428
  """
 
 
 
 
 
 
 
 
 
 
429
  rows = []
430
- line_items = invoice_data.get("Itemized Data", [])
431
 
432
- if not line_items:
433
- # If no line items, create one row with invoice info only
434
- row = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  "Invoice Number": invoice_data.get("Invoice Number", ""),
436
  "Invoice Date": invoice_data.get("Invoice Date", ""),
437
  "Due Date": invoice_data.get("Due Date", ""),
@@ -440,20 +533,19 @@ def flatten_invoice_to_rows(invoice_data) -> list:
440
  "Tax Percentage": invoice_data.get("Tax Percentage", 0.0),
441
  "Total Tax": invoice_data.get("Total Tax", 0.0),
442
  "Total Amount": invoice_data.get("Total Amount", 0.0),
443
- "Sender Name": invoice_data.get("Sender", {}).get("Name", ""),
444
- "Sender Address": invoice_data.get("Sender", {}).get("Address", ""),
445
- "Recipient Name": invoice_data.get("Recipient", {}).get("Name", ""),
446
- "Recipient Address": invoice_data.get("Recipient", {}).get("Address", ""),
447
  }
448
 
449
- # Flatten bank details
450
- bank = invoice_data.get("Bank Details", {})
451
- for k, v in bank.items():
452
- # Avoid double-prefixing if key already contains 'bank_'
453
- key_name = k if str(k).startswith("bank_") else f"bank_{k}"
454
- row[key_name] = v
455
-
456
- # Add empty line item fields
457
  row.update({
458
  "Item Description": "",
459
  "Item Quantity": 0,
@@ -465,40 +557,20 @@ def flatten_invoice_to_rows(invoice_data) -> list:
465
  rows.append(row)
466
  return rows
467
 
468
- # For each line item, create a row with all invoice context
469
  for item in line_items:
470
- row = {
471
- "Invoice Number": invoice_data.get("Invoice Number", ""),
472
- "Invoice Date": invoice_data.get("Invoice Date", ""),
473
- "Due Date": invoice_data.get("Due Date", ""),
474
- "Currency": invoice_data.get("Currency", ""),
475
- "Subtotal": invoice_data.get("Subtotal", 0.0),
476
- "Tax Percentage": invoice_data.get("Tax Percentage", 0.0),
477
- "Total Tax": invoice_data.get("Total Tax", 0.0),
478
- "Total Amount": invoice_data.get("Total Amount", 0.0),
479
- "Sender Name": invoice_data.get("Sender", {}).get("Name", ""),
480
- "Sender Address": invoice_data.get("Sender", {}).get("Address", ""),
481
- "Recipient Name": invoice_data.get("Recipient", {}).get("Name", ""),
482
- "Recipient Address": invoice_data.get("Recipient", {}).get("Address", ""),
483
- }
484
-
485
- # Flatten bank details
486
- bank = invoice_data.get("Bank Details", {})
487
- for k, v in bank.items():
488
- # Avoid double-prefixing if key already contains 'bank_'
489
- key_name = k if str(k).startswith("bank_") else f"bank_{k}"
490
- row[key_name] = v
491
-
492
- # Add line item fields
493
  row.update({
494
- "Item Description": item.get("Description", ""),
495
- "Item Quantity": item.get("Quantity", 0),
496
- "Item Unit Price": item.get("Unit Price", 0.0),
497
- "Item Amount": item.get("Amount", 0.0),
498
- "Item Tax": item.get("Tax", 0.0),
499
- "Item Line Total": item.get("Line Total", item.get("Amount", 0.0)),
500
  })
501
-
502
  rows.append(row)
503
 
504
  return rows
@@ -779,38 +851,121 @@ elif len(st.session_state.batch_results) > 0:
779
 
780
  # ---------- Sender / Recipient ----------
781
  with tabs[1]:
782
- sender_info = form_data.get('Sender', {'Name': '', 'Address': ''})
783
- recipient_info = form_data.get('Recipient', {'Name': '', 'Address': ''})
 
 
 
 
784
  with st.container():
785
- sender_info['Name'] = st.text_input("Sender Name*", value=sender_info.get('Name', ''), key=f"sender_name_{selected_hash}")
786
- sender_info['Address'] = st.text_area("Sender Address*", value=sender_info.get('Address', ''), key=f"sender_address_{selected_hash}")
787
- recipient_info['Name'] = st.text_input("Recipient Name*", value=recipient_info.get('Name', ''), key=f"recipient_name_{selected_hash}")
788
- recipient_info['Address'] = st.text_area("Recipient Address*", value=recipient_info.get('Address', ''), key=f"recipient_address_{selected_hash}")
 
789
  if st.button("⇄ Swap", help="Swap sender and recipient information", key=f"swap_{selected_hash}"):
790
- form_data['Sender'], form_data['Recipient'] = form_data['Recipient'], form_data['Sender']
 
 
791
  st.rerun()
792
 
793
- # ---------- Bank Details ----------
 
794
  with tabs[2]:
795
- bank_info = form_data.get('Bank Details', {}) or {}
 
 
 
 
 
 
 
 
 
 
 
 
796
  with st.container():
797
- bank_info['bank_name'] = st.text_input("Bank Name", value=bank_info.get('bank_name', ''), key=f"bank_name_{selected_hash}")
798
- bank_info['bank_account_number'] = st.text_input("Account Number", value=bank_info.get('bank_account_number', '') or bank_info.get('bank_acc_no',''), key=f"bank_account_{selected_hash}")
799
- bank_info['bank_acc_name'] = st.text_input("Bank Account Name", value=bank_info.get('bank_acc_name', '') or bank_info.get('bank_acc_name', ''), key=f"bank_acc_name_{selected_hash}")
800
- bank_info['bank_iban'] = st.text_input("IBAN", value=bank_info.get('bank_iban', ''), key=f"iban_{selected_hash}")
801
- bank_info['bank_swift'] = st.text_input("SWIFT Code", value=bank_info.get('bank_swift', ''), key=f"swift_code_{selected_hash}")
802
- bank_info['bank_routing'] = st.text_input("Routing Number", value=bank_info.get('bank_routing', ''), key=f"routing_{selected_hash}")
803
- bank_info['bank_branch'] = st.text_input("Branch", value=bank_info.get('bank_branch', ''), key=f"branch_{selected_hash}")
804
- form_data['Bank Details'] = bank_info
 
 
 
 
 
 
 
 
 
805
 
806
  # ---------- Line Items ----------
 
807
  with tabs[3]:
808
  editor_key = f"item_editor_{selected_hash}"
809
- item_rows = form_data.get('Itemized Data', [])
810
- df = pd.DataFrame(item_rows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
  for col in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
812
  if col not in df.columns:
813
  df[col] = ""
 
814
  st.write("✏️ Edit line items below. Press Enter or click outside a cell to confirm each edit.")
815
  edited_df = st.data_editor(
816
  df,
@@ -821,6 +976,7 @@ elif len(st.session_state.batch_results) > 0:
821
  if len(edited_df) == 0:
822
  st.info("No line items found in the invoice.")
823
 
 
824
  # Save button (per file)
825
  if st.button("💾 Save Edits for This File", key=f"save_{selected_hash}"):
826
  # Update line items from data editor
 
207
  # ---------------------------
208
  def map_prediction_to_ui(pred):
209
  import json, re
210
+ from collections import defaultdict
211
 
212
+ # --- parse raw string payloads that embed JSON ---
213
  def safe_json_load(s):
214
  if s is None:
215
  return None
216
  if isinstance(s, (dict, list)):
217
  return s
218
  if isinstance(s, str):
219
+ s = s.strip()
220
+ if s == "":
221
+ return None
222
  try:
223
  return json.loads(s)
224
  except Exception:
225
+ # try to extract balanced-brace substrings (simple approach)
226
+ subs = []
227
+ stack = []
228
+ start = None
229
+ for i, ch in enumerate(s):
230
+ if ch == "{":
231
+ if not stack:
232
+ start = i
233
+ stack.append("{")
234
+ elif ch == "}":
235
+ if stack:
236
+ stack.pop()
237
+ if not stack and start is not None:
238
+ subs.append(s[start:i+1])
239
+ start = None
240
+ for sub in subs:
241
+ try:
242
+ return json.loads(sub)
243
+ except Exception:
244
+ continue
245
  return None
246
 
247
+ # --- normalize numeric strings like "1,800.00" -> float ---
248
  def clean_number(x):
249
  if x is None:
250
  return 0.0
 
253
  s = str(x).strip()
254
  if s == "":
255
  return 0.0
256
+ # remove commas and non-number chars except dot and minus
257
  s = re.sub(r"[,\s]", "", s)
258
  s = re.sub(r"[^\d\.\-]", "", s)
259
  if s in ("", ".", "-", "-."):
 
263
  except Exception:
264
  return 0.0
265
 
266
+ # --- collect all keys -> list of values from pred, recursively ---
267
+ def collect_keys(obj, out):
268
+ if isinstance(obj, dict):
269
+ for k, v in obj.items():
270
+ lk = str(k).strip().lower()
271
+ out[lk].append(v)
272
+ collect_keys(v, out)
273
+ elif isinstance(obj, list):
274
+ for it in obj:
275
+ collect_keys(it, out)
276
+ else:
277
+ # primitive: handled via parent key
278
+ pass
279
+
280
+ # --- find list-of-dicts candidates for items (recursively) ---
281
+ def collect_lists_of_dicts(obj, out_lists):
282
+ if isinstance(obj, dict):
283
+ for v in obj.values():
284
+ if isinstance(v, list) and v and isinstance(v[0], dict):
285
+ out_lists.append(v)
286
+ else:
287
+ collect_lists_of_dicts(v, out_lists)
288
+ elif isinstance(obj, list):
289
+ for it in obj:
290
+ if isinstance(it, list) and it and isinstance(it[0], dict):
291
+ out_lists.append(it)
292
+ else:
293
+ collect_lists_of_dicts(it, out_lists)
294
+
295
+ # --- map item dict -> UI item row using the keys you specified in example ---
296
+ def map_item_dict(it):
297
+ if not isinstance(it, dict):
298
+ return None
299
+ # lowered keys mapping
300
+ lower = {str(k).strip().lower(): v for k, v in it.items()}
301
+ desc = (lower.get("descriptions") or lower.get("description") or lower.get("desc") or lower.get("item") or "")
302
+ qty = lower.get("quantity") or lower.get("qty") or lower.get("count") or ""
303
+ unit_price = lower.get("unit_price") or lower.get("price") or ""
304
+ amount = lower.get("amount") or lower.get("line_total") or lower.get("line total") or lower.get("total") or ""
305
+ tax = lower.get("tax") or lower.get("tax_amount") or ""
306
+ line_total = lower.get("line_total") or lower.get("line_total".lower()) or lower.get("line total") or amount
307
+
308
+ return {
309
+ "Description": str(desc).strip(),
310
+ "Quantity": float(clean_number(qty)),
311
+ "Unit Price": float(clean_number(unit_price)),
312
+ "Amount": float(clean_number(amount)),
313
+ "Tax": float(clean_number(tax)),
314
+ "Line Total": float(clean_number(line_total))
315
+ }
316
+
317
+ # ----------------- Start mapping -----------------
318
+ # Try parse if pred is a JSON-like string
319
+ parsed = safe_json_load(pred) if isinstance(pred, str) else pred
320
+ if parsed is None and isinstance(pred, str):
321
+ # not parseable -> fallback to empty UI
322
+ parsed = None
323
+
324
+ if parsed is None and not isinstance(pred, dict):
325
+ # nothing we can map
326
+ parsed = pred # will still allow collect_keys if it's dict; else produce empty ui
327
 
328
+ # create empty UI template
329
  ui = {
330
  "Invoice Number": "",
331
  "Invoice Date": "",
 
345
  "Itemized Data": []
346
  }
347
 
348
+ # If parsed is a dict, collect all keys and list-of-dict candidates
349
+ key_map = defaultdict(list) # lowercase-key -> list of values
350
+ list_candidates = [] # list of list-of-dicts found
351
+ if isinstance(parsed, dict):
352
+ collect_keys(parsed, key_map)
353
+ collect_lists_of_dicts(parsed, list_candidates)
354
+ elif isinstance(pred, dict):
355
+ # if parsing failed but original pred is dict, use that
356
+ collect_keys(pred, key_map)
357
+ collect_lists_of_dicts(pred, list_candidates)
358
+
359
+ # Helper to pick first non-empty value from candidate keys
360
+ def pick_first(*candidate_keys):
361
+ for k in candidate_keys:
362
+ lk = k.strip().lower()
363
+ if lk in key_map:
364
+ # pick first non-empty
365
+ for v in key_map[lk]:
366
+ if v is None:
367
+ continue
368
+ # return primitive or string immediately; if dict/list, return as-is
369
+ if isinstance(v, (dict, list)):
370
+ return v
371
+ s = str(v).strip()
372
+ if s != "":
373
+ return s
374
+ return None
 
 
 
 
375
 
376
+ # Map simple scalar fields using the exact keys you provided (plus common close variants)
377
+ ui["Invoice Number"] = pick_first("invoice_no", "invoice_number", "invoiceid", "invoice id") or ""
378
+ ui["Invoice Date"] = pick_first("invoice_date", "date", "invoice date") or ""
379
+ ui["Due Date"] = pick_first("due_date", "due_date", "due") or ""
380
+ ui["Sender Name"] = pick_first("sender_name", "sender") or ""
381
+ ui["Sender Address"] = pick_first("sender_addr", "sender_address", "sender addr") or ""
382
+ ui["Recipient Name"] = pick_first("rcpt_name", "recipient_name", "recipient", "rcpt") or ""
383
+ ui["Recipient Address"] = pick_first("rcpt_addr", "recipient_address", "recipient addr") or ""
384
 
385
+ # bank details: gather keys that start with 'bank_' or exact matches
386
  bank = {}
387
+ for bk in ("bank_name", "bank_acc_no", "bank_account_number", "bank_acc_name", "bank_iban", "bank_swift", "bank_routing", "bank_branch", "iban"):
388
+ val = pick_first(bk, bk.replace("bank_", "")) # allow both 'iban' and 'bank_iban'
389
+ if val:
390
+ # normalize key name to bank_* form
391
+ if bk == "iban":
392
+ bank["bank_iban"] = str(val)
393
+ else:
394
+ bank[bk] = str(val)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  ui["Bank Details"] = bank
396
 
397
+ # summary / totals
398
+ ui["Subtotal"] = clean_number(pick_first("subtotal", "sub_total", "sub total") or 0.0)
399
+ ui["Tax Percentage"] = clean_number(pick_first("tax_rate", "tax_percentage", "tax pct", "tax percentage") or 0.0)
400
+ ui["Total Tax"] = clean_number(pick_first("tax_amount", "tax", "total_tax") or 0.0)
401
+ ui["Total Amount"] = clean_number(pick_first("total_amount", "grand_total", "total", "amount") or 0.0)
402
+ ui["Currency"] = (pick_first("currency") or "").strip()
403
+
404
+ # Item extraction:
405
+ items_rows = []
406
+
407
+ # --- Primary approach: detect explicit list-of-dicts candidates first (unchanged) ---
408
+ def list_looks_like_items(lst):
409
+ if not isinstance(lst, list) or not lst:
410
+ return False
411
+ if not isinstance(lst[0], dict):
412
+ return False
413
+ # check if any expected item key present in first element
414
+ expected = {"descriptions", "description", "desc", "item", "quantity", "qty", "amount", "unit_price", "line_total", "line_total".lower(), "line_total"}
415
+ keys0 = {str(k).strip().lower() for k in lst[0].keys()}
416
+ return bool(expected.intersection(keys0))
417
+
418
+ for cand in list_candidates:
419
+ if list_looks_like_items(cand):
420
+ for it in cand:
421
+ row = map_item_dict(it)
422
+ if row is not None:
423
+ items_rows.append(row)
424
+ # prefer first plausible list
425
+ if items_rows:
426
+ break
427
+
428
+ # --- Secondary approach: if parsed is a single dict that itself contains the item fields
429
+ # This is important because your model sometimes emits a single item as a top-level dict
430
+ # (e.g. {"descriptions":"...","quantity":"1.00","unit_price":"35,000.00",...}).
431
+ # We must map that directly (do NOT rely on finding a list named "items").
432
+ if not items_rows:
433
+ single_candidate_keys = {k.strip().lower() for k in (parsed.keys() if isinstance(parsed, dict) else [])} if isinstance(parsed, dict) else set()
434
+ # item-like keys we expect in the raw model output (explicitly include variants the model uses)
435
+ item_like_keys = {"descriptions", "description", "desc", "item", "quantity", "qty", "unit_price", "unit price", "price", "amount", "line_total", "line total", "line_total", "line_total".lower(), "sku", "tax", "tax_amount"}
436
+ if single_candidate_keys and single_candidate_keys.intersection(item_like_keys):
437
+ # map the parsed dict as a single line item
438
+ single_row = map_item_dict(parsed)
439
+ if single_row is not None:
440
+ items_rows.append(single_row)
441
+
442
+ # 2) If no list-of-dicts found, try to find a single dict anywhere that looks like an item (e.g., 'items': {...} as dict)
443
+ if not items_rows:
444
+ # search key_map values for dicts that have item-like keys
445
+ for k, vals in key_map.items():
446
+ for v in vals:
447
+ if isinstance(v, dict):
448
+ # does this dict have an item-like key?
449
+ lower_keys = {str(x).strip().lower() for x in v.keys()}
450
+ if lower_keys.intersection({"descriptions", "description", "desc", "amount", "line_total", "quantity", "qty", "unit_price"}):
451
+ row = map_item_dict(v)
452
+ if row is not None:
453
+ items_rows.append(row)
454
+ # we don't break because there might be multiple item-like dicts at different keys,
455
+ # but continue scanning to collect all.
456
+ # 3) Last resort: if key_map contains 'descriptions' or 'amount' as scalar but no dict, build a single-item row
457
+ if not items_rows:
458
+ desc = pick_first("descriptions", "description")
459
+ amt = pick_first("amount", "line_total")
460
+ qty = pick_first("quantity", "qty")
461
+ unit_price = pick_first("unit_price", "price")
462
+ if desc or amt or qty or unit_price:
463
+ items_rows.append({
464
+ "Description": str(desc or ""),
465
+ "Quantity": float(clean_number(qty)),
466
+ "Unit Price": float(clean_number(unit_price)),
467
+ "Amount": float(clean_number(amt)),
468
+ "Tax": float(clean_number(pick_first("tax", "tax_amount") or 0.0)),
469
+ "Line Total": float(clean_number(amt or 0.0))
470
+ })
471
+
472
+ ui["Itemized Data"] = items_rows
473
+
474
+ # Also set Sender/Recipient convenience fields
475
+ ui["Sender"] = {"Name": ui["Sender Name"], "Address": ui["Sender Address"]}
476
+ ui["Recipient"] = {"Name": ui["Recipient Name"], "Address": ui["Recipient Address"]}
477
 
478
  return ui
479
 
 
484
  """
485
  Converts nested invoice data into a flat list of rows (one per line item),
486
  with invoice-level and sender/recipient/bank fields repeated in each row.
487
+ This version collects bank details from both:
488
+ - invoice_data.get("Bank Details", {}) (nested dict style)
489
+ - top-level keys in invoice_data that start with 'bank_'
490
+ Ensures the expected bank_* columns always exist in the produced rows.
491
  """
492
+ EXPECTED_BANK_FIELDS = [
493
+ "bank_name",
494
+ "bank_account_number",
495
+ "bank_acc_name",
496
+ "bank_iban",
497
+ "bank_swift",
498
+ "bank_routing",
499
+ "bank_branch"
500
+ ]
501
+
502
  rows = []
503
+ invoice_data = invoice_data or {}
504
 
505
+ # Collect line items (if present)
506
+ line_items = invoice_data.get("Itemized Data", []) or []
507
+
508
+ # Collect bank details from nested dict (if any) and from top-level bank_ keys
509
+ bank_details = {}
510
+ nested = invoice_data.get("Bank Details", {}) or {}
511
+ if isinstance(nested, dict):
512
+ for k, v in nested.items():
513
+ key_name = k if str(k).startswith("bank_") else f"bank_{k}"
514
+ bank_details[key_name] = v
515
+
516
+ # also collect flat top-level bank_* keys (these come from your form_data)
517
+ for k, v in invoice_data.items():
518
+ if isinstance(k, str) and k.lower().startswith("bank_"):
519
+ bank_details[k] = v
520
+
521
+ # ensure all expected bank fields are present (empty string if missing)
522
+ for f in EXPECTED_BANK_FIELDS:
523
+ bank_details.setdefault(f, "")
524
+
525
+ # Helper to create base invoice row (shared for empty-items case and per-item rows)
526
+ def base_invoice_info():
527
+ return {
528
  "Invoice Number": invoice_data.get("Invoice Number", ""),
529
  "Invoice Date": invoice_data.get("Invoice Date", ""),
530
  "Due Date": invoice_data.get("Due Date", ""),
 
533
  "Tax Percentage": invoice_data.get("Tax Percentage", 0.0),
534
  "Total Tax": invoice_data.get("Total Tax", 0.0),
535
  "Total Amount": invoice_data.get("Total Amount", 0.0),
536
+ "Sender Name": invoice_data.get("Sender Name", "") or (invoice_data.get("Sender",{}) or {}).get("Name",""),
537
+ "Sender Address": invoice_data.get("Sender Address", "") or (invoice_data.get("Sender",{}) or {}).get("Address",""),
538
+ "Recipient Name": invoice_data.get("Recipient Name", "") or (invoice_data.get("Recipient",{}) or {}).get("Name",""),
539
+ "Recipient Address": invoice_data.get("Recipient Address", "") or (invoice_data.get("Recipient",{}) or {}).get("Address",""),
540
  }
541
 
542
+ # If no line items, emit a single invoice-only row (with empty item columns)
543
+ if not line_items:
544
+ row = base_invoice_info()
545
+ # include all expected bank fields (consistent names)
546
+ for k in EXPECTED_BANK_FIELDS:
547
+ row[k] = bank_details.get(k, "")
548
+ # item columns (empty)
 
549
  row.update({
550
  "Item Description": "",
551
  "Item Quantity": 0,
 
557
  rows.append(row)
558
  return rows
559
 
560
+ # For each line item, create a row with all invoice context + bank fields
561
  for item in line_items:
562
+ row = base_invoice_info()
563
+ for k in EXPECTED_BANK_FIELDS:
564
+ row[k] = bank_details.get(k, "")
565
+ # try to read canonical item keys (safe .get)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  row.update({
567
+ "Item Description": item.get("Description", "") if isinstance(item, dict) else "",
568
+ "Item Quantity": item.get("Quantity", 0) if isinstance(item, dict) else 0,
569
+ "Item Unit Price": item.get("Unit Price", 0.0) if isinstance(item, dict) else 0.0,
570
+ "Item Amount": item.get("Amount", 0.0) if isinstance(item, dict) else 0.0,
571
+ "Item Tax": item.get("Tax", 0.0) if isinstance(item, dict) else 0.0,
572
+ "Item Line Total": item.get("Line Total", item.get("Amount", 0.0)) if isinstance(item, dict) else 0.0,
573
  })
 
574
  rows.append(row)
575
 
576
  return rows
 
851
 
852
  # ---------- Sender / Recipient ----------
853
  with tabs[1]:
854
+ # Get FLAT top-level keys (NOT nested dictionaries)
855
+ sender_name = form_data.get('Sender Name', '')
856
+ sender_address = form_data.get('Sender Address', '')
857
+ recipient_name = form_data.get('Recipient Name', '')
858
+ recipient_address = form_data.get('Recipient Address', '')
859
+
860
  with st.container():
861
+ sender_name = st.text_input("Sender Name*", value=sender_name, key=f"sender_name_{selected_hash}")
862
+ sender_address = st.text_area("Sender Address*", value=sender_address, key=f"sender_address_{selected_hash}")
863
+ recipient_name = st.text_input("Recipient Name*", value=recipient_name, key=f"recipient_name_{selected_hash}")
864
+ recipient_address = st.text_area("Recipient Address*", value=recipient_address, key=f"recipient_address_{selected_hash}")
865
+
866
  if st.button("⇄ Swap", help="Swap sender and recipient information", key=f"swap_{selected_hash}"):
867
+ # Swap individual fields (NOT nested objects)
868
+ form_data['Sender Name'], form_data['Recipient Name'] = form_data['Recipient Name'], form_data['Sender Name']
869
+ form_data['Sender Address'], form_data['Recipient Address'] = form_data['Recipient Address'], form_data['Sender Address']
870
  st.rerun()
871
 
872
+ # ---------- Bank Details ---------- (FIXED)
873
+ # ---------- Bank Details ---------- (FIXED)
874
  with tabs[2]:
875
+ # Get bank details from nested dictionary
876
+ bank_details = form_data.get("Bank Details", {})
877
+ if not isinstance(bank_details, dict):
878
+ bank_details = {}
879
+
880
+ bank_name = bank_details.get('bank_name', '')
881
+ bank_account_number = bank_details.get('bank_account_number', '')
882
+ bank_acc_name = bank_details.get('bank_acc_name', '')
883
+ bank_iban = bank_details.get('bank_iban', '')
884
+ bank_swift = bank_details.get('bank_swift', '')
885
+ bank_routing = bank_details.get('bank_routing', '')
886
+ bank_branch = bank_details.get('bank_branch', '')
887
+
888
  with st.container():
889
+ bank_name = st.text_input("Bank Name", value=bank_name, key=f"bank_name_{selected_hash}")
890
+ bank_account_number = st.text_input("Account Number", value=bank_account_number, key=f"bank_account_{selected_hash}")
891
+ bank_acc_name = st.text_input("Bank Account Name", value=bank_acc_name, key=f"bank_acc_name_{selected_hash}")
892
+ bank_iban = st.text_input("IBAN", value=bank_iban, key=f"iban_{selected_hash}")
893
+ bank_swift = st.text_input("SWIFT Code", value=bank_swift, key=f"swift_code_{selected_hash}")
894
+ bank_routing = st.text_input("Routing Number", value=bank_routing, key=f"routing_{selected_hash}")
895
+ bank_branch = st.text_input("Branch", value=bank_branch, key=f"branch_{selected_hash}")
896
+
897
+ # Update the nested Bank Details dictionary
898
+ form_data.setdefault("Bank Details", {})
899
+ form_data["Bank Details"]["bank_name"] = bank_name
900
+ form_data["Bank Details"]["bank_account_number"] = bank_account_number
901
+ form_data["Bank Details"]["bank_acc_name"] = bank_acc_name
902
+ form_data["Bank Details"]["bank_iban"] = bank_iban
903
+ form_data["Bank Details"]["bank_swift"] = bank_swift
904
+ form_data["Bank Details"]["bank_routing"] = bank_routing
905
+ form_data["Bank Details"]["bank_branch"] = bank_branch
906
 
907
  # ---------- Line Items ----------
908
+ # ---------- Line Items ----------
909
  with tabs[3]:
910
  editor_key = f"item_editor_{selected_hash}"
911
+ item_rows = form_data.get('Itemized Data', []) or []
912
+
913
+ # --- Normalize item keys produced by the model (e.g. "Item Description") into simple keys ---
914
+ def normalize_item_keys(item):
915
+ if not isinstance(item, dict):
916
+ return {
917
+ "Description": "",
918
+ "Quantity": "",
919
+ "Unit Price": "",
920
+ "Amount": "",
921
+ "Tax": "",
922
+ "Line Total": ""
923
+ }
924
+ mapping = {
925
+ "Item Description": "Description",
926
+ "description": "Description",
927
+ "desc": "Description",
928
+ "Item Quantity": "Quantity",
929
+ "quantity": "Quantity",
930
+ "qty": "Quantity",
931
+ "Item Unit Price": "Unit Price",
932
+ "unit_price": "Unit Price",
933
+ "price": "Unit Price",
934
+ "Item Amount": "Amount",
935
+ "amount": "Amount",
936
+ "Item Tax": "Tax",
937
+ "tax": "Tax",
938
+ "Item Line Total": "Line Total",
939
+ "line_total": "Line Total",
940
+ }
941
+ new = {}
942
+ # map keys
943
+ for k, v in item.items():
944
+ key = mapping.get(k, mapping.get(str(k).lower(), k))
945
+ # normalize to our canonical set where possible
946
+ if key in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
947
+ new[key] = v
948
+ else:
949
+ # store unknowns too so user can see them if needed
950
+ new[k] = v
951
+
952
+ # ensure canonical columns exist (avoid missing columns in DataFrame)
953
+ for kk in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
954
+ if kk not in new:
955
+ new[kk] = ""
956
+
957
+ return new
958
+
959
+ normalized_items = [normalize_item_keys(it) for it in item_rows]
960
+
961
+ # Create DataFrame from normalized items (these columns will be visible)
962
+ df = pd.DataFrame(normalized_items)
963
+
964
+ # Ensure columns exist and are named user-friendly
965
  for col in ["Description", "Quantity", "Unit Price", "Amount", "Tax", "Line Total"]:
966
  if col not in df.columns:
967
  df[col] = ""
968
+
969
  st.write("✏️ Edit line items below. Press Enter or click outside a cell to confirm each edit.")
970
  edited_df = st.data_editor(
971
  df,
 
976
  if len(edited_df) == 0:
977
  st.info("No line items found in the invoice.")
978
 
979
+
980
  # Save button (per file)
981
  if st.button("💾 Save Edits for This File", key=f"save_{selected_hash}"):
982
  # Update line items from data editor