gelpi01 commited on
Commit
06cf7b4
·
1 Parent(s): 5f1ccb7

Interfaz guiada paso a paso en español

Browse files
Files changed (1) hide show
  1. app.py +222 -373
app.py CHANGED
@@ -12,6 +12,7 @@ import docx2pdf
12
 
13
  class DocumentProcessor:
14
  def __init__(self):
 
15
  self.excel_data = None
16
  self.mapping = {}
17
  self.conditional_docs = {}
@@ -19,86 +20,57 @@ class DocumentProcessor:
19
  self.column_associations = {}
20
  self.first_df = None
21
  self.second_df = None
22
- self.folder_column = None
23
- self.current_step = 1
24
- self.conditional_files = []
25
 
26
  def load_excel_file(self, excel_file):
27
  """Cargar archivo Excel y retornar información de columnas"""
28
  if excel_file is None:
29
- return "No se ha seleccionado ningún archivo", gr.Dropdown(choices=[]), "", gr.update(visible=False), gr.update(visible=False)
30
 
31
  try:
32
  self.excel_data = pd.read_excel(excel_file.name)
33
  columns = list(self.excel_data.columns)
34
- preview = self.excel_data.head(3).to_string()
35
- success_msg = f"✅ ¡Excelente! Se cargó correctamente el archivo Excel.\n📊 Se encontraron {len(self.excel_data)} filas de datos.\n📋 Columnas disponibles: {', '.join(columns)}"
36
-
37
- return (success_msg,
38
- gr.Dropdown(choices=columns, visible=True),
39
  preview,
40
- gr.update(visible=True),
41
- gr.update(visible=True))
42
  except Exception as e:
43
- return f"Error al cargar el archivo Excel: {str(e)}", gr.Dropdown(choices=[]), "", gr.update(visible=False), gr.update(visible=False)
44
 
45
  def set_folder_column(self, column_name):
46
  """Establecer la columna para nombrar carpetas"""
47
- if not column_name:
48
- return "⚠️ Por favor selecciona una columna"
49
-
50
  self.folder_column = column_name
51
- # Mostrar algunos ejemplos de nombres de carpetas que se crearán
52
- examples = self.excel_data[column_name].head(3).tolist()
53
- examples_text = ", ".join([str(ex).replace(" ", "_") for ex in examples])
54
- return f"✅ Perfecto! Se usará la columna '{column_name}' para nombrar las carpetas.\n\n📁 Ejemplos de carpetas que se crearán: {examples_text}"
55
 
56
  def add_keyword_mapping(self, column_name, keyword):
57
  """Agregar un mapeo individual de palabra clave"""
58
  if not column_name or not keyword:
59
- return "⚠️ Por favor selecciona una columna y escribe una palabra clave", gr.update(visible=False)
60
-
61
- if not keyword.startswith('{{') or not keyword.endswith('}}'):
62
- keyword = '{{' + keyword.strip('{}') + '}}'
63
 
64
  self.mapping[keyword] = column_name
65
 
66
  # Retornar lista actualizada de mapeos
67
- mappings_text = ""
68
- for i, (k, v) in enumerate(self.mapping.items(), 1):
69
- mappings_text += f"{i}. {k} ➜ se reemplazará con datos de la columna '{v}'\n"
70
-
71
- return f"✅ ¡Mapeo agregado correctamente!\n\n📝 Palabras clave configuradas:\n{mappings_text}", gr.update(visible=len(self.mapping) > 0)
72
-
73
- def load_documents(self, files):
74
- """Cargar documentos generales"""
75
- if not files:
76
- return "⚠️ No se han seleccionado archivos", gr.update(visible=False)
77
-
78
- self.general_docs = files
79
- file_names = [os.path.basename(f.name) for f in files]
80
- valid_files = [f for f in file_names if f.endswith(('.docx', '.pdf'))]
81
-
82
- if len(valid_files) == 0:
83
- return "❌ No se encontraron archivos válidos (.docx o .pdf)", gr.update(visible=False)
84
-
85
- return f"✅ ¡Documentos cargados correctamente!\n\n📄 Archivos que se procesarán:\n" + "\n".join([f"• {name}" for name in valid_files]), gr.update(visible=True)
86
 
87
  def load_conditional_files(self, files):
88
  """Cargar archivos para documentos condicionales"""
89
  if not files:
90
- return "⚠️ No se han seleccionado archivos", [], gr.update(visible=False)
91
-
92
- self.conditional_files = files
93
- file_names = [f.name for f in files if f.name.endswith(('.docx', '.pdf'))]
94
- file_basenames = [os.path.basename(f) for f in file_names]
95
 
96
- if len(file_names) == 0:
97
- return "❌ No se encontraron archivos válidos (.docx o .pdf)", [], gr.update(visible=False)
 
 
98
 
99
- return (f" Archivos condicionales cargados:\n" + "\n".join([f"• {name}" for name in file_basenames]),
100
- file_basenames,
101
- gr.update(visible=True))
 
 
 
 
102
 
103
  def get_column_unique_values(self, column_name):
104
  """Obtener valores únicos de una columna específica"""
@@ -110,7 +82,7 @@ class DocumentProcessor:
110
  def set_document_condition(self, column_name, condition_value, selected_files):
111
  """Establecer condición para documentos específicos"""
112
  if not selected_files or not column_name or not condition_value:
113
- return "⚠️ Por favor selecciona archivos, columna y valor de condición"
114
 
115
  # Inicializar estructura si no existe
116
  if column_name not in self.conditional_docs:
@@ -119,24 +91,19 @@ class DocumentProcessor:
119
  if condition_value not in self.conditional_docs[column_name]:
120
  self.conditional_docs[column_name][condition_value] = []
121
 
122
- # Encontrar archivos correspondientes
123
- matching_files = []
124
- for file in self.conditional_files:
125
- if os.path.basename(file.name) in selected_files:
126
- matching_files.append(file)
127
-
128
  # Agregar archivos a la condición
129
- self.conditional_docs[column_name][condition_value] = matching_files
 
 
130
 
131
- # Generar resumen
132
- summary = " Condiciones configuradas:\n\n"
133
  for col, conditions in self.conditional_docs.items():
134
- summary += f"📋 Columna: {col}\n"
135
  for value, files in conditions.items():
136
- summary += f" ▶️ Cuando {col} = '{value}':\n"
137
  for file in files:
138
- file_name = os.path.basename(file.name) if hasattr(file, 'name') else str(file)
139
- summary += f" • {file_name}\n"
140
  summary += "\n"
141
 
142
  return summary
@@ -144,26 +111,26 @@ class DocumentProcessor:
144
  def load_first_excel(self, file):
145
  """Cargar primer archivo Excel para asociaciones"""
146
  if file is None:
147
- return "⚠️ No se ha seleccionado archivo", []
148
 
149
  try:
150
  self.first_df = pd.read_excel(file.name)
151
  columns = list(self.first_df.columns)
152
- return f" Primer Excel cargado correctamente\n📋 Columnas: {', '.join(columns)}", columns
153
  except Exception as e:
154
- return f"Error: {str(e)}", []
155
 
156
  def load_second_excel(self, file):
157
  """Cargar segundo archivo Excel para asociaciones"""
158
  if file is None:
159
- return "⚠️ No se ha seleccionado archivo", []
160
 
161
  try:
162
  self.second_df = pd.read_excel(file.name)
163
  columns = list(self.second_df.columns)
164
- return f" Segundo Excel cargado correctamente\n📋 Columnas: {', '.join(columns)}", columns
165
  except Exception as e:
166
- return f"Error: {str(e)}", []
167
 
168
  def get_column_values(self, df, column_name):
169
  """Obtener valores únicos de una columna"""
@@ -174,82 +141,48 @@ class DocumentProcessor:
174
  def assign_values(self, first_column, first_value, second_column, second_value):
175
  """Asignar valores para asociaciones de columnas"""
176
  if not all([first_column, first_value, second_column, second_value]):
177
- return "⚠️ Por favor completa todos los campos"
178
 
179
  # Crear asociación
180
- self.column_associations[first_value] = second_value
 
181
 
182
  # Retornar resumen actual de asociaciones
183
- associations_text = "✅ Asociaciones configuradas:\n\n"
184
- for k, v in self.column_associations.items():
185
- associations_text += f"• {k} ➜ {v}\n"
186
-
187
- return associations_text
188
 
189
- def check_ready_status(self):
190
- """Verificar si todo está listo para procesar"""
191
- status = []
192
- ready = True
193
 
194
- if self.excel_data is not None:
195
- status.append("✅ Archivo Excel principal cargado")
196
- else:
197
- status.append("❌ Falta cargar archivo Excel principal")
198
- ready = False
199
-
200
- if self.folder_column:
201
- status.append("✅ Columna para carpetas seleccionada")
202
- else:
203
- status.append("❌ Falta seleccionar columna para carpetas")
204
- ready = False
205
-
206
- if len(self.mapping) > 0:
207
- status.append(f"✅ {len(self.mapping)} palabra(s) clave configurada(s)")
208
- else:
209
- status.append("❌ Faltan palabras clave por configurar")
210
- ready = False
211
-
212
- if len(self.general_docs) > 0:
213
- status.append(f"✅ {len(self.general_docs)} documento(s) general(es) cargado(s)")
214
- else:
215
- status.append("❌ Faltan documentos generales por cargar")
216
- ready = False
217
-
218
- # Verificar configuraciones opcionales
219
- if len(self.conditional_docs) > 0:
220
- total_conditions = sum(len(conditions) for conditions in self.conditional_docs.values())
221
- status.append(f"✅ {total_conditions} condición(es) para documentos específicos")
222
- else:
223
- status.append("ℹ️ Sin documentos condicionales (opcional)")
224
-
225
- if len(self.column_associations) > 0:
226
- status.append(f"✅ {len(self.column_associations)} asociación(es) de datos configurada(s)")
227
- else:
228
- status.append("ℹ️ Sin asociaciones de datos (opcional)")
229
-
230
- status_text = "\n".join(status)
231
-
232
- if ready:
233
- status_text += "\n\n🎉 ¡Todo listo para procesar!"
234
- return status_text, gr.update(visible=True)
235
- else:
236
- status_text += "\n\n⏳ Completa los pasos obligatorios para continuar"
237
- return status_text, gr.update(visible=False)
238
 
239
- def process_documents(self, progress=gr.Progress()):
240
  """Procesar todos los documentos"""
241
- if not all([self.excel_data is not None, self.folder_column, self.mapping, self.general_docs]):
242
- return " Faltan datos por configurar. Revisa los pasos anteriores.", None
 
 
 
 
 
 
 
 
 
 
243
 
244
  try:
245
  # Crear directorio temporal para salida
246
  output_dir = tempfile.mkdtemp()
247
- total_rows = len(self.excel_data)
248
 
249
- progress(0, desc="Iniciando procesamiento...")
250
 
251
- for index, row in progress.tqdm(self.excel_data.iterrows(), total=total_rows, desc="Procesando documentos"):
252
- folder_name = str(row[self.folder_column]).strip().replace(" ", "_")
253
  client_folder = os.path.join(output_dir, folder_name)
254
  os.makedirs(client_folder, exist_ok=True)
255
 
@@ -265,8 +198,6 @@ class DocumentProcessor:
265
  for doc_file in conditions[value]:
266
  self.process_file(doc_file, row, client_folder)
267
 
268
- progress(0.8, desc="Creando archivo ZIP...")
269
-
270
  # Crear archivo ZIP con todos los resultados
271
  zip_buffer = BytesIO()
272
  with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
@@ -279,19 +210,17 @@ class DocumentProcessor:
279
  zip_buffer.seek(0)
280
 
281
  # Guardar ZIP temporal
282
- zip_path = os.path.join(tempfile.gettempdir(), "documentos_procesados.zip")
283
  with open(zip_path, 'wb') as f:
284
  f.write(zip_buffer.getvalue())
285
 
286
  # Limpiar directorio temporal
287
  shutil.rmtree(output_dir)
288
 
289
- progress(1.0, desc="¡Completado!")
290
-
291
- return f"🎉 ¡Procesamiento completado con éxito!\n\n📊 Se procesaron {total_rows} registros\n📁 Se crearon {total_rows} carpetas con documentos personalizados\n\n⬇️ Descarga tu archivo ZIP con todos los documentos", zip_path
292
 
293
  except Exception as e:
294
- return f"Error durante el procesamiento: {str(e)}", None
295
 
296
  def process_file(self, file, row, client_folder):
297
  """Procesar un archivo individual"""
@@ -323,7 +252,7 @@ class DocumentProcessor:
323
  """Reemplazar palabras clave en documento"""
324
  for keyword, column in self.mapping.items():
325
  if column in row:
326
- replacement = str(row[column]) if pd.notna(row[column]) else ""
327
 
328
  # Aplicar asociaciones de columnas si existen
329
  if replacement in self.column_associations:
@@ -331,21 +260,17 @@ class DocumentProcessor:
331
 
332
  # Manejar texto multilínea
333
  if '\\n' in replacement:
334
- replacement = replacement.replace('\\n', '\n')
335
 
336
  # Reemplazar en párrafos
337
  for paragraph in doc.paragraphs:
338
  self.replace_text_in_runs(paragraph.runs, keyword, replacement)
339
 
340
- # Reemplazar en encabezados y pies de página
341
  for section in doc.sections:
342
  header = section.header
343
  for paragraph in header.paragraphs:
344
  self.replace_text_in_runs(paragraph.runs, keyword, replacement)
345
-
346
- footer = section.footer
347
- for paragraph in footer.paragraphs:
348
- self.replace_text_in_runs(paragraph.runs, keyword, replacement)
349
 
350
  # Reemplazar en tablas
351
  for table in doc.tables:
@@ -363,6 +288,12 @@ class DocumentProcessor:
363
  for run in runs:
364
  if keyword in run.text:
365
  run.text = run.text.replace(keyword, replacement)
 
 
 
 
 
 
366
 
367
  def replace_text_in_textboxes(self, document, placeholder, replacement):
368
  """Reemplazar texto en cuadros de texto"""
@@ -392,237 +323,161 @@ class DocumentProcessor:
392
  # Inicializar procesador
393
  processor = DocumentProcessor()
394
 
 
395
  def create_interface():
396
- with gr.Blocks(title="Procesador de Documentos", theme=gr.themes.Soft()) as demo:
397
- gr.Markdown("""
398
- # 📄 Procesador Automático de Documentos
399
-
400
- **¡Bienvenido!** Esta herramienta te ayudará a personalizar documentos automáticamente usando datos de Excel.
401
-
402
- **¿Qué hace esta herramienta?**
403
- - Toma tus documentos de Word (.docx) o PDF
404
- - Reemplaza palabras especiales con información de una hoja de Excel
405
- - Crea carpetas personalizadas con documentos únicos para cada fila de tu Excel
406
-
407
- **Por ejemplo:** Si tienes un contrato con {{nombre}} y {{fecha}}, y un Excel con nombres y fechas,
408
- creará un contrato personalizado para cada persona.
409
-
410
- ---
411
- """)
412
-
413
- # PASO 1: Cargar Excel
414
- with gr.Group():
415
- gr.Markdown("## 📊 Paso 1: Carga tu archivo de Excel")
416
- gr.Markdown("""
417
- **Instrucciones:**
418
- - Selecciona tu archivo Excel (.xlsx o .xls)
419
- - Debe contener toda la información que quieres usar en los documentos
420
- - Cada fila será una persona/caso diferente
421
- """)
422
-
423
- excel_file = gr.File(
424
- label="📁 Selecciona tu archivo de Excel",
425
- file_types=[".xlsx", ".xls"],
426
- file_count="single"
427
- )
428
- excel_status = gr.Textbox(label="Estado", interactive=False, lines=3)
429
- excel_preview = gr.Textbox(label="Vista previa de tus datos", lines=8, interactive=False, visible=False)
430
-
431
- # PASO 2: Seleccionar columna para carpetas
432
- with gr.Group():
433
- gr.Markdown("## 📁 Paso 2: Elige cómo nombrar las carpetas")
434
- gr.Markdown("""
435
- **Instrucciones:**
436
- - Selecciona la columna que quieres usar para nombrar las carpetas
437
- - Por ejemplo: si eliges "Nombre", cada carpeta se llamará como el nombre de cada fila
438
- - Se creará una carpeta para cada fila de tu Excel
439
- """)
440
-
441
- folder_column = gr.Dropdown(
442
- label="🏷️ Selecciona la columna para nombrar carpetas",
443
- choices=[],
444
- interactive=True,
445
- visible=False
446
- )
447
- folder_status = gr.Textbox(label="Estado", interactive=False, lines=3, visible=False)
448
-
449
- # PASO 3: Configurar palabras clave
450
- with gr.Group():
451
- gr.Markdown("## 🔑 Paso 3: Configura las palabras clave")
452
- gr.Markdown("""
453
- **Instrucciones:**
454
- - Las palabras clave son las partes de tu documento que quieres reemplazar
455
- - Por ejemplo: si tu documento dice "{{nombre}}", se reemplazará con el nombre de cada fila
456
- - Puedes escribir la palabra clave con o sin las llaves {{ }}
457
- """)
458
 
459
- with gr.Row():
460
- mapping_column = gr.Dropdown(
461
- label="📋 Selecciona la columna de Excel",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  choices=[],
463
  interactive=True
464
  )
465
- keyword_input = gr.Textbox(
466
- label="🔤 Escribe la palabra clave",
467
- placeholder="Ejemplo: nombre, fecha, direccion...",
468
- info="Puedes escribir con o sin {{ }}"
469
- )
470
-
471
- add_mapping_btn = gr.Button("➕ Agregar palabra clave", variant="secondary")
472
- mapping_status = gr.Textbox(label="Palabras clave configuradas", lines=6, interactive=False)
473
- continue_btn1 = gr.Button("✅ Continuar al siguiente paso", variant="primary", visible=False)
474
-
475
- # PASO 4: Cargar documentos
476
- with gr.Group():
477
- gr.Markdown("## 📄 Paso 4: Sube tus documentos")
478
- gr.Markdown("""
479
- **Instrucciones:**
480
- - Sube los documentos que quieres personalizar (.docx o .pdf)
481
- - Estos documentos deben contener las palabras clave que configuraste en el paso anterior
482
- - Por ejemplo: un contrato con {{nombre}} y {{fecha}}
483
- """)
484
-
485
- document_files = gr.File(
486
- label="📎 Selecciona tus documentos generales",
487
- file_count="multiple",
488
- file_types=[".docx", ".pdf"]
489
- )
490
- documents_status = gr.Textbox(label="Estado de los documentos", interactive=False, lines=4)
491
- continue_btn2 = gr.Button("✅ Continuar al paso final", variant="primary", visible=False)
492
-
493
- # PASO 4B: Documentos condicionales (opcional)
494
- with gr.Group():
495
- gr.Markdown("## 📋 Paso 4B: Documentos condicionales (OPCIONAL)")
496
- gr.Markdown("""
497
- **¿Qué son los documentos condicionales?**
498
- - Documentos que solo se incluirán para ciertas personas/casos
499
- - Por ejemplo: un anexo especial solo para clientes VIP
500
- - O un documento adicional solo para mayores de edad
501
-
502
- **Si no necesitas esto, puedes saltarte esta sección.**
503
- """)
504
 
505
- conditional_files = gr.File(
506
- label="📎 Documentos condicionales (opcional)",
507
- file_count="multiple",
508
- file_types=[".docx", ".pdf"]
509
- )
510
- conditional_status = gr.Textbox(label="Estado archivos condicionales", interactive=False, lines=3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
 
512
- with gr.Row():
513
- condition_column = gr.Dropdown(
514
- label="📋 Selecciona la columna para la condición",
515
- choices=[],
516
- interactive=True,
517
- visible=False
518
- )
519
- condition_value = gr.Dropdown(
520
- label="📝 Selecciona el valor específico",
521
- choices=[],
522
- interactive=True,
523
- visible=False
524
  )
 
525
 
526
- selected_conditional_docs = gr.CheckboxGroup(
527
- label=" Selecciona qué documentos incluir para esta condición",
528
- choices=[],
529
- interactive=True,
530
- visible=False
531
- )
532
-
533
- set_condition_btn = gr.Button("➕ Configurar condición", visible=False)
534
- condition_result = gr.Textbox(label="Condiciones configuradas", lines=6, interactive=False, visible=False)
535
-
536
- # PASO 4C: Asociaciones de datos (opcional)
537
- with gr.Group():
538
- gr.Markdown("## 🔗 Paso 4C: Asociación con otro Excel (OPCIONAL)")
539
- gr.Markdown("""
540
- **¿Para qué sirve esto?**
541
- - Si tienes datos en otro archivo Excel que necesitas combinar
542
- - Por ejemplo: tu Excel principal tiene códigos, pero los nombres completos están en otro archivo
543
- - La herramienta puede buscar automáticamente la información correspondiente
544
-
545
- **Si no necesitas esto, puedes saltarte esta sección.**
546
- """)
547
-
548
- with gr.Row():
549
- with gr.Column():
550
- gr.Markdown("**Primer Excel (datos principales)**")
551
- first_excel = gr.File(label="📊 Archivo Excel principal", file_types=[".xlsx", ".xls"])
552
- first_status = gr.Textbox(label="Estado", interactive=False, lines=2)
553
- first_column = gr.Dropdown(label="📋 Columna", choices=[])
554
- first_value = gr.Dropdown(label="📝 Valor", choices=[])
555
 
556
- with gr.Column():
557
- gr.Markdown("**Segundo Excel (datos adicionales)**")
558
- second_excel = gr.File(label="📊 Archivo Excel adicional", file_types=[".xlsx", ".xls"])
559
- second_status = gr.Textbox(label="Estado", interactive=False, lines=2)
560
- second_column = gr.Dropdown(label="📋 Columna", choices=[])
561
- second_value = gr.Dropdown(label="📝 Valor", choices=[])
562
-
563
- assign_btn = gr.Button("🔗 Crear asociación")
564
- associations_display = gr.Textbox(label="Asociaciones configuradas", lines=6, interactive=False)
565
-
566
- # PASO 5: Revisar y procesar
567
- with gr.Group():
568
- gr.Markdown("## 🚀 Paso 5: Revisar y procesar")
569
- gr.Markdown("""
570
- **¡Ya casi terminamos!** Revisa que todo esté correcto y presiona el botón para procesar.
571
- """)
572
-
573
- ready_status = gr.Textbox(
574
- label="Estado de preparación",
575
- interactive=False,
576
- lines=8,
577
- value="⏳ Completa los pasos anteriores para ver el estado aquí"
578
- )
579
-
580
- process_btn = gr.Button(
581
- "🎯 ¡PROCESAR DOCUMENTOS!",
582
- variant="primary",
583
- size="lg",
584
- visible=False
585
- )
586
-
587
- process_status = gr.Textbox(label="Estado del procesamiento", lines=4, interactive=False)
588
- download_file = gr.File(label="📥 Descargar documentos procesados", visible=False)
589
 
590
- # Configurar eventos
 
 
591
  excel_file.change(
592
- fn=lambda file: processor.load_excel_file(file) + (processor.get_excel_columns(),),
 
 
 
 
 
 
593
  inputs=[excel_file],
594
- outputs=[excel_status, folder_column, excel_preview, mapping_column, folder_status, condition_column]
595
- )
596
-
597
- folder_column.change(
598
- fn=processor.set_folder_column,
599
- inputs=[folder_column],
600
- outputs=[folder_status]
601
  )
602
 
603
  add_mapping_btn.click(
604
  fn=processor.add_keyword_mapping,
605
  inputs=[mapping_column, keyword_input],
606
- outputs=[mapping_status, continue_btn1]
607
  )
608
 
609
- document_files.change(
610
- fn=processor.load_documents,
611
- inputs=[document_files],
612
- outputs=[documents_status, continue_btn2]
613
  )
614
 
615
- # Eventos para documentos condicionales
616
  conditional_files.change(
617
- fn=lambda files: processor.load_conditional_files(files) + (
618
- gr.update(visible=len(files) > 0 if files else False),
619
- gr.update(visible=len(files) > 0 if files else False),
620
- gr.update(visible=len(files) > 0 if files else False),
621
- gr.update(visible=len(files) > 0 if files else False),
622
- gr.update(visible=len(files) > 0 if files else False)
623
  ),
624
  inputs=[conditional_files],
625
- outputs=[conditional_status, selected_conditional_docs, condition_column, condition_value, selected_conditional_docs, set_condition_btn, condition_result]
626
  )
627
 
628
  condition_column.change(
@@ -632,15 +487,14 @@ def create_interface():
632
  )
633
 
634
  set_condition_btn.click(
635
- fn=lambda col, val, selected_docs: (
636
- processor.set_document_condition(col, val, selected_docs),
637
- gr.update(visible=True)
638
  ),
639
- inputs=[condition_column, condition_value, selected_conditional_docs],
640
- outputs=[condition_result, condition_result]
641
  )
642
 
643
- # Eventos para asociaciones de datos
644
  first_excel.change(
645
  fn=processor.load_first_excel,
646
  inputs=[first_excel],
@@ -671,23 +525,18 @@ def create_interface():
671
  outputs=[associations_display]
672
  )
673
 
674
- # Actualizar estado de preparación cuando se hace clic en continuar
675
- continue_btn1.click(
676
- fn=processor.check_ready_status,
677
- outputs=[ready_status, process_btn]
678
- )
679
-
680
- continue_btn2.click(
681
- fn=processor.check_ready_status,
682
- outputs=[ready_status, process_btn]
683
  )
684
 
 
685
  process_btn.click(
686
- fn=lambda: processor.process_documents(),
 
687
  outputs=[process_status, download_file]
688
- ).then(
689
- fn=lambda: gr.update(visible=True),
690
- outputs=[download_file]
691
  )
692
 
693
  return demo
 
12
 
13
  class DocumentProcessor:
14
  def __init__(self):
15
+ self.input_files = []
16
  self.excel_data = None
17
  self.mapping = {}
18
  self.conditional_docs = {}
 
20
  self.column_associations = {}
21
  self.first_df = None
22
  self.second_df = None
23
+ self.folder_column = None # Agregar para recordar la columna de carpetas
 
 
24
 
25
  def load_excel_file(self, excel_file):
26
  """Cargar archivo Excel y retornar información de columnas"""
27
  if excel_file is None:
28
+ return "No file selected", gr.Dropdown(choices=[]), "", gr.Dropdown(choices=[])
29
 
30
  try:
31
  self.excel_data = pd.read_excel(excel_file.name)
32
  columns = list(self.excel_data.columns)
33
+ preview = self.excel_data.head().to_string()
34
+ return (f"Excel file loaded successfully. Columns: {', '.join(columns)}",
35
+ gr.Dropdown(choices=columns),
 
 
36
  preview,
37
+ gr.Dropdown(choices=columns))
 
38
  except Exception as e:
39
+ return f"Error loading Excel file: {str(e)}", gr.Dropdown(choices=[]), "", gr.Dropdown(choices=[])
40
 
41
  def set_folder_column(self, column_name):
42
  """Establecer la columna para nombrar carpetas"""
 
 
 
43
  self.folder_column = column_name
44
+ return f"Folder column set to: {column_name}"
 
 
 
45
 
46
  def add_keyword_mapping(self, column_name, keyword):
47
  """Agregar un mapeo individual de palabra clave"""
48
  if not column_name or not keyword:
49
+ return "Please select a column and enter a keyword"
 
 
 
50
 
51
  self.mapping[keyword] = column_name
52
 
53
  # Retornar lista actualizada de mapeos
54
+ mappings_text = "\n".join([f"{k} -> {v}" for k, v in self.mapping.items()])
55
+ return f"Mapping added successfully!\n\nCurrent mappings:\n{mappings_text}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  def load_conditional_files(self, files):
58
  """Cargar archivos para documentos condicionales"""
59
  if not files:
60
+ return "No files selected"
 
 
 
 
61
 
62
+ file_list = []
63
+ for file in files:
64
+ if file.name.endswith(('.docx', '.pdf')):
65
+ file_list.append(file.name)
66
 
67
+ return f"Loaded {len(file_list)} conditional files: {', '.join([os.path.basename(f) for f in file_list])}"
68
+
69
+ def get_excel_columns(self):
70
+ """Obtener columnas del Excel cargado"""
71
+ if self.excel_data is not None:
72
+ return list(self.excel_data.columns)
73
+ return []
74
 
75
  def get_column_unique_values(self, column_name):
76
  """Obtener valores únicos de una columna específica"""
 
82
  def set_document_condition(self, column_name, condition_value, selected_files):
83
  """Establecer condición para documentos específicos"""
84
  if not selected_files or not column_name or not condition_value:
85
+ return "Please select files, column name, and condition value"
86
 
87
  # Inicializar estructura si no existe
88
  if column_name not in self.conditional_docs:
 
91
  if condition_value not in self.conditional_docs[column_name]:
92
  self.conditional_docs[column_name][condition_value] = []
93
 
 
 
 
 
 
 
94
  # Agregar archivos a la condición
95
+ for file in selected_files:
96
+ if file not in self.conditional_docs[column_name][condition_value]:
97
+ self.conditional_docs[column_name][condition_value].append(file)
98
 
99
+ # Generar resumen de todas las condiciones
100
+ summary = "Current Conditional Documents:\n\n"
101
  for col, conditions in self.conditional_docs.items():
102
+ summary += f"Column: {col}\n"
103
  for value, files in conditions.items():
104
+ summary += f" When {col} = '{value}':\n"
105
  for file in files:
106
+ summary += f" - {os.path.basename(file.name) if hasattr(file, 'name') else file}\n"
 
107
  summary += "\n"
108
 
109
  return summary
 
111
  def load_first_excel(self, file):
112
  """Cargar primer archivo Excel para asociaciones"""
113
  if file is None:
114
+ return "No file selected", gr.Dropdown(choices=[])
115
 
116
  try:
117
  self.first_df = pd.read_excel(file.name)
118
  columns = list(self.first_df.columns)
119
+ return f"First Excel loaded: {', '.join(columns)}", gr.Dropdown(choices=columns)
120
  except Exception as e:
121
+ return f"Error: {str(e)}", gr.Dropdown(choices=[])
122
 
123
  def load_second_excel(self, file):
124
  """Cargar segundo archivo Excel para asociaciones"""
125
  if file is None:
126
+ return "No file selected", gr.Dropdown(choices=[])
127
 
128
  try:
129
  self.second_df = pd.read_excel(file.name)
130
  columns = list(self.second_df.columns)
131
+ return f"Second Excel loaded: {', '.join(columns)}", gr.Dropdown(choices=columns)
132
  except Exception as e:
133
+ return f"Error: {str(e)}", gr.Dropdown(choices=[])
134
 
135
  def get_column_values(self, df, column_name):
136
  """Obtener valores únicos de una columna"""
 
141
  def assign_values(self, first_column, first_value, second_column, second_value):
142
  """Asignar valores para asociaciones de columnas"""
143
  if not all([first_column, first_value, second_column, second_value]):
144
+ return "Please fill all fields"
145
 
146
  # Crear asociación
147
+ if first_value not in self.column_associations:
148
+ self.column_associations[first_value] = second_value
149
 
150
  # Retornar resumen actual de asociaciones
151
+ associations_text = "\n".join([f"{k} -> {v}" for k, v in self.column_associations.items()])
152
+ return f"Association added. Current associations:\n{associations_text}"
 
 
 
153
 
154
+ def load_general_documents(self, files):
155
+ """Cargar documentos generales"""
156
+ if not files:
157
+ return "No files selected"
158
 
159
+ self.general_docs = files
160
+ file_names = [os.path.basename(f.name) for f in files]
161
+ return f"Loaded {len(files)} general documents: {', '.join(file_names)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ def process_documents(self, folder_column=None, progress=gr.Progress()):
164
  """Procesar todos los documentos"""
165
+ if self.excel_data is None:
166
+ return "Please load an Excel file first", None
167
+
168
+ # Usar la columna guardada si no se proporciona una
169
+ if folder_column is None:
170
+ folder_column = self.folder_column
171
+
172
+ if not folder_column:
173
+ return "Please select a column for folder naming in Main Setup tab", None
174
+
175
+ if folder_column not in self.excel_data.columns:
176
+ return f"Column '{folder_column}' not found in Excel file", None
177
 
178
  try:
179
  # Crear directorio temporal para salida
180
  output_dir = tempfile.mkdtemp()
 
181
 
182
+ total_rows = len(self.excel_data)
183
 
184
+ for index, row in progress.tqdm(self.excel_data.iterrows(), total=total_rows, desc="Processing documents"):
185
+ folder_name = str(row[folder_column]).strip().replace(" ", "_")
186
  client_folder = os.path.join(output_dir, folder_name)
187
  os.makedirs(client_folder, exist_ok=True)
188
 
 
198
  for doc_file in conditions[value]:
199
  self.process_file(doc_file, row, client_folder)
200
 
 
 
201
  # Crear archivo ZIP con todos los resultados
202
  zip_buffer = BytesIO()
203
  with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
 
210
  zip_buffer.seek(0)
211
 
212
  # Guardar ZIP temporal
213
+ zip_path = os.path.join(tempfile.gettempdir(), "processed_documents.zip")
214
  with open(zip_path, 'wb') as f:
215
  f.write(zip_buffer.getvalue())
216
 
217
  # Limpiar directorio temporal
218
  shutil.rmtree(output_dir)
219
 
220
+ return f"Processing complete! Generated documents for {total_rows} entries.", zip_path
 
 
221
 
222
  except Exception as e:
223
+ return f"Error during processing: {str(e)}", None
224
 
225
  def process_file(self, file, row, client_folder):
226
  """Procesar un archivo individual"""
 
252
  """Reemplazar palabras clave en documento"""
253
  for keyword, column in self.mapping.items():
254
  if column in row:
255
+ replacement = str(row[column])
256
 
257
  # Aplicar asociaciones de columnas si existen
258
  if replacement in self.column_associations:
 
260
 
261
  # Manejar texto multilínea
262
  if '\\n' in replacement:
263
+ replacement = self.format_multiline_text(replacement)
264
 
265
  # Reemplazar en párrafos
266
  for paragraph in doc.paragraphs:
267
  self.replace_text_in_runs(paragraph.runs, keyword, replacement)
268
 
269
+ # Reemplazar en encabezados
270
  for section in doc.sections:
271
  header = section.header
272
  for paragraph in header.paragraphs:
273
  self.replace_text_in_runs(paragraph.runs, keyword, replacement)
 
 
 
 
274
 
275
  # Reemplazar en tablas
276
  for table in doc.tables:
 
288
  for run in runs:
289
  if keyword in run.text:
290
  run.text = run.text.replace(keyword, replacement)
291
+ run.font.highlight_color = None
292
+
293
+ def format_multiline_text(self, text):
294
+ """Formatear texto multilínea"""
295
+ lines = text.split('\\n')
296
+ return '\n'.join(lines)
297
 
298
  def replace_text_in_textboxes(self, document, placeholder, replacement):
299
  """Reemplazar texto en cuadros de texto"""
 
323
  # Inicializar procesador
324
  processor = DocumentProcessor()
325
 
326
+ # Crear interfaz Gradio
327
  def create_interface():
328
+ with gr.Blocks(title="Document Processor", theme=gr.themes.Soft()) as demo:
329
+ gr.Markdown("# Document Processor")
330
+ gr.Markdown("Process documents by replacing keywords with Excel data")
331
+
332
+ with gr.Tabs():
333
+ # Pestaña Principal
334
+ with gr.TabItem("Main Setup"):
335
+ with gr.Row():
336
+ excel_file = gr.File(label="Upload Excel File", file_types=[".xlsx", ".xls"])
337
+ folder_column = gr.Dropdown(label="Select Column for Folder Naming", choices=[], interactive=True)
338
+
339
+ excel_status = gr.Textbox(label="Excel Status", interactive=False)
340
+ excel_preview = gr.Textbox(label="Excel Preview", lines=10, interactive=False)
341
+
342
+ gr.Markdown("### Keyword Mapping")
343
+ gr.Markdown("Select a column and enter the keyword that will be replaced in documents")
344
+
345
+ with gr.Row():
346
+ mapping_column = gr.Dropdown(label="Select Column", choices=[], interactive=True)
347
+ keyword_input = gr.Textbox(label="Enter Keyword (e.g., {{name}}, {{address}})", placeholder="{{keyword}}")
348
+
349
+ add_mapping_btn = gr.Button("Add Mapping", variant="secondary")
350
+ mapping_status = gr.Textbox(label="Current Mappings", lines=8, interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
+ # Pestaña Documentos Condicionales
353
+ with gr.TabItem("Conditional Documents"):
354
+ gr.Markdown("### Set up conditional documents that will only be included based on Excel data")
355
+
356
+ conditional_files = gr.File(
357
+ label="Upload Conditional Documents",
358
+ file_count="multiple",
359
+ file_types=[".docx", ".pdf"]
360
+ )
361
+ conditional_status = gr.Textbox(label="Conditional Files Status", interactive=False)
362
+
363
+ gr.Markdown("### Set Conditions")
364
+ gr.Markdown("Choose which documents to include based on Excel column values")
365
+
366
+ with gr.Row():
367
+ condition_column = gr.Dropdown(
368
+ label="Select Excel Column for Condition",
369
+ choices=[],
370
+ interactive=True
371
+ )
372
+ condition_value = gr.Dropdown(
373
+ label="Select Value from Column",
374
+ choices=[],
375
+ interactive=True
376
+ )
377
+
378
+ selected_docs = gr.CheckboxGroup(
379
+ label="Select Documents to Include for this Condition",
380
  choices=[],
381
  interactive=True
382
  )
383
+
384
+ set_condition_btn = gr.Button("Set Condition for Selected Documents")
385
+ condition_result = gr.Textbox(label="Condition Status", lines=8, interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
+ # Pestaña Asociaciones de Columnas
388
+ with gr.TabItem("Column Associations"):
389
+ gr.Markdown("### Set up value mappings between different Excel files")
390
+
391
+ with gr.Row():
392
+ with gr.Column():
393
+ gr.Markdown("**First Excel File**")
394
+ first_excel = gr.File(label="First Excel File", file_types=[".xlsx", ".xls"])
395
+ first_status = gr.Textbox(label="First Excel Status", interactive=False)
396
+ first_column = gr.Dropdown(label="First Column", choices=[])
397
+ first_value = gr.Dropdown(label="First Value", choices=[])
398
+
399
+ with gr.Column():
400
+ gr.Markdown("**Second Excel File**")
401
+ second_excel = gr.File(label="Second Excel File", file_types=[".xlsx", ".xls"])
402
+ second_status = gr.Textbox(label="Second Excel Status", interactive=False)
403
+ second_column = gr.Dropdown(label="Second Column", choices=[])
404
+ second_value = gr.Dropdown(label="Second Value", choices=[])
405
+
406
+ assign_btn = gr.Button("Create Association")
407
+ associations_display = gr.Textbox(label="Current Associations", lines=10, interactive=False)
408
 
409
+ # Pestaña Documentos Generales
410
+ with gr.TabItem("General Documents"):
411
+ gr.Markdown("### Upload documents that will be processed for all entries")
412
+
413
+ general_files = gr.File(
414
+ label="Upload General Documents",
415
+ file_count="multiple",
416
+ file_types=[".docx", ".pdf"]
 
 
 
 
417
  )
418
+ general_status = gr.Textbox(label="General Documents Status", interactive=False)
419
 
420
+ # Pestaña de Procesamiento
421
+ with gr.TabItem("Process Documents"):
422
+ gr.Markdown("## Ready to Process?")
423
+ gr.Markdown("Make sure you have completed all the setup in the previous tabs:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
+ with gr.Row():
426
+ with gr.Column():
427
+ gr.Markdown("""
428
+ ### Checklist:
429
+ **Main Setup**: Excel file loaded and folder column selected
430
+ **Keyword Mapping**: Keywords mapped to Excel columns
431
+ ✅ **Conditional Documents**: Documents with conditions set (optional)
432
+ **Column Associations**: Value mappings configured (optional)
433
+ **General Documents**: Documents for all entries uploaded (optional)
434
+ """)
435
+
436
+ with gr.Column():
437
+ process_btn = gr.Button(
438
+ "🚀 Process All Documents",
439
+ variant="primary",
440
+ size="lg"
441
+ )
442
+
443
+ process_status = gr.Textbox(label="Processing Status", lines=3, interactive=False)
444
+ download_file = gr.File(label="📥 Download Processed Documents")
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
+ # CONFIGURAR TODOS LOS EVENTOS DESPUÉS DE DEFINIR TODOS LOS COMPONENTES
447
+
448
+ # Eventos de Main Setup
449
  excel_file.change(
450
+ fn=lambda file: (
451
+ processor.load_excel_file(file)[0], # status
452
+ processor.load_excel_file(file)[1], # folder_column choices
453
+ processor.load_excel_file(file)[2], # preview
454
+ processor.load_excel_file(file)[3], # mapping_column choices
455
+ gr.Dropdown(choices=processor.get_excel_columns()) # condition_column choices
456
+ ),
457
  inputs=[excel_file],
458
+ outputs=[excel_status, folder_column, excel_preview, mapping_column, condition_column]
 
 
 
 
 
 
459
  )
460
 
461
  add_mapping_btn.click(
462
  fn=processor.add_keyword_mapping,
463
  inputs=[mapping_column, keyword_input],
464
+ outputs=[mapping_status]
465
  )
466
 
467
+ folder_column.change(
468
+ fn=processor.set_folder_column,
469
+ inputs=[folder_column],
470
+ outputs=[]
471
  )
472
 
473
+ # Eventos de Conditional Documents
474
  conditional_files.change(
475
+ fn=lambda files: (
476
+ processor.load_conditional_files(files),
477
+ gr.CheckboxGroup(choices=[os.path.basename(f.name) for f in files] if files else [])
 
 
 
478
  ),
479
  inputs=[conditional_files],
480
+ outputs=[conditional_status, selected_docs]
481
  )
482
 
483
  condition_column.change(
 
487
  )
488
 
489
  set_condition_btn.click(
490
+ fn=lambda col, val, selected_doc_names, files: processor.set_document_condition(
491
+ col, val, [f for f in files if os.path.basename(f.name) in selected_doc_names] if files else []
 
492
  ),
493
+ inputs=[condition_column, condition_value, selected_docs, conditional_files],
494
+ outputs=[condition_result]
495
  )
496
 
497
+ # Eventos para asociaciones
498
  first_excel.change(
499
  fn=processor.load_first_excel,
500
  inputs=[first_excel],
 
525
  outputs=[associations_display]
526
  )
527
 
528
+ # Eventos de General Documents
529
+ general_files.change(
530
+ fn=processor.load_general_documents,
531
+ inputs=[general_files],
532
+ outputs=[general_status]
 
 
 
 
533
  )
534
 
535
+ # Eventos de procesamiento
536
  process_btn.click(
537
+ fn=lambda: processor.process_documents(None),
538
+ inputs=[],
539
  outputs=[process_status, download_file]
 
 
 
540
  )
541
 
542
  return demo