Files
utilerias-recon-2/app/generar_manual_estructuras.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

438 lines
20 KiB
Python

"""Manual de usuario de la pestana Estructuras SCAII. 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 Estructuras SCAII')
r.bold = True; r.font.size = Pt(34); r.font.color.rgb = RGBColor(0x15, 0x65, 0xC0)
su = doc.add_paragraph(); su.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = su.add_run('Generacion de reportes y\narchivos para SCAII')
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. Reportes de Pedimentos',
' • Estructura CAT Pedimentos',
' • Estructura CAT Pedimentos Rectificados',
' • Rastreo de Rectificaciones (y como ver el historial)',
'5. Encabezado de Facturas (Importacion y Exportacion)',
'6. Tipo de Cambio (con alertas en rojo)',
'7. Catalogo de NUMPARTES (preparar el diccionario)',
'8. Estructura de Partidas (como se asignan los NUMPARTE)',
'9. ¿Algo no funciona? Lo mas comun',
'10. 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 Estructuras SCAII genera los reportes y archivos en Excel que entregas al '
'cliente o que cargas en SCAII. Toma la informacion que ya esta en la herramienta '
'(la que subiste con la pestana DataStage) y la organiza segun los formatos que el '
'cliente espera.')
info('En palabras simples',
'DataStage es donde le das de comer a la herramienta. Estructuras SCAII es donde le pides '
'que cocine y te entregue platillos listos (reportes). Si la pestana DataStage esta '
'vacia, aqui no hay nada que generar.')
# ====== 2 ======
h1('2. Antes de empezar')
bullet('Asegurate que la pestana DataStage tenga datos cargados. Si no, ve primero a '
'DataStage y carga tus archivos .asc.')
bullet('Identifica el rango de fechas del periodo que quieres reportar. La mayoria de los '
'reportes te lo piden.')
bullet('Para la seccion de Partidas vas a necesitar el catalogo del cliente '
'(Excel con numero de parte, descripcion, unidad de medida y fraccion). Si no lo '
'tienes, igual funciona pero el numero de parte sera inventado por la herramienta.')
tip('Sugerencia',
'Antes de generar un reporte de un ano entero, prueba con un mes para revisar que el '
'resultado se ve como esperas. Despues amplia el rango.')
# ====== 3 ======
h1('3. Como se ve la pantalla')
parr('La pestana esta dividida en cinco bloques, uno debajo del otro:')
table_simple(
['Bloque', 'Color', '¿Que genera?'],
[
['Reportes de Pedimentos', 'Morado', 'CAT Pedimentos, CAT Rectificados, Rastreo de Rectificaciones.'],
['Encabezado de Facturas', 'Cian', 'Encabezados de facturas para Importacion y Exportacion.'],
['Tipo de Cambio', 'Naranja', 'Tipo de cambio por pedimento, con alertas en rojo si hay inconsistencias.'],
['Catalogo de NUMPARTES', 'Morado', 'Subes el catalogo del cliente para que la herramienta lo use.'],
['Estructura de Partidas', 'Morado', 'Genera las partidas Impo o Expo listas para SCAII.'],
],
col_widths=[4, 2.5, 9])
parr('Cada bloque tiene su(s) campo(s) de fechas, su boton para generar el reporte, su boton '
'para exportar a Excel y su area de resultados donde se muestra la informacion.')
# ====== 4 ======
h1('4. Reportes de Pedimentos')
h2('4.1 Estructura CAT Pedimentos')
parr('Genera el catalogo completo de pedimentos del periodo. Incluye:')
bullet('Numero de pedimento, tipo (importacion o exportacion), clave.')
bullet('Fecha de pago real, seccion aduanera, medios de transporte.')
bullet('Una marca de SI o NO indicando si el pedimento fue rectificado.')
bullet('Si fue rectificado, el numero del pedimento que lo rectifico.')
parr('Como generarlo:')
numbered('Indica fecha inicio y fecha fin.')
numbered('Da clic en "Generar".')
numbered('Revisa la vista previa (primeras 200 filas).')
numbered('Da clic en "Exportar Excel" para obtener el archivo completo.')
h2('4.2 Estructura CAT Pedimentos Rectificados')
parr('Es el reporte gemelo del anterior, pero parte desde los pedimentos rectificados. '
'Para cada rectificacion muestra a que pedimento original corrigio. Util cuando '
'necesitas un reporte enfocado en las rectificaciones.')
parr('Funciona igual: fecha inicio, fecha fin, Generar, Exportar Excel.')
h2('4.3 Rastreo de Rectificaciones')
parr('Lista todos los pedimentos del periodo que fueron rectificados al menos una vez. '
'Tiene un campo extra de busqueda para filtrar por numero de pedimento, patente o clave.')
h3('Ver el historial de un pedimento')
parr('Si necesitas ver toda la cadena de rectificaciones de un pedimento especifico:')
numbered('Localiza el pedimento en la lista del Rastreo.')
numbered('Copia los datos a los campos de abajo: Patente, Pedimento, Seccion Aduanera y Ano.')
numbered('Da clic en "Ver historial".')
numbered('La herramienta te muestra todas las rectificaciones ordenadas por fecha.')
info('Para que sirve',
'Sirve para reconstruir el "arbol genealogico" de un pedimento: el original, su primera '
'rectificacion, la rectificacion de la rectificacion, etc.')
# ====== 5 ======
h1('5. Encabezado de Facturas (Importacion y Exportacion)')
parr('Genera el encabezado de facturas en el formato que el cliente sube a SCAII. Tiene dos '
'sub-bloques, uno para Importacion y otro para Exportacion. Funcionan exactamente igual; '
'lo unico que cambia es que filtran pedimentos de importacion o exportacion respectivamente.')
h2('¿Que columnas trae?')
parr('Las que el cliente espera para SCAII: numero de pedimento, remesa, numero de factura, '
'fecha, tipo de cambio, claves de proveedor / vendido a / enviado a, agente aduanal, '
'medios de transporte, monedas, fletes, seguros, embalajes, otros incrementables, '
'fecha de emision, tipo de peso, aduana de cruce, observaciones y localizacion.')
nota('Sobre las columnas vacias',
'Varias columnas salen en blanco (CLAVE TRANSPORTISTA, NOMBRE CONDUCTOR, FLETES, '
'SEGUROS, etc.) porque no estan en los archivos del SAAI. El cliente las llena '
'manualmente despues. Esto es como funcionaba antes el sistema viejo; no hay cambios.')
h2('Como generarlo')
numbered('Define fecha inicio y fecha fin.')
numbered('Da clic en "Generar".')
numbered('Revisa la vista previa.')
numbered('Da clic en "Exportar Excel".')
alerta('Pedimentos rectificados se omiten',
'Si un pedimento ya fue rectificado, NO aparece en este reporte. Aparece solo el '
'pedimento rectificador (con R1 en la clave). Esto es a proposito: SCAII solo necesita '
'la version vigente.')
# ====== 6 ======
h1('6. Tipo de Cambio')
parr('Genera la lista de pedimentos del periodo con su tipo de cambio y la fecha. Util para '
'verificar que todos los pedimentos del mismo dia tienen el mismo tipo de cambio.')
h2('Alertas en rojo')
parr('La herramienta detecta automaticamente cuando un mismo dia tiene mas de un valor de '
'tipo de cambio entre sus pedimentos (eso no deberia pasar normalmente). Cuando lo '
'detecta:')
bullet('En pantalla: la celda del tipo de cambio aparece resaltada en rojo.')
bullet('En el Excel exportado: las celdas inconsistentes salen con fondo rojo y letra blanca, '
'para que sean facilmente visibles cuando el archivo se abre en Excel.')
alerta('¿Que hacer si ves rojo?',
'Significa que hay un error de captura o algo raro en el SAAI. Revisa esos pedimentos '
'en SCAII y corrige el tipo de cambio antes de entregar el reporte definitivo al '
'cliente.')
h2('Como generarlo')
parr('Define fecha inicio y fecha fin, da clic en "Generar", revisa la lista, exporta con el '
'boton "Exportar Excel".')
# ====== 7 ======
h1('7. Catalogo de NUMPARTES (diccionario del cliente)')
parr('Este bloque sirve para que la herramienta tenga un "diccionario" con los numeros de '
'parte oficiales del cliente. Se usa en el siguiente bloque (Estructura de Partidas) '
'para que, cuando la herramienta vea una descripcion en los archivos del SAAI, sepa '
'que numero de parte oficial asignarle.')
info('¿Por que se necesita esto?',
'Los archivos del SAAI no traen el numero de parte que usa el cliente; solo traen una '
'descripcion libre y la fraccion. Sin un catalogo, no hay forma de saber a que numero '
'de parte corresponde cada linea. Si tienes el catalogo del cliente, dejalo cargado '
'una sola vez y se queda guardado.')
h2('7.1 Formato del Excel que debes subir')
table_simple(
['Columna', 'Que poner ahi'],
[
['NUMPARTE', 'El identificador oficial del cliente (ABC-12345, PT-001, etc.).'],
['DESCRIPCION', 'La descripcion del producto tal como aparece en los pedimentos.'],
['UNIDAD DE MEDIDA', 'La sigla de la unidad (PZA, KGS, LT, etc.), NO el numero.'],
['FRACCION', 'La fraccion arancelaria completa (8 digitos).'],
],
col_widths=[4, 11])
nota('Variantes aceptadas',
'Los nombres de columna pueden venir con o sin guiones bajos, en mayusculas o '
'minusculas. La herramienta acepta variantes razonables (FACTURAIMPO, FACTURA_IMPO, '
'FACTURA, etc.). Solo cuida que las cuatro columnas existan en el archivo.')
h2('7.2 Cargar el catalogo')
numbered('Da clic en "Subir Excel" y selecciona tu archivo.')
numbered('Da clic en "Cargar a base (upsert)".')
numbered('La herramienta te dice cuantas filas se cargaron o actualizaron.')
info('"Upsert" significa acumular',
'Cada vez que subes un Excel, la herramienta agrega los numeros de parte nuevos y '
'actualiza los que ya existian (por si cambio la descripcion o la fraccion). No borra '
'los que ya tienes — los conserva.')
h2('7.3 Ver el catalogo actual')
parr('Da clic en "Ver base actual" para ver las primeras 500 filas del catalogo que tiene '
'la herramienta en este momento.')
h2('7.4 Limpiar el catalogo (empezar de cero)')
parr('Si necesitas reemplazar todo el catalogo:')
numbered('Marca la casilla "Confirmo TRUNCATE de base_numpartes".')
numbered('Da clic en el boton rojo "Limpiar base".')
numbered('Sube tu nuevo Excel con el catalogo limpio.')
# ====== 8 ======
h1('8. Estructura de Partidas (Importacion y Exportacion)')
parr('Este es el bloque mas importante de la pestana. Genera las partidas (lineas de cada '
'pedimento) en el formato que el cliente carga a SCAII. La parte clave es que la '
'herramienta tiene que asignarle un NUMERO DE PARTE a cada linea — y los archivos del '
'SAAI no traen esa informacion. Aqui es donde entra la magia.')
h2('8.1 Como asigna el numero de parte')
parr('Por cada linea del pedimento, la herramienta sigue este orden:')
numbered('Busca en tu catalogo (el de la seccion 7) un registro con la MISMA fraccion (4 '
'primeros digitos) y la MISMA unidad de medida.')
numbered('Si encuentra candidatos, compara la descripcion. Si la descripcion del pedimento '
'es parecida a la descripcion de algun candidato (por arriba del umbral que '
'configures), le asigna ese numero de parte. Esto se marca como BASE en el reporte.')
numbered('Si NO encuentra match en el catalogo, agrupa las partidas similares entre si y '
'les inventa un numero de parte automatico con el formato MP<FRACCION4>-R<NNN>. '
'Esto se marca como AUTO en el reporte.')
numbered('Las partidas que ni siquiera traen descripcion en el SAAI reciben un numero de '
'parte inventado individual. Se marcan como AUTO_SINDESC.')
info('¿Que es el umbral?',
'Es como un "nivel de exigencia". Va de 0.50 a 1.00. Por default esta en 0.80. '
'Mientras mas cercano a 1.00, mas estricta es la comparacion (solo acepta '
'descripciones casi identicas). Mientras mas cercano a 0.50, mas permisiva.')
h2('8.2 ¿Cuando ajustar el umbral?')
table_simple(
['Si ves...', '...ajusta el umbral asi'],
[
['Muchas asignaciones BASE que NO te convencen', 'Subelo a 0.85 o 0.90 (mas estricto).'],
['Muchas asignaciones AUTO que SI deberian estar en el catalogo', 'Bajalo a 0.70 o 0.65 (mas permisivo).'],
['Resultado razonable', 'Dejalo en 0.80 (default).'],
],
col_widths=[8.5, 7])
h2('8.3 Como generarlo')
numbered('Elige el sub-bloque: Partidas Importacion o Partidas Exportacion.')
numbered('Define fecha inicio y fecha fin.')
numbered('Ajusta el umbral si quieres (default 0.80 funciona en la mayoria de casos).')
numbered('Da clic en "Generar". El proceso puede tardar varios minutos si son muchas '
'partidas; la barra de progreso te indica el avance.')
numbered('Revisa la vista previa. Presta atencion a la columna MATCH_TIPO.')
numbered('Si todo se ve bien, exporta con "Exportar Excel".')
h2('8.4 ¿Que significa la columna MATCH_TIPO?')
table_simple(
['Valor', 'Significado'],
[
['BASE (0.92)', 'Encontro un numero de parte en tu catalogo con 92% de similitud.'],
['AUTO', 'No encontro en el catalogo. La herramienta invento un numero MP...-R...'],
['AUTO_SINDESC', 'La partida no tenia descripcion. Se le invento un numero individual.'],
],
col_widths=[3.5, 12])
alerta('Sobre las columnas vacias',
'Igual que con Facturas, varias columnas del reporte salen vacias por diseno (CLIENTE, '
'FORMA DE PAGO, LOCALIZACION, etc.). El cliente las llena despues o vienen de otro '
'cruce que no tenemos en esta herramienta.')
h2('8.5 ¿Como se ve el numero de parte automatico?')
parr('Cuando la herramienta no encuentra match en tu catalogo, inventa numeros con esta forma:')
bullet('MP8471-R001 (primer producto huerfano de la fraccion 8471)')
bullet('MP8471-R002 (segundo producto huerfano de la fraccion 8471)')
bullet('MP7318-R001 (primer producto huerfano de la fraccion 7318)')
parr('El numero al final se reinicia para cada fraccion (no es un contador global).')
info('Recomendacion practica',
'Si vas a usar este reporte para entregar a un cliente, lo ideal es que el catalogo de '
'la seccion 7 este lo mas completo posible. Asi la columna NUMERO DE PARTE se llena con '
'los numeros oficiales del cliente (BASE) y no con los inventados (AUTO).')
# ====== 9 ======
h1('9. ¿Algo no funciona? Lo mas comun')
h3('Genere un reporte y salio vacio (cero filas)')
bullet('Verifica que el rango de fechas cubra periodos donde hayas cargado datos en la '
'pestana DataStage. Si cargaste datos de 2024 y pones fechas de 2025, sale vacio.')
bullet('Ve a la pestana DataStage → Estadisticas y confirma que las tablas tienen filas.')
h3('Las Partidas salieron todas con AUTO_SINDESC')
parr('Significa que la columna de descripcion en los archivos del SAAI viene vacia. Revisa '
'el archivo original con la pestana DataStage → Ver datos del Registro551 — si la '
'columna DescripcionMercancia esta vacia, no hay forma de comparar.')
h3('Las Partidas salieron todas con AUTO aunque tengo catalogo cargado')
bullet('Verifica que cargaste el catalogo: en la seccion 7, da clic en "Ver base actual".')
bullet('Verifica que las fracciones de tu catalogo empiezan con los mismos 4 digitos que las '
'de tus pedimentos.')
bullet('Verifica que la unidad de medida en tu catalogo es SIGLA (PZA, KGS), no un numero.')
bullet('Baja el umbral a 0.65 y vuelve a generar — quizas la similitud es menor a la esperada.')
h3('El Tipo de Cambio sale TODO en rojo')
parr('Es probable que la fecha en la base se este interpretando mal. Avisa a TI.')
h3('El boton "Generar" no responde')
parr('Verifica que arriba diga "Postgres conectado". Si no, hay un problema con la base de '
'datos interna de la herramienta. Avisa a TI.')
h3('El Excel exportado se ve raro al abrirlo')
parr('Asegurate de abrirlo con Excel y no con otro programa. Algunos visores muestran mal '
'los archivos generados con formato. Si Excel tampoco lo abre bien, copia el archivo '
'a tu maquina local y abrelo desde ahi.')
# ====== 10 ======
h1('10. Glosario rapido')
table_simple(
['Termino', 'Significado'],
[
['CAT Pedimentos', 'Catalogo de todos los pedimentos de un periodo, listo para SCAII.'],
['Rectificacion', 'Un pedimento que corrige a otro anterior.'],
['R1', 'Clave que indica que un pedimento es una rectificacion.'],
['Historial', 'Cadena completa de rectificaciones de un pedimento (original → rect 1 → rect 2 → ...).'],
['NUMPARTE', 'Numero de parte oficial del cliente.'],
['Catalogo', 'Tabla con los numeros de parte del cliente y sus descripciones. Se usa para asignar NUMPARTE a las partidas.'],
['Upsert', 'Cuando subes el catalogo: agrega los nuevos y actualiza los existentes, sin borrar lo demas.'],
['Umbral', 'Nivel de exigencia para comparar descripciones. De 0.50 (permisivo) a 1.00 (estricto).'],
['BASE', 'Marca que el numero de parte vino del catalogo del cliente.'],
['AUTO', 'Marca que el numero de parte fue inventado por la herramienta.'],
['MP<F4>-R<NNN>', 'Formato del numero de parte inventado: MP + primeros 4 de la fraccion + R + secuencial.'],
['TipoOperacion', 'Codigo que indica si el pedimento es Importacion (1) o Exportacion (2).'],
],
col_widths=[3.5, 12])
import os
base = os.path.dirname(os.path.abspath(__file__))
out = os.path.join(base, 'Manual_EstructurasSCAII.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_EstructurasSCAII_{ts}.docx')
doc.save(out)
print(f'Manual generado: {out}')