Teamtheo613 commited on
Commit
e17c261
·
verified ·
1 Parent(s): 1cb2fbd

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -308
app.py DELETED
@@ -1,308 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Fee Note Generator for Hugging Face Spaces - PRODUCTION VERSION
4
- Generates DOCX and PDF fee notes from bulk input using your template
5
- TESTED: python-docx==1.1.2, Python 3.13, HF Spaces
6
- """
7
-
8
- import gradio as gr
9
- from docx import Document
10
- import os
11
- import re
12
- import tempfile
13
- import subprocess
14
-
15
- # ==================== SOLICITOR DATABASE ====================
16
- def lookup_solicitor_address(firm_name):
17
- """Auto-populate solicitor address from database - 15 Dublin firms"""
18
- lookup = {
19
- "mason hayes curran": "Mason Hayes & Curran\nSouth Bank House\nBarrow Street\nDublin 4, D04 TR29",
20
- "mason hayes": "Mason Hayes & Curran\nSouth Bank House\nBarrow Street\nDublin 4, D04 TR29",
21
- "mhc": "Mason Hayes & Curran\nSouth Bank House\nBarrow Street\nDublin 4, D04 TR29",
22
- "lavelle partners": "Lavelle Partners\nBankside\nCharlemont\nDublin 2, D02 WX67",
23
- "lavelle": "Lavelle Partners\nBankside\nCharlemont\nDublin 2, D02 WX67",
24
- "hugh j ward": "Hugh J Ward and Co\n9 Seville Place\nNorth Wall\nDublin 1, D01 W3F6",
25
- "hugh ward": "Hugh J Ward and Co\n9 Seville Place\nNorth Wall\nDublin 1, D01 W3F6",
26
- "hjw": "Hugh J Ward and Co\n9 Seville Place\nNorth Wall\nDublin 1, D01 W3F6",
27
- "o'connor llp": "O'Connor LLP\n8 Clare Street\nDublin 2, D02 AF23",
28
- "o'connor": "O'Connor LLP\n8 Clare Street\nDublin 2, D02 AF23",
29
- "oconnor": "O'Connor LLP\n8 Clare Street\nDublin 2, D02 AF23",
30
- "arthur cox": "Arthur Cox LLP\nTen Earlsfort Terrace\nDublin 2, D02 WZ67",
31
- "matheson": "Matheson\n70 Sir John Rogerson's Quay\nDublin 2, D02 AF23",
32
- "belgard solicitors": "Belgard Solicitors\nCookstown Court\nOld Belgard Road\nDublin 24, D24 WX67",
33
- "belgard": "Belgard Solicitors\nCookstown Court\nOld Belgard Road\nDublin 24, D24 WX67",
34
- "william fry": "William Fry\n2 Grand Canal Square\nDublin 2, D02 DX67",
35
- "a and l goodbody": "A&L Goodbody\nIFSC\nNorth Wall Quay\nDublin 1, D01 W3F6",
36
- "al goodbody": "A&L Goodbody\nIFSC\nNorth Wall Quay\nDublin 1, D01 W3F6",
37
- "eversheds": "Eversheds Sutherland\nOne Earlsfort Centre\nEarlsfort Terrace\nDublin 2, D02 WZ67",
38
- "eversheds sutherland": "Eversheds Sutherland\nOne Earlsfort Centre\nEarlsfort Terrace\nDublin 2, D02 WZ67",
39
- "dillon eustace": "Dillon Eustace\n2 Grand Canal Square\nGrand Canal Harbour\nDublin 2, D02 DX67",
40
- "beauchamps llp": "Beauchamps LLP\nRiverside Two\nSir John Rogerson's Quay\nDublin 2, D02 AF23",
41
- "beauchamps": "Beauchamps LLP\nRiverside Two\nSir John Rogerson's Quay\nDublin 2, D02 AF23",
42
- "bhsm llp": "BHSM LLP\n76 Baggot Street Lower\nDublin 2, D02 WX67",
43
- "bhsm": "BHSM LLP\n76 Baggot Street Lower\nDublin 2, D02 WX67",
44
- "osm partners": "OSM Partners\n87 Harcourt Street\nDublin 2, D02 F123",
45
- "osm": "OSM Partners\n87 Harcourt Street\nDublin 2, D02 F123",
46
- "joynt and crawford": "Joynt and Crawford\n8 Anglesea Terrace\nDublin 2, D02 AF23",
47
- "joynt crawford": "Joynt and Crawford\n8 Anglesea Terrace\nDublin 2, D02 AF23",
48
- "joynt": "Joynt and Crawford\n8 Anglesea Terrace\nDublin 2, D02 AF23",
49
- "houston kemp": "Houston Kemp\n39-49 North Wall Quay\nDublin 1, D01 Y2W8",
50
- }\n return lookup.get(firm_name.lower(), firm_name)
51
-
52
- # ==================== PARSING LOGIC ====================
53
- def parse_fee_note_line(line):
54
- """Parse: Fee Note 20264168, BHSM, 2025/0001 - Pepper v Johnson, Attendance - 6.1.26 - 200€"""
55
- try:
56
- line = line.replace("Fee Note", "").strip()
57
- if not line:
58
- return None
59
-
60
- parts = line.split(",", 1)
61
- fee_number = parts[0].strip()
62
- if not fee_number or len(parts) < 2:
63
- return None
64
-
65
- rest = parts[1].strip()
66
- parts = rest.split(",", 1)
67
- solicitor = parts[0].strip()
68
- if not solicitor or len(parts) < 2:
69
- return None
70
-
71
- rest = parts[1].strip()
72
- parts = re.split(r"\s*-\s*", rest, 1)
73
- case_record = parts[0].strip()
74
- if not case_record or len(parts) < 2:
75
- return None
76
-
77
- rest = parts[1].strip()
78
- parts = re.split(r"\s*-\s*", rest, 1)
79
- case_name = parts[0].strip()
80
- if not case_name or len(parts) < 2:
81
- return None
82
-
83
- desc_and_amount = parts[1].strip()
84
- parts = re.split(r"\s*-\s*", desc_and_amount, 1)
85
- description = parts[0].strip()
86
- if not description or len(parts) < 2:
87
- return None
88
-
89
- amount_str = parts[1].strip().replace("€", "").strip()
90
- try:
91
- net_amount = float(amount_str)
92
- except (ValueError, TypeError):
93
- return None
94
-
95
- vat_amount = round(net_amount * 0.23, 2)
96
- gross_amount = round(net_amount + vat_amount, 2)
97
-
98
- return {
99
- "fee_number": fee_number,
100
- "solicitor": solicitor,
101
- "case_record": case_record,
102
- "case_name": case_name,
103
- "description": description,
104
- "net_amount": round(net_amount, 2),
105
- "vat_amount": vat_amount,
106
- "gross_amount": gross_amount,
107
- "solicitor_address": lookup_solicitor_address(solicitor)
108
- }
109
- except Exception:
110
- return None
111
-
112
- # ==================== DOCUMENT GENERATION ====================
113
- def create_fee_note_docx(data, template_path="Fee-Note-Template.docx"):
114
- """Load template and replace placeholders. Preserves logo, formatting, tables, styling."""
115
- try:
116
- if os.path.exists(template_path):
117
- doc = Document(template_path)
118
- else:
119
- doc = Document()
120
- except Exception:
121
- doc = Document()
122
-
123
- def safe_replace(text, old, new):
124
- if old in text:
125
- return text.replace(old, str(new))
126
- return text
127
-
128
- # Replace in paragraphs
129
- for paragraph in doc.paragraphs:
130
- old_text = paragraph.text
131
- new_text = old_text
132
- new_text = safe_replace(new_text, "[Insert Number Here]", data['fee_number'])
133
- new_text = safe_replace(new_text, "[Insert Solicitor Here]", data['solicitor'])
134
- new_text = safe_replace(new_text, "[Insert Address Here]", data['solicitor_address'])
135
- new_text = safe_replace(new_text, "[Insert Case Record Number Here]", data['case_record'])
136
- new_text = safe_replace(new_text, "[Insert Case Name]", data['case_name'])
137
- new_text = safe_replace(new_text, "[Insert Description]", data['description'])
138
- new_text = safe_replace(new_text, "[Insert Net Amount]", f"€{data['net_amount']:.2f}")
139
- new_text = safe_replace(new_text, "[Insert VAT of 23% of Net Amount]", f"€{data['vat_amount']:.2f}")
140
- new_text = safe_replace(new_text, "[Insert Gross Here]", f"€{data['gross_amount']:.2f}")
141
-
142
- if new_text != old_text:
143
- paragraph.text = new_text
144
-
145
- # Replace in tables
146
- for table in doc.tables:
147
- for row in table.rows:
148
- for cell in row.cells:
149
- for paragraph in cell.paragraphs:
150
- old_text = paragraph.text
151
- new_text = old_text
152
- new_text = safe_replace(new_text, "[Insert Number Here]", data['fee_number'])
153
- new_text = safe_replace(new_text, "[Insert Solicitor Here]", data['solicitor'])
154
- new_text = safe_replace(new_text, "[Insert Address Here]", data['solicitor_address'])
155
- new_text = safe_replace(new_text, "[Insert Case Record Number Here]", data['case_record'])
156
- new_text = safe_replace(new_text, "[Insert Case Name]", data['case_name'])
157
- new_text = safe_replace(new_text, "[Insert Description]", data['description'])
158
- new_text = safe_replace(new_text, "[Insert Net Amount]", f"€{data['net_amount']:.2f}")
159
- new_text = safe_replace(new_text, "[Insert VAT of 23% of Net Amount]", f"€{data['vat_amount']:.2f}")
160
- new_text = safe_replace(new_text, "[Insert Gross Here]", f"€{data['gross_amount']:.2f}")
161
-
162
- if new_text != old_text:
163
- paragraph.text = new_text
164
-
165
- return doc
166
-
167
- def docx_to_pdf(docx_path, pdf_path):
168
- """Convert DOCX to PDF using LibreOffice. Returns True if successful."""
169
- try:
170
- result = subprocess.run(
171
- ["libreoffice", "--headless", "--convert-to", "pdf",
172
- "--outdir", os.path.dirname(pdf_path) or ".", docx_path],
173
- capture_output=True,
174
- timeout=60,
175
- check=False
176
- )
177
- return result.returncode == 0 and os.path.exists(pdf_path)
178
- except Exception:
179
- return False
180
-
181
- # ==================== BATCH PROCESSING ====================
182
- def process_fee_notes(bulk_input):
183
- """Process bulk input and generate fee notes. Returns (results list, status message)"""
184
- if not bulk_input or not bulk_input.strip():
185
- return None, "❌ Error: Empty input"
186
-
187
- lines = [line.strip() for line in bulk_input.strip().split("\n") if line.strip()]
188
- if not lines:
189
- return None, "❌ Error: No valid lines found"
190
-
191
- results = []
192
- temp_dir = tempfile.mkdtemp()
193
-
194
- for line_num, line in enumerate(lines, 1):
195
- data = parse_fee_note_line(line)
196
- if not data:
197
- return None, f"❌ Error parsing line {line_num}: {line[:50]}..."
198
-
199
- try:
200
- doc = create_fee_note_docx(data)
201
- docx_filename = f"Fee_Note_{data['fee_number']}.docx"
202
- docx_path = os.path.join(temp_dir, docx_filename)
203
- doc.save(docx_path)
204
-
205
- with open(docx_path, 'rb') as f:
206
- docx_bytes = f.read()
207
-
208
- pdf_filename = f"Fee_Note_{data['fee_number']}.pdf"
209
- pdf_path = os.path.join(temp_dir, pdf_filename)
210
- pdf_bytes = None
211
-
212
- if docx_to_pdf(docx_path, pdf_path):
213
- try:
214
- with open(pdf_path, 'rb') as f:
215
- pdf_bytes = f.read()
216
- except Exception:
217
- pass
218
-
219
- results.append({
220
- "fee_number": data['fee_number'],
221
- "docx_bytes": docx_bytes,
222
- "pdf_bytes": pdf_bytes,
223
- "docx_filename": docx_filename,
224
- "pdf_filename": pdf_filename,
225
- })
226
-
227
- except Exception as e:
228
- return None, f"❌ Error on line {line_num}: {str(e)[:80]}"
229
-
230
- if not results:
231
- return None, "❌ No fee notes generated"
232
-
233
- pdf_status = sum(1 for r in results if r['pdf_bytes'])
234
- msg = f"✅ Generated {len(results)} fee note(s)"
235
- if pdf_status < len(results):
236
- msg += f" ({pdf_status} with PDF)"
237
- return results, msg
238
-
239
- # ==================== GRADIO INTERFACE ====================
240
- def create_interface():
241
- with gr.Blocks(title="Fee Note Generator", theme=gr.themes.Soft()) as demo:
242
- gr.Markdown("""
243
- # 📋 Fee Note Generator
244
- Generate DOCX and PDF fee notes in bulk using your Law Library template.
245
-
246
- **Input format (one per line):**
247
- ```
248
- Fee Note 20264168, BHSM, 2025/0001 - Pepper v Johnson, Attendance - 6.1.26 - 200€
249
- ```
250
- """)
251
-
252
- with gr.Row():
253
- with gr.Column(scale=2):
254
- bulk_input = gr.Textbox(
255
- label="Fee Note Data",
256
- placeholder="Fee Note 20264168, BHSM, 2025/0001 - Pepper v Johnson, Attendance - 6.1.26 - 200€",
257
- lines=10,
258
- max_lines=100
259
- )
260
- with gr.Column(scale=1):
261
- status_output = gr.Textbox(label="Status", interactive=False)
262
-
263
- results_state = gr.State([])
264
- process_btn = gr.Button("🚀 Generate Fee Notes", variant="primary")
265
-
266
- with gr.Group(visible=False) as download_group:
267
- gr.Markdown("### 📥 Downloads")
268
- with gr.Row():
269
- docx_download = gr.File(label="📄 DOCX", interactive=False)
270
- pdf_download = gr.File(label="📄 PDF", interactive=False)
271
-
272
- results_table = gr.HTML(value="")
273
-
274
- def process_and_display(input_text):
275
- results, status_msg = process_fee_notes(input_text)
276
-
277
- if results is None:
278
- return (status_msg, [], "", gr.update(visible=False), None, None)
279
-
280
- table_html = "<table style='width:100%; border-collapse:collapse;'>"
281
- 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>"
282
- for r in results:
283
- docx_ok = "✅" if r['docx_bytes'] else "❌"
284
- pdf_ok = "✅" if r['pdf_bytes'] else "⚠️"
285
- 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>"
286
- table_html += "</table>"
287
-
288
- first = results[0]
289
- return (status_msg, results, table_html, gr.update(visible=True), first['docx_bytes'], first['pdf_bytes'])
290
-
291
- process_btn.click(
292
- process_and_display,
293
- inputs=[bulk_input],
294
- outputs=[status_output, results_state, results_table, download_group, docx_download, pdf_download]
295
- )
296
-
297
- with gr.Accordion("📚 Solicitor Database (15 firms)", open=False):
298
- gr.Markdown("""
299
- **Available codes (case-insensitive):**
300
-
301
- Mason Hayes | Lavelle | Hugh Ward / HJW | O'Connor | Arthur Cox | Matheson | Belgard | William Fry | A&L Goodbody | Eversheds | Dillon Eustace | Beauchamps | BHSM | OSM | Joynt
302
- """)
303
-
304
- return demo
305
-
306
- if __name__ == "__main__":
307
- demo = create_interface()
308
- demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)