angelsg213 commited on
Commit
0a02adf
·
verified ·
1 Parent(s): c9bd63a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +488 -228
app.py CHANGED
@@ -6,18 +6,12 @@ import pandas as pd
6
  import re
7
  from datetime import datetime
8
  from huggingface_hub import InferenceClient
9
-
10
- # Importar Google Drive solo si está disponible
11
- try:
12
- from google.oauth2.credentials import Credentials
13
- from google_auth_oauthlib.flow import InstalledAppFlow
14
- from google.auth.transport.requests import Request
15
- from googleapiclient.discovery import build
16
- from googleapiclient.http import MediaFileUpload
17
- import pickle
18
- DRIVE_DISPONIBLE = True
19
- except ImportError:
20
- DRIVE_DISPONIBLE = False
21
 
22
  # ============= EXTRAER TEXTO DEL PDF =============
23
  def extraer_texto_pdf(pdf_file):
@@ -38,10 +32,8 @@ def analizar_y_convertir_json(texto):
38
  if not token:
39
  return None, None, "Error: Falta configurar HF_TOKEN en Settings → Secrets"
40
 
41
- # Limitar texto
42
  texto_limpio = texto[:8000]
43
 
44
- # Prompt para que el LLM decida la estructura JSON
45
  prompt = f"""Eres un experto en análisis de facturas. Lee esta factura y conviértela a JSON.
46
 
47
  TEXTO DE LA FACTURA:
@@ -85,7 +77,6 @@ FORMATO JSON (ajusta según lo que encuentres):
85
 
86
  Responde SOLO con el JSON válido (sin explicaciones, sin markdown):"""
87
 
88
- # Lista de modelos que funcionan
89
  modelos = [
90
  "Qwen/Qwen2.5-72B-Instruct",
91
  "meta-llama/Llama-3.2-3B-Instruct",
@@ -95,10 +86,9 @@ Responde SOLO con el JSON válido (sin explicaciones, sin markdown):"""
95
 
96
  for modelo in modelos:
97
  try:
98
- print(f"\n🤖 Probando: {modelo}")
99
  client = InferenceClient(token=token)
100
 
101
- # Llamar al modelo
102
  response = client.chat.completions.create(
103
  model=modelo,
104
  messages=[
@@ -108,36 +98,28 @@ Responde SOLO con el JSON válido (sin explicaciones, sin markdown):"""
108
  temperature=0.1
109
  )
110
 
111
- # Extraer respuesta
112
  resultado = response.choices[0].message.content
113
-
114
- # Limpiar respuesta (quitar markdown si existe)
115
  resultado = resultado.strip()
116
  resultado = re.sub(r'```json\s*', '', resultado)
117
  resultado = re.sub(r'```\s*', '', resultado)
118
  resultado = resultado.strip()
119
 
120
- # Buscar JSON en la respuesta
121
  match = re.search(r'\{.*\}', resultado, re.DOTALL)
122
  if match:
123
  json_str = match.group(0)
124
  try:
125
  datos_json = json.loads(json_str)
126
- print(f"JSON válido extraído con {modelo}")
127
 
128
- # Generar resumen de información útil
129
  resumen_util = generar_resumen_util(texto_limpio, modelo, client)
130
 
131
  return datos_json, resumen_util, f"Procesado con {modelo}"
132
  except json.JSONDecodeError as e:
133
- print(f"⚠️ JSON inválido: {str(e)[:50]}")
134
  continue
135
- else:
136
- print(f"⚠️ No se encontró JSON en la respuesta")
137
- continue
138
 
139
  except Exception as e:
140
- print(f"{modelo} falló: {str(e)[:100]}")
141
  continue
142
 
143
  return None, None, "Ningún modelo LLM pudo extraer el JSON. Verifica tu HF_TOKEN."
@@ -179,12 +161,10 @@ def json_a_csv(datos_json):
179
 
180
  filas = []
181
 
182
- # === INFORMACIÓN GENERAL ===
183
  filas.append({'Campo': '=== INFORMACIÓN GENERAL ===', 'Valor': ''})
184
  filas.append({'Campo': 'Número de Factura', 'Valor': datos_json.get('numero_factura', 'N/A')})
185
  filas.append({'Campo': 'Fecha', 'Valor': datos_json.get('fecha', 'N/A')})
186
 
187
- # === EMISOR ===
188
  if 'emisor' in datos_json:
189
  filas.append({'Campo': '', 'Valor': ''})
190
  filas.append({'Campo': '=== EMISOR ===', 'Valor': ''})
@@ -195,7 +175,6 @@ def json_a_csv(datos_json):
195
  else:
196
  filas.append({'Campo': 'Nombre', 'Valor': str(emisor)})
197
 
198
- # === CLIENTE ===
199
  if 'cliente' in datos_json:
200
  filas.append({'Campo': '', 'Valor': ''})
201
  filas.append({'Campo': '=== CLIENTE ===', 'Valor': ''})
@@ -206,7 +185,6 @@ def json_a_csv(datos_json):
206
  else:
207
  filas.append({'Campo': 'Nombre', 'Valor': str(cliente)})
208
 
209
- # === PRODUCTOS/SERVICIOS ===
210
  productos = datos_json.get('productos', datos_json.get('conceptos', datos_json.get('items', [])))
211
  if productos and len(productos) > 0:
212
  filas.append({'Campo': '', 'Valor': ''})
@@ -219,13 +197,11 @@ def json_a_csv(datos_json):
219
  filas.append({'Campo': ' Total', 'Valor': f"{prod.get('total', 0)}€"})
220
  filas.append({'Campo': '', 'Valor': ''})
221
 
222
- # === TOTALES ===
223
  totales = datos_json.get('totales', {})
224
  if totales or 'base_imponible' in datos_json or 'total' in datos_json:
225
  filas.append({'Campo': '', 'Valor': ''})
226
  filas.append({'Campo': '=== TOTALES ===', 'Valor': ''})
227
 
228
- # Buscar totales en varios lugares del JSON
229
  base = totales.get('base_imponible', datos_json.get('base_imponible', 0))
230
  iva = totales.get('iva', datos_json.get('iva', 0))
231
  porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0))
@@ -240,166 +216,457 @@ def json_a_csv(datos_json):
240
 
241
  return pd.DataFrame(filas)
242
 
243
- # ============= AUTENTICAR GOOGLE DRIVE =============
244
- def autenticar_drive():
245
- """Inicia el proceso de autenticación con Google Drive"""
246
 
247
- if not DRIVE_DISPONIBLE:
248
- return "Error: Librerías de Google Drive no instaladas.\n\nAgrega al requirements.txt:\ngoogle-auth-oauthlib\ngoogle-auth-httplib2\ngoogle-api-python-client", False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
- SCOPES = ['https://www.googleapis.com/auth/drive.file']
 
 
251
 
252
- try:
253
- # Verificar si existe credentials.json
254
- if not os.path.exists('credentials.json'):
255
- return "Error: Falta el archivo credentials.json.\n\nPasos:\n1. Ve a https://console.cloud.google.com/\n2. Crea un proyecto\n3. Activa Google Drive API\n4. Crea credenciales OAuth 2.0\n5. Descarga credentials.json\n6. Súbelo a tu aplicación", False
256
-
257
- # Verificar si ya hay una sesión activa
258
- if os.path.exists('token.pickle'):
259
- with open('token.pickle', 'rb') as token:
260
- creds = pickle.load(token)
261
- if creds and creds.valid:
262
- return "Ya estás conectado a Google Drive", True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
- # Iniciar flujo de autenticación
265
- flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
266
- creds = flow.run_local_server(port=8080)
 
 
 
 
267
 
268
- # Guardar credenciales
269
- with open('token.pickle', 'wb') as token:
270
- pickle.dump(creds, token)
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
- return "Autenticación exitosa! Ahora puedes guardar archivos en Google Drive", True
 
273
 
274
- except Exception as e:
275
- return f"Error en la autenticación: {str(e)}", False
276
-
277
- # ============= VERIFICAR ESTADO DE DRIVE =============
278
- def verificar_sesion_drive():
279
- """Verifica si hay una sesión activa de Google Drive"""
280
-
281
- if not DRIVE_DISPONIBLE:
282
- return "Librerías no instaladas", False
283
-
284
- if not os.path.exists('token.pickle'):
285
- return "No conectado", False
286
 
287
- try:
288
- with open('token.pickle', 'rb') as token:
289
- creds = pickle.load(token)
290
- if creds and creds.valid:
291
- return "Conectado a Google Drive", True
292
- else:
293
- return "Sesión expirada", False
294
- except:
295
- return "Error al verificar sesión", False
 
 
 
 
 
 
 
 
 
 
296
 
297
- # ============= CERRAR SESIÓN DE DRIVE =============
298
- def cerrar_sesion_drive():
299
- """Cierra la sesión de Google Drive"""
300
 
301
- try:
302
- if os.path.exists('token.pickle'):
303
- os.remove('token.pickle')
304
- return "Sesión cerrada correctamente", False
305
- else:
306
- return "No había sesión activa", False
307
- except Exception as e:
308
- return f"Error al cerrar sesión: {str(e)}", False
309
- def subir_a_drive(archivo_csv):
310
- """Sube el archivo CSV a Google Drive"""
 
 
 
 
 
 
 
311
 
312
- if not DRIVE_DISPONIBLE:
313
- return "Error: Librerías de Google Drive no instaladas.\n\nAgrega al requirements.txt:\ngoogle-auth-oauthlib\ngoogle-auth-httplib2\ngoogle-api-python-client"
314
 
315
- SCOPES = ['https://www.googleapis.com/auth/drive.file']
316
- creds = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
- try:
319
- # Verificar si existen credenciales guardadas
320
- if os.path.exists('token.pickle'):
321
- with open('token.pickle', 'rb') as token:
322
- creds = pickle.load(token)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
- # Si no hay credenciales válidas, solicitar login
325
- if not creds or not creds.valid:
326
- if creds and creds.expired and creds.refresh_token:
327
- creds.refresh(Request())
328
- else:
329
- # Verificar si existe credentials.json
330
- if not os.path.exists('credentials.json'):
331
- return "Error: Falta el archivo credentials.json.\n\nDescárgalo desde Google Cloud Console:\nhttps://console.cloud.google.com/"
332
-
333
- flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
334
- creds = flow.run_local_server(port=0)
335
-
336
- # Guardar credenciales para la próxima vez
337
- with open('token.pickle', 'wb') as token:
338
- pickle.dump(creds, token)
339
 
340
- # Crear servicio de Drive
341
- service = build('drive', 'v3', credentials=creds)
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
- # Metadatos del archivo
344
- file_metadata = {
345
- 'name': os.path.basename(archivo_csv),
346
- 'mimeType': 'text/csv'
347
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
- media = MediaFileUpload(archivo_csv, mimetype='text/csv', resumable=True)
 
 
 
 
 
 
350
 
351
- # Subir archivo
352
- file = service.files().create(
353
- body=file_metadata,
354
- media_body=media,
355
- fields='id, webViewLink'
356
- ).execute()
 
 
 
 
 
 
 
 
 
 
357
 
358
- return f"Archivo subido exitosamente a Google Drive\n\nEnlace: {file.get('webViewLink')}"
 
359
 
360
- except Exception as e:
361
- return f"Error al subir a Google Drive: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
  # ============= FUNCIÓN PRINCIPAL =============
364
- def procesar_factura(pdf_file, guardar_en_drive):
365
  if pdf_file is None:
366
- return "", None, None, "", "Sube un PDF primero", ""
367
 
368
- # PASO 1: Extraer texto del PDF
369
  print("\n--- Extrayendo texto del PDF...")
370
  texto = extraer_texto_pdf(pdf_file)
371
 
372
  if texto.startswith("Error"):
373
- return "", None, None, "", f"Error: {texto}", ""
374
 
375
- # Mostrar preview del texto
376
  texto_preview = f"{texto[:1500]}..." if len(texto) > 1500 else texto
377
 
378
- # PASO 2: LLM analiza y convierte a JSON
379
  print("--- El LLM está analizando la factura y creando el JSON...")
380
  datos_json, resumen_util, mensaje = analizar_y_convertir_json(texto)
381
 
382
  if not datos_json:
383
- return texto_preview, None, None, "", mensaje, ""
384
 
385
- # PASO 3: Convertir JSON a DataFrame
386
  print("--- Convirtiendo JSON a CSV...")
387
  df = json_a_csv(datos_json)
388
 
389
- # PASO 4: Guardar CSV
390
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
391
  numero = datos_json.get('numero_factura', 'factura')
392
  numero = re.sub(r'[^\w\-]', '_', str(numero))
393
  csv_filename = f"{numero}_{timestamp}.csv"
394
  df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
395
 
396
- # PASO 5: Subir a Drive si está seleccionado
397
- mensaje_drive = ""
398
- if guardar_en_drive:
399
- print("--- Subiendo a Google Drive...")
400
- mensaje_drive = subir_a_drive(csv_filename)
401
-
402
- # PASO 6: Crear resumen técnico
403
  resumen_tecnico = f"""## Factura Procesada Exitosamente
404
 
405
  **Modelo utilizado:** {mensaje}
@@ -431,98 +698,104 @@ def procesar_factura(pdf_file, guardar_en_drive):
431
  """
432
 
433
  print(f"--- CSV guardado: {csv_filename}")
434
- return texto_preview, df, csv_filename, resumen_tecnico, resumen_util, mensaje_drive
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
  # ============= INTERFAZ GRADIO =============
437
- with gr.Blocks(title="Extractor IA de Facturas") as demo:
 
 
 
438
 
439
- # Título principal
440
  gr.Markdown("""
441
- # Extractor Inteligente de Facturas
442
- ### Análisis automático de facturas PDF con Inteligencia Artificial
443
  """)
444
 
445
  gr.Markdown("---")
446
 
447
  with gr.Row():
448
- # COLUMNA IZQUIERDA - INPUT
449
  with gr.Column(scale=1):
450
- gr.Markdown("### Cargar Documento")
451
  gr.Markdown("")
452
 
453
  pdf_input = gr.File(
454
- label="Seleccionar factura PDF",
455
  file_types=[".pdf"],
456
  type="filepath"
457
  )
458
 
 
 
 
 
 
 
 
 
459
  gr.Markdown("")
460
  gr.Markdown("---")
461
  gr.Markdown("")
462
 
463
- # Sección de Google Drive
464
- gr.Markdown("### Google Drive")
465
-
466
- with gr.Row():
467
- btn_conectar_drive = gr.Button(
468
- "Conectar con Google Drive",
469
- size="sm",
470
- variant="secondary"
471
- )
472
- btn_cerrar_sesion = gr.Button(
473
- "Cerrar Sesión",
474
- size="sm"
475
- )
476
 
 
 
477
  gr.Markdown("")
478
 
479
- drive_status_auth = gr.Textbox(
480
- label="Estado de conexión",
481
- value="No conectado",
482
- interactive=False,
483
- lines=2
484
- )
485
-
486
  gr.Markdown("")
487
 
488
- # Checkbox para Google Drive
489
- drive_checkbox = gr.Checkbox(
490
- label="Guardar en Google Drive",
491
- value=False,
492
- info="Primero debes conectar tu cuenta"
493
  )
494
 
495
- gr.Markdown("")
496
- gr.Markdown("---")
497
  gr.Markdown("")
498
 
499
- btn = gr.Button(
500
- "Procesar Factura",
501
- variant="primary",
502
  size="lg"
503
  )
504
 
505
- gr.Markdown("")
506
- gr.Markdown("---")
507
  gr.Markdown("")
508
 
509
- csv_output = gr.File(label="Descargar archivo CSV generado")
510
 
511
- gr.Markdown("")
512
-
513
- # Estado de subida a Drive
514
- drive_upload_status = gr.Textbox(
515
- label="Estado de subida a Drive",
516
  interactive=False,
517
- lines=3
518
  )
519
 
520
- # COLUMNA DERECHA - RESULTADOS
521
  with gr.Column(scale=2):
522
  gr.Markdown("### Resultados del Análisis")
523
  gr.Markdown("")
524
 
525
- # Información útil destacada
526
  gr.Markdown("#### Información Útil para Administrativos")
527
  info_util = gr.Markdown(
528
  value="*Aquí aparecerá información relevante una vez procesada la factura*"
@@ -532,7 +805,6 @@ with gr.Blocks(title="Extractor IA de Facturas") as demo:
532
  gr.Markdown("---")
533
  gr.Markdown("")
534
 
535
- # Tabs para información detallada
536
  with gr.Tabs():
537
  with gr.Tab("Vista Previa CSV"):
538
  gr.Markdown("")
@@ -558,41 +830,29 @@ with gr.Blocks(title="Extractor IA de Facturas") as demo:
558
  gr.Markdown("---")
559
  gr.Markdown("")
560
 
561
- # Footer con información
562
  gr.Markdown("""
563
- **Sistema de extracción automática de datos mediante modelos de lenguaje**
564
-
565
- *Configuración requerida:*
566
- - *HF_TOKEN en Settings Secrets (obligatorio)*
567
- - *credentials.json de Google Cloud para usar Drive (opcional)*
568
-
569
- **Pasos para configurar Google Drive:**
570
- 1. Ve a [Google Cloud Console](https://console.cloud.google.com/)
571
- 2. Crea un proyecto y activa Google Drive API
572
- 3. Crea credenciales OAuth 2.0 (Tipo: Aplicación de escritorio)
573
- 4. Descarga el archivo credentials.json
574
- 5. Súbelo al directorio raíz de tu aplicación
575
- 6. Haz clic en "Conectar con Google Drive"
576
  """)
577
 
578
  # Conectar botones
579
- btn_conectar_drive.click(
580
- fn=autenticar_drive,
581
- inputs=[],
582
- outputs=[drive_status_auth, drive_checkbox]
583
- )
584
-
585
- btn_cerrar_sesion.click(
586
- fn=cerrar_sesion_drive,
587
- inputs=[],
588
- outputs=[drive_status_auth, drive_checkbox]
589
  )
590
 
591
- # Conectar botón principal
592
- btn.click(
593
- fn=procesar_factura,
594
- inputs=[pdf_input, drive_checkbox],
595
- outputs=[texto_extraido, tabla_preview, csv_output, resumen_tecnico, info_util, drive_upload_status]
596
  )
597
 
598
  if __name__ == "__main__":
 
6
  import re
7
  from datetime import datetime
8
  from huggingface_hub import InferenceClient
9
+ from reportlab.lib.pagesizes import letter, A4
10
+ from reportlab.lib import colors
11
+ from reportlab.lib.units import inch
12
+ from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
13
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
14
+ from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT
 
 
 
 
 
 
15
 
16
  # ============= EXTRAER TEXTO DEL PDF =============
17
  def extraer_texto_pdf(pdf_file):
 
32
  if not token:
33
  return None, None, "Error: Falta configurar HF_TOKEN en Settings → Secrets"
34
 
 
35
  texto_limpio = texto[:8000]
36
 
 
37
  prompt = f"""Eres un experto en análisis de facturas. Lee esta factura y conviértela a JSON.
38
 
39
  TEXTO DE LA FACTURA:
 
77
 
78
  Responde SOLO con el JSON válido (sin explicaciones, sin markdown):"""
79
 
 
80
  modelos = [
81
  "Qwen/Qwen2.5-72B-Instruct",
82
  "meta-llama/Llama-3.2-3B-Instruct",
 
86
 
87
  for modelo in modelos:
88
  try:
89
+ print(f"\nProbando: {modelo}")
90
  client = InferenceClient(token=token)
91
 
 
92
  response = client.chat.completions.create(
93
  model=modelo,
94
  messages=[
 
98
  temperature=0.1
99
  )
100
 
 
101
  resultado = response.choices[0].message.content
 
 
102
  resultado = resultado.strip()
103
  resultado = re.sub(r'```json\s*', '', resultado)
104
  resultado = re.sub(r'```\s*', '', resultado)
105
  resultado = resultado.strip()
106
 
 
107
  match = re.search(r'\{.*\}', resultado, re.DOTALL)
108
  if match:
109
  json_str = match.group(0)
110
  try:
111
  datos_json = json.loads(json_str)
112
+ print(f"JSON válido extraído con {modelo}")
113
 
 
114
  resumen_util = generar_resumen_util(texto_limpio, modelo, client)
115
 
116
  return datos_json, resumen_util, f"Procesado con {modelo}"
117
  except json.JSONDecodeError as e:
118
+ print(f"JSON inválido: {str(e)[:50]}")
119
  continue
 
 
 
120
 
121
  except Exception as e:
122
+ print(f"{modelo} falló: {str(e)[:100]}")
123
  continue
124
 
125
  return None, None, "Ningún modelo LLM pudo extraer el JSON. Verifica tu HF_TOKEN."
 
161
 
162
  filas = []
163
 
 
164
  filas.append({'Campo': '=== INFORMACIÓN GENERAL ===', 'Valor': ''})
165
  filas.append({'Campo': 'Número de Factura', 'Valor': datos_json.get('numero_factura', 'N/A')})
166
  filas.append({'Campo': 'Fecha', 'Valor': datos_json.get('fecha', 'N/A')})
167
 
 
168
  if 'emisor' in datos_json:
169
  filas.append({'Campo': '', 'Valor': ''})
170
  filas.append({'Campo': '=== EMISOR ===', 'Valor': ''})
 
175
  else:
176
  filas.append({'Campo': 'Nombre', 'Valor': str(emisor)})
177
 
 
178
  if 'cliente' in datos_json:
179
  filas.append({'Campo': '', 'Valor': ''})
180
  filas.append({'Campo': '=== CLIENTE ===', 'Valor': ''})
 
185
  else:
186
  filas.append({'Campo': 'Nombre', 'Valor': str(cliente)})
187
 
 
188
  productos = datos_json.get('productos', datos_json.get('conceptos', datos_json.get('items', [])))
189
  if productos and len(productos) > 0:
190
  filas.append({'Campo': '', 'Valor': ''})
 
197
  filas.append({'Campo': ' Total', 'Valor': f"{prod.get('total', 0)}€"})
198
  filas.append({'Campo': '', 'Valor': ''})
199
 
 
200
  totales = datos_json.get('totales', {})
201
  if totales or 'base_imponible' in datos_json or 'total' in datos_json:
202
  filas.append({'Campo': '', 'Valor': ''})
203
  filas.append({'Campo': '=== TOTALES ===', 'Valor': ''})
204
 
 
205
  base = totales.get('base_imponible', datos_json.get('base_imponible', 0))
206
  iva = totales.get('iva', datos_json.get('iva', 0))
207
  porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0))
 
216
 
217
  return pd.DataFrame(filas)
218
 
219
+ # ============= GENERAR PDF DESDE CSV - TEMPLATE CLÁSICO =============
220
+ def generar_pdf_clasico(csv_file, datos_json):
221
+ """Template clásico - Estilo tradicional corporativo"""
222
 
223
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
224
+ pdf_filename = f"factura_clasica_{timestamp}.pdf"
225
+
226
+ doc = SimpleDocTemplate(pdf_filename, pagesize=A4)
227
+ story = []
228
+ styles = getSampleStyleSheet()
229
+
230
+ # Estilos personalizados
231
+ titulo_style = ParagraphStyle(
232
+ 'CustomTitle',
233
+ parent=styles['Heading1'],
234
+ fontSize=24,
235
+ textColor=colors.HexColor('#1a1a1a'),
236
+ spaceAfter=30,
237
+ alignment=TA_CENTER
238
+ )
239
 
240
+ # Título
241
+ story.append(Paragraph("FACTURA", titulo_style))
242
+ story.append(Spacer(1, 0.3*inch))
243
 
244
+ # Información básica
245
+ info_data = [
246
+ ['Número de Factura:', datos_json.get('numero_factura', 'N/A')],
247
+ ['Fecha:', datos_json.get('fecha', 'N/A')]
248
+ ]
249
+
250
+ info_table = Table(info_data, colWidths=[2*inch, 4*inch])
251
+ info_table.setStyle(TableStyle([
252
+ ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
253
+ ('FONTSIZE', (0, 0), (-1, -1), 11),
254
+ ('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#666666')),
255
+ ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
256
+ ('ALIGN', (1, 0), (1, -1), 'LEFT'),
257
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 12),
258
+ ]))
259
+
260
+ story.append(info_table)
261
+ story.append(Spacer(1, 0.3*inch))
262
+
263
+ # Emisor y Cliente
264
+ emisor = datos_json.get('emisor', {})
265
+ cliente = datos_json.get('cliente', {})
266
+
267
+ partes_data = [
268
+ ['EMISOR', 'CLIENTE'],
269
+ [
270
+ emisor.get('nombre', 'N/A') if isinstance(emisor, dict) else str(emisor),
271
+ cliente.get('nombre', 'N/A') if isinstance(cliente, dict) else str(cliente)
272
+ ],
273
+ [
274
+ emisor.get('nif', '') if isinstance(emisor, dict) else '',
275
+ cliente.get('nif', '') if isinstance(cliente, dict) else ''
276
+ ]
277
+ ]
278
+
279
+ partes_table = Table(partes_data, colWidths=[3*inch, 3*inch])
280
+ partes_table.setStyle(TableStyle([
281
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
282
+ ('FONTSIZE', (0, 0), (-1, 0), 12),
283
+ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e0e0e0')),
284
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#1a1a1a')),
285
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
286
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
287
+ ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
288
+ ('FONTSIZE', (0, 1), (-1, -1), 10),
289
+ ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#cccccc')),
290
+ ('TOPPADDING', (0, 0), (-1, -1), 10),
291
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 10),
292
+ ('LEFTPADDING', (0, 0), (-1, -1), 10),
293
+ ]))
294
+
295
+ story.append(partes_table)
296
+ story.append(Spacer(1, 0.4*inch))
297
+
298
+ # Productos
299
+ productos = datos_json.get('productos', datos_json.get('conceptos', []))
300
+
301
+ if productos:
302
+ productos_data = [['Descripción', 'Cantidad', 'Precio Unit.', 'Total']]
303
 
304
+ for prod in productos:
305
+ productos_data.append([
306
+ str(prod.get('descripcion', '')),
307
+ str(prod.get('cantidad', '')),
308
+ f"{prod.get('precio_unitario', 0):.2f} €",
309
+ f"{prod.get('total', 0):.2f} €"
310
+ ])
311
 
312
+ productos_table = Table(productos_data, colWidths=[3*inch, 1*inch, 1.5*inch, 1.5*inch])
313
+ productos_table.setStyle(TableStyle([
314
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
315
+ ('FONTSIZE', (0, 0), (-1, 0), 11),
316
+ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4a4a4a')),
317
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
318
+ ('ALIGN', (0, 0), (0, -1), 'LEFT'),
319
+ ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
320
+ ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
321
+ ('FONTSIZE', (0, 1), (-1, -1), 10),
322
+ ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#cccccc')),
323
+ ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f5f5f5')]),
324
+ ('TOPPADDING', (0, 0), (-1, -1), 8),
325
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
326
+ ]))
327
 
328
+ story.append(productos_table)
329
+ story.append(Spacer(1, 0.3*inch))
330
 
331
+ # Totales
332
+ totales = datos_json.get('totales', {})
333
+ base = totales.get('base_imponible', datos_json.get('base_imponible', 0))
334
+ iva = totales.get('iva', datos_json.get('iva', 0))
335
+ porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0))
336
+ total = totales.get('total', datos_json.get('total', 0))
337
+
338
+ totales_data = [
339
+ ['Base Imponible:', f"{base:.2f} "],
340
+ [f'IVA ({porcentaje_iva}%):', f"{iva:.2f} €"],
341
+ ['TOTAL:', f"{total:.2f} €"]
342
+ ]
343
 
344
+ totales_table = Table(totales_data, colWidths=[4.5*inch, 1.5*inch])
345
+ totales_table.setStyle(TableStyle([
346
+ ('FONTNAME', (0, 0), (-1, 1), 'Helvetica'),
347
+ ('FONTNAME', (0, 2), (-1, 2), 'Helvetica-Bold'),
348
+ ('FONTSIZE', (0, 0), (-1, 1), 11),
349
+ ('FONTSIZE', (0, 2), (-1, 2), 14),
350
+ ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
351
+ ('ALIGN', (1, 0), (1, -1), 'RIGHT'),
352
+ ('BACKGROUND', (0, 2), (-1, 2), colors.HexColor('#4a4a4a')),
353
+ ('TEXTCOLOR', (0, 2), (-1, 2), colors.white),
354
+ ('TOPPADDING', (0, 0), (-1, -1), 8),
355
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
356
+ ('RIGHTPADDING', (0, 0), (-1, -1), 10),
357
+ ]))
358
+
359
+ story.append(totales_table)
360
+
361
+ doc.build(story)
362
+ return pdf_filename
363
 
364
+ # ============= GENERAR PDF - TEMPLATE MODERNO =============
365
+ def generar_pdf_moderno(csv_file, datos_json):
366
+ """Template moderno - Estilo minimalista y limpio"""
367
 
368
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
369
+ pdf_filename = f"factura_moderna_{timestamp}.pdf"
370
+
371
+ doc = SimpleDocTemplate(pdf_filename, pagesize=A4)
372
+ story = []
373
+ styles = getSampleStyleSheet()
374
+
375
+ # Título moderno
376
+ titulo_style = ParagraphStyle(
377
+ 'ModernTitle',
378
+ parent=styles['Heading1'],
379
+ fontSize=32,
380
+ textColor=colors.HexColor('#2196F3'),
381
+ spaceAfter=10,
382
+ alignment=TA_LEFT,
383
+ fontName='Helvetica-Bold'
384
+ )
385
 
386
+ story.append(Paragraph("FACTURA", titulo_style))
 
387
 
388
+ # Subtítulo
389
+ subtitulo = f"No. {datos_json.get('numero_factura', 'N/A')} | {datos_json.get('fecha', 'N/A')}"
390
+ subtitulo_style = ParagraphStyle(
391
+ 'Subtitle',
392
+ parent=styles['Normal'],
393
+ fontSize=11,
394
+ textColor=colors.HexColor('#757575'),
395
+ spaceAfter=30
396
+ )
397
+ story.append(Paragraph(subtitulo, subtitulo_style))
398
+ story.append(Spacer(1, 0.3*inch))
399
+
400
+ # Emisor y Cliente en cajas
401
+ emisor = datos_json.get('emisor', {})
402
+ cliente = datos_json.get('cliente', {})
403
+
404
+ info_boxes = [
405
+ [
406
+ Paragraph(f"<b>DE:</b><br/>{emisor.get('nombre', 'N/A') if isinstance(emisor, dict) else str(emisor)}<br/>{emisor.get('nif', '') if isinstance(emisor, dict) else ''}", styles['Normal']),
407
+ Paragraph(f"<b>PARA:</b><br/>{cliente.get('nombre', 'N/A') if isinstance(cliente, dict) else str(cliente)}<br/>{cliente.get('nif', '') if isinstance(cliente, dict) else ''}", styles['Normal'])
408
+ ]
409
+ ]
410
 
411
+ boxes_table = Table(info_boxes, colWidths=[3*inch, 3*inch])
412
+ boxes_table.setStyle(TableStyle([
413
+ ('BACKGROUND', (0, 0), (0, 0), colors.HexColor('#E3F2FD')),
414
+ ('BACKGROUND', (1, 0), (1, 0), colors.HexColor('#FFF3E0')),
415
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
416
+ ('TOPPADDING', (0, 0), (-1, -1), 15),
417
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 15),
418
+ ('LEFTPADDING', (0, 0), (-1, -1), 15),
419
+ ('RIGHTPADDING', (0, 0), (-1, -1), 15),
420
+ ]))
421
+
422
+ story.append(boxes_table)
423
+ story.append(Spacer(1, 0.4*inch))
424
+
425
+ # Productos con estilo moderno
426
+ productos = datos_json.get('productos', datos_json.get('conceptos', []))
427
+
428
+ if productos:
429
+ productos_data = [['DESCRIPCIÓN', 'CANT.', 'PRECIO', 'TOTAL']]
430
 
431
+ for prod in productos:
432
+ productos_data.append([
433
+ str(prod.get('descripcion', '')),
434
+ str(prod.get('cantidad', '')),
435
+ f"{prod.get('precio_unitario', 0):.2f} €",
436
+ f"{prod.get('total', 0):.2f} €"
437
+ ])
 
 
 
 
 
 
 
 
438
 
439
+ productos_table = Table(productos_data, colWidths=[3*inch, 0.8*inch, 1.5*inch, 1.7*inch])
440
+ productos_table.setStyle(TableStyle([
441
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
442
+ ('FONTSIZE', (0, 0), (-1, 0), 9),
443
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#757575')),
444
+ ('ALIGN', (0, 0), (0, -1), 'LEFT'),
445
+ ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
446
+ ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
447
+ ('FONTSIZE', (0, 1), (-1, -1), 10),
448
+ ('LINEBELOW', (0, 0), (-1, 0), 2, colors.HexColor('#2196F3')),
449
+ ('LINEBELOW', (0, 1), (-1, -2), 0.5, colors.HexColor('#e0e0e0')),
450
+ ('TOPPADDING', (0, 0), (-1, -1), 10),
451
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 10),
452
+ ]))
453
 
454
+ story.append(productos_table)
455
+ story.append(Spacer(1, 0.4*inch))
456
+
457
+ # Totales modernos
458
+ totales = datos_json.get('totales', {})
459
+ base = totales.get('base_imponible', datos_json.get('base_imponible', 0))
460
+ iva = totales.get('iva', datos_json.get('iva', 0))
461
+ porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0))
462
+ total = totales.get('total', datos_json.get('total', 0))
463
+
464
+ totales_data = [
465
+ ['Subtotal', f"{base:.2f} €"],
466
+ [f'IVA {porcentaje_iva}%', f"{iva:.2f} €"],
467
+ ['', ''],
468
+ ['TOTAL', f"{total:.2f} €"]
469
+ ]
470
+
471
+ totales_table = Table(totales_data, colWidths=[5*inch, 2*inch])
472
+ totales_table.setStyle(TableStyle([
473
+ ('FONTNAME', (0, 0), (-1, 2), 'Helvetica'),
474
+ ('FONTNAME', (0, 3), (-1, 3), 'Helvetica-Bold'),
475
+ ('FONTSIZE', (0, 0), (-1, 2), 11),
476
+ ('FONTSIZE', (0, 3), (-1, 3), 16),
477
+ ('ALIGN', (0, 0), (-1, -1), 'RIGHT'),
478
+ ('TEXTCOLOR', (0, 3), (-1, 3), colors.HexColor('#2196F3')),
479
+ ('LINEABOVE', (0, 3), (-1, 3), 2, colors.HexColor('#2196F3')),
480
+ ('TOPPADDING', (0, 0), (-1, -1), 8),
481
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
482
+ ]))
483
+
484
+ story.append(totales_table)
485
+
486
+ doc.build(story)
487
+ return pdf_filename
488
+
489
+ # ============= GENERAR PDF - TEMPLATE ELEGANTE =============
490
+ def generar_pdf_elegante(csv_file, datos_json):
491
+ """Template elegante - Estilo premium con detalles"""
492
+
493
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
494
+ pdf_filename = f"factura_elegante_{timestamp}.pdf"
495
+
496
+ doc = SimpleDocTemplate(pdf_filename, pagesize=A4)
497
+ story = []
498
+ styles = getSampleStyleSheet()
499
+
500
+ # Encabezado elegante
501
+ header_style = ParagraphStyle(
502
+ 'ElegantHeader',
503
+ parent=styles['Heading1'],
504
+ fontSize=28,
505
+ textColor=colors.HexColor('#1a237e'),
506
+ spaceAfter=5,
507
+ alignment=TA_CENTER,
508
+ fontName='Helvetica-Bold'
509
+ )
510
+
511
+ story.append(Paragraph("F A C T U R A", header_style))
512
+
513
+ # Línea decorativa
514
+ line_data = [['']]
515
+ line_table = Table(line_data, colWidths=[6.5*inch])
516
+ line_table.setStyle(TableStyle([
517
+ ('LINEBELOW', (0, 0), (-1, 0), 3, colors.HexColor('#7986cb')),
518
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 20),
519
+ ]))
520
+ story.append(line_table)
521
+
522
+ # Información de factura
523
+ info_data = [[
524
+ f"No. {datos_json.get('numero_factura', 'N/A')}",
525
+ f"Fecha: {datos_json.get('fecha', 'N/A')}"
526
+ ]]
527
+
528
+ info_table = Table(info_data, colWidths=[3.25*inch, 3.25*inch])
529
+ info_table.setStyle(TableStyle([
530
+ ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
531
+ ('FONTSIZE', (0, 0), (-1, -1), 10),
532
+ ('TEXTCOLOR', (0, 0), (-1, -1), colors.HexColor('#424242')),
533
+ ('ALIGN', (0, 0), (0, -1), 'LEFT'),
534
+ ('ALIGN', (1, 0), (1, -1), 'RIGHT'),
535
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 15),
536
+ ]))
537
+
538
+ story.append(info_table)
539
+ story.append(Spacer(1, 0.2*inch))
540
+
541
+ # Emisor y Cliente elegante
542
+ emisor = datos_json.get('emisor', {})
543
+ cliente = datos_json.get('cliente', {})
544
+
545
+ partes_data = [
546
+ ['Emisor', 'Cliente'],
547
+ [
548
+ f"{emisor.get('nombre', 'N/A') if isinstance(emisor, dict) else str(emisor)}\n{emisor.get('nif', '') if isinstance(emisor, dict) else ''}\n{emisor.get('direccion', '') if isinstance(emisor, dict) else ''}",
549
+ f"{cliente.get('nombre', 'N/A') if isinstance(cliente, dict) else str(cliente)}\n{cliente.get('nif', '') if isinstance(cliente, dict) else ''}"
550
+ ]
551
+ ]
552
+
553
+ partes_table = Table(partes_data, colWidths=[3.25*inch, 3.25*inch])
554
+ partes_table.setStyle(TableStyle([
555
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
556
+ ('FONTSIZE', (0, 0), (-1, 0), 11),
557
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#1a237e')),
558
+ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e8eaf6')),
559
+ ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
560
+ ('FONTSIZE', (0, 1), (-1, -1), 9),
561
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
562
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
563
+ ('BOX', (0, 0), (-1, -1), 1.5, colors.HexColor('#7986cb')),
564
+ ('INNERGRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#c5cae9')),
565
+ ('TOPPADDING', (0, 0), (-1, -1), 12),
566
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 12),
567
+ ('LEFTPADDING', (0, 0), (-1, -1), 12),
568
+ ]))
569
+
570
+ story.append(partes_table)
571
+ story.append(Spacer(1, 0.3*inch))
572
+
573
+ # Productos elegantes
574
+ productos = datos_json.get('productos', datos_json.get('conceptos', []))
575
+
576
+ if productos:
577
+ productos_data = [['Descripción', 'Cant.', 'Precio Unitario', 'Total']]
578
 
579
+ for prod in productos:
580
+ productos_data.append([
581
+ str(prod.get('descripcion', '')),
582
+ str(prod.get('cantidad', '')),
583
+ f"{prod.get('precio_unitario', 0):.2f} €",
584
+ f"{prod.get('total', 0):.2f} €"
585
+ ])
586
 
587
+ productos_table = Table(productos_data, colWidths=[2.8*inch, 0.8*inch, 1.4*inch, 1.5*inch])
588
+ productos_table.setStyle(TableStyle([
589
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
590
+ ('FONTSIZE', (0, 0), (-1, 0), 10),
591
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
592
+ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#5c6bc0')),
593
+ ('ALIGN', (0, 0), (0, -1), 'LEFT'),
594
+ ('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
595
+ ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
596
+ ('FONTSIZE', (0, 1), (-1, -1), 9),
597
+ ('BOX', (0, 0), (-1, -1), 1, colors.HexColor('#7986cb')),
598
+ ('LINEBELOW', (0, 0), (-1, 0), 1.5, colors.HexColor('#3f51b5')),
599
+ ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#fafafa')]),
600
+ ('TOPPADDING', (0, 0), (-1, -1), 10),
601
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 10),
602
+ ]))
603
 
604
+ story.append(productos_table)
605
+ story.append(Spacer(1, 0.3*inch))
606
 
607
+ # Totales elegantes
608
+ totales = datos_json.get('totales', {})
609
+ base = totales.get('base_imponible', datos_json.get('base_imponible', 0))
610
+ iva = totales.get('iva', datos_json.get('iva', 0))
611
+ porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0))
612
+ total = totales.get('total', datos_json.get('total', 0))
613
+
614
+ totales_data = [
615
+ ['', 'Base Imponible:', f"{base:.2f} €"],
616
+ ['', f'IVA ({porcentaje_iva}%):', f"{iva:.2f} €"],
617
+ ['', '', ''],
618
+ ['', 'TOTAL A PAGAR:', f"{total:.2f} €"]
619
+ ]
620
+
621
+ totales_table = Table(totales_data, colWidths=[2.5*inch, 2.5*inch, 1.5*inch])
622
+ totales_table.setStyle(TableStyle([
623
+ ('FONTNAME', (1, 0), (-1, 2), 'Helvetica'),
624
+ ('FONTNAME', (1, 3), (-1, 3), 'Helvetica-Bold'),
625
+ ('FONTSIZE', (1, 0), (-1, 2), 10),
626
+ ('FONTSIZE', (1, 3), (-1, 3), 14),
627
+ ('ALIGN', (1, 0), (1, -1), 'RIGHT'),
628
+ ('ALIGN', (2, 0), (2, -1), 'RIGHT'),
629
+ ('BACKGROUND', (1, 3), (-1, 3), colors.HexColor('#1a237e')),
630
+ ('TEXTCOLOR', (1, 3), (-1, 3), colors.white),
631
+ ('TOPPADDING', (0, 0), (-1, -1), 8),
632
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
633
+ ('RIGHTPADDING', (0, 0), (-1, -1), 12),
634
+ ('LEFTPADDING', (1, 3), (-1, 3), 12),
635
+ ]))
636
+
637
+ story.append(totales_table)
638
+
639
+ doc.build(story)
640
+ return pdf_filename
641
 
642
  # ============= FUNCIÓN PRINCIPAL =============
643
+ def procesar_factura(pdf_file):
644
  if pdf_file is None:
645
+ return "", None, None, "", "", None, None
646
 
 
647
  print("\n--- Extrayendo texto del PDF...")
648
  texto = extraer_texto_pdf(pdf_file)
649
 
650
  if texto.startswith("Error"):
651
+ return "", None, None, "", f"Error: {texto}", None, None
652
 
 
653
  texto_preview = f"{texto[:1500]}..." if len(texto) > 1500 else texto
654
 
 
655
  print("--- El LLM está analizando la factura y creando el JSON...")
656
  datos_json, resumen_util, mensaje = analizar_y_convertir_json(texto)
657
 
658
  if not datos_json:
659
+ return texto_preview, None, None, "", mensaje, None, None
660
 
 
661
  print("--- Convirtiendo JSON a CSV...")
662
  df = json_a_csv(datos_json)
663
 
 
664
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
665
  numero = datos_json.get('numero_factura', 'factura')
666
  numero = re.sub(r'[^\w\-]', '_', str(numero))
667
  csv_filename = f"{numero}_{timestamp}.csv"
668
  df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
669
 
 
 
 
 
 
 
 
670
  resumen_tecnico = f"""## Factura Procesada Exitosamente
671
 
672
  **Modelo utilizado:** {mensaje}
 
698
  """
699
 
700
  print(f"--- CSV guardado: {csv_filename}")
701
+ return texto_preview, df, csv_filename, resumen_tecnico, resumen_util, datos_json, csv_filename
702
+
703
+ # ============= GENERAR PDF CON TEMPLATE SELECCIONADO =============
704
+ def generar_pdf_con_template(template, csv_file, datos_json):
705
+ if not datos_json:
706
+ return None, "Error: Primero debes procesar una factura"
707
+
708
+ try:
709
+ if template == "Clásico":
710
+ pdf_file = generar_pdf_clasico(csv_file, datos_json)
711
+ elif template == "Moderno":
712
+ pdf_file = generar_pdf_moderno(csv_file, datos_json)
713
+ elif template == "Elegante":
714
+ pdf_file = generar_pdf_elegante(csv_file, datos_json)
715
+ else:
716
+ return None, "Template no válido"
717
+
718
+ return pdf_file, f"PDF generado exitosamente: {pdf_file}"
719
+ except Exception as e:
720
+ return None, f"Error al generar PDF: {str(e)}"
721
 
722
  # ============= INTERFAZ GRADIO =============
723
+ with gr.Blocks(title="Extractor y Generador de Facturas") as demo:
724
+
725
+ datos_json_state = gr.State()
726
+ csv_file_state = gr.State()
727
 
 
728
  gr.Markdown("""
729
+ # Extractor y Generador de Facturas
730
+ ### Extrae datos de facturas PDF y genera nuevas facturas con diferentes estilos
731
  """)
732
 
733
  gr.Markdown("---")
734
 
735
  with gr.Row():
736
+ # COLUMNA IZQUIERDA
737
  with gr.Column(scale=1):
738
+ gr.Markdown("### Paso 1: Extraer Datos")
739
  gr.Markdown("")
740
 
741
  pdf_input = gr.File(
742
+ label="Subir factura PDF para extraer datos",
743
  file_types=[".pdf"],
744
  type="filepath"
745
  )
746
 
747
+ gr.Markdown("")
748
+
749
+ btn_extraer = gr.Button(
750
+ "Extraer Datos de la Factura",
751
+ variant="primary",
752
+ size="lg"
753
+ )
754
+
755
  gr.Markdown("")
756
  gr.Markdown("---")
757
  gr.Markdown("")
758
 
759
+ csv_output = gr.File(label="Descargar CSV con los datos extraídos")
 
 
 
 
 
 
 
 
 
 
 
 
760
 
761
+ gr.Markdown("")
762
+ gr.Markdown("---")
763
  gr.Markdown("")
764
 
765
+ # Generador de PDF
766
+ gr.Markdown("### Paso 2: Generar PDF")
 
 
 
 
 
767
  gr.Markdown("")
768
 
769
+ template_selector = gr.Radio(
770
+ choices=["Clásico", "Moderno", "Elegante"],
771
+ value="Moderno",
772
+ label="Seleccionar estilo de factura",
773
+ info="Elige el diseño que prefieras"
774
  )
775
 
 
 
776
  gr.Markdown("")
777
 
778
+ btn_generar_pdf = gr.Button(
779
+ "Generar Factura PDF",
780
+ variant="secondary",
781
  size="lg"
782
  )
783
 
 
 
784
  gr.Markdown("")
785
 
786
+ pdf_output = gr.File(label="Descargar factura PDF generada")
787
 
788
+ pdf_status = gr.Textbox(
789
+ label="Estado",
 
 
 
790
  interactive=False,
791
+ lines=2
792
  )
793
 
794
+ # COLUMNA DERECHA
795
  with gr.Column(scale=2):
796
  gr.Markdown("### Resultados del Análisis")
797
  gr.Markdown("")
798
 
 
799
  gr.Markdown("#### Información Útil para Administrativos")
800
  info_util = gr.Markdown(
801
  value="*Aquí aparecerá información relevante una vez procesada la factura*"
 
805
  gr.Markdown("---")
806
  gr.Markdown("")
807
 
 
808
  with gr.Tabs():
809
  with gr.Tab("Vista Previa CSV"):
810
  gr.Markdown("")
 
830
  gr.Markdown("---")
831
  gr.Markdown("")
832
 
 
833
  gr.Markdown("""
834
+ **Sistema de extracción y generación de facturas con IA**
835
+
836
+ **Características:**
837
+ - Extrae datos automáticamente de facturas PDF
838
+ - Genera CSV estructurado
839
+ - Crea facturas PDF profesionales con 3 templates diferentes
840
+ - Análisis inteligente con modelos de lenguaje
841
+
842
+ *Configuración requerida: HF_TOKEN en Settings Secrets*
 
 
 
 
843
  """)
844
 
845
  # Conectar botones
846
+ btn_extraer.click(
847
+ fn=procesar_factura,
848
+ inputs=[pdf_input],
849
+ outputs=[texto_extraido, tabla_preview, csv_output, resumen_tecnico, info_util, datos_json_state, csv_file_state]
 
 
 
 
 
 
850
  )
851
 
852
+ btn_generar_pdf.click(
853
+ fn=generar_pdf_con_template,
854
+ inputs=[template_selector, csv_file_state, datos_json_state],
855
+ outputs=[pdf_output, pdf_status]
 
856
  )
857
 
858
  if __name__ == "__main__":