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

Upload app (5).py

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