Spaces:
Sleeping
Sleeping
| # imports | |
| import gradio as gr | |
| from invoice import ( | |
| do_parse, do_generate, load_pricing, compute_totals, | |
| today_str, new_invoice_code, logo_data_uri | |
| ) | |
| import os, re, shutil, uuid | |
| import io | |
| from pathlib import Path | |
| PREVIEW_LOGO = logo_data_uri() # empty string if no asset file is present | |
| # ---------- helpers ---------- | |
| TMP_DIR = Path("/tmp") | |
| def _stage_file(maybe_bytes_or_path, filename: str | None): | |
| """ | |
| Accepts: file path (str/Path) OR bytes-like (bytes/BytesIO) OR None. | |
| Returns: string path that exists on disk, or None. | |
| """ | |
| if not maybe_bytes_or_path: | |
| return None | |
| # If it's already a path and exists, use it | |
| if isinstance(maybe_bytes_or_path, (str, Path)): | |
| p = Path(maybe_bytes_or_path) | |
| return str(p) if p.exists() and p.is_file() else None | |
| # If it's bytes / BytesIO, write it | |
| data = None | |
| if isinstance(maybe_bytes_or_path, bytes): | |
| data = maybe_bytes_or_path | |
| elif isinstance(maybe_bytes_or_path, io.BytesIO): | |
| data = maybe_bytes_or_path.getvalue() | |
| if data is None: | |
| return None | |
| # pick a safe name | |
| safe = (filename or "file.bin").replace("/", "_") | |
| out = TMP_DIR / safe | |
| out.write_bytes(data) | |
| return str(out) if out.exists() else None | |
| def _stage_for_download(path): | |
| """ | |
| Copy `path` to /tmp with a safe filename so Hugging Face Spaces can serve it. | |
| Returns the new path or None. | |
| """ | |
| if not path or not os.path.isfile(path): | |
| return None | |
| base = os.path.basename(path) | |
| root, ext = os.path.splitext(base) | |
| # sanitize: keep only safe chars | |
| safe_root = re.sub(r'[^A-Za-z0-9_.-]+', '_', root).strip('_') or f"file_{uuid.uuid4().hex[:8]}" | |
| safe_ext = ext if ext else "" | |
| safe = f"/tmp/{safe_root}{safe_ext}" | |
| try: | |
| if os.path.abspath(path) != os.path.abspath(safe): | |
| shutil.copyfile(path, safe) | |
| return safe | |
| except Exception: | |
| return None | |
| def _unit(modules, unit_override): | |
| tiers = load_pricing() | |
| try: | |
| return float(unit_override) if unit_override not in (None, "", "auto") else tiers.get(int(modules or 1), 650.0) | |
| except Exception: | |
| return tiers.get(int(modules or 1), 650.0) | |
| def _preview(inv_type, inv_date, inv_number, modules, audit_type, | |
| s_name, s_addr, s_email, s_phone, s_audit_date, unit_override): | |
| modules = int(modules or 1) | |
| unit = _unit(modules, unit_override) | |
| admin, subtotal, gst, total = compute_totals(modules, inv_type, unit) | |
| inv_date = inv_date or today_str() | |
| if not inv_number or inv_number == "Auto": | |
| inv_number = new_invoice_code(inv_type) | |
| customer = s_name or "Customer" | |
| # springy contact block | |
| springy_contact = "" | |
| if inv_type == "springy": | |
| lines = [x for x in [s_addr, s_email, s_phone, (s_audit_date or "")] if x] | |
| if lines: | |
| springy_contact = "<div style='margin-top:6px'>" + "<br/>".join(lines) + "</div>" | |
| # third-party notes | |
| tp_rows = "" | |
| if inv_type == "third_party": | |
| detail_lines = [f"{customer} NHVR audit {s_audit_date or ''}", s_addr, s_email, s_phone] | |
| for d in [x for x in detail_lines if x]: | |
| tp_rows += f"<tr><td colspan='5' class='left' style='font-style:italic;background:#f9f9f9;'>{d}</td></tr>" | |
| # header branding | |
| header_logo = f'<img src="{PREVIEW_LOGO}" alt="" style="display:block;margin:0 auto;height:70px;max-width:100%;object-fit:contain;border:0;" />' if PREVIEW_LOGO else "" | |
| brand_html = "" if PREVIEW_LOGO else """ | |
| <div style="text-align:center"> | |
| <div class="brand1">SPRINGY CONSULTING SERVICES</div> | |
| <div class="brand2">HEAVY VEHICLE AUDITING & COMPLIANCE</div> | |
| </div> | |
| """ | |
| return f""" | |
| <style> | |
| :root {{ --brand:#6FB643; --grid:#e2e8f0; --head:#0f172a; }} | |
| /* kill any inherited fading/filters from Gradio */ | |
| .invoice-preview, .invoice-preview * {{ | |
| opacity: 1 !important; filter: none !important; mix-blend-mode: normal !important; | |
| -webkit-text-fill-color: currentColor !important; color:#0f172a !important; | |
| }} | |
| .invoice-preview {{ | |
| width:100%; max-width:860px; margin:0 auto; background:#fff; border:1px solid #cbd5e1; | |
| border-radius:12px; font-family: Arial, Helvetica, sans-serif; font-size:12px; line-height:1.3; | |
| box-sizing:border-box; | |
| }} | |
| .header {{ padding:14px 18px 6px 18px; }} | |
| .meta .row {{ display:flex; gap:10px; margin:2px 0; }} | |
| .meta .lbl {{ font-weight:900; min-width:92px; color:#111827 !important; }} | |
| .brand1 {{ color:var(--brand); font-size:26px; font-weight:900; letter-spacing:.2px; }} | |
| .brand2 {{ font-size:12px; font-weight:900; text-transform:uppercase; }} | |
| table.inv {{ | |
| width:calc(100% - 36px); margin:10px 18px; border-collapse:collapse; border-spacing:0; | |
| table-layout:fixed; box-sizing:border-box; | |
| }} | |
| table.inv th {{ | |
| background:var(--head) !important; color:#fff !important; font-weight:700; | |
| padding:8px 10px; border:1px solid var(--head); white-space:nowrap; box-sizing:border-box; | |
| }} | |
| table.inv td {{ | |
| border:1px solid var(--grid); padding:8px 10px; box-sizing:border-box; | |
| }} | |
| .right {{ text-align:right; font-variant-numeric:tabular-nums; }} | |
| .left {{ text-align:left; }} | |
| .topgrid {{ display:grid; grid-template-columns:1fr 1fr; gap:18px; padding:0 18px 8px 18px; }} | |
| .totals {{ display:grid; grid-template-columns:2fr 1fr; gap:16px; margin:12px 18px 14px 18px; }} | |
| .payinfo {{ line-height:1.5; }} | |
| .box table {{ width:100%; border-collapse:collapse; }} | |
| .box td {{ border:1px solid var(--grid); padding:8px 10px; }} | |
| .box td:first-child {{ font-weight:900; background:#f5f5f5; }} | |
| .footer {{ background:var(--brand); color:#fff !important; text-align:center; padding:8px; font-weight:900; border-radius:0 0 12px 12px; }} | |
| </style> | |
| <div class="invoice-preview"> | |
| <div class="header"> | |
| {header_logo}{brand_html} | |
| <div class="meta" style="display:flex; justify-content:flex-end; gap:24px; margin-top:10px;"> | |
| <div> | |
| <div class="row"><span class="lbl">Invoice Date</span><span>{inv_date}</span></div> | |
| <div class="row"><span class="lbl">Invoice #</span><span>{inv_number}</span></div> | |
| <div class="row"><span class="lbl">ABN</span><span>646 382 464 92</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="padding:0 18px 8px 18px; font-weight:900;">Tax Invoice</div> | |
| <div class="topgrid"> | |
| <div> | |
| <div><b>Customer</b> {customer}</div> | |
| {springy_contact if inv_type=='springy' else ''} | |
| </div> | |
| <div></div> | |
| </div> | |
| <table class="inv"> | |
| <thead> | |
| <tr><th style="width:60px;">Item</th><th style="width:auto;">Description</th><th style="width:90px;">Qty/Hours</th><th style="width:100px;">Unit Price</th><th style="width:100px;">Price</th></tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td class="right">{modules}</td> | |
| <td class="left">{audit_type or "NHVR Audit"}</td> | |
| <td class="right">1</td> | |
| <td class="right">{unit:.2f}</td> | |
| <td class="right">{unit:.2f}</td> | |
| </tr> | |
| {tp_rows if inv_type=='third_party' else ''} | |
| {'' if inv_type!='third_party' or admin==0 else f'<tr><td></td><td class="left">JC Auditing administration fee</td><td class="right">1</td><td class="right"></td><td class="right">{admin:.2f}</td></tr>'} | |
| </tbody> | |
| </table> | |
| <div class="totals"> | |
| <div class="payinfo"> | |
| <div><b>All payments can be made by direct deposit to the following</b></div><br/> | |
| <div><b>NAB</b></div> | |
| <div>BSB 085 005</div> | |
| <div>Account 898 164 211</div><br/> | |
| <div>Invoice due in 14 days</div><br/> | |
| <div>contact@springyconsultingservices.com</div> | |
| </div> | |
| <div class="box"> | |
| <table> | |
| <tr><td>Invoice Subtotal</td><td class="right">${subtotal:.2f}</td></tr> | |
| <tr><td>Tax Rate</td><td class="right">10%</td></tr> | |
| <tr><td>GST</td><td class="right">${gst:.2f}</td></tr> | |
| <tr><td><b>Total</b></td><td class="right"><b>${total:.2f}</b></td></tr> | |
| </table> | |
| </div> | |
| </div> | |
| <div style="padding:0 18px 12px 18px; font-weight:900;">Thankyou for your Business</div> | |
| <div class="footer">0417 664 190 | P.O. BOX 14, O’Halloran Hill, SA 5158 | www.springyconsultingservices.com</div> | |
| </div> | |
| """ | |
| # ---------- audit choices ---------- | |
| AUDIT_CHOICES_SPRINGY = [ | |
| "NHVR Maintenance Audit", | |
| "NHVR Mass Audit", | |
| "NHVR Fatigue Audit", | |
| "NHVR Maintenance & Mass Audit", | |
| "NHVR Maintenance & Fatigue Audit", | |
| "NHVR Maintenance, Mass & Fatigue Audit", | |
| "NHVR Mass & Fatigue Audit", | |
| "Accreditation Manual NHVR & WA Main Roads", | |
| "Policy & Procedure Manual NHVR", | |
| "Policy & Procedure Manual WA Main roads", | |
| "Compliance", | |
| "Consulting", | |
| "WA Maintenance, Fatigue, Dimensions & Loading Audit", | |
| "WA Maintenance, Fatigue, Dimensions & Loading, Mass Audit", | |
| "Travel", | |
| "Submission of NHVR audit summary report", | |
| "Pre Trip Inspection Books", | |
| ] | |
| AUDIT_CHOICES_THIRDPARTY = [ | |
| "NHVR Maintenance Audit", | |
| "NHVR Mass Audit", | |
| "NHVR Fatigue Audit", | |
| "NHVR Maintenance & Mass Audit", | |
| "NHVR Maintenance & Fatigue Audit", | |
| "NHVR Maintenance, Mass & Fatigue Audit", | |
| "NHVR Mass & Fatigue Audit", | |
| "Manual NHVR", | |
| "Policy & Procedure Manual NHVR", | |
| "Policy & Procedure Manual WA Main roads", | |
| "Compliance", | |
| "Consulting", | |
| "WA Maintenance, Fatigue, Dimensions & Loading", | |
| ] | |
| def _choices_for(inv_type: str): | |
| return AUDIT_CHOICES_SPRINGY if inv_type == "springy" else AUDIT_CHOICES_THIRDPARTY | |
| def _normalize_choice(inv_type: str, parsed: str): | |
| choices = _choices_for(inv_type) | |
| return parsed if parsed in choices else choices[0] | |
| # ---------- callbacks ---------- | |
| def on_upload(files): | |
| f = None | |
| if files is None: | |
| return "springy", 1, "NHVR Audit", "", "", "", "", today_str(), "", "" | |
| if isinstance(files, list) and files: | |
| f = files[0] | |
| else: | |
| f = files | |
| meta, inv_type, modules, audit_type, audit_date, name, address, email, phone = do_parse(f) | |
| # normalize parsed audit type to the proper list | |
| audit_type = _normalize_choice(inv_type, audit_type or "") | |
| return inv_type, modules, audit_type, name, address, email, phone, audit_date, today_str(), "" | |
| def on_change(inv_type, inv_date, inv_number, modules, audit_type, | |
| s_name, s_addr, s_email, s_phone, s_audit_date, unit_override): | |
| return _preview(inv_type, inv_date, inv_number, modules, audit_type, | |
| s_name, s_addr, s_email, s_phone, s_audit_date, unit_override) | |
| def on_generate(uploaded_file, inv_date, inv_num, inv_type, modules, audit_type, s_audit_date, s_name, s_addr, s_email, s_phone, unit_override): | |
| # backend returns (meta, xlsx, pdf) where xlsx/pdf may be bytes or a path | |
| meta, xlsx, pdf = do_generate( | |
| uploaded_file, inv_type, modules, audit_type, s_audit_date, s_name, s_addr, s_email, s_phone | |
| ) | |
| # ensure we always have real files on disk for gr.DownloadButton | |
| # use recognizable names so the browser saves with the right extension | |
| inv_code = inv_num if inv_num and inv_num != "Auto" else new_invoice_code(inv_type) | |
| xlsx_path = _stage_file(xlsx, f"{inv_code}.xlsx") | |
| pdf_path = _stage_file(pdf, f"{inv_code}.pdf") | |
| preview_html = _preview(inv_type, inv_date, inv_code, modules, audit_type, s_name, s_addr, s_email, s_phone, s_audit_date, unit_override) | |
| return ( | |
| preview_html, | |
| gr.update(value=xlsx_path, visible=bool(xlsx_path)), | |
| gr.update(value=pdf_path, visible=bool(pdf_path)), | |
| ) | |
| def update_audit_dropdown(inv_type_value, current_audit): | |
| choices = _choices_for(inv_type_value) | |
| value = current_audit if current_audit in choices else choices[0] | |
| return gr.update(choices=choices, value=value), value | |
| # ---------- UI ---------- | |
| custom_css = """ | |
| .preview-panel, .preview-panel *, .gradio-html, .gradio-html * { opacity: 1 !important; filter: none !important; pointer-events: auto !important; } | |
| .gradio-html { opacity: 1 !important; } | |
| /* Header Styles */ | |
| .app-header { | |
| text-align: center; | |
| margin-bottom: 16px; | |
| padding: 18px; | |
| background: rgba(255,255,255,0.95); | |
| border-radius: 14px; | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.1); | |
| } | |
| .app-title { | |
| font-size: 26px; | |
| font-weight: 900; | |
| background: linear-gradient(135deg, #5A8E37, #2c5530); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 4px; | |
| letter-spacing: -0.3px; | |
| } | |
| .app-subtitle { | |
| font-size: 13px; | |
| color: #64748b; | |
| font-weight: 500; | |
| } | |
| /* Main Layout - Fixed Height, No Scroll */ | |
| .main-container { | |
| display: grid; | |
| grid-template-columns: 380px 1fr; | |
| gap: 16px; | |
| height: calc(100vh - 140px); | |
| min-height: 700px; | |
| max-height: 820px; | |
| } | |
| /* Controls Panel */ | |
| .controls-panel { | |
| background: rgba(255,255,255,0.95); | |
| border-radius: 14px; | |
| padding: 18px; | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.1); | |
| border: 1px solid rgba(255,255,255,0.3); | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| overflow: hidden; | |
| box-sizing: border-box; | |
| } | |
| .controls-title { | |
| font-size: 15px; | |
| font-weight: 700; | |
| color: #1e293b; | |
| margin-bottom: 12px; | |
| text-align: center; | |
| flex-shrink: 0; | |
| } | |
| /* Upload Section */ | |
| .upload-section { | |
| margin-bottom: 12px; | |
| padding: 12px; | |
| background: linear-gradient(135deg, #f8fafc, #e2e8f0); | |
| border-radius: 8px; | |
| border: 2px dashed #cbd5e1; | |
| transition: all 0.3s ease; | |
| flex-shrink: 0; | |
| } | |
| .upload-section:hover { | |
| border-color: #5A8E37; | |
| background: linear-gradient(135deg, #f0fdf4, #dcfce7); | |
| } | |
| /* Form Controls — fill available height to remove the large empty block */ | |
| .form-controls { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; /* fills the vertical gap */ | |
| gap: 8px; | |
| overflow: hidden; | |
| } | |
| /* Generate Button */ | |
| .generate-section { | |
| margin-top: auto; | |
| flex-shrink: 0; | |
| padding-top: 12px; | |
| border-top: 1px solid #e5e7eb; | |
| } | |
| .generate-btn { | |
| background: linear-gradient(135deg, #5A8E37, #4ade80) !important; | |
| border: none !important; | |
| border-radius: 6px !important; | |
| padding: 10px 20px !important; | |
| font-weight: 700 !important; | |
| font-size: 12px !important; | |
| color: white !important; | |
| cursor: pointer !important; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 3px 12px rgba(90, 142, 55, 0.3) !important; | |
| width: 100% !important; | |
| height: 36px !important; | |
| } | |
| /* Preview Panel */ | |
| .preview-panel { | |
| background: rgba(255,255,255,0.95); | |
| border-radius: 14px; | |
| padding: 16px; | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.1); | |
| border: 1px solid rgba(255,255,255,0.3); | |
| display: flex; | |
| flex-direction: column; | |
| height: 100%; | |
| overflow: hidden; | |
| box-sizing: border-box; | |
| } | |
| .preview-title { | |
| font-size: 15px; | |
| font-weight: 700; | |
| color: #1e293b; | |
| margin-bottom: 12px; | |
| text-align: center; | |
| flex-shrink: 0; | |
| } | |
| .preview-container { | |
| flex: 1; | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| overflow: auto; | |
| min-height: 0; | |
| padding: 8px; | |
| box-sizing: border-box; | |
| } | |
| /* Download Section */ | |
| .download-section { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 12px; | |
| padding-top: 12px; | |
| border-top: 1px solid #e5e7eb; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .download-btn { | |
| flex: 1; | |
| background: linear-gradient(135deg, #3b82f6, #1d4ed8) !important; | |
| border: none !important; | |
| border-radius: 4px !important; | |
| padding: 8px 14px !important; | |
| color: white !important; | |
| font-weight: 600 !important; | |
| font-size: 11px !important; | |
| transition: all 0.3s ease !important; | |
| height: 32px !important; | |
| } | |
| /* Labels */ | |
| .gradio-textbox > label, .gradio-dropdown > label, .gradio-radio > label { | |
| font-size: 11px !important; | |
| font-weight: 600 !important; | |
| color: #374151 !important; | |
| margin-bottom: 4px !important; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 1200px) { | |
| .main-container { grid-template-columns: 1fr; height: auto; max-height: none; gap: 12px; } | |
| .controls-panel { order: 1; height: auto; max-height: 350px; overflow-y: auto; } | |
| .preview-panel { order: 2; height: 500px; } | |
| } | |
| .gradio-group { gap: 8px !important; } | |
| .gradio-row { gap: 8px !important; } | |
| .gradio-column { min-width: 0 !important; } | |
| .gradio-textbox, .gradio-dropdown, .gradio-radio { margin-bottom: 0 !important; } | |
| """ | |
| with gr.Blocks(title="Professional Invoice Generator", css=custom_css) as demo: | |
| gr.HTML(''' | |
| <div class="app-header"> | |
| <div class="app-title">NHVR Audit Invoice Generator</div> | |
| <div class="app-subtitle">Professional • Fast • Accurate</div> | |
| </div> | |
| ''') | |
| with gr.Row(elem_classes=["main-container"]): | |
| # Left Panel | |
| with gr.Column(elem_classes=["controls-panel"], scale=1): | |
| gr.HTML('<div class="controls-title">Invoice Settings</div>') | |
| with gr.Group(elem_classes=["upload-section"]): | |
| gr.HTML('<div style="text-align: center; margin-bottom: 8px; font-weight: 600; color: #374151; font-size: 11px;">Upload Report</div>') | |
| up = gr.File(label="Upload Report (.pdf or .docx)", file_types=[".pdf", ".docx"], file_count="single") | |
| # ADD THIS LINE HERE | |
| file_display = gr.File(label="Selected file", interactive=False, visible=False) | |
| gr.HTML('<div style="font-size: 10px; color: #6b7280; text-align: center; margin-top: 4px;">Auto-fill invoice details</div>') | |
| with gr.Group(elem_classes=["form-controls"]): | |
| inv_type = gr.Radio( | |
| [("Springy Direct", "springy"), ("Third Party (JC)", "third_party")], | |
| value="springy", | |
| label="Invoice Type" | |
| ) | |
| modules = gr.Dropdown(choices=[1, 2, 3, 4], value=1, label="Modules") | |
| audit_type = gr.Dropdown(choices=AUDIT_CHOICES_SPRINGY, value=AUDIT_CHOICES_SPRINGY[0], label="Audit Type") | |
| with gr.Group(elem_classes=["generate-section"]): | |
| gen_btn = gr.Button("Generate Invoice", variant="primary", elem_classes=["generate-btn"]) | |
| # Right Panel | |
| with gr.Column(elem_classes=["preview-panel"], scale=2): | |
| gr.HTML('<div class="preview-title">Live Preview</div>') | |
| # startup: professional invoice, not placeholder | |
| with gr.Group(elem_classes=["preview-container"]): | |
| preview = gr.HTML(_preview( | |
| "springy", today_str(), new_invoice_code("springy"), | |
| 1, "NHVR Maintenance Audit", "", "", "", "", "", "auto" | |
| )) | |
| with gr.Group(elem_classes=["download-section"]): | |
| dl_xlsx = gr.DownloadButton("Download Excel", elem_classes=["download-btn"], size="sm", visible=False) | |
| dl_pdf = gr.DownloadButton("Download PDF", elem_classes=["download-btn"], size="sm", visible=False) | |
| # Hidden states | |
| s_name = gr.State("") | |
| s_addr = gr.State("") | |
| s_email = gr.State("") | |
| s_phone = gr.State("") | |
| s_audit_date = gr.State("") | |
| uploaded_file = gr.State(None) | |
| inv_date = gr.State(today_str()) # hidden | |
| inv_num = gr.State("Auto") # hidden | |
| unit_override = gr.State("auto") # hidden | |
| # Upload handler: normalize audit choice to the list for detected type | |
| def _remember_and_parse(files): | |
| uploaded = files[0] if isinstance(files, list) else files | |
| out = on_upload(files) | |
| inv_t, mods, parsed_audit = out[0], out[1], out[2] | |
| normalized = _normalize_choice(inv_t, parsed_audit or "") | |
| return ( | |
| gr.update(value=uploaded, visible=True), # <- show file | |
| uploaded, # <- save to state | |
| inv_t, mods, normalized, | |
| out[3], out[4], out[5], out[6], out[7], | |
| today_str(), "Auto" | |
| ) | |
| up.upload( | |
| _remember_and_parse, | |
| inputs=[up], | |
| outputs=[file_display, uploaded_file, inv_type, modules, audit_type, s_name, s_addr, s_email, s_phone, s_audit_date, inv_date, inv_num], | |
| api_name=False | |
| ) | |
| inv_type.change( | |
| update_audit_dropdown, | |
| inputs=[inv_type, audit_type], | |
| outputs=[audit_type, audit_type], | |
| api_name=False | |
| ).then( | |
| on_change, | |
| inputs=[inv_type, inv_date, inv_num, modules, audit_type, s_name, s_addr, s_email, s_phone, s_audit_date, unit_override], | |
| outputs=[preview], | |
| api_name=False | |
| ) | |
| for w in [modules, audit_type]: | |
| w.change( | |
| on_change, | |
| inputs=[inv_type, inv_date, inv_num, modules, audit_type, s_name, s_addr, s_email, s_phone, s_audit_date, unit_override], | |
| outputs=[preview], | |
| api_name=False | |
| ) | |
| up.upload( | |
| on_change, | |
| inputs=[inv_type, inv_date, inv_num, modules, audit_type, s_name, s_addr, s_email, s_phone, s_audit_date, unit_override], | |
| outputs=[preview], | |
| api_name=False | |
| ) | |
| gen_btn.click( | |
| on_generate, | |
| inputs=[uploaded_file, inv_date, inv_num, inv_type, modules, audit_type, s_audit_date, s_name, s_addr, s_email, s_phone, unit_override], | |
| outputs=[preview, dl_xlsx, dl_pdf], | |
| api_name=False | |
| ) | |
| if __name__ == "__main__": | |
| import os | |
| on_spaces = bool(os.getenv("SPACE_ID")) | |
| # IMPORTANT: hide OpenAPI/schema (works around the json-schema bug) | |
| demo.launch( | |
| server_name="0.0.0.0" if not on_spaces else None, | |
| server_port=int(os.getenv("PORT", "7860")) if not on_spaces else None, | |
| show_error=True, | |
| show_api=False, # <—— prevents the schema from being built | |
| share=False # <—— Spaces manages the URL; share=True not allowed | |
| ) | |