yonilev commited on
Commit
49e610e
·
verified ·
1 Parent(s): c3728fa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +307 -59
app.py CHANGED
@@ -213,7 +213,7 @@ def recommend(query_text, k=3):
213
  # ═════════════════════════════════════════════════════════════════════════════
214
  # 5. HTML renderers
215
  # ═════════════════════════════════════════════════════════════════════════════
216
- def render_document(c):
217
  issuer = c.get("issuer", {})
218
  client = c.get("client", {})
219
  lines = c.get("lines", [])
@@ -289,15 +289,16 @@ def render_document(c):
289
  </table>
290
 
291
  <div style="background:#1e293b;border-radius:10px;padding:16px 18px;">
 
292
  <div style="display:flex;justify-content:space-between;font-size:13px;color:#64748b;margin-bottom:8px;">
293
- <span>סכום לפני מע"מ</span>
294
  <span style="color:#94a3b8;">₪{c.get("subtotal",0):,.2f}</span>
295
  </div>
296
  <div style="display:flex;justify-content:space-between;font-size:13px;color:#64748b;
297
  padding-bottom:12px;border-bottom:1px solid #334155;margin-bottom:12px;">
298
- <span>מע"מ {vat_pct}%</span>
299
  <span style="color:#94a3b8;">₪{c.get("vat_amount",0):,.2f}</span>
300
- </div>
301
  <div style="display:flex;justify-content:space-between;align-items:baseline;">
302
  <span style="font-size:15px;font-weight:600;color:#e2e8f0;">סה"כ לתשלום</span>
303
  <span style="font-size:24px;font-weight:700;color:#2dd4bf;">₪{c.get("total",0):,.2f}</span>
@@ -335,13 +336,47 @@ def render_recommendations(recs):
335
  # ═════════════════════════════════════════════════════════════════════════════
336
  # 6. Pipeline
337
  # ═════════════════════════════════════════════════════════════════════════════
338
- DEMO_ISSUER = {"name": "מים שקטים", "tax_id": "962569844", "status": "authorized_dealer"}
339
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
- def render_parse_panel(parse, completed):
342
- """'Behind the scenes' panel: what the MODEL extracted vs what the
343
- DETERMINISTIC engine computed. Demonstrates the model/rules separation —
344
- the model predicts only the extraction; all arithmetic is rule-based."""
345
  items = parse.get("items", [])
346
  item_rows = "".join(
347
  f'<div style="display:flex;justify-content:space-between;font-size:12px;'
@@ -361,13 +396,17 @@ def render_parse_panel(parse, completed):
361
  alloc_line = ""
362
  if completed.get("allocation_required"):
363
  alloc_line = ('<div style="font-size:12px;color:#334155;padding:3px 0;">'
364
- 'מספר הקצאה <span style="color:#0d9488;">✓ נדרש</span> '
365
- '(תנאי: חשבונית · עסק · ≥ סף)</div>')
 
 
 
 
 
366
 
367
  return f"""
368
  <div dir="rtl" style="font-family:'Segoe UI',Arial,sans-serif;max-width:660px;
369
  margin:14px auto 0;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
370
-
371
  <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
372
  <div style="font-size:11px;font-weight:700;color:#0d9488;letter-spacing:.05em;
373
  text-transform:uppercase;margin-bottom:10px;">🧠 המודל חילץ</div>
@@ -376,12 +415,10 @@ def render_parse_panel(parse, completed):
376
  <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
377
  padding-top:8px;">gemma-2-2b + LoRA · חוזה רק <code>parse</code></div>
378
  </div>
379
-
380
  <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
381
  <div style="font-size:11px;font-weight:700;color:#7c3aed;letter-spacing:.05em;
382
  text-transform:uppercase;margin-bottom:10px;">⚙️ המנוע חישב</div>
383
- <div style="font-size:12px;color:#334155;padding:3px 0;">מע"מ {int(round(completed.get("vat_rate",0)*100))}% ·
384
- ₪{completed.get("vat_amount",0):,.2f}</div>
385
  <div style="font-size:12px;color:#334155;padding:3px 0;">סה"כ ₪{completed.get("total",0):,.2f}</div>
386
  <div style="font-size:12px;color:#334155;padding:3px 0;">מס׳ מסמך {completed.get("serial_number","")}</div>
387
  {alloc_line}
@@ -391,50 +428,227 @@ def render_parse_panel(parse, completed):
391
  </div>"""
392
 
393
 
394
- _DOC_CHOICE_MAP = {
395
- "קבלה": "receipt",
396
- "חשבונית מס": "tax_invoice",
397
- "חשבונית מס וקבלה": "tax_invoice_receipt",
398
- }
399
-
400
- def generate(note, doc_choice):
401
- """Single-shot, stateless pipeline:
402
- note (Textbox) + doc_choice (Radio) → (status, parse_html, doc_html, recs_html).
403
- No gr.State, no second handler — the input value cannot be lost.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  """
405
  note = (note or "").strip()
406
  if not note:
407
- return "✏️ הקלד הערת הכנסה ולחץ הפק מסמך.", "", "", ""
 
408
 
409
  _lazy_init()
410
  parse = model_parse(note)
411
 
412
  if parse is None or _missing_fields(parse) == ["parse_failed"]:
413
  return ("🤔 לא הצלחתי לחלץ פרטים. נסח מחדש — לדוגמה: "
414
- "'קיבלתי 500₪ ממשה על ייעוץ'.", "", "", "")
415
-
416
- # ---- agentic clarification: only the doc-type can be genuinely ambiguous ----
417
- answers = {}
418
- if doc_choice and doc_choice in _DOC_CHOICE_MAP:
419
- answers["doc_type"] = doc_choice # user picked explicitly
420
- elif not parse.get("doc_type"):
421
- # model didn't determine it and user left it on auto → ask
422
- hint = ""
423
- if _needs_allocation_clarification(parse):
424
- hint = " (סכום גבוה ללקוח עסקי — לרוב חשבונית מס וקבלה)"
425
- return (f"❓ צריך פרט אחד נוסף: **בחר סוג מסמך** מהרשימה למעלה{hint} "
426
- "ולחץ ��פק מסמך שוב.", "", "", "")
427
-
428
- final = apply_answers(parse, answers)
 
429
  try:
430
- completed = core.complete(DEMO_ISSUER, final, _STATE["rng"])
431
  except Exception as e:
432
- return (f"⚠️ שגיאה בעיבוד: {e}", "", "", "")
 
 
 
433
 
434
  return ("✅ המסמך הופק בהצלחה.",
435
- render_parse_panel(final, completed),
436
- render_document(completed),
437
- render_recommendations(recommend(note)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
 
439
 
440
  # ═════════════════════════════════════════════════════════════════════════════
@@ -467,12 +681,19 @@ with gr.Blocks(title="Text2Receipt", theme=gr.themes.Soft(
467
  <div style="font-size:30px;margin-bottom:6px;">🧾</div>
468
  <div style="font-size:26px;font-weight:800;color:#0d9488;letter-spacing:-.4px;">Text2Receipt</div>
469
  <div style="font-size:14px;color:#475569;margin-top:6px;">
470
- הערת הכנסה חופשית בעברית &nbsp;→&nbsp; מסמך פיסקלי ישראלי תקין
471
  </div>
472
  </div>""")
473
 
 
 
 
 
 
 
 
474
  note_input = gr.Textbox(
475
- label="הערת הכנסה", lines=2,
476
  placeholder='לדוגמה: קיבלתי 1,200 ש"ח ממשה כהן על ייעוץ עסקי',
477
  rtl=True, autofocus=True,
478
  )
@@ -480,15 +701,29 @@ with gr.Blocks(title="Text2Receipt", theme=gr.themes.Soft(
480
  doc_choice = gr.Radio(
481
  ["זיהוי אוטומטי", "קבלה", "חשבונית מס", "חשבונית מס וקבלה"],
482
  value="זיהוי אוטומטי", label="סוג מסמך",
 
483
  )
484
 
485
  with gr.Row():
486
- submit_btn = gr.Button("הפק מסמך ⚡", variant="primary", scale=2,
487
- elem_classes=["btn-send"])
488
  clear_btn = gr.Button("נקה 🗑", scale=1)
489
 
490
  status_output = gr.Markdown("")
491
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  gr.HTML('<div dir="rtl" style="font-size:11px;color:#94a3b8;margin:10px 0 6px;'
493
  'letter-spacing:.07em;text-transform:uppercase;">דוגמאות מהירות</div>')
494
  with gr.Row():
@@ -500,16 +735,27 @@ with gr.Blocks(title="Text2Receipt", theme=gr.themes.Soft(
500
  doc_output = gr.HTML()
501
  recs_output = gr.HTML()
502
 
503
- # ── single, stateless handler — value cannot be lost ──
 
 
 
 
 
 
504
  submit_btn.click(
505
- fn=generate,
506
- inputs=[note_input, doc_choice],
507
- outputs=[status_output, parse_output, doc_output, recs_output],
508
  )
 
 
 
 
 
509
  clear_btn.click(
510
- fn=lambda: ("", "זיהוי אוטומטי", "", "", "", ""),
511
- outputs=[note_input, doc_choice, status_output,
512
- parse_output, doc_output, recs_output],
513
  )
514
 
515
  # ═════════════════════════════════════════════════════════════════════════════
@@ -566,7 +812,9 @@ def _discord_pipeline(raw_text: str) -> str:
566
  return "❌ לא הצלחתי לחלץ את הפרטים. נסה לנסח מחדש."
567
  final = apply_answers(parse, {}) # apply_answers fills safe defaults
568
  try:
569
- completed = core.complete(DEMO_ISSUER, final, _STATE["rng"])
 
 
570
  except Exception as e:
571
  return f"❌ שגיאה בעיבוד: {e}"
572
  return _format_doc_for_discord(completed)
 
213
  # ═════════════════════════════════════════════════════════════════════════════
214
  # 5. HTML renderers
215
  # ═════════════════════════════════════════════════════════════════════════════
216
+ def render_document(c, show_vat=True):
217
  issuer = c.get("issuer", {})
218
  client = c.get("client", {})
219
  lines = c.get("lines", [])
 
289
  </table>
290
 
291
  <div style="background:#1e293b;border-radius:10px;padding:16px 18px;">
292
+ {"" if not show_vat else f'''
293
  <div style="display:flex;justify-content:space-between;font-size:13px;color:#64748b;margin-bottom:8px;">
294
+ <span>סכום לפני מע&quot;מ</span>
295
  <span style="color:#94a3b8;">₪{c.get("subtotal",0):,.2f}</span>
296
  </div>
297
  <div style="display:flex;justify-content:space-between;font-size:13px;color:#64748b;
298
  padding-bottom:12px;border-bottom:1px solid #334155;margin-bottom:12px;">
299
+ <span>מע&quot;מ {vat_pct}%</span>
300
  <span style="color:#94a3b8;">₪{c.get("vat_amount",0):,.2f}</span>
301
+ </div>'''}
302
  <div style="display:flex;justify-content:space-between;align-items:baseline;">
303
  <span style="font-size:15px;font-weight:600;color:#e2e8f0;">סה"כ לתשלום</span>
304
  <span style="font-size:24px;font-weight:700;color:#2dd4bf;">₪{c.get("total",0):,.2f}</span>
 
336
  # ═════════════════════════════════════════════════════════════════════════════
337
  # 6. Pipeline
338
  # ═════════════════════════════════════════════════════════════════════════════
339
+ DEMO_ISSUER_BASE = {"name": "מים שקטים", "tax_id": "962569844"}
340
+
341
+ # ── VAT cue patterns ─────────────────────────────────────────────────────────
342
+ import re as _re
343
+
344
+ _GROSS_PATTERNS = [r"כולל\s*מע[\"״]?מ", r"כולל\s*מס", r"גרוס", r"ברוטו",
345
+ r"מחיר\s*כולל"]
346
+ _NET_PATTERNS = [r"לפני\s*מע[\"״]?מ", r"\+\s*מע[\"״]?מ", r"בתוספת\s*מע[\"״]?מ",
347
+ r"פלוס\s*מע[\"״]?מ", r"נטו", r"לא\s*כולל\s*מע[\"״]?מ",
348
+ r"בלי\s*מע[\"״]?מ", r"לפני\s*מס"]
349
+ _EXEMPT_PATTERNS= [r"עוסק\s*פטור", r"פטור\s*ממע[\"״]?מ", r"ללא\s*מע[\"״]?מ"]
350
+ _PAYMENT_RECV = [r"קיבלתי", r"שילמ", r"העביר", r"שולם", r"נכנס", r"הועבר",
351
+ r"ביט", r"פיי", r"מזומן"]
352
+
353
+ def _text_has(text, patterns):
354
+ t = text.lower()
355
+ return any(_re.search(p, t) for p in patterns)
356
+
357
+ def _detect_date_from_text(text):
358
+ """Try to extract a date from free text. Returns ISO string or None."""
359
+ patterns = [
360
+ (r"(\d{1,2})[/\-\.](\d{1,2})[/\-\.](\d{2,4})", "dmy"),
361
+ (r"(\d{4})[/\-\.](\d{1,2})[/\-\.](\d{1,2})", "ymd"),
362
+ ]
363
+ for pat, order in patterns:
364
+ m = _re.search(pat, text)
365
+ if m:
366
+ try:
367
+ g = [int(x) for x in m.groups()]
368
+ if order == "dmy":
369
+ d, mo, y = g
370
+ if y < 100: y += 2000
371
+ else:
372
+ y, mo, d = g
373
+ return f"{y:04d}-{mo:02d}-{d:02d}"
374
+ except Exception:
375
+ pass
376
+ return None
377
 
378
+ def render_parse_panel(parse, completed, show_vat=True):
379
+ """'Behind the scenes' panel model extraction vs deterministic computation."""
 
 
380
  items = parse.get("items", [])
381
  item_rows = "".join(
382
  f'<div style="display:flex;justify-content:space-between;font-size:12px;'
 
396
  alloc_line = ""
397
  if completed.get("allocation_required"):
398
  alloc_line = ('<div style="font-size:12px;color:#334155;padding:3px 0;">'
399
+ 'מספר הקצאה <span style="color:#0d9488;">✓ נדרש</span></div>')
400
+
401
+ vat_line = (f'<div style="font-size:12px;color:#334155;padding:3px 0;">'
402
+ f'מע"מ {int(round(completed.get("vat_rate",0)*100))}% · '
403
+ f'₪{completed.get("vat_amount",0):,.2f}</div>'
404
+ if show_vat else
405
+ '<div style="font-size:12px;color:#94a3b8;padding:3px 0;">ללא פירוק מע"מ (קבלה)</div>')
406
 
407
  return f"""
408
  <div dir="rtl" style="font-family:'Segoe UI',Arial,sans-serif;max-width:660px;
409
  margin:14px auto 0;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
 
410
  <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
411
  <div style="font-size:11px;font-weight:700;color:#0d9488;letter-spacing:.05em;
412
  text-transform:uppercase;margin-bottom:10px;">🧠 המודל חילץ</div>
 
415
  <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
416
  padding-top:8px;">gemma-2-2b + LoRA · חוזה רק <code>parse</code></div>
417
  </div>
 
418
  <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
419
  <div style="font-size:11px;font-weight:700;color:#7c3aed;letter-spacing:.05em;
420
  text-transform:uppercase;margin-bottom:10px;">⚙️ המנוע חישב</div>
421
+ {vat_line}
 
422
  <div style="font-size:12px;color:#334155;padding:3px 0;">סה"כ ₪{completed.get("total",0):,.2f}</div>
423
  <div style="font-size:12px;color:#334155;padding:3px 0;">מס׳ מסמך {completed.get("serial_number","")}</div>
424
  {alloc_line}
 
428
  </div>"""
429
 
430
 
431
+ def decide_fiscal(text, issuer_status, parse, explicit_doc_choice,
432
+ clarify_vat_basis=None, clarify_client_tax_id=None):
433
+ """
434
+ Returns (issuer_dict, final_parse, questions_dict) where:
435
+ - issuer_dict: ready for core.complete()
436
+ - final_parse: enriched parse ready for core.complete()
437
+ - questions_dict: {field: question_text} for any remaining mandatory gaps
438
+ """
439
+ questions = {}
440
+ today = _today()
441
+
442
+ # ── 1. Issuer ────────────────────────────────────────────────────────────
443
+ if not issuer_status:
444
+ questions["issuer_status"] = "מי אתה? בחר סטטוס מנפיק למעלה (עוסק פטור / עוסק מורשה)."
445
+ exempt = (issuer_status == "פטור")
446
+ issuer = {**DEMO_ISSUER_BASE,
447
+ "status": "exempt_dealer" if exempt else "authorized_dealer",
448
+ "is_company": False}
449
+
450
+ # ── 2. Mandatory content fields ──────────────────────────────────────────
451
+ if not parse.get("client_name", "").strip():
452
+ questions["client_name"] = "שם הלקוח / המשלם?"
453
+ if not parse.get("items"):
454
+ questions["items"] = "מה השירות/המוצר ובאיזה סכום? (למשל: ייעוץ עסקי ₪500)"
455
+
456
+ # ── 3. Date: extract from text → today ───────────────────────────────────
457
+ detected_date = _detect_date_from_text(text) or today.isoformat()
458
+ parse = dict(parse)
459
+ parse.setdefault("date", detected_date)
460
+
461
+ # ── 4. Doc type decision ─────────────────────────────────────────────────
462
+ is_gross = _text_has(text, _GROSS_PATTERNS) or clarify_vat_basis == "כולל מע״מ"
463
+ is_net = _text_has(text, _NET_PATTERNS) or clarify_vat_basis == "לפני מע״מ"
464
+ has_vat_cue = is_gross or is_net
465
+
466
+ if exempt:
467
+ # עוסק פטור → always receipt, no VAT
468
+ doc_type = "receipt"
469
+ amount_basis = "net" # irrelevant (vat_rate = 0)
470
+ elif explicit_doc_choice and explicit_doc_choice != "זיהוי אוטומטי":
471
+ doc_type = {
472
+ "קבלה": "receipt",
473
+ "חשבונית מס": "tax_invoice",
474
+ "חשבונית מס וקבלה": "tax_invoice_receipt",
475
+ }.get(explicit_doc_choice, "receipt")
476
+ # amount basis for tax-invoice types
477
+ if doc_type != "receipt":
478
+ if is_gross: amount_basis = "gross"
479
+ elif is_net: amount_basis = "net"
480
+ elif clarify_vat_basis is None:
481
+ questions["vat_basis"] = (
482
+ f"הסכום {'₪' + str(int(sum(i.get('unit_price',0)*i.get('quantity',1) for i in parse.get('items',[]))))+ ' ' if parse.get('items') else ''}—"
483
+ f" **כולל מע״מ** או **לפני מע״מ**?")
484
+ amount_basis = "net" # placeholder until answered
485
+ else:
486
+ amount_basis = "net"
487
+ else:
488
+ amount_basis = "net"
489
+ else:
490
+ # Auto mode
491
+ if has_vat_cue:
492
+ doc_type = "tax_invoice_receipt"
493
+ amount_basis = "gross" if is_gross else "net"
494
+ else:
495
+ # No VAT mention → receipt without VAT breakdown
496
+ doc_type = "receipt"
497
+ amount_basis = "net"
498
+
499
+ # ── 5. Payment received vs. demanded ─────────────────────────────────────
500
+ # חשבונית מס (no קבלה) = money not yet received — explicit choice only
501
+ # Auto never produces pure tax_invoice (always /קבלה if VAT)
502
+
503
+ # ── 6. Client tax ID for allocation ──────────────────────────────────────
504
+ if doc_type in ("tax_invoice", "tax_invoice_receipt") and not exempt:
505
+ subtotal = sum(
506
+ it.get("unit_price", 0) * it.get("quantity", 1)
507
+ for it in parse.get("items", []))
508
+ threshold = core.allocation_threshold_for_date(
509
+ _dt.date.fromisoformat(parse["date"]))
510
+ client_biz = parse.get("client_is_business", False)
511
+ if client_biz and subtotal >= threshold and not parse.get("client_tax_id"):
512
+ if clarify_client_tax_id:
513
+ parse["client_tax_id"] = clarify_client_tax_id.strip()
514
+ else:
515
+ questions["client_tax_id"] = (
516
+ f"ח.פ. הלקוח — נדרש למספר הקצאה (עסקה ≥ ₪{threshold:,})")
517
+
518
+ parse["doc_type"] = doc_type
519
+ parse["amount_basis"] = amount_basis
520
+ parse.setdefault("payment_method", "bank_transfer")
521
+ parse.setdefault("currency", "ILS")
522
+ parse.setdefault("client_tax_id", None)
523
+ parse.setdefault("client_is_business", False)
524
+ parse.setdefault("client_name", "—")
525
+ parse.setdefault("items", [])
526
+
527
+ return issuer, parse, questions
528
+
529
+
530
+ def _build_clarification_message(questions: dict, note_text: str) -> str:
531
+ """Build ONE unified clarification message listing all missing fields."""
532
+ lines = []
533
+ for key, q in questions.items():
534
+ if key == "issuer_status":
535
+ lines.append(f"👤 {q}")
536
+ elif key == "client_name":
537
+ lines.append(f"👤 **שם לקוח:** {q}")
538
+ elif key == "items":
539
+ lines.append(f"📦 **פריטים:** {q}")
540
+ elif key == "vat_basis":
541
+ lines.append(f"💰 **בסיס מע״מ:** {q}")
542
+ elif key == "client_tax_id":
543
+ lines.append(f"🔖 **הקצאה:** {q}")
544
+ else:
545
+ lines.append(f"• {q}")
546
+
547
+ fields_text = "\n".join(lines)
548
+ return (f"❓ **כדי להפיק את המסמך, חסר מידע:**\n\n"
549
+ f"{fields_text}\n\n"
550
+ f"הוסף את הפרטים לפתק ולחץ **הפק מסמך** שוב.")
551
+
552
+
553
+ def generate(note, issuer_radio, doc_choice,
554
+ clarify_vat, clarify_tax_id):
555
+ """
556
+ Single-shot, stateless pipeline. Returns:
557
+ (status_md, clarify_html, parse_html, doc_html, recs_html,
558
+ clarify_visible)
559
  """
560
  note = (note or "").strip()
561
  if not note:
562
+ return ("✏️ כתוב פתק הכנסה ולחץ הפק מסמך.",
563
+ "", "", "", "", False)
564
 
565
  _lazy_init()
566
  parse = model_parse(note)
567
 
568
  if parse is None or _missing_fields(parse) == ["parse_failed"]:
569
  return ("🤔 לא הצלחתי לחלץ פרטים. נסח מחדש — לדוגמה: "
570
+ "'קיבלתי 500₪ ממשה על ייעוץ'.",
571
+ "", "", "", "", False)
572
+
573
+ issuer_status = issuer_radio if issuer_radio in ("פטור", "מורשה") else None
574
+
575
+ # apply clarification answers from the note text itself
576
+ cvat = clarify_vat if clarify_vat != "לא צוין" else None
577
+ ctax = clarify_tax_id.strip() if clarify_tax_id and clarify_tax_id.strip() else None
578
+
579
+ issuer, final, questions = decide_fiscal(
580
+ note, issuer_status, parse, doc_choice, cvat, ctax)
581
+
582
+ if questions:
583
+ msg = _build_clarification_message(questions, note)
584
+ return (msg, "", "", "", "", True) # show clarification fields
585
+
586
  try:
587
+ completed = core.complete(issuer, final, _STATE["rng"])
588
  except Exception as e:
589
+ return (f"⚠️ שגיאה: {e}", "", "", "", "", False)
590
+
591
+ # override VAT display for receipt (no breakdown)
592
+ show_vat = (final["doc_type"] != "receipt")
593
 
594
  return ("✅ המסמך הופק בהצלחה.",
595
+ "",
596
+ render_parse_panel(final, completed, show_vat),
597
+ render_document(completed, show_vat),
598
+ render_recommendations(recommend(note)),
599
+ False)
600
+
601
+ """'Behind the scenes' panel: what the MODEL extracted vs what the
602
+ DETERMINISTIC engine computed. Demonstrates the model/rules separation —
603
+ the model predicts only the extraction; all arithmetic is rule-based."""
604
+ items = parse.get("items", [])
605
+ item_rows = "".join(
606
+ f'<div style="display:flex;justify-content:space-between;font-size:12px;'
607
+ f'color:#334155;padding:3px 0;"><span>{it.get("description","")}</span>'
608
+ f'<span style="color:#64748b;">{it.get("quantity",1)} × ₪{it.get("unit_price",0):,.0f}</span></div>'
609
+ for it in items)
610
+
611
+ chips = (
612
+ f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
613
+ f'border-radius:999px;padding:2px 9px;font-size:11px;">לקוח: {parse.get("client_name","—")}</span>'
614
+ f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
615
+ f'border-radius:999px;padding:2px 9px;font-size:11px;">'
616
+ f'{"עסק" if parse.get("client_is_business") else "פרטי"}</span>'
617
+ f'<span style="background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;'
618
+ f'border-radius:999px;padding:2px 9px;font-size:11px;">{len(items)} פריטים</span>')
619
+
620
+ alloc_line = ""
621
+ if completed.get("allocation_required"):
622
+ alloc_line = ('<div style="font-size:12px;color:#334155;padding:3px 0;">'
623
+ 'מספר הקצאה <span style="color:#0d9488;">✓ נדרש</span> '
624
+ '(תנאי: חשבונית · עסק · ≥ סף)</div>')
625
+
626
+ return f"""
627
+ <div dir="rtl" style="font-family:'Segoe UI',Arial,sans-serif;max-width:660px;
628
+ margin:14px auto 0;display:grid;grid-template-columns:1fr 1fr;gap:12px;">
629
+
630
+ <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
631
+ <div style="font-size:11px;font-weight:700;color:#0d9488;letter-spacing:.05em;
632
+ text-transform:uppercase;margin-bottom:10px;">🧠 המודל חילץ</div>
633
+ <div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:10px;">{chips}</div>
634
+ {item_rows}
635
+ <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
636
+ padding-top:8px;">gemma-2-2b + LoRA · חוזה רק <code>parse</code></div>
637
+ </div>
638
+
639
+ <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px 16px;">
640
+ <div style="font-size:11px;font-weight:700;color:#7c3aed;letter-spacing:.05em;
641
+ text-transform:uppercase;margin-bottom:10px;">⚙️ המנוע חישב</div>
642
+ <div style="font-size:12px;color:#334155;padding:3px 0;">מע"מ {int(round(completed.get("vat_rate",0)*100))}% ·
643
+ ₪{completed.get("vat_amount",0):,.2f}</div>
644
+ <div style="font-size:12px;color:#334155;padding:3px 0;">סה"כ ₪{completed.get("total",0):,.2f}</div>
645
+ <div style="font-size:12px;color:#334155;padding:3px 0;">מס׳ מסמך {completed.get("serial_number","")}</div>
646
+ {alloc_line}
647
+ <div style="font-size:10px;color:#94a3b8;margin-top:10px;border-top:1px dashed #e2e8f0;
648
+ padding-top:8px;">דטרמיניסטי · אפס הזיות בחשבון</div>
649
+ </div>
650
+ </div>"""
651
+
652
 
653
 
654
  # ═════════════════════════════════════════════════════════════════════════════
 
681
  <div style="font-size:30px;margin-bottom:6px;">🧾</div>
682
  <div style="font-size:26px;font-weight:800;color:#0d9488;letter-spacing:-.4px;">Text2Receipt</div>
683
  <div style="font-size:14px;color:#475569;margin-top:6px;">
684
+ פתק הכנסה חופשי בעברית &nbsp;→&nbsp; מסמך פיסקלי ישראלי תקין
685
  </div>
686
  </div>""")
687
 
688
+ # ── Who am I (required, no default) ─────────────────────────────────────
689
+ issuer_radio = gr.Radio(
690
+ ["פטור", "מורשה"], value=None,
691
+ label="מי אני (סטטוס מנפיק)",
692
+ info="עוסק פטור = ללא מע\"מ · עוסק מורשה = עם מע\"מ",
693
+ )
694
+
695
  note_input = gr.Textbox(
696
+ label="פתק / תוכן ההכנסה", lines=2,
697
  placeholder='לדוגמה: קיבלתי 1,200 ש"ח ממשה כהן על ייעוץ עסקי',
698
  rtl=True, autofocus=True,
699
  )
 
701
  doc_choice = gr.Radio(
702
  ["זיהוי אוטומטי", "קבלה", "חשבונית מס", "חשבונית מס וקבלה"],
703
  value="זיהוי אוטומטי", label="סוג מסמך",
704
+ info="חשבונית מס = דרישת תשלום (טרם שולם) · חשבונית מס וקבלה = שניים באחד (שולם)",
705
  )
706
 
707
  with gr.Row():
708
+ submit_btn = gr.Button("הפק מסמך ⚡", variant="primary", scale=2)
 
709
  clear_btn = gr.Button("נקה 🗑", scale=1)
710
 
711
  status_output = gr.Markdown("")
712
 
713
+ # ── Clarification fields — visible only when needed ──────────────────────
714
+ with gr.Group(visible=False) as clarify_group:
715
+ gr.HTML('<div dir="rtl" style="font-size:12px;font-weight:600;color:#7c3aed;'
716
+ 'margin-bottom:8px;">📋 פרטים נוספים נדרשים</div>')
717
+ clarify_vat = gr.Radio(
718
+ ["כולל מע״מ", "לפני מע״מ", "לא צוין"],
719
+ value="לא צוין", label="הסכום המצוין בפתק",
720
+ )
721
+ clarify_tax_id = gr.Textbox(
722
+ label="ח.פ. / ת.ז. לקוח (נדרש למספר הקצאה)",
723
+ placeholder="למשל: 514123458",
724
+ value="",
725
+ )
726
+
727
  gr.HTML('<div dir="rtl" style="font-size:11px;color:#94a3b8;margin:10px 0 6px;'
728
  'letter-spacing:.07em;text-transform:uppercase;">דוגמאות מהירות</div>')
729
  with gr.Row():
 
735
  doc_output = gr.HTML()
736
  recs_output = gr.HTML()
737
 
738
+ # ── Handlers ─────────────────────────────────────────────────────────────
739
+ def _on_generate(note, issuer_r, doc_c, cvat, ctax):
740
+ status, chtml, phtml, dhtml, rhtml, show_clarify = generate(
741
+ note, issuer_r, doc_c, cvat, ctax)
742
+ return (status, phtml, dhtml, rhtml,
743
+ gr.update(visible=show_clarify))
744
+
745
  submit_btn.click(
746
+ fn=_on_generate,
747
+ inputs=[note_input, issuer_radio, doc_choice, clarify_vat, clarify_tax_id],
748
+ outputs=[status_output, parse_output, doc_output, recs_output, clarify_group],
749
  )
750
+
751
+ def _on_clear():
752
+ return ("", None, "זיהוי אוטומטי", "לא צוין", "", "", "", "", "",
753
+ gr.update(visible=False))
754
+
755
  clear_btn.click(
756
+ fn=_on_clear,
757
+ outputs=[note_input, issuer_radio, doc_choice, clarify_vat, clarify_tax_id,
758
+ status_output, parse_output, doc_output, recs_output, clarify_group],
759
  )
760
 
761
  # ═════════════════════════════════════════════════════════════════════════════
 
812
  return "❌ לא הצלחתי לחלץ את הפרטים. נסה לנסח מחדש."
813
  final = apply_answers(parse, {}) # apply_answers fills safe defaults
814
  try:
815
+ completed = core.complete(
816
+ {**DEMO_ISSUER_BASE, "status": "authorized_dealer", "is_company": False},
817
+ final, _STATE["rng"])
818
  except Exception as e:
819
  return f"❌ שגיאה בעיבוד: {e}"
820
  return _format_doc_for_discord(completed)