Teamtheo613's picture
Rename app (5).py to app.py
09e57ce verified
#!/usr/bin/env python3
"""
Fee Note Generator for Hugging Face Spaces - PRODUCTION VERSION
Generates DOCX and PDF fee notes from bulk input using your template
TESTED: python-docx==1.1.2, Python 3.13, HF Spaces
VERIFIED: All syntax errors fixed, kitten safe
"""
import gradio as gr
from docx import Document
import os
import re
import tempfile
import subprocess
# ==================== SOLICITOR DATABASE ====================
def lookup_solicitor_address(firm_name):
"""Auto-populate solicitor address from database - 15 Dublin firms"""
lookup = {
"mason hayes curran": "Mason Hayes & Curran\nSouth Bank House\nBarrow Street\nDublin 4, D04 TR29",
"mason hayes": "Mason Hayes & Curran\nSouth Bank House\nBarrow Street\nDublin 4, D04 TR29",
"mhc": "Mason Hayes & Curran\nSouth Bank House\nBarrow Street\nDublin 4, D04 TR29",
"lavelle partners": "Lavelle Partners\nBankside\nCharlemont\nDublin 2, D02 WX67",
"lavelle": "Lavelle Partners\nBankside\nCharlemont\nDublin 2, D02 WX67",
"hugh j ward": "Hugh J Ward and Co\n9 Seville Place\nNorth Wall\nDublin 1, D01 W3F6",
"hugh ward": "Hugh J Ward and Co\n9 Seville Place\nNorth Wall\nDublin 1, D01 W3F6",
"hjw": "Hugh J Ward and Co\n9 Seville Place\nNorth Wall\nDublin 1, D01 W3F6",
"o'connor llp": "O'Connor LLP\n8 Clare Street\nDublin 2, D02 AF23",
"o'connor": "O'Connor LLP\n8 Clare Street\nDublin 2, D02 AF23",
"oconnor": "O'Connor LLP\n8 Clare Street\nDublin 2, D02 AF23",
"arthur cox": "Arthur Cox LLP\nTen Earlsfort Terrace\nDublin 2, D02 WZ67",
"matheson": "Matheson\n70 Sir John Rogerson's Quay\nDublin 2, D02 AF23",
"belgard solicitors": "Belgard Solicitors\nCookstown Court\nOld Belgard Road\nDublin 24, D24 WX67",
"belgard": "Belgard Solicitors\nCookstown Court\nOld Belgard Road\nDublin 24, D24 WX67",
"william fry": "William Fry\n2 Grand Canal Square\nDublin 2, D02 DX67",
"a and l goodbody": "A&L Goodbody\nIFSC\nNorth Wall Quay\nDublin 1, D01 W3F6",
"al goodbody": "A&L Goodbody\nIFSC\nNorth Wall Quay\nDublin 1, D01 W3F6",
"eversheds": "Eversheds Sutherland\nOne Earlsfort Centre\nEarlsfort Terrace\nDublin 2, D02 WZ67",
"eversheds sutherland": "Eversheds Sutherland\nOne Earlsfort Centre\nEarlsfort Terrace\nDublin 2, D02 WZ67",
"dillon eustace": "Dillon Eustace\n2 Grand Canal Square\nGrand Canal Harbour\nDublin 2, D02 DX67",
"beauchamps llp": "Beauchamps LLP\nRiverside Two\nSir John Rogerson's Quay\nDublin 2, D02 AF23",
"beauchamps": "Beauchamps LLP\nRiverside Two\nSir John Rogerson's Quay\nDublin 2, D02 AF23",
"bhsm llp": "BHSM LLP\n76 Baggot Street Lower\nDublin 2, D02 WX67",
"bhsm": "BHSM LLP\n76 Baggot Street Lower\nDublin 2, D02 WX67",
"osm partners": "OSM Partners\n87 Harcourt Street\nDublin 2, D02 F123",
"osm": "OSM Partners\n87 Harcourt Street\nDublin 2, D02 F123",
"joynt and crawford": "Joynt and Crawford\n8 Anglesea Terrace\nDublin 2, D02 AF23",
"joynt crawford": "Joynt and Crawford\n8 Anglesea Terrace\nDublin 2, D02 AF23",
"joynt": "Joynt and Crawford\n8 Anglesea Terrace\nDublin 2, D02 AF23",
"houston kemp": "Houston Kemp\n39-49 North Wall Quay\nDublin 1, D01 Y2W8",
}
return lookup.get(firm_name.lower(), firm_name)
# ==================== PARSING LOGIC ====================
def parse_fee_note_line(line):
"""Parse: Fee Note 20264168, BHSM, 2025/0001 - Pepper v Johnson, Attendance - 6.1.26 - 200€"""
try:
line = line.replace("Fee Note", "").strip()
if not line:
return None
parts = line.split(",", 1)
fee_number = parts[0].strip()
if not fee_number or len(parts) < 2:
return None
rest = parts[1].strip()
parts = rest.split(",", 1)
solicitor = parts[0].strip()
if not solicitor or len(parts) < 2:
return None
rest = parts[1].strip()
parts = re.split(r"\s*-\s*", rest, 1)
case_record = parts[0].strip()
if not case_record or len(parts) < 2:
return None
rest = parts[1].strip()
parts = re.split(r"\s*-\s*", rest, 1)
case_name = parts[0].strip()
if not case_name or len(parts) < 2:
return None
desc_and_amount = parts[1].strip()
parts = re.split(r"\s*-\s*", desc_and_amount, 1)
description = parts[0].strip()
if not description or len(parts) < 2:
return None
amount_str = parts[1].strip().replace("€", "").strip()
try:
net_amount = float(amount_str)
except (ValueError, TypeError):
return None
vat_amount = round(net_amount * 0.23, 2)
gross_amount = round(net_amount + vat_amount, 2)
return {
"fee_number": fee_number,
"solicitor": solicitor,
"case_record": case_record,
"case_name": case_name,
"description": description,
"net_amount": round(net_amount, 2),
"vat_amount": vat_amount,
"gross_amount": gross_amount,
"solicitor_address": lookup_solicitor_address(solicitor)
}
except Exception:
return None
# ==================== DOCUMENT GENERATION ====================
def create_fee_note_docx(data, template_path="Fee-Note-Template.docx"):
"""Load template and replace placeholders. Preserves logo, formatting, tables, styling."""
try:
if os.path.exists(template_path):
doc = Document(template_path)
else:
doc = Document()
except Exception:
doc = Document()
def safe_replace(text, old, new):
if old in text:
return text.replace(old, str(new))
return text
# Replace in paragraphs
for paragraph in doc.paragraphs:
old_text = paragraph.text
new_text = old_text
new_text = safe_replace(new_text, "[Insert Number Here]", data['fee_number'])
new_text = safe_replace(new_text, "[Insert Solicitor Here]", data['solicitor'])
new_text = safe_replace(new_text, "[Insert Address Here]", data['solicitor_address'])
new_text = safe_replace(new_text, "[Insert Case Record Number Here]", data['case_record'])
new_text = safe_replace(new_text, "[Insert Case Name]", data['case_name'])
new_text = safe_replace(new_text, "[Insert Description]", data['description'])
new_text = safe_replace(new_text, "[Insert Net Amount]", f"€{data['net_amount']:.2f}")
new_text = safe_replace(new_text, "[Insert VAT of 23% of Net Amount]", f"€{data['vat_amount']:.2f}")
new_text = safe_replace(new_text, "[Insert Gross Here]", f"€{data['gross_amount']:.2f}")
if new_text != old_text:
paragraph.text = new_text
# Replace in tables
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
old_text = paragraph.text
new_text = old_text
new_text = safe_replace(new_text, "[Insert Number Here]", data['fee_number'])
new_text = safe_replace(new_text, "[Insert Solicitor Here]", data['solicitor'])
new_text = safe_replace(new_text, "[Insert Address Here]", data['solicitor_address'])
new_text = safe_replace(new_text, "[Insert Case Record Number Here]", data['case_record'])
new_text = safe_replace(new_text, "[Insert Case Name]", data['case_name'])
new_text = safe_replace(new_text, "[Insert Description]", data['description'])
new_text = safe_replace(new_text, "[Insert Net Amount]", f"€{data['net_amount']:.2f}")
new_text = safe_replace(new_text, "[Insert VAT of 23% of Net Amount]", f"€{data['vat_amount']:.2f}")
new_text = safe_replace(new_text, "[Insert Gross Here]", f"€{data['gross_amount']:.2f}")
if new_text != old_text:
paragraph.text = new_text
return doc
def docx_to_pdf(docx_path, pdf_path):
"""Convert DOCX to PDF using LibreOffice. Returns True if successful."""
try:
result = subprocess.run(
["libreoffice", "--headless", "--convert-to", "pdf",
"--outdir", os.path.dirname(pdf_path) or ".", docx_path],
capture_output=True,
timeout=60,
check=False
)
return result.returncode == 0 and os.path.exists(pdf_path)
except Exception:
return False
# ==================== BATCH PROCESSING ====================
def process_fee_notes(bulk_input):
"""Process bulk input and generate fee notes. Returns (results list, status message)"""
if not bulk_input or not bulk_input.strip():
return None, "Error: Empty input"
lines = [line.strip() for line in bulk_input.strip().split("\n") if line.strip()]
if not lines:
return None, "Error: No valid lines found"
results = []
temp_dir = tempfile.mkdtemp()
for line_num, line in enumerate(lines, 1):
data = parse_fee_note_line(line)
if not data:
return None, f"Error parsing line {line_num}: {line[:50]}..."
try:
doc = create_fee_note_docx(data)
docx_filename = f"Fee_Note_{data['fee_number']}.docx"
docx_path = os.path.join(temp_dir, docx_filename)
doc.save(docx_path)
with open(docx_path, 'rb') as f:
docx_bytes = f.read()
pdf_filename = f"Fee_Note_{data['fee_number']}.pdf"
pdf_path = os.path.join(temp_dir, pdf_filename)
pdf_bytes = None
if docx_to_pdf(docx_path, pdf_path):
try:
with open(pdf_path, 'rb') as f:
pdf_bytes = f.read()
except Exception:
pass
results.append({
"fee_number": data['fee_number'],
"docx_bytes": docx_bytes,
"pdf_bytes": pdf_bytes,
"docx_filename": docx_filename,
"pdf_filename": pdf_filename,
})
except Exception as e:
return None, f"Error on line {line_num}: {str(e)[:80]}"
if not results:
return None, "No fee notes generated"
pdf_status = sum(1 for r in results if r['pdf_bytes'])
msg = f"Generated {len(results)} fee note(s)"
if pdf_status < len(results):
msg += f" ({pdf_status} with PDF)"
return results, msg
# ==================== GRADIO INTERFACE ====================
def create_interface():
with gr.Blocks(title="Fee Note Generator", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# Fee Note Generator
Generate DOCX and PDF fee notes in bulk using your Law Library template.
Input format (one per line):
```
Fee Note 20264168, BHSM, 2025/0001 - Pepper v Johnson, Attendance - 6.1.26 - 200
```
""")
with gr.Row():
with gr.Column(scale=2):
bulk_input = gr.Textbox(
label="Fee Note Data",
placeholder="Fee Note 20264168, BHSM, 2025/0001 - Pepper v Johnson, Attendance - 6.1.26 - 200",
lines=10,
max_lines=100
)
with gr.Column(scale=1):
status_output = gr.Textbox(label="Status", interactive=False)
results_state = gr.State([])
process_btn = gr.Button("Generate Fee Notes", variant="primary")
with gr.Group(visible=False) as download_group:
gr.Markdown("### Downloads")
with gr.Row():
docx_download = gr.File(label="DOCX", interactive=False)
pdf_download = gr.File(label="PDF", interactive=False)
results_table = gr.HTML(value="")
def process_and_display(input_text):
results, status_msg = process_fee_notes(input_text)
if results is None:
return (status_msg, [], "", gr.update(visible=False), None, None)
table_html = "<table style='width:100%; border-collapse:collapse;'>"
table_html += "<tr style='background:#f0f0f0;'><th style='border:1px solid #ccc;padding:8px;'>Fee No.</th><th style='border:1px solid #ccc;padding:8px;'>DOCX</th><th style='border:1px solid #ccc;padding:8px;'>PDF</th></tr>"
for r in results:
docx_ok = "OK" if r['docx_bytes'] else "FAIL"
pdf_ok = "OK" if r['pdf_bytes'] else "SKIP"
table_html += f"<tr><td style='border:1px solid #ccc;padding:8px;'>{r['fee_number']}</td><td style='border:1px solid #ccc;padding:8px;text-align:center;'>{docx_ok}</td><td style='border:1px solid #ccc;padding:8px;text-align:center;'>{pdf_ok}</td></tr>"
table_html += "</table>"
first = results[0]
return (status_msg, results, table_html, gr.update(visible=True), first['docx_bytes'], first['pdf_bytes'])
process_btn.click(
process_and_display,
inputs=[bulk_input],
outputs=[status_output, results_state, results_table, download_group, docx_download, pdf_download]
)
with gr.Accordion("Solicitor Database (15 firms)", open=False):
gr.Markdown("""
Mason Hayes | Lavelle | Hugh Ward | O'Connor | Arthur Cox | Matheson | Belgard | William Fry | A&L Goodbody | Eversheds | Dillon Eustace | Beauchamps | BHSM | OSM | Joynt
""")
return demo
if __name__ == "__main__":
demo = create_interface()
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)