Spaces:
Running
Running
Update app.py
Browse files
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>סכום לפני מע
|
| 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>מע
|
| 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 |
-
|
| 339 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
-
def render_parse_panel(parse, completed):
|
| 342 |
-
"""'Behind the scenes' panel
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 395 |
-
|
| 396 |
-
"
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
"""
|
| 402 |
-
|
| 403 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
|
|
|
| 429 |
try:
|
| 430 |
-
completed = core.complete(
|
| 431 |
except Exception as e:
|
| 432 |
-
return (f"⚠️ שגיאה
|
|
|
|
|
|
|
|
|
|
| 433 |
|
| 434 |
return ("✅ המסמך הופק בהצלחה.",
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 471 |
</div>
|
| 472 |
</div>""")
|
| 473 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
note_input = gr.Textbox(
|
| 475 |
-
label="
|
| 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 |
-
# ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
submit_btn.click(
|
| 505 |
-
fn=
|
| 506 |
-
inputs=[note_input, doc_choice],
|
| 507 |
-
outputs=[status_output, parse_output, doc_output, recs_output],
|
| 508 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
clear_btn.click(
|
| 510 |
-
fn=
|
| 511 |
-
outputs=[note_input, doc_choice,
|
| 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(
|
|
|
|
|
|
|
| 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>סכום לפני מע"מ</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>מע"מ {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 |
+
פתק הכנסה חופשי בעברית → מסמך פיסקלי ישראלי תקין
|
| 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)
|