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

BIN
app/Manual_CTM.docx Normal file

Binary file not shown.

BIN
app/Manual_DataStage.docx Normal file

Binary file not shown.

Binary file not shown.

BIN
app/Manual_GENPACT_V2.docx Normal file

Binary file not shown.

Binary file not shown.

BIN
app/Manual_Valores.docx Normal file

Binary file not shown.

4701
app/app.ipynb Normal file

File diff suppressed because it is too large Load Diff

21
app/docker-compose.yml Normal file
View File

@@ -0,0 +1,21 @@
services:
postgres-datastage:
image: postgres:16
container_name: postgres-datastage
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-dbSat}
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- postgres_datastage_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-dbSat}"]
interval: 5s
timeout: 5s
retries: 10
volumes:
postgres_datastage_data:

645
app/generar_manual.py Normal file
View File

@@ -0,0 +1,645 @@
"""
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)}')

497
app/generar_manual_ctm.py Normal file
View File

@@ -0,0 +1,497 @@
"""
Genera el manual del módulo CTM (Reasignación de descargas de Cambio de Régimen).
Lenguaje accesible para usuario final + sección técnica al final.
"""
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(text):
p = doc.add_heading(text, level=1)
for r in p.runs: r.font.color.rgb = RGBColor(0x15, 0x65, 0xC0)
def h2(text):
p = doc.add_heading(text, level=2)
for r in p.runs: r.font.color.rgb = RGBColor(0x1E, 0x88, 0xE5)
def h3(text):
p = doc.add_heading(text, level=3)
for r in p.runs: r.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):
p = doc.add_paragraph(text, style='List Bullet')
p.paragraph_format.left_indent = Cm(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 caja(titulo, text, color_fill='FFF8E1', color_text=RGBColor(0xE6, 0x5C, 0x00)):
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'), color_fill)
pPr.append(shd)
r = p.add_run(titulo + ': '); r.bold = True; r.font.color.rgb = color_text
p.add_run(text)
def nota(titulo, text): caja(titulo, text, 'FFF8E1', RGBColor(0xE6, 0x5C, 0x00))
def tip(titulo, text): caja(titulo, text, 'E8F5E9', RGBColor(0x2E, 0x7D, 0x32))
def alerta(titulo, text): caja(titulo, text, 'FFEBEE', RGBColor(0xC6, 0x28, 0x28))
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\nMódulo CTM')
r.bold = True; r.font.size = Pt(38); r.font.color.rgb = RGBColor(0x15, 0x65, 0xC0)
sub = doc.add_paragraph(); sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = sub.add_run('Reasignación de Descargas de\nCambio de Régimen')
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 Utilerías 2.0 SCAII\nManual de Uso')
r.font.size = Pt(14)
r = p.add_run(f'\n\nVersión {datetime.date.today().isoformat()}')
r.font.size = Pt(11); r.italic = True
doc.add_page_break()
# ====== CONTENIDO ======
h1('Contenido')
for it in [
'1. ¿Qué es el módulo CTM?',
'2. ¿Por qué se necesita esta herramienta?',
'3. Antes de empezar: lo que la herramienta requiere',
'4. Abrir la pestaña CTM',
'5. Componentes de la pantalla',
'6. Paso a paso: cómo correr un análisis',
'7. Interpretar los resultados',
' • Tabla Resumen',
' • Tabla Plan Detalle',
' • Estatus de cada fila',
'8. Exportar a Excel',
'9. Excel del cliente: formato esperado',
'10. Casos típicos',
'11. Limitaciones actuales (Paso A vs Paso B)',
'12. Lo que viene en el Paso B (ejecución real)',
'13. Preguntas frecuentes',
'14. Glosario CTM',
'15. Apéndice técnico: cómo funciona el matching por dentro',
]:
doc.add_paragraph(it)
doc.add_page_break()
# ====== 1 ======
h1('1. ¿Qué es el módulo CTM?')
parr('CTM es un tipo especial de factura en SCAII (TIPOFACTURA = \'CTM\') que se usa para registrar '
'cambios de régimen aduanal: mover mercancía temporal a definitiva, transferirla a otra IMMEX, '
'regresarla al extranjero, o canalizarla a operaciones especiales.')
parr('A diferencia de una factura de exportación normal, donde el inventario se descarga de los lotes '
'IMPO directamente, una factura CTM normalmente debería descargar inventario que ya fue '
'"liberado" en un trámite previo de Cambio de Régimen (módulo CR de SCAII).')
parr('Para que SCAII pueda hacer ese vínculo correctamente, debe existir un registro en SDescargaT '
'que apunte a la factura CTM en lugar de a la factura del Cambio de Régimen. En la práctica, '
'muchas facturas CTM quedan sin descarga propia: el inventario "vive" en las descargas del CR '
'pero no se reasigna a la CTM. Eso es justamente lo que esta herramienta resuelve.')
# ====== 2 ======
h1('2. ¿Por qué se necesita esta herramienta?')
parr('Sin la herramienta, el área de reconstrucciones debe revisar manualmente:')
bullet('Cada partida de cada factura CTM')
bullet('Explotar el BOM si la partida es un Producto Terminado (PT) o Subensamble (SE)')
bullet('Buscar en las miles de descargas registradas en facturas de Cambio de Régimen cuáles componentes coinciden')
bullet('Validar sustitutos comunes que sí pueden cubrir el faltante')
bullet('Respetar fechas (PEPS) para no asignar lotes "del futuro" a operaciones "del pasado"')
bullet('Distribuir saldos cuando una misma factura CTM toma inventario de varios pedimentos F4')
bullet('No generar sobre-descargas ni duplicados')
parr('La herramienta automatiza todo eso. Te entrega un plan de reasignación listo para revisar, '
'y en una segunda fase, lo aplica a la base de datos con todas las validaciones.')
# ====== 3 ======
h1('3. Antes de empezar: lo que la herramienta requiere')
h3('En la base de datos')
bullet('Las facturas CTM deben estar registradas en SCAII con TIPOFACTURA = \'CTM\'')
bullet('Las descargas que se quieren reasignar deben existir en facturas marcadas como ESCAMBIOREGIMEN = \'S\'')
bullet('El BOM (SMatBOM) debe estar capturado para los productos PT o SE que aparezcan en las CTM')
bullet('Si se usarán sustitutos, el catálogo SPartesSustitutos debe tenerlos definidos')
h3('Opcional pero recomendado: Excel del cliente')
parr('El cliente normalmente entrega un Excel con el mapeo de qué factura CTM corresponde a qué '
'pedimento F4 de cambio de régimen. Cargar este Excel permite que la herramienta priorice '
'descargas del pedimento "correcto" antes de buscar en el pool general.')
caja('Sin el Excel también funciona',
'la herramienta usa solo la regla PEPS (más antigua primero) para escoger qué descarga '
'tomar. El Excel solo agrega prioridad a las que coinciden con el mapeo del cliente.',
'E3F2FD', RGBColor(0x15, 0x65, 0xC0))
# ====== 4 ======
h1('4. Abrir la pestaña CTM')
parr('Inicia la aplicación como siempre (doble clic en run_dev.bat o el ejecutable). Cuando se abre '
'el navegador, verás las pestañas en la parte superior:')
bullet('Conexión')
bullet('Descargas')
bullet('Análisis Saldos')
bullet('Sustitutos NLP')
bullet('Descarga % KGS')
bullet('CTM ← esta es la nueva')
parr('Verifica primero en la pestaña "Conexión" que estés trabajando sobre la base de datos correcta. '
'Después cambia a la pestaña "CTM".')
# ====== 5 ======
h1('5. Componentes de la pantalla')
parr('La pestaña CTM tiene tres bloques de arriba abajo:')
h3('Bloque 1: Excel del cliente')
parr('Un botón "Excel cliente" que abre un selector de archivos. Sirve para subir el archivo '
'proporcionado por el cliente con el mapeo Factura CTM ↔ Pedimento.')
parr('Al lado, una casilla "Usar mapping para priorizar". Si la desmarcas, la herramienta ignora el '
'Excel aunque lo hayas subido.')
h3('Bloque 2: Botones de acción')
bullet('"Analizar CTM (sin escribir)" — corre el matching y genera un plan de reasignación. NO modifica la base.')
bullet('"Exportar Excel" — toma el último análisis y lo guarda como archivo Excel con tres hojas.')
h3('Bloque 3: Resultado')
parr('Una barra de progreso indica el avance. Debajo, dos áreas:')
bullet('Área de log: mensajes de qué está haciendo')
bullet('Área de tablas: resumen y plan detalle del análisis')
doc.add_page_break()
# ====== 6 ======
h1('6. Paso a paso: cómo correr un análisis')
numbered('Abre la pestaña "CTM"')
numbered('(Opcional) Click en el botón "Excel cliente" y selecciona el archivo .xlsx que te dio el cliente')
numbered('Si subiste el Excel, deja marcada la casilla "Usar mapping para priorizar". Si no quieres usarlo, desmárcala.')
numbered('Click en "Analizar CTM (sin escribir)"')
numbered('Espera mientras la barra de progreso avanza. El análisis típico tarda entre 30 segundos y 2 minutos según el volumen.')
numbered('Cuando termina, revisa el log y las dos tablas que aparecen abajo')
numbered('Si quieres llevarte el resultado a Excel, click en "Exportar Excel"')
tip('Buena práctica',
'Antes de ejecutar el Paso B (que sí escribe en la base), corre el análisis varias veces '
'con y sin Excel cargado para entender la diferencia. Si el cliente entrega el mapeo, '
'cargarlo te dará asignaciones que respetan el origen físico de la mercancía.')
# ====== 7 ======
h1('7. Interpretar los resultados')
h2('Tabla "Resumen"')
parr('Una fila por cada combinación (Factura CTM, Línea, Componente). Te dice qué tan bien quedó '
'cubierto cada requerimiento del BOM.')
table_simple(
['Columna', 'Qué significa'],
[
['FACTURA_CTM', 'Número de factura CTM destino'],
['LINEA', 'Número de línea/partida dentro de la factura CTM'],
['COMPONENTE', 'Componente MP que se necesita (puede ser el mismo NUMPARTE o uno del BOM si era PT/SE)'],
['UNIMED', 'Unidad de medida del requerimiento'],
['CANT_REQUERIDA', 'Cuánto se necesitaba descargar para cubrir esa línea'],
['cubierto', 'Cuánto se logró cubrir con descargas existentes del CR'],
['faltante', 'Cuánto quedó sin cubrir (no hubo suficiente saldo en el pool de CR)'],
['filas_cr_usadas','Número de descargas CR distintas que se tomaron'],
['pct_cobertura', 'Porcentaje cubierto = cubierto / requerido × 100'],
],
col_widths_cm=[3.5, 12]
)
h2('Tabla "Plan Detalle"')
parr('Es la lista granular: una fila por cada descarga CR que se va a tomar (o por cada faltante). '
'En pantalla solo se muestran las primeras 50 filas; el detalle completo está en el Excel.')
table_simple(
['Columna', 'Qué significa'],
[
['FACTURA_CTM', 'CTM destino'],
['LINEA', 'Línea de la factura CTM'],
['PT_O_MP', 'NUMPARTE original de la partida CTM (puede ser PT o MP)'],
['TIPOMAT', 'PT / SE / MP'],
['COMPONENTE', 'Componente que se busca cubrir'],
['CANT_REQUERIDA', 'Cantidad total requerida del componente'],
['STATUS', 'ASIGNADO / FALTANTE / SIN_REQUERIMIENTO'],
['CONSECUTIVO_CR', 'Si ASIGNADO: el ID de la descarga CR que se toma'],
['FACTURA_CR', 'Factura del Cambio de Régimen origen'],
['NUMPARTE_CR', 'NUMPARTE registrado en la descarga CR'],
['PARTEORIGINAL_CR', 'PARTE ORIGINAL del BOM cuando la descarga CR es de un sustituto'],
['CANT_DISPONIBLE_CR', 'Cuánto tenía esa descarga CR disponible'],
['CANT_A_TOMAR', 'Cuánto se va a tomar de esa descarga CR'],
['FECHA_DESC_CR', 'Fecha (ISO) en que se hizo la descarga CR original'],
['PEDIMENTOIMPO_CR', 'Pedimento de importación de la descarga CR'],
['PRIORIDAD_MAPPING', 'True si esa descarga venía del pedimento mapeado en el Excel cliente'],
],
col_widths_cm=[4, 11.5]
)
h2('Estatus de cada fila')
table_simple(
['Estatus', 'Significa', 'Acción típica'],
[
['ASIGNADO', 'Se encontró una descarga CR que cubre (total o parcial) el requerimiento',
'En Paso B, se reasignará esa descarga a la CTM'],
['FALTANTE', 'No hubo suficiente saldo en el pool CR para cubrir el requerimiento',
'Revisar manualmente: tal vez falta un sustituto en SPartesSustitutos o la descarga CR no existe aún'],
['SIN_REQUERIMIENTO', 'La partida CTM no genera consumo (sin BOM o cantidad = 0)',
'Generalmente se ignora; verificar que el BOM esté capturado si era PT/SE'],
],
col_widths_cm=[3.5, 7, 5]
)
doc.add_page_break()
# ====== 8 ======
h1('8. Exportar a Excel')
parr('El botón verde "Exportar Excel" genera un archivo llamado:')
code_block('analisis_ctm_AAAAMMDD_HHMMSS.xlsx')
parr('Con tres hojas:')
table_simple(
['Hoja', 'Contenido'],
[
['Resumen', 'Una fila por (Factura CTM, Línea, Componente) con cubierto/faltante/% cobertura'],
['Plan_Detalle', 'Cada fila individual del plan, incluyendo cada descarga CR que se va a tomar'],
['Faltantes', 'Filtrado: solo las filas con STATUS = FALTANTE (las que no se pudieron cubrir)'],
],
col_widths_cm=[4, 12]
)
# ====== 9 ======
h1('9. Excel del cliente: formato esperado')
parr('El Excel del cliente debe tener al menos estas dos columnas en la primera hoja:')
table_simple(
['Columna', 'Ejemplo'],
[
['Facturas CTM', 'AAU112023RFR0481, NIS112023RFR0035'],
['PEDIMENTO COMPLETO', '75-3076-4021492'],
],
col_widths_cm=[5, 11]
)
parr('Notas:')
bullet('La celda "Facturas CTM" puede traer **varias facturas separadas por coma**. La herramienta las separa automáticamente y crea una fila por cada combinación CTM-Pedimento.')
bullet('Los nombres de las columnas pueden tener mayúsculas o minúsculas. La herramienta busca cualquier columna que contenga "FACTURA" y "CTM" en su título, y cualquiera que contenga "PEDIMENTO" y "COMPLETO".')
bullet('Otras columnas (PATENTE, ADUANA, PEDIMENTO, Operación, Clave de pedimento) se ignoran, pero pueden estar presentes sin causar problemas.')
nota('Una misma CTM puede tener varios pedimentos',
'Si en el Excel hay varias filas con la misma CTM pero distinto pedimento, la herramienta '
'reconoce a TODOS esos pedimentos como prioritarios para esa CTM.')
# ====== 10 ======
h1('10. Casos típicos')
h3('Caso 1: CTM con cobertura 100% por mapeo del cliente')
parr('Cargas el Excel cliente, ejecutas el análisis y todas las filas quedan ASIGNADAS con '
'"PRIORIDAD_MAPPING = True". Esto es lo ideal: cada descarga viene exactamente del pedimento '
'que el cliente indicó.')
h3('Caso 2: CTM cubierta pero por pool genérico (sin mapping)')
parr('No cargas el Excel, o lo cargas pero la CTM no estaba en él. La herramienta igual encuentra '
'descargas CR que coinciden por NUMPARTE y cantidad, pero ordenadas solo por PEPS. La columna '
'"PRIORIDAD_MAPPING" sale en False.')
tip('Recomendación',
'Acepta este resultado si conoces tu inventario y sabes que cualquier pedimento del mismo MP '
'es intercambiable. Si necesitas trazabilidad estricta por pedimento, exige el Excel del cliente.')
h3('Caso 3: CTM con FALTANTES')
parr('Algunas líneas no se pudieron cubrir. Posibles causas:')
bullet('El BOM del PT no está capturado (revisa con un SELECT en SMatBOM)')
bullet('La descarga CR que debería usarse aún no se ha generado en el sistema')
bullet('La cantidad consumida en el CR es menor a la que pide la CTM (over-allocation)')
bullet('Falta un sustituto en SPartesSustitutos')
h3('Caso 4: SIN_REQUERIMIENTO')
parr('Una línea aparece con STATUS = SIN_REQUERIMIENTO. Esto puede deberse a:')
bullet('La partida es PT/SE pero no tiene BOM definido — al no haber componentes, no genera requerimiento')
bullet('La cantidad declarada en la partida es 0 (raro pero posible)')
# ====== 11 ======
h1('11. Limitaciones actuales (Paso A vs Paso B)')
parr('La versión actual de la pestaña CTM implementa únicamente el **Paso A: análisis**. Es decir, '
'genera un plan de reasignación pero NO modifica la base de datos.')
alerta('Importante',
'Al terminar el análisis, las descargas CR siguen exactamente como estaban. Las facturas CTM '
'siguen sin descarga propia. El Excel exportado es solo para revisión.')
parr('Esto es intencional: queremos que valides el matching contra varias facturas reales antes de '
'activar la escritura. El Paso B se implementará cuando confirmes que los resultados del Paso A '
'están correctos para tu caso.')
# ====== 12 ======
h1('12. Lo que viene en el Paso B (ejecución real)')
parr('Cuando aprobemos el comportamiento del Paso A, se agregará a la misma pestaña:')
bullet('Casilla "DRY_RUN" (modo simulación, igual que en las otras pestañas)')
bullet('Selector de "MODO" (NATURAL vs DIRIGIDA)')
bullet('Botón "Ejecutar reasignación"')
parr('Al ejecutar, por cada fila ASIGNADA del plan se hará:')
bullet('UPDATE SDescargaT SET FACTEXPO = factura_CTM WHERE CONSECUTIVO = X')
bullet('Si la descarga CR se toma parcial: dividir en dos filas (una queda en la CR con el saldo sobrante, otra apunta a la CTM con lo tomado)')
bullet('UPDATE SFacExp SET ESTATUS = \'AC\' WHERE FACTURAEXPO = factura_CTM')
parr('Todo dentro de transacciones atómicas por factura: si una falla, no rompe a las demás.')
# ====== 13 ======
h1('13. Preguntas frecuentes')
h3('¿Puedo correr el análisis varias veces seguidas?')
parr('Sí. Cada corrida es independiente y no modifica nada. Puedes experimentar con y sin Excel, '
'comparar resultados, y exportar varias veces a Excel sin riesgo.')
h3('¿Qué pasa si la factura CTM ya tiene algunas descargas asignadas?')
parr('La versión actual no descuenta lo que ya está asignado a la CTM. Si una CTM ya tiene descargas '
'directas, podría sobre-asignarse. Esto se cubrirá en el Paso B con una validación previa que '
'reste lo ya descargado.')
h3('¿La herramienta usa los mismos sustitutos del catálogo SPartesSustitutos?')
parr('Sí. Lee SPartesSustitutos y considera como candidatos al COMPONENTE original y a todos sus '
'sustitutos registrados.')
h3('¿Puedo limitar el análisis a una sola factura CTM?')
parr('Actualmente no — el análisis siempre cubre todas las facturas TIPOFACTURA=\'CTM\'. En el Paso B '
'se agregará un filtro por factura específica si lo necesitas.')
h3('¿Por qué algunas descargas CR no se consideran aunque coinciden por NUMPARTE?')
parr('Si la descarga CR ya quedó "agotada" en una iteración anterior del análisis (otra factura CTM '
'la tomó primero por PEPS), no estará disponible. El orden de procesamiento de facturas CTM es '
'por FECHA del CR ascendente, no por orden de la CTM.')
# ====== 14 ======
h1('14. Glosario CTM')
table_simple(
['Término', 'Significado'],
[
['CTM', 'Tipo de factura en SCAII para operaciones de cambio de régimen / transferencias / regularizaciones'],
['Cambio de Régimen', 'Módulo de SCAII donde se registran descargas iniciales que después deben reasignarse a CTM'],
['Pedimento F4', 'Clave de pedimento aduanero asociada al cambio de régimen'],
['Pool CR', 'Conjunto de descargas registradas en SDescargaT cuyo factor SFacExp.ESCAMBIOREGIMEN = \'S\''],
['Reasignación', 'Acción de cambiar el FACTEXPO de una descarga de la factura CR original a la factura CTM destino'],
['Apartado CTM', 'Campo APARTADOCTM en SDescargaT que identifica el origen (A, B, C1, C2, C3) según el tipo de operación'],
['Explosión BOM', 'Convertir una partida de PT o SE en sus componentes MP mediante el catálogo SMatBOM'],
['Mapping cliente', 'Excel que relaciona Factura CTM ↔ Pedimento F4 para priorizar la reasignación'],
['PEPS', 'Primero en entrar, primero en salir. La descarga CR más antigua se asigna primero'],
['ASIGNADO', 'Estatus del plan: la descarga CR se va a reasignar a la CTM'],
['FALTANTE', 'Estatus del plan: no se encontró suficiente saldo en CR para cubrir el requerimiento'],
['SIN_REQUERIMIENTO', 'Estatus del plan: la partida CTM no genera consumo (sin BOM o cantidad cero)'],
],
col_widths_cm=[4, 12]
)
# ====== 15 ======
h1('15. Apéndice técnico: cómo funciona el matching por dentro')
parr('Esta sección es para usuarios avanzados o el área de desarrollo. Describe a alto nivel el '
'algoritmo que ejecuta el botón "Analizar CTM".')
h3('15.1 Carga de datos')
parr('Se ejecutan cuatro consultas iniciales:')
bullet('Facturas CTM: SELECT FACTURAEXPO ... FROM SFacExp WHERE TIPOFACTURA=\'CTM\'')
bullet('Partidas CTM con explosión BOM (PT/SE → componentes vía SMatBOM)')
bullet('Pool CR: SDescargaT JOIN SFacExp WHERE ESCAMBIOREGIMEN=\'S\' ordenado por FECHADESC')
bullet('Sustitutos: SPartesSustitutos → diccionario {NUMPARTE: [sustitutos]}')
h3('15.2 Si hay Excel del cliente')
parr('Se parsea para construir un diccionario {FACTURA_CTM: set(pedimentos)}. Esto se usa después '
'para priorizar.')
h3('15.3 Matching')
parr('Por cada partida CTM (factura, línea, componente):')
numbered('Se calcula la cantidad requerida = CANTEXPO × BOM.CANTIDAD (si era PT/SE) o CANTEXPO (si era MP directo)')
numbered('Se construye la lista de candidatos: el componente original más todos sus sustitutos')
numbered('Se filtra el pool CR por NUMPARTE o PARTEORIGINAL coincidente con algún candidato')
numbered('Si hay mapping, se marca con PRIORIDAD = True las descargas cuyo PEDIMENTOIMPO coincida con los pedimentos mapeados para esa CTM')
numbered('Se ordena: PRIORIDAD descendente → FECHADESC ascendente (PEPS) → CONSECUTIVO ascendente')
numbered('Se va tomando cantidad de cada descarga CR (modificando el saldo virtual en memoria) hasta cubrir el requerimiento o agotar candidatos')
numbered('Cada fila tomada genera un registro ASIGNADO en el plan; si queda faltante, se genera un registro FALTANTE')
h3('15.4 Salida')
parr('Dos DataFrames en memoria:')
bullet('plan: una fila por cada descarga CR tomada o por cada faltante')
bullet('resumen: agregado por (FACTURA_CTM, LINEA, COMPONENTE) con cantidades cubiertas, faltantes y % de cobertura')
parr('Ninguna escritura en la base de datos en esta fase.')
# ====== GUARDAR ======
import os
out_path = 'Manual_CTM.docx'
try:
doc.save(out_path)
except PermissionError:
out_path = f'Manual_CTM_{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)}')

View File

@@ -0,0 +1,303 @@
"""Manual de usuario de la pestana DataStage. Lenguaje accesible, sin SQL ni
codigo. Pensado para que cualquier usuario sin conocimientos tecnicos pueda
operar la pestana."""
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 DataStage')
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('Subir y consultar archivos\ndel SAAI en la herramienta')
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 tus archivos',
'5. Como ver lo que ya esta cargado',
'6. Como limpiar todo y empezar de cero',
'7. ¿Algo no funciona? Lo mas comun',
'8. 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 DataStage te permite cargar a la herramienta los archivos del SAAI '
'(los .asc que descargas cada periodo). Una vez cargados, otras pestanas de la '
'herramienta los usan para generar reportes, validaciones y estructuras de Excel '
'que entregas al cliente o subes a SCAII.')
info('En palabras simples',
'Aqui le das de comer a la herramienta. Si los archivos del SAAI no estan cargados, '
'las demas pestanas (Estructuras SCAII, Saldos Vencidos, etc.) no tienen de donde '
'sacar la informacion.')
# ====== 2 ======
h1('2. Antes de empezar')
parr('Verifica estas tres cosas en la parte superior de la pestana:')
numbered('Que diga "Postgres conectado" en verde. Si dice "Postgres no disponible" en rojo, '
'avisale al area de TI o quien instalo la herramienta.')
numbered('Que tengas a la mano la ruta de la carpeta donde estan tus archivos .asc. '
'Por ejemplo: C:\\DATASTAGE_HONDA\\2024.')
numbered('Que los archivos sigan la nomenclatura habitual: terminan en _501.asc, _551.asc, '
'_701.asc, _Inci.asc, etc. La herramienta los reconoce por ese final.')
tip('Sugerencia para la primera vez',
'Empieza cargando una sola carpeta de un mes para ver que todo funciona. Una vez que '
'verifiques el resultado, puedes subir varios meses o anos juntos.')
# ====== 3 ======
h1('3. Como se ve la pantalla')
parr('La pestana esta dividida en tres bloques, uno debajo del otro:')
table_simple(
['Bloque', '¿Para que sirve?'],
[
['Subir DataStage', 'Apuntar a tu carpeta de archivos .asc y cargarlos a la herramienta.'],
['Estadisticas', 'Ver cuanto se cargo (totales por tipo de registro) y revisar muestras.'],
['Zona peligrosa', 'Borrar todo lo que cargaste, si necesitas empezar de cero.'],
],
col_widths=[4.5, 11])
parr('Cada bloque tiene sus propios botones y su propia caja de resultados (la zona gris '
'donde aparecen los mensajes despues de cada accion).')
# ====== 4 ======
h1('4. Como subir tus archivos')
h2('Paso 1 — Indica la carpeta')
parr('En el campo "Carpeta" escribe o pega la ruta donde estan tus archivos .asc. '
'Acepta dos formas de organizar la carpeta:')
bullet('Carpeta con subcarpetas por ano (ej. C:\\DATASTAGE_HONDA\\2024, '
'C:\\DATASTAGE_HONDA\\2025, etc.). La herramienta entra a cada subcarpeta de ano y '
'toma los .asc que esten directamente ahi.')
bullet('Carpeta plana con los .asc directos (sin subcarpetas).')
h2('Paso 2 — Listar archivos (opcional pero recomendado)')
parr('Da clic en el boton "Listar archivos". La herramienta te muestra una tabla con cada '
'archivo detectado, a que tipo de registro pertenece y cuanto pesa.')
tip('Para que sirve este paso',
'Es como una "vista previa". Te permite verificar que la herramienta detecto todos tus '
'archivos antes de cargar nada. Si ves algun archivo con "(no detectado)" en la columna '
'"tabla destino", revisa el nombre — es probable que no termine en el formato esperado.')
h2('Paso 3 — Verifica que las tablas existan')
parr('Da clic en "Ver tablas Registro en Postgres". Debes ver una lista con muchos nombres '
'(Registro501, Registro502, Registro551, etc.). Si la lista esta vacia, falta que TI '
'prepare la base de datos. Avisale antes de continuar.')
h2('Paso 4 — Cargar todos a Postgres')
parr('Da clic en "Cargar todos a Postgres". La herramienta procesa archivo por archivo. '
'Al terminar te muestra:')
bullet('Una tabla de resultado con una fila por archivo: si cargo bien (OK), si se omitio '
'(SKIP), o si hubo un problema (ERROR).')
bullet('Un resumen total: cuantos archivos cargaron, cuantos se omitieron, cuantos fallaron, '
'y cuantas filas en total entraron a la base.')
alerta('IMPORTANTE',
'Si vuelves a subir los mismos archivos que ya cargaste antes, los datos se duplican. '
'Si quieres recargar un periodo, primero usa la Zona peligrosa para limpiar (ver punto 6).')
nota('¿Que significa SKIP?',
'Un archivo se omite cuando: (a) su nombre no es reconocido, o (b) no existe en la base '
'de datos una tabla para guardarlo. Avisa a TI si te pasa con archivos que sabes que '
'deberian entrar.')
# ====== 5 ======
h1('5. Como ver lo que ya esta cargado')
h2('Ver estadisticas')
parr('Da clic en "Ver estadisticas". La herramienta cuenta cuantas filas tiene cada '
'Registro y te muestra:')
bullet('Un resumen general (cuantas tablas tienes, cuantas con datos, total de filas).')
bullet('Una tabla con cada Registro y su cantidad de filas.')
h2('Explorar el contenido de una tabla')
parr('Despues de ver estadisticas, en el desplegable "Tabla" puedes elegir cualquier '
'Registro que tenga datos. Ajusta cuantas filas quieres ver (por default 100) y da clic '
'en "Ver datos". La herramienta te muestra las primeras filas en pantalla.')
h2('Exportar a Excel')
parr('Despues de cargar la muestra con "Ver datos", da clic en "Exportar a Excel". Se genera '
'un archivo .xlsx en la carpeta de la herramienta con las filas que estabas viendo.')
tip('¿Para que sirve esto?',
'Para validar rapidamente que la informacion se ve correcta antes de generar reportes '
'completos en otras pestanas. Si ves filas con valores extranos, conviene revisar el '
'.asc original o avisarle a TI.')
# ====== 6 ======
h1('6. Como limpiar todo y empezar de cero')
alerta('USA ESTA SECCION CON CUIDADO',
'El boton de la Zona peligrosa elimina TODOS los datos de TODOS los Registros. '
'No hay forma de recuperarlos. Solo usalo cuando estes seguro de que necesitas '
'reiniciar.')
parr('Cuando usar la Zona peligrosa:')
bullet('Cuando vas a recargar el mismo periodo (para no duplicar datos).')
bullet('Cuando cargaste algo con errores y prefieres limpiar y volver a cargar.')
bullet('Cuando cambias de cliente o periodo y quieres empezar limpio.')
parr('Como hacerlo:')
numbered('Marca la casilla "Confirmo limpiar TODAS las tablas Registro*".')
numbered('Da clic en el boton rojo "Limpiar todas las tablas".')
numbered('La herramienta cuenta cuantas filas habia en cada tabla y las elimina. Al final '
'te muestra cuantas filas se eliminaron por cada Registro.')
nota('Despues de limpiar',
'Tendras que volver a subir los archivos .asc que necesites. La carpeta de archivos no '
'se borra (la herramienta solo borra los datos de la base de datos, no tus archivos en '
'el disco).')
# ====== 7 ======
h1('7. ¿Algo no funciona? Lo mas comun')
h3('La pestana dice "Postgres no disponible"')
parr('La herramienta no puede conectarse a su base de datos interna. Causas tipicas:')
bullet('El servicio de Docker no esta corriendo (avisar a TI).')
bullet('La herramienta se instalo sin terminar la configuracion.')
parr('En cualquier caso, contacta a quien te instalo la herramienta.')
h3('Dice "No hay tablas Registro* en Postgres"')
parr('Significa que la base de datos esta vacia, sin estructura. Es algo que TI tiene que '
'preparar una sola vez al inicio. Avisales y comparte este texto para que sepan a que '
'te refieres.')
h3('Muchos archivos quedaron como SKIP')
parr('Revisa que los nombres terminen en el formato esperado (algo_NNN.asc o algo_palabra.asc). '
'Si los nombres son correctos pero igual quedan como SKIP, es probable que TI no haya '
'creado todas las tablas; avisales.')
h3('Algunos archivos quedaron como ERROR')
parr('Lee el mensaje de error en la columna "mensaje". Las causas mas frecuentes son:')
bullet('El archivo tiene un caracter raro (NUL) que la base de datos no acepta. Esto la '
'herramienta lo intenta limpiar automaticamente; si aun asi falla, avisa a TI con el '
'archivo problematico.')
bullet('El archivo tiene mas columnas de las que la tabla espera, o menos.')
bullet('La conexion a la base se cayo a mitad del proceso.')
parr('En general, la herramienta te dice cuales archivos fallaron pero los demas SI se '
'cargaron correctamente — no tienes que volver a subir todo, solo los que fallaron.')
h3('Subi los mismos archivos dos veces por error')
parr('Los datos quedaron duplicados. Lo mas seguro es:')
numbered('Usar la Zona peligrosa para limpiar TODAS las tablas.')
numbered('Volver a cargar los archivos del periodo que necesitas, esta vez una sola vez.')
# ====== 8 ======
h1('8. Glosario rapido')
table_simple(
['Termino', 'Significado'],
[
['Archivo .asc', 'Archivo de texto del SAAI con la informacion de pedimentos, partidas, etc. Es lo que cargas en esta pestana.'],
['Registro NNN', 'Tabla interna de la herramienta donde se guarda un tipo especifico de informacion (501 = pedimentos, 551 = partidas, etc.).'],
['Carga', 'Accion de tomar tus archivos .asc y meterlos en la herramienta.'],
['Limpiar', 'Borrar todos los datos cargados. Util para reiniciar desde cero.'],
['OK', 'Estatus de un archivo que se cargo correctamente.'],
['SKIP', 'Estatus cuando un archivo se omitio (nombre no reconocido o tabla inexistente).'],
['ERROR', 'Estatus cuando un archivo no se pudo cargar por algun problema.'],
['Postgres', 'La base de datos interna donde la herramienta guarda lo que cargas.'],
],
col_widths=[3.5, 12])
import os
base = os.path.dirname(os.path.abspath(__file__))
out = os.path.join(base, 'Manual_DataStage.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_DataStage_{ts}.docx')
doc.save(out)
print(f'Manual generado: {out}')

View File

@@ -0,0 +1,437 @@
"""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}')

View File

@@ -0,0 +1,503 @@
"""
Genera el manual del módulo Saldos Vencidos (Utilería Forma 5).
Cubre los dos modos de uso: por Excel y automático por rango de fechas.
"""
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(text):
p = doc.add_heading(text, level=1)
for r in p.runs: r.font.color.rgb = RGBColor(0x15, 0x65, 0xC0)
def h2(text):
p = doc.add_heading(text, level=2)
for r in p.runs: r.font.color.rgb = RGBColor(0x1E, 0x88, 0xE5)
def h3(text):
p = doc.add_heading(text, level=3)
for r in p.runs: r.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):
p = doc.add_paragraph(text, style='List Bullet')
p.paragraph_format.left_indent = Cm(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 caja(titulo, text, color_fill='FFF8E1', color_text=RGBColor(0xE6, 0x5C, 0x00)):
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'), color_fill)
pPr.append(shd)
r = p.add_run(titulo + ': '); r.bold = True; r.font.color.rgb = color_text
p.add_run(text)
def nota(titulo, text): caja(titulo, text, 'FFF8E1', RGBColor(0xE6, 0x5C, 0x00))
def tip(titulo, text): caja(titulo, text, 'E8F5E9', RGBColor(0x2E, 0x7D, 0x32))
def alerta(titulo, text): caja(titulo, text, 'FFEBEE', RGBColor(0xC6, 0x28, 0x28))
def info(titulo, text): caja(titulo, text, 'E3F2FD', RGBColor(0x0D, 0x47, 0xA1))
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\nMódulo Saldos Vencidos')
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('Utilería Forma 5\nProrrateo masivo de saldos IMPO vencidos')
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 Utilerías 2.0 SCAII\nManual de Uso')
r.font.size = Pt(14)
r = p.add_run(f'\n\nVersión {datetime.date.today().isoformat()}')
r.font.size = Pt(11); r.italic = True
doc.add_page_break()
# ====== CONTENIDO ======
h1('Contenido')
for it in [
'1. ¿Qué es el módulo Saldos Vencidos?',
'2. ¿Por qué se necesita esta herramienta?',
'3. Antes de empezar',
'4. Abrir la pestaña Saldos Vencidos',
'5. Los dos modos de uso',
'6. Modo Excel — paso a paso',
' • Descargar la plantilla',
' • Subir el Excel y definir fechas',
' • Analizar (Paso A)',
' • Exportar el plan a Excel',
' • Ejecutar el prorrateo (Paso B)',
'7. Modo Automático — paso a paso',
' • Rango de fechas',
' • Buscar saldos vencidos',
' • Exportar y ejecutar',
'8. Cómo funciona el prorrateo',
'9. Validación contra el saldo real',
'10. Estatus de cada saldo en el resumen',
'11. Diferencias entre los dos modos',
'12. Casos típicos y preguntas frecuentes',
'13. Apéndice técnico',
'14. Glosario',
]:
parr(it).paragraph_format.left_indent = Cm(0.4)
doc.add_page_break()
# ====== 1 ======
h1('1. ¿Qué es el módulo Saldos Vencidos?')
parr('Saldos Vencidos es una pestaña dentro del Sistema de Utilerías 2.0 que automatiza '
'la Utilería Forma 5: tomar los saldos pendientes de una factura de importación '
'que ya vencieron y repartirlos (prorratearlos) entre las descargas de exportación '
'donde esa misma mercancía se haya consumido.')
parr('En vez de hacer el cálculo línea por línea desde la utilería tradicional —que en bases '
'grandes puede tardar muchísimo— esta pestaña carga, calcula y aplica los movimientos '
'masivamente con un proceso mucho más rápido.')
nota('En palabras simples',
'Tienes saldos de importación que ya se vencieron y necesitas "amortizarlos" en las '
'descargas de exportación donde el material se fue. Esta herramienta los encuentra, '
'los distribuye proporcionalmente y aplica los UPDATEs en SDescargaT y SSaldoTem.')
# ====== 2 ======
h1('2. ¿Por qué se necesita esta herramienta?')
parr('La utilería original recorre todas las facturas de exportación que comparten fracción '
'arancelaria con cada saldo y reparte el saldo entre las facturas encontradas. En una base '
'pesada (millones de filas) este proceso puede llevar horas o quedarse colgado.')
parr('Esta herramienta hace lo mismo pero:')
bullet('Solo carga las filas que realmente necesita (filtros por rango de fechas).')
bullet('Agrupa las descargas en lotes para reducir consultas.')
bullet('Hace todos los UPDATEs por transacción atómica por saldo (rollback automático ante error).')
bullet('Permite ver el plan antes de aplicar (DRY_RUN).')
# ====== 3 ======
h1('3. Antes de empezar')
bullet('Necesitas estar conectado a la base SCAII (revísalo en la pestaña Conexión).')
bullet('Define con claridad el rango de fechas que quieres procesar. El rango filtra '
'la columna FECHAFACTURA_ISO de la tabla SSaldoTem (la fecha de la factura IMPO).')
bullet('Si vas a usar el modo Excel: prepara el archivo con las 3 columnas obligatorias '
'(o descarga la plantilla desde la misma pestaña).')
alerta('Recomendación',
'La primera vez SIEMPRE corre el Paso B con DRY_RUN activado. Te muestra qué se '
'iba a actualizar sin tocar la base. Cuando confirmes los números, lo desactivas y '
'aplicas en firme.')
# ====== 4 ======
h1('4. Abrir la pestaña Saldos Vencidos')
parr('En la barra superior de la app verás varias pestañas (Conexión, Descargas, Análisis '
'Saldos, Sustitutos NLP, Descarga % KGS, CTM, Saldos Vencidos). Da clic en '
'"Saldos Vencidos".')
# ====== 5 ======
h1('5. Los dos modos de uso')
parr('La pestaña tiene dos secciones, una arriba de la otra. Ambas hacen exactamente lo '
'mismo internamente; lo único que cambia es de dónde sacan la lista de saldos a procesar:')
table_simple(
['Modo', '¿De dónde toma los saldos?', '¿Cuándo usarlo?'],
[
['Excel (sección superior)',
'De un archivo .xlsx que tú subes con las facturas IMPO específicas que quieres prorratear.',
'Cuando ya tienes una lista filtrada del cliente o del área legal con los saldos exactos a tocar.'],
['Automático (sección inferior)',
'El sistema busca solo en SSaldoTem todos los saldos con FECHAVENC_ISO menor a hoy y SALDO_DISPONIBLE > 0 dentro del rango que indiques.',
'Cuando quieres "barrer" todo lo vencido de un periodo sin armar listas previas.'],
],
col_widths_cm=[3.5, 6.5, 6])
# ====== 6 ======
h1('6. Modo Excel — paso a paso')
h2('6.1 Descargar la plantilla')
parr('Si es la primera vez, da clic en el botón "Descargar plantilla Excel". Se generará '
'un archivo con 3 columnas de ejemplo. Ábrelo, borra las filas de ejemplo y pega tus datos.')
h2('6.2 Estructura del Excel')
table_simple(
['Columna', 'Significado', 'Ejemplo'],
[
['FACTURAIMPO', 'Número de la factura de importación cuyo saldo está vencido.', 'F1234567'],
['CANTIDAD_SALDO', 'Cantidad pendiente a prorratear (en la UM del saldo).', '100.0'],
['FRACCION_IMPO', 'Fracción arancelaria de importación.', '85044010'],
],
col_widths_cm=[3.5, 8, 4])
nota('Sobre los nombres de columna',
'La herramienta acepta variaciones (FACTURAIMPO / FACTURA_IMPO / FACTURA, '
'FRACCION_IMPO / FRACCIONIMPO / FRACCION, CANTIDAD_SALDO / CANT_SALDO / CANTIDAD / SALDO). '
'No hace falta que sea exactamente el nombre, pero sí debe coincidir con alguno de los aceptados.')
h2('6.3 Subir el Excel y definir fechas')
numbered('Da clic en "Subir Excel" y selecciona el archivo.')
numbered('Pon una fecha de inicio y una fecha de fin. Estas fechas filtran SSaldoTem por la '
'columna FECHAFACTURA_ISO (la fecha de la factura IMPO).')
tip('Para qué sirve el rango',
'Aunque el Excel ya trae las facturas IMPO específicas, el rango limita la consulta a '
'SSaldoTem para que sea más rápida. Si no estás seguro, pon un rango amplio que cubra '
'todas las fechas de tus facturas.')
h2('6.4 Analizar (Paso A — solo lectura)')
parr('Da clic en "Analizar (sin escribir)". El sistema:')
bullet('Consulta SSaldoTem dentro del rango definido.')
bullet('Cruza con tu Excel por (FACTURAIMPO, FRACCIONIMPO).')
bullet('Para cada saldo, busca en SDescargaT las descargas que comparten (FACTIMPO, NUMPARTE, UNIMED).')
bullet('Calcula la porción que le toca a cada descarga (proporcional a su CANTDESC).')
bullet('Muestra el resumen y las primeras 50 filas del plan.')
alerta('Paso A es 100% lectura',
'No escribe nada en la base. Sirve para verificar números antes de aplicar.')
h2('6.5 Exportar el plan a Excel')
parr('Si quieres ver TODAS las filas del plan (no solo las 50 que se muestran en pantalla), '
'da clic en "Exportar Excel completo". El archivo tiene dos hojas:')
bullet('Plan_Detalle: una fila por cada descarga que recibe prorrateo (con la porción exacta de cantidad, valores y pesos).')
bullet('Resumen: una fila por saldo con su estatus (PRORRATEADO, SIN_DESCARGAS, CANTDESC_CERO).')
h2('6.6 Ejecutar el prorrateo (Paso B)')
parr('Cuando el análisis se ve bien:')
numbered('Mantén DRY_RUN activado y da clic en "Ejecutar prorrateo (Paso B)". La herramienta '
'simula los UPDATEs y te muestra los conteos exactos.')
numbered('Si los números cuadran, desactiva DRY_RUN y vuelve a dar clic.')
numbered('La herramienta corre las transacciones; al terminar verás un resumen con cuántas '
'descargas y cuántos saldos se actualizaron, y si hubo capeos (saldos donde el prorrateo '
'habría excedido el disponible real).')
alerta('Importante sobre el capeo',
'Si entre el momento en que corres el Paso A y el Paso B alguien más consume saldo '
'desde otra utilería, la herramienta vuelve a consultar el SALDO_DISPONIBLE real al '
'momento de ejecutar y aplica un factor de escala para que NUNCA se rebase el 100%. '
'Esto se registra como "CAPEO" en el log.')
# ====== 7 ======
h1('7. Modo Automático — paso a paso')
parr('La sección inferior de la pestaña hace lo mismo, pero sin Excel: el sistema busca él '
'mismo qué saldos están vencidos y los procesa.')
h2('7.1 Define el rango de fechas')
bullet('Fecha inicio y fecha fin filtran SSaldoTem por FECHAFACTURA_ISO (la fecha de la factura IMPO).')
bullet('Adicionalmente la herramienta filtra por FECHAVENC_ISO < hoy. Es decir, solo trae '
'saldos que YA están vencidos al día en que corres el análisis.')
info('Criterio exacto',
'Saldo vencido = FECHAFACTURA_ISO dentro del rango + FECHAVENC_ISO menor a la fecha de '
'hoy + (CANTEXITENCIA CANTUSADA CANTUSADADESP) mayor a cero.')
h2('7.2 Buscar saldos vencidos')
parr('Da clic en "Buscar saldos vencidos". El sistema muestra cuántos saldos encontró y '
'genera el plan de prorrateo (igual que el modo Excel, pero sin cruzar con un archivo).')
h2('7.3 Exportar y ejecutar')
parr('Los botones "Exportar Excel completo" y "Ejecutar prorrateo (Paso B)" funcionan '
'exactamente igual que en el modo Excel. El comportamiento de DRY_RUN y el capeo es idéntico.')
nota('Estado independiente',
'El plan del modo Automático se guarda por separado del plan del modo Excel. Puedes '
'usar los dos modos en la misma sesión sin que uno pise al otro.')
# ====== 8 ======
h1('8. Cómo funciona el prorrateo')
parr('Para cada saldo vencido la herramienta hace lo siguiente:')
numbered('Identifica el saldo en SSaldoTem por (FACTURAIMPO, NUMPARTE, UMEXITENCIA). '
'Lee CANTEXITENCIA, CANTUSADA, CANTUSADADESP, VALORIMPOMN/ME, PESONETO/BRUTO.')
numbered('Calcula SALDO_DISPONIBLE = CANTEXITENCIA CANTUSADA CANTUSADADESP. '
'Si está en cero, no hace nada.')
numbered('Calcula SALDO_APLICABLE = mínimo entre lo que viene en el Excel (modo Excel) '
'y el SALDO_DISPONIBLE real. En modo Automático, SALDO_APLICABLE = SALDO_DISPONIBLE.')
numbered('Busca todas las descargas en SDescargaT que cumplan: '
'FACTIMPO = FACTURAIMPO, NUMPARTE = NUMPARTE del saldo, UNIMED = UMEXITENCIA del saldo.')
numbered('Suma todos los CANTDESC de esas descargas. Cada descarga recibe una porción '
'proporcional a su tamaño: PORCION = SALDO_APLICABLE × (CANTDESC_descarga / SUMA_CANTDESC).')
numbered('Para los valores y pesos: la herramienta usa las tasas del saldo IMPO original '
'(VALORIMPOMN / CANTEXITENCIA, etc.) en lugar de las de cada descarga. Esto mantiene '
'consistencia con el costo real del lote vencido.')
h2('Ejemplo numérico')
parr('Imagina un saldo vencido de FACTURAIMPO=F1001, NUMPARTE=ABC123, UM=PZ con:')
code_block('CANTEXITENCIA = 1000\n'
'CANTUSADA + CANTUSADADESP = 700\n'
'SALDO_DISPONIBLE = 300\n'
'VALORIMPOMN = 5000 → tasa_vmn = 5000 / 1000 = 5.0 por pieza')
parr('Y tres descargas EXPO que comparten la llave:')
table_simple(
['CONSECUTIVO', 'FACTEXPO', 'CANTDESC actual'],
[
['101', 'EXP001', '60'],
['102', 'EXP002', '40'],
['103', 'EXP003', '100'],
],
col_widths_cm=[3, 4, 4])
parr('Suma de CANTDESC = 200. Cada descarga recibe (suponiendo Excel con CANTIDAD_SALDO=300):')
table_simple(
['Descarga', 'Proporción', 'PORCION_CANT', 'PORCION_VMN'],
[
['101', '60/200 = 30%', '300 × 30% = 90', '90 × 5.0 = 450'],
['102', '40/200 = 20%', '300 × 20% = 60', '60 × 5.0 = 300'],
['103', '100/200 = 50%', '300 × 50% = 150', '150 × 5.0 = 750'],
],
col_widths_cm=[3, 4, 4, 4])
# ====== 9 ======
h1('9. Validación contra el saldo real (capeo automático)')
parr('Antes de aplicar cualquier UPDATE en el Paso B, la herramienta vuelve a consultar el '
'SALDO_DISPONIBLE real en SSaldoTem. Si la suma de porciones (calculadas en el Paso A) '
'es mayor que ese disponible —porque alguien más consumió saldo entre el análisis y la '
'ejecución— la herramienta:')
bullet('Calcula un factor = SALDO_DISPONIBLE_actual / suma_de_porciones.')
bullet('Multiplica todas las porciones de ese saldo por el factor.')
bullet('Registra en el log: "CAPEO {factura}/{parte}/{um}: sum=X > disp=Y (factor=Z)".')
alerta('Por qué importa',
'Esto garantiza que NUNCA se rebase el 100% del saldo en SSaldoTem. La utilería '
'tradicional no hace esta verificación, por eso a veces dejaba CANTUSADA mayor a '
'CANTEXITENCIA. Aquí se previene en automático.')
# ====== 10 ======
h1('10. Estatus de cada saldo en el resumen')
table_simple(
['Estatus', '¿Qué significa?'],
[
['PRORRATEADO', 'El saldo encontró descargas EXPO con la misma llave y se prorrateó completamente.'],
['SIN_DESCARGAS', 'El saldo existe en SSaldoTem pero no hay ninguna fila en SDescargaT con esa combinación de FACTIMPO + NUMPARTE + UNIMED. No se puede repartir.'],
['CANTDESC_CERO', 'Hay descargas con esa llave pero todas tienen CANTDESC = 0. No hay base para prorratear.'],
],
col_widths_cm=[3.5, 12])
tip('Qué hacer con SIN_DESCARGAS',
'Revísalos manualmente: pueden ser saldos que el cliente quiere amortizar con descargas '
'que aún no se cargaron, o casos donde la UM en el saldo no coincide con la UM en la '
'descarga (típico cuando hay sustitutos con UNIDADMEDIDA1/UNIDADMEDIDA2).')
# ====== 11 ======
h1('11. Diferencias entre los dos modos')
table_simple(
['Aspecto', 'Modo Excel', 'Modo Automático'],
[
['Origen de los saldos', 'Tu archivo .xlsx', 'Búsqueda directa en SSaldoTem'],
['Filtro de vencimiento', 'Tú decides qué incluir', 'FECHAVENC_ISO < hoy'],
['Filtro de rango', 'FECHAFACTURA_ISO BETWEEN inicio Y fin', 'Igual'],
['SALDO_APLICABLE', 'min(CANTIDAD_SALDO Excel, SALDO_DISPONIBLE)', 'SALDO_DISPONIBLE completo'],
['Prorrateo', 'Igual', 'Igual'],
['Validación 100%', 'Igual', 'Igual'],
['UPDATEs aplicados', 'SDescargaT + SSaldoTem', 'Igual'],
['Tablas no tocadas', 'SFacExp', 'Igual'],
],
col_widths_cm=[3.5, 6, 6])
# ====== 12 ======
h1('12. Casos típicos y preguntas frecuentes')
h3('"Corrí el Paso B con DRY_RUN apagado por error pero todo está bien, ¿pasa algo?"')
parr('No, mientras los números del log se vean correctos. Si DRY_RUN está apagado y la '
'herramienta dice "X SDescargaT actualizadas, Y SSaldoTem actualizadas, 0 errores", '
'la información se aplicó como esperabas.')
h3('"Quiero deshacer un Paso B"')
parr('La herramienta no tiene botón de "deshacer". El procedimiento correcto es:')
bullet('Antes de ejecutar, exporta el plan a Excel y guárdalo como respaldo.')
bullet('Si necesitas revertir, usa el Excel del plan para construir los UPDATEs inversos '
'(restar las porciones aplicadas en SDescargaT y SSaldoTem).')
bullet('Considera respaldar la BD antes de Paso B si los volúmenes son grandes.')
h3('"En el modo Automático no aparecen saldos que yo sé que están vencidos"')
parr('Revisa estos tres puntos en SSaldoTem:')
bullet('FECHAFACTURA_ISO está dentro del rango que pusiste.')
bullet('FECHAVENC_ISO existe (NOT NULL) y es menor a hoy.')
bullet('SALDO_DISPONIBLE = CANTEXITENCIA CANTUSADA CANTUSADADESP es mayor a cero.')
parr('Si alguno de estos tres no se cumple, el saldo no aparece.')
h3('"Las cantidades del plan no me cuadran con lo que esperaba"')
parr('Verifica que las descargas en SDescargaT realmente tengan la misma UNIMED que el '
'UMEXITENCIA del saldo. Si hay descargas con UM distinta (típico con sustitutos), no '
'matchean y el saldo aparece como SIN_DESCARGAS aunque "haya material".')
# ====== 13 ======
h1('13. Apéndice técnico')
h2('Columnas leídas de SSaldoTem')
code_block('FACTURAIMPO, FRACCIONIMPO, NUMPARTE, UMEXITENCIA, FECHAFACTURA_ISO, FECHAVENC_ISO,\n'
'CANTEXITENCIA, CANTUSADA, CANTUSADADESP,\n'
'VALORIMPOMN, VALORIMPOME, PESONETO, PESOBRUTO')
h2('Columnas leídas de SDescargaT')
code_block('CONSECUTIVO, FACTIMPO, NUMPARTE, UNIMED, FACTEXPO, CANTDESC')
h2('UPDATE aplicado a cada descarga prorrateada')
code_block('UPDATE SDescargaT\n'
' SET CANTDESC = ISNULL(CANTDESC,0) + ?,\n'
' VALORMN = ISNULL(VALORMN,0) + ?,\n'
' VALORME = ISNULL(VALORME,0) + ?,\n'
' PESONETO = ISNULL(PESONETO,0) + ?,\n'
' PESOBRUTO= ISNULL(PESOBRUTO,0)+ ?\n'
' WHERE CONSECUTIVO = ?')
h2('UPDATE aplicado al saldo en SSaldoTem')
code_block('UPDATE SSaldoTem\n'
' SET CANTUSADA = ISNULL(CANTUSADA,0) + ?,\n'
' VALORUSADOMN = ISNULL(VALORUSADOMN,0) + ?,\n'
' VALORUSADOME = ISNULL(VALORUSADOME,0) + ?,\n'
' PESOUSADO = ISNULL(PESOUSADO,0) + ?,\n'
' PESOBRUTOUSADO = ISNULL(PESOBRUTOUSADO,0) + ?\n'
' WHERE FACTURAIMPO=? AND NUMPARTE=? AND UMEXITENCIA=?')
h2('Transacciones')
bullet('Una transacción por saldo (combinación FACTURAIMPO + NUMPARTE + UMEXITENCIA).')
bullet('Commit automático al terminar todas las descargas + el UPDATE de SSaldoTem.')
bullet('Rollback automático si cualquier UPDATE de la transacción falla.')
h2('Tablas que NO se tocan')
bullet('SFacExp: no cambia ESTATUS, APLICADESCMANUAL ni CANT_PARTIDAS.')
bullet('SDescargaM: este módulo no genera filas espejo (a diferencia de CTM).')
# ====== 14 ======
h1('14. Glosario')
table_simple(
['Término', 'Significado'],
[
['SSaldoTem', 'Tabla maestra de saldos de importación temporales en SCAII.'],
['SDescargaT', 'Tabla de descargas de las facturas de exportación contra saldos IMPO.'],
['CANTEXITENCIA', 'Cantidad original que entró al lote IMPO (sic, así está escrito en SCAII).'],
['CANTUSADA', 'Cantidad ya consumida del lote por descargas normales.'],
['CANTUSADADESP', 'Cantidad usada por desperdicios.'],
['SALDO_DISPONIBLE', 'CANTEXITENCIA CANTUSADA CANTUSADADESP.'],
['CANTDESC', 'Cantidad descargada en una fila específica de SDescargaT.'],
['UMEXITENCIA', 'Unidad de medida del saldo en SSaldoTem (sic).'],
['UNIMED', 'Unidad de medida de la descarga en SDescargaT.'],
['FECHAFACTURA_ISO', 'Fecha de la factura IMPO en formato ISO.'],
['FECHAVENC_ISO', 'Fecha de vencimiento del saldo IMPO en formato ISO.'],
['Prorrateo', 'Repartir un total entre varios destinos proporcionalmente a un peso.'],
['DRY_RUN', 'Modo de simulación: la herramienta calcula y muestra los UPDATEs sin ejecutarlos.'],
['Capeo', 'Reducir proporcionalmente las porciones para no rebasar el SALDO_DISPONIBLE real.'],
],
col_widths_cm=[4, 12])
# ====== Guardar ======
import os
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Manual_SaldosVencidos.docx')
doc.save(out)
print(f'Manual generado: {out}')

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}')

121
app/launcher.py Normal file
View File

@@ -0,0 +1,121 @@
"""
Launcher de la app GENPACT V2.
Arranca un servidor Voila local en background y abre el browser apuntando al app.ipynb.
Cuando se cierra la consola, el servidor de Voila se mata.
Funciona tanto desde Python directo como empaquetado con PyInstaller.
"""
import os
import sys
import time
import socket
import threading
import webbrowser
import subprocess
from pathlib import Path
# ------------------------------------------------------------------
# Localizar app.ipynb y .env
# ------------------------------------------------------------------
def base_dir() -> Path:
"""Carpeta donde estan los recursos de la app.
- En modo PyInstaller (--onedir), sys._MEIPASS apunta a la carpeta temporal.
- En modo dev, es la carpeta del script.
"""
if getattr(sys, 'frozen', False):
# PyInstaller --onedir: los datos quedan al lado del .exe
return Path(sys.executable).parent
return Path(__file__).resolve().parent
BASE = base_dir()
APP_NB = BASE / 'app.ipynb'
# Si el .env esta un nivel arriba (proyecto), copialo o setea el cwd
if not (BASE / '.env').exists() and (BASE.parent / '.env').exists():
os.chdir(BASE.parent)
else:
os.chdir(BASE)
# ------------------------------------------------------------------
# Buscar puerto libre
# ------------------------------------------------------------------
def find_free_port(preferido: int = 8866) -> int:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.bind(('127.0.0.1', preferido))
sock.close()
return preferido
except OSError:
sock.close()
# Si el preferido esta ocupado, deja que el SO asigne uno
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 0))
p = s.getsockname()[1]
s.close()
return p
# ------------------------------------------------------------------
# Arrancar Voila
# ------------------------------------------------------------------
def main():
if not APP_NB.exists():
print(f'ERROR: no se encuentra {APP_NB}')
sys.exit(1)
port = find_free_port(8866)
# Cuando Voila apunta a un notebook especifico, lo sirve en la raiz "/"
url = f'http://127.0.0.1:{port}/'
print('=' * 60)
print(' GENPACT V2 - Sistema de Descargas SCAII')
print('=' * 60)
print(f' Iniciando servidor Voila en puerto {port}...')
print(f' URL: {url}')
print(' Cierra esta ventana para detener la app.')
print('=' * 60)
# Construir comando de Voila
if getattr(sys, 'frozen', False):
# En modo empaquetado, llamamos al binario de python integrado
cmd = [sys.executable, '-m', 'voila',
str(APP_NB),
f'--port={port}',
'--no-browser',
'--Voila.ip=127.0.0.1',
'--strip_sources=True']
else:
cmd = [sys.executable, '-m', 'voila',
str(APP_NB),
f'--port={port}',
'--no-browser',
'--Voila.ip=127.0.0.1',
'--strip_sources=True']
# Lanzar proceso
proc = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
# Abrir browser cuando el server este listo
def _open():
for _ in range(60):
try:
with socket.create_connection(('127.0.0.1', port), timeout=0.5):
break
except OSError:
time.sleep(0.5)
webbrowser.open(url)
threading.Thread(target=_open, daemon=True).start()
try:
proc.wait()
except KeyboardInterrupt:
print('\nDeteniendo Voila...')
proc.terminate()
if __name__ == '__main__':
main()

15
app/requirements.txt Normal file
View File

@@ -0,0 +1,15 @@
pandas>=2.0
numpy>=1.24
openpyxl>=3.1
python-dotenv>=1.0
pyodbc>=5.0
matplotlib>=3.7
ipywidgets>=8.0
voila>=0.5
nbconvert>=7.0
jupyter_server>=2.0
notebook>=7.0
scikit-learn>=1.3
pyinstaller>=6.0
psycopg2-binary>=2.9
sqlalchemy>=2.0

97
app/run_dev.bat Normal file
View File

@@ -0,0 +1,97 @@
@echo off
REM Ejecuta la app en modo desarrollo (sin empaquetar)
REM Requiere Python instalado
cd /d "%~dp0"
echo Directorio: %CD%
echo.
REM Verificar Python
where python >nul 2>nul
if errorlevel 1 (
echo [ERROR] Python no esta en PATH.
echo Instala Python 3.11+ y marca "Add Python to PATH" durante la instalacion.
goto :end
)
python --version
if not exist .venv goto :crear_venv
call .venv\Scripts\activate.bat
goto :sync_deps
:crear_venv
echo.
echo Creando venv local, puede tardar 2-3 minutos...
python -m venv .venv
if errorlevel 1 (
echo [ERROR] No se pudo crear el venv.
goto :end
)
call .venv\Scripts\activate.bat
python -m pip install --upgrade pip
:sync_deps
REM Reinstala requirements solo si requirements.txt es mas nuevo que el marcador
if not exist .venv\.deps_synced goto :do_install
for /f %%i in ('dir /b /od requirements.txt .venv\.deps_synced 2^>nul') do set _newer=%%i
if /i "%_newer%"==".deps_synced" goto :run_app
:do_install
echo.
echo [INFO] Sincronizando dependencias (requirements.txt)...
python -m pip install -r requirements.txt
if errorlevel 1 (
echo [ERROR] Fallo la instalacion de dependencias.
goto :end
)
echo. > .venv\.deps_synced
:run_app
REM ===== Postgres en Docker (DataStage) =====
where docker >nul 2>nul
if errorlevel 1 (
echo [INFO] Docker no esta instalado. La pestaña DataStage no funcionara.
echo Instala Docker Desktop si vas a usarla.
goto :launch_voila
)
docker ps --format "{{.Names}}" | findstr /B /C:"postgres-datastage" >nul 2>nul
if not errorlevel 1 (
echo [INFO] Contenedor postgres-datastage ya esta corriendo.
goto :launch_voila
)
echo [INFO] Levantando contenedor postgres-datastage...
docker compose up -d postgres-datastage
if errorlevel 1 (
echo [WARN] No se pudo levantar Postgres. La pestaña DataStage no funcionara.
goto :launch_voila
)
echo [INFO] Esperando a que Postgres acepte conexiones...
docker compose exec -T postgres-datastage pg_isready >nul 2>nul
set /a _retries=0
:wait_pg
set /a _retries+=1
if %_retries% GTR 20 (
echo [WARN] Postgres tardo en levantar. Continuamos de todos modos.
goto :launch_voila
)
docker compose exec -T postgres-datastage pg_isready >nul 2>nul
if errorlevel 1 (
timeout /t 1 /nobreak >nul
goto :wait_pg
)
echo [INFO] Postgres listo.
:launch_voila
echo.
echo Lanzando la app...
echo (Si el browser no abre, ve a http://127.0.0.1:8866/)
echo.
python launcher.py
:end
echo.
echo ============================================================
echo Presiona cualquier tecla para cerrar...
pause >nul

558
app/schema_registro.sql Normal file
View File

@@ -0,0 +1,558 @@
--
-- PostgreSQL database dump
--
\restrict NkLZPgiIuy1DEwGxiddM38TIL7f97hE2jK7DUCZtx3dZAdwepDWiHdqf075r6Nt
-- Dumped from database version 16.13
-- Dumped by pg_dump version 16.13
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: Registro501; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro501" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"TipoOperacion" character varying(5),
"ClaveDocumento" character varying(10),
"SeccionAduaneraEntrada" character varying(10),
"CurpContribuyente" character varying(25),
"Rfc" character varying(20),
"CurpAgenteA" character varying(25),
"TipoCambio" numeric(15,5),
"TotalFletes" numeric(18,2),
"TotalSeguros" numeric(18,2),
"TotalEmbalajes" numeric(18,2),
"TotalIncrementables" numeric(18,2),
"TotalDeducibles" numeric(18,2),
"PesoBrutoMercancia" numeric(18,3),
"MedioTransporteSalida" character varying(5),
"MedioTransporteArribo" character varying(5),
"MedioTransporteEntrada_Salida" character varying(5),
"DestinoMercancia" character varying(5),
"NombreContribuyente" character varying(200),
"CalleContribuyente" character varying(200),
"NumInteriorContribuyente" character varying(20),
"NumExteriorContribuyente" character varying(20),
"CPContribuyente" character varying(10),
"MunicipioContribuyente" character varying(100),
"EntidadFedContribuyente" character varying(5),
"PaisContribuyente" character varying(5),
"TipoPedimento" character varying(5),
"FechaRecepcionPedimento" timestamp without time zone,
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro502; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro502" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"RfcTransportista" character varying(20),
"CurpTransportista" character varying(25),
"NombreTransportista" character varying(200),
"PaisTransporte" character varying(5),
"IdentificadorTransporte" character varying(50),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro503; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro503" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"NumeroGuia" character varying(50),
"TipoGuia" character varying(5),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro504; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro504" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"NumContenedor" character varying(50),
"TipoContenedor" character varying(10),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro505; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro505" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"FechaFacturacion" character varying(20),
"NumeroFactura" character varying(50),
"TerminoFacturacion" character varying(10),
"MonedaFacturacion" character varying(10),
"ValorDolares" numeric(18,2),
"ValorMonedaExtranjera" numeric(18,2),
"PaisFacturacion" character varying(5),
"EntidadFedFacturacion" character varying(5),
"IndentFiscalProveedor" character varying(50),
"ProveedorMercancia" character varying(200),
"CalleProveedor" character varying(200),
"NumInteriorProveedor" character varying(20),
"NumExteriorProveedor" character varying(20),
"CpProveedor" character varying(20),
"MunicipioProveedor" character varying(100),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro506; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro506" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"TipoFecha" character varying(5),
"FechaOperacion" character varying(20),
"FechaValidacionPagoR" timestamp without time zone
);
--
-- Name: Registro507; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro507" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"ClaveCaso" character varying(10),
"IdentificadorCaso" character varying(50),
"TipoPedimento" character varying(5),
"ComplementoCaso" character varying(100),
"FechaValidacionPagoR" timestamp without time zone
);
--
-- Name: Registro508; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro508" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"InstitucionEmisora" character varying(50),
"NumeroCuenta" character varying(50),
"FolioConstancia" character varying(50),
"FechaConstancia" character varying(20),
"TipoCuenta" character varying(10),
"ClaveGarantia" character varying(10),
"ValorUnitarioTitulo" numeric(18,5),
"TotalGarantia" numeric(18,2),
"CantidadUnidades" numeric(18,3),
"TitulosAsignados" character varying(50),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro509; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro509" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"ClaveContribucion" character varying(10),
"TasaContribucion" numeric(15,5),
"TipoTasa" character varying(5),
"TipoPedimento" character varying(5),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro510; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro510" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"ClaveContribucion" character varying(10),
"FormaPago" character varying(5),
"ImportePago" numeric(18,2),
"TipoPedimento" character varying(5),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro511; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro511" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"SecuenciaObservacion" character varying(10),
"Observaciones" text,
"TipoPedimento" character varying(5),
"FechaValidacionPagoR" timestamp without time zone
);
--
-- Name: Registro512; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro512" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"PatenteAduanalOrig" character varying(10),
"PedimentoOriginal" character varying(20),
"SeccionAduaneraDespOrig" character varying(10),
"DocumentoOriginal" character varying(10),
"FechaOperacionOrig" character varying(20),
"FraccionOriginal" character varying(15),
"UnidadMedida" character varying(5),
"MercanciaDescargada" numeric(18,3),
"TipoPedimento" character varying(5),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro520; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro520" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"IndentFiscalDestinatario" character varying(50),
"NombreDestinatarioMercancia" character varying(200),
"CalleDestinatario" character varying(200),
"NumInteriorDestinatario" character varying(20),
"NumExteriorDestinatario" character varying(20),
"CpDestinatario" character varying(20),
"MunicpioDestinatario" character varying(100),
"PaisDestinatario" character varying(5),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro551; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro551" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"Fraccion" character varying(15),
"SecuenciaFraccion" character varying(10),
"SubdivisionFraccion" character varying(10),
"DescripcionMercancia" text,
"PrecioUnitario" numeric(18,5),
"ValorAduana" numeric(18,2),
"ValorComercial" numeric(18,2),
"ValorDolares" numeric(18,2),
"CantidadUMComercial" numeric(18,3),
"UnidadMedidaComercial" character varying(5),
"CantidadUMTarifa" numeric(18,5),
"UnidadMedidaTarifa" character varying(5),
"ValorAgregado" numeric(18,2),
"ClaveVinculacion" character varying(5),
"MetodoValorizacion" character varying(5),
"CodigoMercanciaProducto" character varying(50),
"MarcaMercanciaProducto" character varying(100),
"ModeloMercanciaProducto" character varying(100),
"PaisOrigenDestino" character varying(5),
"PaisCompradorVendedor" character varying(5),
"EntidadFedOrigen" character varying(5),
"EntidadFedDestino" character varying(5),
"EntidadFedComprador" character varying(5),
"EntidadFedVendedor" character varying(5),
"TipoOperacion" character varying(5),
"ClaveDocumento" character varying(10),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro552; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro552" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"Fraccion" character varying(15),
"SecuenciaFraccion" character varying(10),
"VinNumeroSerie" character varying(50),
"KilometrajeVehiculo" character varying(20),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro553; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro553" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"Fraccion" character varying(15),
"SecuenciaFraccion" character varying(10),
"ClavePermiso" character varying(10),
"FirmaDescargo" character varying(10),
"NumeroPermiso" character varying(50),
"ValorComercialDolares" numeric(18,2),
"CantidadMUMTarifa" numeric(18,5),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro554; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro554" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"Fraccion" character varying(15),
"SecuenciaFraccion" character varying(10),
"ClaveCaso" character varying(10),
"IdentificadorCaso" character varying(50),
"ComplementoCaso" character varying(100),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro555; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro555" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"Fraccion" character varying(15),
"SecuenciaFraccion" character varying(10),
"InstitucionEmisora" character varying(50),
"NumeroCuenta" character varying(50),
"FolioConstancia" character varying(50),
"FechaConstancia" character varying(20),
"ClaveGarantia" character varying(10),
"ValorUnitarioTitulo" numeric(18,5),
"TotalGarantia" numeric(18,2),
"CantidadUnidadesMedida" numeric(18,3),
"TitulosAsignados" character varying(50),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro556; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro556" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"Fraccion" character varying(15),
"SecuenciaFraccion" character varying(10),
"ClaveContribucion" character varying(10),
"TasaContribucion" numeric(15,5),
"TipoTasa" character varying(5),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro557; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro557" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"Fraccion" character varying(15),
"SecuenciaFraccion" character varying(10),
"ClaveContribucion" character varying(10),
"FormaPago" character varying(5),
"ImportePago" numeric(18,2),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro558; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro558" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"Fraccion" character varying(15),
"SecuenciaFraccion" character varying(10),
"SecuenciaObservacion" character varying(10),
"Observaciones" text,
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro701; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro701" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"ClaveDocumento" character varying(10),
"FechaPago" character varying(20),
"PedimentoAnterior" character varying(20),
"PatenteAnterior" character varying(10),
"SeccionAduaneraAnterior" character varying(10),
"DocumentoAnterior" character varying(10),
"FechaOperacionAnterior" character varying(20),
"PedimentoOriginal" character varying(20),
"PatenteAduanalOrig" character varying(10),
"SeccionAduaneraDespOrig" character varying(10),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: Registro702; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."Registro702" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"ClaveContribucion" character varying(10),
"FormaPago" character varying(5),
"ImportePago" numeric(18,2),
"TipoPedimento" character varying(5),
"FechaPagoReal" timestamp without time zone
);
--
-- Name: RegistroInci; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."RegistroInci" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"ConsecutivoRemesa" character varying(10),
"NumeroSeleccion" character varying(10),
"FechaInicioReconocimiento" character varying(20),
"HoraInicioReconocimiento" character varying(10),
"FechaFinReconocimiento" character varying(20),
"HoraFinReconocimiento" character varying(10),
"Fraccion" character varying(15),
"SecuenciaFraccion" character varying(10),
"ClaveDocumento" character varying(10),
"TipoOperacion" character varying(5),
"GradoIncidencia" character varying(5),
"FechaSeleccion" character varying(20)
);
--
-- Name: RegistroResumen; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."RegistroResumen" (
"Folio" character varying(20),
"RFCoPatenteAduanal" character varying(20),
"Fecha_Inicial" character varying(20),
"Fecha_Final" character varying(20),
"Fecha_Ejecucion" character varying(20),
"Total_Fracciones" integer,
"Total_Contribuciones" integer
);
--
-- Name: RegistroSel; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."RegistroSel" (
"Patente" character varying(10),
"Pedimento" character varying(20),
"SeccionAduanera" character varying(10),
"ConsecutivoRemesa" character varying(10),
"NumeroSeleccion" character varying(10),
"FechaSeleccion" character varying(20),
"HoraSeleccion" character varying(10),
"SemaforoFiscal" character varying(5),
"ClaveDocumento" character varying(10),
"TipoOperacion" character varying(5)
);
--
-- Name: base_numpartes; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.base_numpartes (
numparte text NOT NULL,
descripcion text,
unimed text,
fraccion text
);
--
-- Name: base_numpartes base_numpartes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.base_numpartes
ADD CONSTRAINT base_numpartes_pkey PRIMARY KEY (numparte);
--
-- PostgreSQL database dump complete
--
\unrestrict NkLZPgiIuy1DEwGxiddM38TIL7f97hE2jK7DUCZtx3dZAdwepDWiHdqf075r6Nt