Files
utilerias-recon-2/app/generar_manual.py
Ernesto Herrera a0081f54d2 chore: initial commit del deploy de utilerias-recon-2
Incluye:
- app.ipynb con las 10 pestanas (Conexion, Descargas, Analisis, NLP, KGS,
  CTM, Saldos Vencidos, DataStage, Estructuras SCAII, Valores).
- launcher.py + run_dev.bat para lanzar Voila localmente.
- docker-compose.yml para levantar Postgres 16 con healthcheck.
- schema_registro.sql con las 27 tablas (Registro501..Registro701,
  RegistroInci/Resumen/Sel, base_numpartes).
- requirements.txt con pandas, pyodbc, psycopg2-binary, sqlalchemy,
  scikit-learn, openpyxl, voila, ipywidgets, python-docx.
- 6 manuales de usuario (CTM, SaldosVencidos, DataStage, EstructurasSCAII,
  Valores, GENPACT_V2 general) en .docx.
- 6 generadores de manuales para regenerar los .docx tras editar.
- .gitignore que excluye .env, .venv, outputs generados y caches.
- README.md con instrucciones de setup en server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 08:18:13 -06:00

646 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Genera el manual de usuario final del Sistema de Utilerias 2.0 SCAII.
Lenguaje accesible, sin tecnicismos.
"""
from docx import Document
from docx.shared import Pt, RGBColor, Inches, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
import datetime
doc = Document()
for sec in doc.sections:
sec.top_margin = Cm(2.5)
sec.bottom_margin = Cm(2.5)
sec.left_margin = Cm(2.2)
sec.right_margin = Cm(2.2)
style = doc.styles['Normal']
style.font.name = 'Calibri'
style.font.size = Pt(11)
# ====== Helpers de formato ======
def h1(text):
p = doc.add_heading(text, level=1)
for run in p.runs:
run.font.color.rgb = RGBColor(0x15, 0x65, 0xC0)
def h2(text):
p = doc.add_heading(text, level=2)
for run in p.runs:
run.font.color.rgb = RGBColor(0x1E, 0x88, 0xE5)
def h3(text):
p = doc.add_heading(text, level=3)
for run in p.runs:
run.font.color.rgb = RGBColor(0x42, 0xA5, 0xF5)
def parr(text, bold=False):
p = doc.add_paragraph()
r = p.add_run(text)
r.bold = bold
return p
def code_block(text):
p = doc.add_paragraph()
p.paragraph_format.left_indent = Cm(0.4)
r = p.add_run(text)
r.font.name = 'Consolas'
r.font.size = Pt(9.5)
pPr = p._p.get_or_add_pPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:val'), 'clear')
shd.set(qn('w:color'), 'auto')
shd.set(qn('w:fill'), 'F0F0F0')
pPr.append(shd)
return p
def bullet(text, level=0):
p = doc.add_paragraph(text, style='List Bullet')
p.paragraph_format.left_indent = Cm(0.6 + level*0.6)
return p
def numbered(text):
p = doc.add_paragraph(text, style='List Number')
p.paragraph_format.left_indent = Cm(0.6)
return p
def nota(titulo, text):
"""Caja de nota con titulo en color."""
p = doc.add_paragraph()
p.paragraph_format.left_indent = Cm(0.4)
pPr = p._p.get_or_add_pPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:val'), 'clear')
shd.set(qn('w:color'), 'auto')
shd.set(qn('w:fill'), 'FFF8E1')
pPr.append(shd)
r = p.add_run(titulo + ': ')
r.bold = True
r.font.color.rgb = RGBColor(0xE6, 0x5C, 0x00)
p.add_run(text)
def tip(titulo, text):
"""Caja de tip con verde."""
p = doc.add_paragraph()
p.paragraph_format.left_indent = Cm(0.4)
pPr = p._p.get_or_add_pPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:val'), 'clear')
shd.set(qn('w:color'), 'auto')
shd.set(qn('w:fill'), 'E8F5E9')
pPr.append(shd)
r = p.add_run(titulo + ': ')
r.bold = True
r.font.color.rgb = RGBColor(0x2E, 0x7D, 0x32)
p.add_run(text)
def table_simple(headers, rows, col_widths_cm=None):
t = doc.add_table(rows=1+len(rows), cols=len(headers))
t.style = 'Light Grid Accent 1'
hdr = t.rows[0].cells
for i, h in enumerate(headers):
hdr[i].text = h
for p in hdr[i].paragraphs:
for r in p.runs:
r.bold = True
r.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
tcPr = hdr[i]._tc.get_or_add_tcPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:val'), 'clear')
shd.set(qn('w:fill'), '1565C0')
tcPr.append(shd)
for r_idx, row in enumerate(rows, start=1):
cells = t.rows[r_idx].cells
for c_idx, val in enumerate(row):
cells[c_idx].text = str(val)
if col_widths_cm:
for i, w in enumerate(col_widths_cm):
for row in t.rows:
row.cells[i].width = Cm(w)
return t
# ====== PORTADA ======
title = doc.add_paragraph()
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = title.add_run('\n\n\nSistema de Utilerías 2.0')
r.bold = True; r.font.size = Pt(36); r.font.color.rgb = RGBColor(0x15, 0x65, 0xC0)
sub = doc.add_paragraph()
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = sub.add_run('SCAII')
r.font.size = Pt(22); r.font.color.rgb = RGBColor(0x42, 0xA5, 0xF5)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run('\n\nManual de Usuario\n')
r.font.size = Pt(15)
r = p.add_run(f'\nVersión {datetime.date.today().isoformat()}')
r.font.size = Pt(12); r.italic = True
doc.add_page_break()
# ====== CONTENIDO ======
h1('Contenido')
indice = [
'1. ¿Para qué sirve esta aplicación?',
'2. Antes de empezar',
'3. Cómo abrir la aplicación',
'4. La pantalla principal',
'5. Pestaña 1: Conexión (elegir base de datos)',
'6. Pestaña 2: Descargas',
' • Paso 8 — Pronóstico',
' • Paso 9 — Generar descargas pendientes',
' • Paso 10 — Complementar descargas ya hechas',
'7. Pestaña 3: Análisis de Saldos',
' • Saldo disponible por año',
' • Comparativo Importaciones vs Exportaciones',
' • Tabla de pesos: IMPO / EXPO / CONSUMIDO / DESCARGAS',
' • Tabla de cantidades por año (totales)',
' • Tabla de cantidades por año + Unidad de Medida',
' • Exportar a Excel',
'8. Pestaña 4: Sustitutos NLP (sugerencias automáticas)',
'9. Pestaña 5: Descarga por % de KGS',
'10. Barras de progreso — qué significan',
'11. Modo prueba (DRY_RUN) — la herramienta más importante',
'12. Si algo sale mal',
'13. Glosario',
]
for it in indice:
doc.add_paragraph(it)
doc.add_page_break()
# ====== 1. INTRODUCCION ======
h1('1. ¿Para qué sirve esta aplicación?')
parr('Es una herramienta para automatizar y revisar el proceso de descargas de inventario '
'contra facturas de exportación en SCAII. En lugar de hacer las descargas factura por factura '
'a mano en el sistema, esta aplicación las procesa por lotes y deja registro de todo.')
parr('Lo que puedes hacer con la app:')
bullet('Ver cuántas facturas tienes pendientes y si hay suficiente inventario para cubrirlas')
bullet('Generar las descargas de forma masiva')
bullet('Complementar facturas que se cerraron pero quedaron con faltantes')
bullet('Analizar el estado de tus saldos: cuánto te queda, qué tan viejo es, comparar contra lo que importaste/exportaste')
bullet('Encontrar sustitutos automáticamente para componentes con descripciones similares')
tip('Bueno saber', 'Antes de escribir cualquier cosa en la base de datos, la app te permite '
'simular el resultado con el "modo prueba". Así nunca trabajas a ciegas.')
# ====== 2. ANTES DE EMPEZAR ======
h1('2. Antes de empezar')
parr('Tu computadora (o el servidor donde se instaló) debe tener:')
bullet('Python 3.11 o superior')
bullet('Microsoft ODBC Driver 18 for SQL Server')
bullet('Acceso a la base de datos SCAII (usuario y contraseña)')
parr('Si alguien del área técnica te entregó la aplicación, es probable que ya tenga todo configurado. '
'Solo necesitas saber dónde está la carpeta y darle doble clic al archivo de inicio.')
# ====== 3. ABRIR LA APP ======
h1('3. Cómo abrir la aplicación')
parr('En la carpeta de la aplicación verás varios archivos. El que te interesa es:')
code_block('run_dev.bat')
parr('Pasos:')
numbered('Doble clic en run_dev.bat')
numbered('Se abre una ventana negra de consola con texto. NO LA CIERRES — si la cierras, la app se apaga.')
numbered('Después de unos segundos, tu navegador (Edge, Chrome) se abre automáticamente con la aplicación')
numbered('Si por alguna razón el navegador no abre, puedes escribir esta dirección manualmente:')
code_block('http://127.0.0.1:8866/')
nota('Importante', 'La consola negra debe quedarse abierta todo el tiempo que estés usando la aplicación. '
'Cuando termines de trabajar, cierra primero el navegador y después cierra la consola.')
# ====== 4. LA PANTALLA ======
h1('4. La pantalla principal')
parr('Al abrir la app verás tres cosas:')
bullet('Una barra azul en la parte de arriba con el nombre del sistema y la base de datos a la que estás conectado')
bullet('Cinco pestañas: Conexión, Descargas, Análisis Saldos, Sustitutos NLP, Descarga % KGS')
bullet('El contenido de la pestaña que tengas seleccionada')
nota('Si la barra está roja', 'Significa que no hay conexión con la base de datos. Revisa con el área de '
'sistemas que las credenciales sean correctas y que el servidor esté disponible.')
# ====== 5. CONEXION ======
h1('5. Pestaña 1: Conexión (elegir base de datos)')
parr('La primera pestaña te permite escoger sobre cuál base de datos quieres trabajar. Esto es útil cuando '
'tu servidor tiene varias bases (por ejemplo, una de producción y otra de pruebas).')
h3('Cómo cambiar de base de datos')
numbered('Abre la lista desplegable "Base de datos"')
numbered('Selecciona la base que quieres usar')
numbered('Presiona el botón azul "Conectar a esta DB"')
numbered('Espera unos segundos. Cuando termina, te aparece el mensaje "OK. Cache reseteado."')
tip('Cómo saber a qué base estás conectado',
'En cada pestaña, cada vez que ejecutas una operación, la primera línea del resultado dice '
'"(DB actual: NOMBRE)". También está siempre visible en la barra azul de arriba.')
nota('Al cambiar de base',
'La aplicación borra todos los cálculos anteriores. Si ya habías hecho un pronóstico, '
'tendrás que volver a hacerlo en la nueva base.')
# ====== 6. DESCARGAS ======
h1('6. Pestaña 2: Descargas')
parr('Esta es la pestaña principal. Contiene tres pasos en orden:')
bullet('Paso 8 — Pronóstico (simulación)')
bullet('Paso 9 — Generar descargas de las facturas pendientes')
bullet('Paso 10 — Complementar facturas que ya fueron cerradas pero con faltantes')
doc.add_paragraph()
h2('Paso 8 — Pronóstico')
parr('El pronóstico es una simulación: te dice qué pasaría si intentaras descargar todas las facturas '
'pendientes en este momento. No escribe nada en la base; solo calcula y muestra resultados.')
parr('¿Para qué sirve?')
bullet('Saber cuántas facturas se pueden cerrar al 100% con el inventario actual')
bullet('Detectar qué componentes están escasos antes de intentar la descarga real')
bullet('Es obligatorio correrlo antes del Paso 9 (porque el paso 9 usa lo que calculó el 8)')
parr('Cómo correrlo:')
numbered('Presiona el botón "Calcular pronóstico (paso 8)"')
numbered('Espera mientras la barra de progreso avanza (procesa una factura por cada paso)')
numbered('Al terminar verás un resumen como este:')
code_block('Tiempo: 5.2s | Filas: 2,165 | Facturas: 235\n'
' 100% cobertura: 56\n'
' parcial: 179')
parr('Esto significa: 56 facturas se pueden cerrar completamente, las otras 179 quedarían con algún faltante.')
doc.add_paragraph()
h2('Paso 9 — Generar descargas de las facturas pendientes')
parr('Aquí es donde realmente se generan los movimientos. Toma las facturas que están pendientes y, '
'usando el pronóstico que calculó el Paso 8, crea los registros de descarga en SCAII.')
h3('Las dos formas de descargar: NATURAL vs DIRIGIDA')
table_simple(
['Modo', 'Qué hace', 'Cuándo usarla'],
[
['NATURAL', 'Solo procesa facturas donde TODOS los componentes del BOM se pueden cubrir al 100%. '
'Si una factura tiene aunque sea un solo componente con faltante, NO la toca.',
'Cuando quieres descargas "limpias" sin faltantes. Es lo más conservador.'],
['DIRIGIDA', 'Procesa TODAS las facturas pendientes. Descarga lo que sí se puede, y deja sin '
'descargar los componentes con faltante. La factura igual queda cerrada.',
'Cuando necesitas cerrar facturas aunque queden incompletas (por ejemplo, '
'para reportes o cierre de mes).'],
],
col_widths_cm=[2.5, 8, 5]
)
h3('Filtro por fechas')
parr('Si quieres procesar solo un rango de facturas (por ejemplo, las de enero a marzo), '
'usa las cajas "Desde" y "Hasta". El formato debe ser:')
code_block('2025-01-01')
parr('Si las dejas vacías, se procesan todas las pendientes.')
h3('Modo prueba (DRY_RUN)')
parr('La casilla "DRY_RUN (simular)" es tu red de seguridad. Cuando está marcada (palomita), '
'la herramienta calcula y te dice qué iba a hacer, PERO NO ESCRIBE NADA en la base. '
'Es como ver una previsualización antes de imprimir.')
tip('Buena práctica',
'SIEMPRE corre primero con DRY_RUN marcado. Revisa los números. Si todo cuadra, desmarca '
'la casilla y vuelve a ejecutar para hacerlo de verdad.')
parr('Pasos completos:')
numbered('Verifica que ya corriste el Paso 8 (Pronóstico)')
numbered('Elige el modo: NATURAL o DIRIGIDA')
numbered('Deja "DRY_RUN" marcado para la primera corrida')
numbered('Opcionalmente, pon un rango de fechas')
numbered('Presiona "Ejecutar paso 9 (NA->AC)"')
numbered('Revisa el resultado: cuántas facturas se procesaron, cuántas filas se insertarían')
numbered('Si todo está bien, desmarca DRY_RUN y vuelve a presionar el botón')
numbered('Ahora sí se generan las descargas y las facturas pasan a estado cerrado (AC)')
doc.add_paragraph()
h2('Paso 10 — Complementar descargas ya hechas')
parr('A veces cerraste una factura en modo DIRIGIDA y quedó con faltantes. Tiempo después llega nuevo '
'inventario que sí podría cubrir esos faltantes. Aquí es donde Paso 10 entra: agrega descargas '
'adicionales a facturas que ya están cerradas (AC).')
parr('Cómo funciona:')
bullet('Toma todas las facturas en estado AC (o las que filtres por fecha)')
bullet('Calcula cuánto se descargó vs cuánto pedía el BOM')
bullet('Para los faltantes, busca si ahora sí hay inventario disponible')
bullet('Genera las descargas adicionales')
nota('La factura NO cambia de estado',
'Las facturas siguen siendo AC después del Paso 10. Solo se agregan más movimientos de descarga.')
h3('Procesar facturas específicas')
parr('Si solo quieres trabajar con ciertas facturas, escribe sus números separados por comas en la caja "Facturas":')
code_block('DESP19-001,DESP19-002,DESP19-005')
parr('Si la dejas vacía, se procesan todas las AC en el rango de fechas.')
doc.add_page_break()
# ====== 7. ANALISIS ======
h1('7. Pestaña 3: Análisis de Saldos')
parr('Esta pestaña no genera movimientos. Es solo para ver el estado actual del inventario y compararlo '
'contra lo que se ha importado y exportado.')
h3('Cómo usarla')
numbered('Presiona el botón "Cargar y analizar SSaldoTem"')
numbered('Espera mientras la barra de progreso avanza')
numbered('Aparecen tres secciones con tablas y gráficas')
h2('Sección 1: Saldo disponible por año (de entrada del lote)')
parr('Te muestra cuánto saldo te queda agrupado por el año en que entró ese lote a inventario. '
'Por ejemplo, "del lote que entró en 2023, te quedan tantas piezas disponibles".')
parr('Te ayuda a:')
bullet('Detectar inventario muy viejo que no se ha consumido')
bullet('Ver cómo está distribuido tu saldo en el tiempo')
h2('Sección 2: Comparativo IMPO vs EXPO por año')
parr('Muestra para cada año cuántas partidas importaste y exportaste, junto con sus pesos y valores. '
'Es útil para tener una vista general de la actividad del cliente.')
h2('Sección 3: Tabla de pesos por año (IMPO / EXPO / CONSUMIDO / DESCARGAS)')
parr('Esta es la más rica para entender el flujo de inventario. Por cada año te muestra cuatro pesos diferentes:')
table_simple(
['Columna', 'Qué significa'],
[
['peso_impo', 'Cuántos KGs entraron al inventario ese año (por importaciones)'],
['peso_expo', 'Cuántos KGs salieron en facturas de exportación de ese año'],
['peso_consumido','De los lotes que entraron ese año, cuánto ya se consumió (acumulado a hoy)'],
['peso_descargas','Cuántos KGs aparecen registrados como descargas en las facturas de exportación de ese año'],
],
col_widths_cm=[3.5, 12]
)
parr('¿Cómo interpretar la tabla?')
bullet('peso_expo vs peso_descargas (mismo año): si son iguales, todas las facturas exportadas en ese año '
'ya tienen su descarga generada. Si peso_expo es mayor, significa que hay facturas pendientes (en NA).')
bullet('peso_impo vs peso_consumido (mismo año): te dice qué proporción del inventario que entró ya se utilizó. '
'Un peso_consumido bajo indica inventario viejo sin movimiento.')
tip('Caso típico',
'Si ves que en un año peso_expo = 50,000 KG pero peso_descargas = 30,000 KG, hay '
'20,000 KG de facturas exportadas que aún no tienen su descarga registrada. Esas '
'son las que el Paso 9 va a procesar.')
h2('Sección 4: Tabla de cantidades por año (totales)')
parr('Misma idea que la tabla de pesos, pero ahora midiendo cantidades (piezas, litros, kilos, etc.) '
'en lugar de peso. Te muestra cuántas partidas y cuánta cantidad se importó y exportó cada año.')
table_simple(
['Columna', 'Qué significa'],
[
['partidas_impo', 'Número de partidas de importación que entraron ese año'],
['cantidad_impo', 'Suma total de cantidades importadas (todas las UMs mezcladas)'],
['partidas_expo', 'Número de partidas de exportación de ese año'],
['cantidad_expo', 'Suma total de cantidades exportadas (todas las UMs mezcladas)'],
['diferencia', 'cantidad_expo cantidad_impo (positivo: se exportó más de lo que entró ese año)'],
],
col_widths_cm=[3.5, 12]
)
nota('Cuidado al interpretar',
'Esta tabla SUMA cantidades sin importar la unidad de medida. Si tienes una partida de '
'100 PZA y otra de 50 KG, la tabla suma "150" como si fueran lo mismo. Está bien para '
'tener un vistazo general, pero si tu inventario maneja UMs mezcladas y necesitas '
'precisión, usa la tabla siguiente que separa por unidad.')
h2('Sección 5: Tabla de cantidades por año + Unidad de Medida')
parr('Versión más precisa de la tabla anterior: una fila por cada combinación de año y unidad '
'de medida. Aquí ya no se mezclan UMs entre sí.')
table_simple(
['Columna', 'Qué significa'],
[
['ANIO', 'Año'],
['UM', 'Unidad de medida (KGS, PZA, LBS, etc.)'],
['partidas_impo', 'Partidas IMPO con esa UM ese año'],
['cantidad_impo', 'Suma de cantidades importadas en esa UM'],
['partidas_expo', 'Partidas EXPO con esa UM ese año'],
['cantidad_expo', 'Suma de cantidades exportadas en esa UM'],
['diferencia', 'cantidad_expo cantidad_impo (positivo: se exportó más de lo importado en esa UM ese año)'],
],
col_widths_cm=[3.5, 12]
)
parr('La UM se normaliza automáticamente: "Kgs", "KGS" y " kgs " se consideran la misma. '
'Esto evita filas duplicadas por diferencias de mayúsculas o espacios.')
tip('Cómo leerla',
'Por ejemplo: si en 2024, UM = KGS te muestra cantidad_impo = 12,000 y cantidad_expo = 9,500, '
'significa que ese año entraron 12,000 KG de productos y se exportaron 9,500 KG. Los 2,500 KG '
'restantes están en saldo o se exportarán en años siguientes.')
parr('La gráfica que acompaña esta tabla muestra las **4 UMs más usadas** (las que tienen mayor '
'volumen acumulado). Cada UM tiene su propia gráfica de barras IMPO vs EXPO por año. Las UMs '
'menos frecuentes solo aparecen en la tabla, no en la gráfica.')
h3('Exportar a Excel')
parr('El botón verde "Exportar Excel" genera un archivo con TODO el análisis. El nombre del archivo '
'incluye la fecha y hora de generación. Contiene las siguientes hojas:')
table_simple(
['Hoja', 'Contenido'],
[
['SSaldoTem_Detalle', 'Cada lote de inventario con todos sus campos calculados'],
['Por_Anio', 'Saldo disponible agrupado por año de entrada'],
['IMPO_x_Anio', 'Partidas de importación resumidas por año'],
['EXPO_x_Anio', 'Partidas de exportación resumidas por año'],
['IMPO_vs_EXPO', 'Comparativo completo IMPO vs EXPO con peso y valor ME'],
['Pesos_x_Anio', 'Pesos por año: IMPO / EXPO / CONSUMIDO / DESCARGAS'],
['Cantidades_x_Anio', 'Cantidades totales por año (mezclando UMs)'],
['Cantidades_Anio_UM', 'Cantidades por año separadas por unidad de medida'],
],
col_widths_cm=[5, 11]
)
doc.add_page_break()
# ====== 8. SUSTITUTOS NLP ======
h1('8. Pestaña 4: Sustitutos NLP (sugerencias automáticas)')
parr('Esta pestaña te ayuda a llenar el catálogo de sustitutos de partes (SPartesSustitutos). En lugar '
'de capturarlos uno por uno, la aplicación los sugiere automáticamente comparando descripciones.')
h3('Cómo funciona (sin entrar en detalles técnicos)')
parr('La aplicación lee la descripción de cada componente del BOM y la compara con todas las descripciones '
'del catálogo de partes. Para las parejas que se parecen lo suficiente, las sugiere como posibles sustitutos.')
parr('Por ejemplo, si una parte se describe como "TORNILLO HEX M6 ACERO" y existe otra como '
'"TORNILLO HEXAGONAL M6 INOX", la aplicación detecta la similitud y las sugiere como sustitutos.')
h3('Controles')
table_simple(
['Control', 'Qué hace'],
[
['Min similitud', 'Qué tan parecidas deben ser las descripciones (50% a 100%). Recomendado: 80%.'],
['Top-N', 'Cuántos sustitutos máximo guardar por componente. Recomendado: 3.'],
['DRY_RUN', 'Modo prueba — solo muestra qué sugeriría, no guarda nada.'],
],
col_widths_cm=[3.5, 12]
)
parr('Pasos:')
numbered('Ajusta el slider de similitud (más alto = más estricto)')
numbered('Ajusta el slider de Top-N')
numbered('Marca DRY_RUN para ver primero qué sugiere')
numbered('Presiona "Generar sustitutos NLP"')
numbered('Revisa la muestra de los primeros 10 que sugeriría')
numbered('Si las sugerencias se ven bien, desmarca DRY_RUN y vuelve a ejecutar')
nota('Solo se agregan nuevos',
'La aplicación nunca duplica. Si un par (componente → sustituto) ya existe en SPartesSustitutos, '
'lo omite. Solo agrega los pares que no estaban.')
doc.add_page_break()
# ====== 9. DESCARGA % KGS ======
h1('9. Pestaña 5: Descarga por % de KGS (Paso 12)')
parr('Esta es una forma alternativa de hacer descargas. Es útil cuando tu BOM en lugar de decir "cantidad de piezas", '
'dice "porcentaje del peso". Por ejemplo:')
table_simple(
['Componente', 'CANTIDAD en BOM', 'Interpretación'],
[
['Componente A', '60', '60% del peso total de la factura'],
['Componente B', '30', '30% del peso total'],
['Componente C', '10', '10% del peso total'],
],
col_widths_cm=[5, 5, 6]
)
parr('Si la factura tiene PESONETO = 500 KG, este modo descarga:')
bullet('300 KG del Componente A (60% × 500)')
bullet('150 KG del Componente B')
bullet('50 KG del Componente C')
parr('Funciona exactamente igual que el Paso 9, con los mismos controles (NATURAL/DIRIGIDA, DRY_RUN, '
'fechas, complementaria). La única diferencia es la fórmula de cálculo.')
nota('¿Cuándo usar Paso 12 en lugar de Paso 9?',
'Si tu BOM está expresado en porcentajes de peso (más común en industrias como química, '
'cartón, plásticos), usa Paso 12. Si tu BOM está en cantidades unitarias por pieza '
'(más común en ensamblaje), usa Paso 9. Pregúntale al área de operaciones cuál aplica.')
doc.add_page_break()
# ====== 10. BARRAS DE PROGRESO ======
h1('10. Barras de progreso — qué significan')
parr('Cada operación pesada tiene una barra de progreso debajo del botón. Te ayuda a saber:')
bullet('Que la operación está activa (no se trabó)')
bullet('Cuánto falta para terminar')
bullet('En qué etapa va')
parr('Colores de la barra:')
table_simple(
['Color', 'Significa'],
[
['Azul', 'En proceso — la operación sigue corriendo'],
['Verde', 'Terminó correctamente'],
['Rojo', 'Hubo un error. Revisa el cuadro de texto debajo del botón.'],
],
col_widths_cm=[3, 12]
)
tip('Si la barra no avanza',
'Espera un momento. Al cargar los datos por primera vez (especialmente el pronóstico) la '
'aplicación primero consulta varias tablas grandes antes de empezar a procesar. Es normal '
'que tarde unos segundos antes de que la barra se mueva.')
# ====== 11. DRY_RUN ======
h1('11. Modo prueba (DRY_RUN) — la herramienta más importante')
parr('DRY_RUN es una casilla que aparece en casi todos los pasos. Su propósito es uno solo:')
parr('Cuando está marcada (✓), la aplicación calcula lo que haría pero NO escribe nada en la base de datos. '
'Te da la información para que tú decidas si proceder.', bold=True)
parr('Beneficios de usarla siempre:')
bullet('Ves cuántas facturas se procesarían')
bullet('Ves cuántos movimientos se generarían')
bullet('Detectas anomalías ANTES de hacer cambios irreversibles')
bullet('Si hay un error de configuración o un dato raro, te enteras sin haber roto nada')
tip('Regla de oro',
'Toda corrida real debe ir precedida de una corrida en modo prueba. Si los números no '
'cuadran, NO desmarques DRY_RUN — primero entiende qué pasa.')
# ====== 12. SI ALGO SALE MAL ======
h1('12. Si algo sale mal')
h3('La aplicación no abre / la consola se cierra')
bullet('Verifica que Python esté instalado y configurado en PATH')
bullet('Borra la carpeta .venv y vuelve a darle doble clic al run_dev.bat (se reinstala automáticamente)')
bullet('Si el problema persiste, contacta al área técnica')
h3('Sale "ERROR de conexión" en la barra azul')
bullet('Las credenciales en el archivo .env son incorrectas')
bullet('El servidor de base de datos está caído')
bullet('La base seleccionada no existe o no tienes permisos')
h3('Un botón devuelve un error largo (traceback rojo)')
bullet('Toma captura de pantalla del error completo')
bullet('Envíala al área técnica indicando qué botón presionaste y qué configuración tenías')
h3('Las descargas se duplicaron')
parr('Si por accidente corriste Paso 9 dos veces (por ejemplo, una en NATURAL y otra en DIRIGIDA), '
'pueden aparecer descargas duplicadas. La aplicación tiene una protección automática que evita '
'esto, pero si pasó por algún caso edge, contacta al área técnica — hay un proceso de limpieza.')
h3('El pronóstico tarda mucho')
parr('Es normal que tarde 30 segundos a 2 minutos según el volumen de datos. La barra de progreso '
'te indica avance. Si pasados varios minutos no se mueve, presiona Ctrl+C en la consola negra '
'y vuelve a abrir la aplicación.')
doc.add_page_break()
# ====== 13. GLOSARIO ======
h1('13. Glosario')
table_simple(
['Término', 'Significado'],
[
['SCAII', 'Sistema de Control Aduanero Inteligente — la base de datos de SQL Server donde vive todo'],
['Factura de exportación (EXPO)', 'Documento que registra una venta al extranjero. Tiene partidas (líneas)'],
['Factura de importación (IMPO)', 'Documento que registra una compra del extranjero. Genera saldos de inventario'],
['Partida', 'Una línea dentro de una factura. Cada partida tiene un producto y una cantidad'],
['BOM', 'Bill of Materials. Receta que indica qué componentes se necesitan para producir cada producto'],
['PT', 'Producto Terminado — el producto final que se exporta'],
['MP', 'Materia Prima — los componentes que se consumen para fabricar el PT'],
['Saldo', 'Cantidad disponible de un componente en inventario'],
['Descarga', 'Movimiento que reduce un saldo. Cada exportación genera descargas según el BOM'],
['PEPS / FIFO', 'Primero que Entra, Primero que Sale. Política para decidir qué lote consumir primero'],
['NA', 'Estado de factura pendiente de descarga'],
['AC', 'Estado de factura ya cerrada (con sus descargas generadas)'],
['NATURAL', 'Modo de descarga que solo procesa facturas con cobertura completa'],
['DIRIGIDA', 'Modo de descarga que acepta cubrir parcialmente'],
['Sustituto', 'Componente alterno que puede reemplazar a otro cuando no hay saldo del original'],
['DRY_RUN', 'Modo prueba — calcula sin escribir en la base'],
['Pronóstico', 'Simulación de las descargas que se podrían hacer en este momento'],
['NLP', 'Tecnología que permite comparar descripciones de texto para encontrar similitudes'],
],
col_widths_cm=[4, 12]
)
# ====== GUARDAR ======
import os
out_path = 'Manual_GENPACT_V2.docx'
try:
doc.save(out_path)
except PermissionError:
out_path = f'Manual_GENPACT_V2_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.docx'
doc.save(out_path)
print('(El anterior estaba abierto en Word. Generado con nombre alternativo.)')
print(f'Manual generado: {os.path.abspath(out_path)}')