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>
646 lines
28 KiB
Python
646 lines
28 KiB
Python
"""
|
||
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)}')
|