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>
This commit is contained in:
Ernesto Herrera
2026-05-21 08:18:13 -06:00
commit a0081f54d2
21 changed files with 8509 additions and 0 deletions

View File

@@ -0,0 +1,388 @@
"""Manual de usuario de la pestana Valores. Lenguaje accesible, sin SQL ni
codigo. Para usuarios sin conocimientos tecnicos."""
from docx import Document
from docx.shared import Pt, RGBColor, 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)
def h1(t):
p = doc.add_heading(t, level=1)
for r in p.runs: r.font.color.rgb = RGBColor(0x15, 0x65, 0xC0)
def h2(t):
p = doc.add_heading(t, level=2)
for r in p.runs: r.font.color.rgb = RGBColor(0x1E, 0x88, 0xE5)
def h3(t):
p = doc.add_heading(t, level=3)
for r in p.runs: r.font.color.rgb = RGBColor(0x42, 0xA5, 0xF5)
def parr(t, bold=False):
p = doc.add_paragraph(); r = p.add_run(t); r.bold = bold; return p
def bullet(t):
p = doc.add_paragraph(t, style='List Bullet')
p.paragraph_format.left_indent = Cm(0.6); return p
def numbered(t):
p = doc.add_paragraph(t, style='List Number')
p.paragraph_format.left_indent = Cm(0.6); return p
def caja(titulo, text, fill, 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'), fill)
pPr.append(shd)
r = p.add_run(titulo + ': '); r.bold = True; r.font.color.rgb = color
p.add_run(text)
def nota(t, x): caja(t, x, 'FFF8E1', RGBColor(0xE6, 0x5C, 0x00))
def tip(t, x): caja(t, x, 'E8F5E9', RGBColor(0x2E, 0x7D, 0x32))
def alerta(t, x): caja(t, x, 'FFEBEE', RGBColor(0xC6, 0x28, 0x28))
def info(t, x): caja(t, x, 'E3F2FD', RGBColor(0x0D, 0x47, 0xA1))
def table_simple(headers, rows, col_widths=None):
t = doc.add_table(rows=1+len(rows), cols=len(headers))
t.style = 'Light Grid Accent 1'
for i, h in enumerate(headers):
c = t.rows[0].cells[i]; c.text = h
for p in c.paragraphs:
for r in p.runs: r.bold = True; r.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
tcPr = c._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 ri, row in enumerate(rows, start=1):
for ci, val in enumerate(row):
t.rows[ri].cells[ci].text = str(val)
if col_widths:
for i, w in enumerate(col_widths):
for row in t.rows: row.cells[i].width = Cm(w)
return t
# ====== PORTADA ======
ti = doc.add_paragraph(); ti.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = ti.add_run('\n\n\nPestana Valores')
r.bold = True; r.font.size = Pt(38); r.font.color.rgb = RGBColor(0x15, 0x65, 0xC0)
su = doc.add_paragraph(); su.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = su.add_run('Ajuste prorrateado de\nVALORTOTALME y VALORTOTALMN')
r.font.size = Pt(18); r.font.color.rgb = RGBColor(0x42, 0xA5, 0xF5)
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run('\n\nSistema de Utilerias 2.0\nGuia rapida para el usuario')
r.font.size = Pt(14)
r = p.add_run(f'\n\nVersion {datetime.date.today().isoformat()}')
r.font.size = Pt(11); r.italic = True
doc.add_page_break()
# ====== CONTENIDO ======
h1('Contenido')
for it in [
'1. ¿Para que sirve esta pestana?',
'2. Antes de empezar',
'3. Como se ve la pantalla',
'4. Como subir tu Excel y generar el plan',
'5. Modos: Aplicar siempre vs Usar umbral',
'6. Shelter: buscar pedimentos en otras bases de datos',
'7. Paso A — Analizar (sin escribir)',
'8. Paso B — Ejecutar el ajuste',
'9. Estatus posibles por pedimento',
'10. ¿Algo no funciona? Lo mas comun',
'11. Glosario rapido',
]:
parr(it).paragraph_format.left_indent = Cm(0.4)
doc.add_page_break()
# ====== 1 ======
h1('1. ¿Para que sirve esta pestana?')
parr('La pestana Valores te permite ajustar los valores de exportacion '
'(VALORTOTALME y VALORTOTALMN) de las partidas en SCAII para que coincidan '
'con los valores que el cliente espera. Subes un Excel con los pedimentos '
'y sus valores correctos, y la herramienta reparte la diferencia entre las '
'partidas proporcionalmente, garantizando que la suma quede exacta.')
info('En palabras simples',
'Si la suma de los valores actuales de las partidas de un pedimento es '
'900 y el cliente dice que debe ser 1,000, la herramienta sube cada '
'partida en la proporcion que le corresponde hasta que la suma de '
'exactamente 1,000.')
parr('Ademas, si trabajas con varias bases de datos del cliente en la misma '
'instancia SQL Server, la pestana puede buscar automaticamente en cual '
'vive cada pedimento (modo Shelter) y aplicar el ajuste en la base '
'correcta sin que tu tengas que cambiar la conexion.')
# ====== 2 ======
h1('2. Antes de empezar')
bullet('Asegurate que arriba diga "Conectado" en verde. Si no, ve a la pestana '
'Conexion y conecta primero.')
bullet('Ten listo tu Excel con los valores esperados. Debe tener 3 columnas: '
'PEDIMENTO, VALOR_ME, VALOR_MN.')
bullet('Identifica si los pedimentos de tu Excel viven en la base de datos '
'que tienes conectada o en otra. Si no estas seguro, deja activado el '
'modo Shelter y la herramienta lo descubre sola.')
alerta('Recomendacion fundamental',
'SIEMPRE corre primero con DRY_RUN activado. La pestana te muestra '
'que va a actualizar sin tocar la base. Cuando confirmes que los '
'numeros se ven bien, desactivas DRY_RUN y ejecutas en firme.')
# ====== 3 ======
h1('3. Como se ve la pantalla')
parr('La pestana esta dividida en tres bloques de arriba hacia abajo:')
table_simple(
['Bloque', '¿Que hace?'],
[
['Carga y configuracion', 'Subir Excel, descargar plantilla, elegir modo y umbral, activar Shelter.'],
['Paso A — Analizar', 'Genera el plan sin escribir nada en la base.'],
['Paso B — Ejecutar', 'Aplica los UPDATE en SCAII (con DRY_RUN como red de seguridad).'],
],
col_widths=[4.5, 11])
parr('Al final hay un bloque extra "Shelter" donde puedes hacer la busqueda '
'manual de los pedimentos en todas las bases (sin generar el plan, solo '
'para ver donde estan).')
# ====== 4 ======
h1('4. Como subir tu Excel y generar el plan')
h2('Paso 1 — Descarga la plantilla')
parr('Si es la primera vez, da clic en "Descargar plantilla". Se genera un '
'archivo de ejemplo con las 3 columnas que la herramienta espera. Abrelo, '
'borra las filas de ejemplo y pega los datos reales.')
h2('Paso 2 — Llena el Excel')
table_simple(
['Columna', 'Significado', 'Ejemplo'],
[
['PEDIMENTO', 'Numero de pedimento completo (con guiones).', '07-3429-4015540'],
['VALOR_ME', 'Valor en moneda extranjera que debe cuadrar.', '12345.67'],
['VALOR_MN', 'Valor en moneda nacional que debe cuadrar.', '234567.89'],
],
col_widths=[3.5, 8, 4])
nota('Variantes aceptadas',
'Los nombres de columna pueden venir con o sin guiones bajos, en '
'mayusculas o minusculas (VALOR_ME, VALORME, VALOR ME). La herramienta '
'acepta variantes razonables.')
h2('Paso 3 — Sube el Excel')
parr('Da clic en "Subir Excel" y selecciona tu archivo. No hace nada todavia; '
'solo lo deja listo para cuando aprietes Analizar.')
# ====== 5 ======
h1('5. Modos: Aplicar siempre vs Usar umbral')
parr('Antes de analizar, elige uno de los dos modos:')
h2('Modo "Aplicar siempre"')
parr('La herramienta ajusta los valores sin importar la magnitud de la '
'diferencia. Si los valores actuales son 10 y los esperados 10,000, '
'igual los multiplica por 1,000.')
tip('Cuando usar este modo',
'Cuando estas seguro de los numeros del Excel y quieres que cuadren si '
'o si, aunque la diferencia sea enorme.')
h2('Modo "Usar umbral %"')
parr('La herramienta solo aplica el ajuste si el cambio esta dentro del '
'porcentaje que tu definas con el slider. Por ejemplo, con umbral del '
'50%, los pedimentos donde el valor actual y el esperado difieran en '
'mas del 50% se marcan como FUERA_DE_UMBRAL y NO se ajustan.')
tip('Cuando usar este modo',
'Cuando quieres una proteccion adicional contra errores de captura en '
'el Excel. Si alguien escribio 100,000 en lugar de 1,000, el umbral lo '
'atrapa y no destroza los valores en la base.')
# ====== 6 ======
h1('6. Shelter: buscar pedimentos en otras bases de datos')
parr('Si tu cliente tiene varias bases de datos en el mismo servidor SQL '
'Server (por ejemplo, una por ano o por planta) y tus pedimentos pueden '
'estar en cualquiera, activa la casilla "Buscar en todas las BDs '
'(shelter)" antes de dar clic en Analizar.')
parr('Lo que hace el Shelter:')
numbered('Recorre todas las bases de datos online del servidor donde tu '
'usuario tenga permisos.')
numbered('Detecta cuales tienen la tabla SPedimentos.')
numbered('Busca cada pedimento de tu Excel en esas bases.')
numbered('Arma el plan poniendo en la columna BaseDeDatos a que base le toca '
'el ajuste por cada partida.')
numbered('Al ejecutar el Paso B, los UPDATE se aplican en la base correcta, '
'sin que tu hayas cambiado nada en la conexion.')
info('Pedimento en mas de una base',
'Si un pedimento existe en varias bases, la herramienta aplica el '
'ajuste en TODAS donde aparezca. Esto es lo correcto cuando el cliente '
'mantiene copias sincronizadas, pero presta atencion al resumen para '
'confirmarlo.')
parr('Si solo trabajas con una base, deja la casilla DESACTIVADA. La '
'herramienta usa la base que esta conectada actualmente y es mas rapido.')
h2('Boton "Buscar pedimentos en todas las BDs" (al final de la pestana)')
parr('Este boton hace solo el rastreo (sin generar plan, sin ajustar nada). '
'Te muestra 3 tablas:')
bullet('En que base(s) esta cada pedimento.')
bullet('Resumen por base: cuantos pedimentos del Excel encontro en cada una.')
bullet('Lista de pedimentos que no aparecen en ninguna base.')
parr('Es util para validar tu Excel antes de ajustar nada.')
# ====== 7 ======
h1('7. Paso A — Analizar (sin escribir)')
parr('Da clic en el boton "Analizar". La herramienta:')
numbered('Carga tu Excel.')
numbered('Si activaste Shelter, busca cada pedimento en todas las bases.')
numbered('Por cada pedimento (en cada base donde este), trae sus partidas de '
'exportacion y suma los VALORTOTALME y VALORTOTALMN actuales.')
numbered('Calcula los factores: cuanto hay que multiplicar para que cuadren.')
numbered('Genera el plan: una fila por cada partida que se va a tocar, con '
'el valor actual, el ajuste y el nuevo valor.')
numbered('Te muestra resumen + plan en pantalla.')
alerta('IMPORTANTE',
'El Paso A no escribe nada en la base. Es seguro correrlo las veces '
'que quieras.')
h2('Exportar a Excel')
parr('Despues de Analizar, da clic en "Exportar Excel" para guardar el plan '
'completo (con todas las filas, no solo las primeras 50) en un archivo '
'xlsx. Util para mandarlo por correo, revisarlo con calma o entregarlo '
'al cliente para validar antes de aplicar.')
# ====== 8 ======
h1('8. Paso B — Ejecutar el ajuste')
parr('Cuando el plan se ve bien:')
numbered('Mantén marcado el checkbox "DRY_RUN (simular sin escribir)" la '
'primera vez.')
numbered('Da clic en "Ejecutar ajuste (Paso B)". Con DRY_RUN activado solo '
'simula los UPDATE; ves los conteos pero la base no cambia.')
numbered('Revisa los numeros del log:')
bullet('"Partidas actualizadas" debe ser igual al numero de filas del plan.')
bullet('"Pedimentos procesados" debe ser igual a los pedimentos de tu Excel '
'(menos los que dieron error o no fueron encontrados).')
bullet('"BDs tocadas" te dice en cuales bases se aplicaria el cambio.')
bullet('"Errores" debe ser 0.')
numbered('Si todo cuadra, desmarca DRY_RUN y vuelve a dar clic en Ejecutar.')
numbered('Esta vez los cambios SI se aplican en la base. Al terminar veras '
'los mismos conteos pero los valores en SCAII ya estan ajustados.')
alerta('Sobre el rollback',
'La herramienta usa transacciones por pedimento. Si alguna partida '
'falla a mitad del proceso, las que ya se actualizaron de ESE '
'pedimento se revierten automaticamente. Las de los pedimentos '
'anteriores SI quedan guardadas (el rollback es solo por pedimento, '
'no global).')
# ====== 9 ======
h1('9. Estatus posibles por pedimento')
table_simple(
['Estatus', '¿Que significa?'],
[
['AJUSTAR', 'El pedimento se va a ajustar. Es el caso normal y el unico que produce filas en el plan.'],
['SIN_PARTIDAS', 'El pedimento existe pero no tiene partidas de exportacion para ajustar. Revisa SCAII manualmente.'],
['SIN_BASE_ME', 'La suma actual de VALORTOTALME es cero pero el Excel pide un valor positivo. No hay forma de prorratear desde cero.'],
['SIN_BASE_MN', 'Igual que el anterior pero para VALORTOTALMN.'],
['FUERA_DE_UMBRAL', 'Solo aparece en modo "Usar umbral". La diferencia entre actual y esperado supera el porcentaje configurado. No se ajusta.'],
['NO_ENCONTRADO_EN_BDS', 'Solo aparece con Shelter activado. El pedimento no esta en ninguna base accesible. Revisa que este bien escrito.'],
['ERROR: ...', 'Algo fallo en la consulta a la base. Lee el detalle del mensaje y avisa a TI si no se entiende.'],
],
col_widths=[4, 12])
# ====== 10 ======
h1('10. ¿Algo no funciona? Lo mas comun')
h3('"ERROR: corre primero Analizar"')
parr('Diste clic en Ejecutar (Paso B) sin haber corrido Analizar (Paso A). '
'Corre primero Analizar para generar el plan.')
h3('"Sube un Excel primero"')
parr('No has subido archivo o se borro la seleccion. Usa el boton "Subir '
'Excel" otra vez.')
h3('Todos los pedimentos salen como SIN_PARTIDAS')
bullet('El formato del pedimento en tu Excel no coincide con el de SCAII. '
'Verifica que tengan los guiones y los ceros a la izquierda correctos.')
bullet('Estas conectado a una base distinta de donde viven los pedimentos. '
'Activa el Shelter para buscarlos en todas.')
h3('Todos los pedimentos salen como NO_ENCONTRADO_EN_BDS')
bullet('Tu usuario de base de datos no tiene permisos en las bases donde '
'viven los pedimentos. Avisa a TI.')
bullet('Los pedimentos del Excel no existen en ninguna base. Verifica la '
'lista que te dieron.')
h3('La suma del plan no cuadra con el Excel por unos centavos')
parr('La herramienta ajusta automaticamente la ultima fila de cada pedimento '
'para que la suma sea exacta. Si aun asi ves diferencias muy pequenas, '
'es por redondeo a 6 decimales — el efecto en pesos es despreciable.')
h3('Apliqué el Paso B sin DRY_RUN y se me fue un ajuste mal')
parr('No hay deshacer automatico. Tienes que:')
numbered('Exportar a Excel el plan que se aplico (si lo tienes guardado).')
numbered('Construir el "plan inverso" en otro Excel (restando lo que sumaste '
'o viceversa).')
numbered('Subirlo y aplicarlo otra vez para revertir.')
parr('Por eso es CRITICO siempre correr DRY_RUN primero y exportar el plan a '
'Excel antes de ejecutar en firme.')
h3('Un pedimento aparece en mas BDs de las que esperaba')
parr('Significa que el cliente tiene copias en varias bases. Decide si '
'realmente quieres ajustar en todas o solo en una. Si solo en una, '
'desactiva el Shelter y conecta la pestana a la base especifica '
'antes de ejecutar.')
# ====== 11 ======
h1('11. Glosario rapido')
table_simple(
['Termino', 'Significado'],
[
['Prorrateo', 'Repartir una diferencia entre varias partidas, en proporcion al valor actual de cada una.'],
['VALORTOTALME', 'Valor total de una partida en moneda extranjera.'],
['VALORTOTALMN', 'Valor total de una partida en moneda nacional.'],
['Pedimento', 'Documento aduanero. En esta pestana lo identificamos por su numero completo: anio-aduana-patente-numero.'],
['Partida', 'Renglon dentro de una factura de exportacion (un producto especifico con su cantidad y valor).'],
['Umbral', 'Porcentaje maximo de diferencia permitido. Solo aplica si elegiste el modo "Usar umbral".'],
['Shelter', 'Funcion que busca cada pedimento en todas las bases de datos del servidor y aplica el ajuste donde lo encuentre.'],
['DRY_RUN', 'Modo simulacion: la herramienta calcula y muestra todo pero NO escribe en la base.'],
['Plan', 'Tabla detallada con una fila por cada partida que se va a actualizar.'],
['Resumen', 'Tabla con una fila por pedimento, indicando si va a ajustarse o no y por que.'],
],
col_widths=[3.5, 12])
import os
base = os.path.dirname(os.path.abspath(__file__))
out = os.path.join(base, 'Manual_Valores.docx')
try:
doc.save(out)
except PermissionError:
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
out = os.path.join(base, f'Manual_Valores_{ts}.docx')
doc.save(out)
print(f'Manual generado: {out}')