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:
18
.env.example
Normal file
18
.env.example
Normal file
@@ -0,0 +1,18 @@
|
||||
# Copia este archivo como .env y completa los valores reales.
|
||||
# NO subas el .env al repositorio (esta en .gitignore).
|
||||
|
||||
# Configuracion de conexion a PostgreSQL (DataStage)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=dbSat
|
||||
DB_USER=servicemanager
|
||||
DB_PASSWORD=cambia_esto
|
||||
|
||||
# Configuracion de conexion a SQL Server (SCAII)
|
||||
# SCAII_TRUSTED=yes -> usa Windows Authentication (ignora SCAII_USER/PASSWORD)
|
||||
# SCAII_TRUSTED=no -> usa SCAII_USER y SCAII_PASSWORD
|
||||
SCAII_SERVER=localhost
|
||||
SCAII_DATABASE=" GENPACT-CORRECCION"
|
||||
SCAII_TRUSTED=no
|
||||
SCAII_USER=sa
|
||||
SCAII_PASSWORD=cambia_esto
|
||||
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# Credenciales y entorno local
|
||||
.env
|
||||
*.env.local
|
||||
|
||||
# Python
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.pytest_cache/
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# OS
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
|
||||
# Outputs generados por la app
|
||||
app/Estructura_*.xlsx
|
||||
app/Estructura_*.csv
|
||||
app/plan_*.xlsx
|
||||
app/plan_*.csv
|
||||
app/Resultados_*.csv
|
||||
app/Manual_*_[0-9]*.docx
|
||||
app/Registro551.xlsx
|
||||
app/_upload_*.xlsx
|
||||
app/_*.py
|
||||
app/app.ipynb.bak
|
||||
|
||||
# Docker volumes locales
|
||||
postgres_data/
|
||||
162
README.md
Normal file
162
README.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Utilerias RECON 2
|
||||
|
||||
Sistema de utilerias 2.0 para automatizacion de procesos aduaneros SCAII +
|
||||
DataStage. Construido sobre Voila (Jupyter widgets) con backend dual:
|
||||
SQL Server (SCAII) + PostgreSQL (DataStage en Docker).
|
||||
|
||||
## Pestanas
|
||||
|
||||
- **Conexion** — selecciona la base SCAII a conectar.
|
||||
- **Descargas** — procesa facturas de exportacion contra saldos IMPO (paso 9/10).
|
||||
- **Analisis Saldos** — graficas y pivotes de SSaldoTem.
|
||||
- **Sustitutos NLP** — propone sustitutos en SPartesSustitutos via TF-IDF.
|
||||
- **Descarga % KGS** — analiza cobertura de descargas en KGS.
|
||||
- **CTM** — reasigna descargas de Cambio de Regimen a CTM.
|
||||
- **Saldos Vencidos** — prorratea saldos IMPO vencidos en descargas EXPO.
|
||||
- **DataStage** — carga archivos .asc del SAAI a tablas Registro* en Postgres.
|
||||
- **Estructuras SCAII** — reportes CAT/Pedimentos/Facturas/Partidas a Excel.
|
||||
- **Valores** — ajusta VALORTOTALME/MN de SPartidasExpo por prorrateo.
|
||||
|
||||
## Stack
|
||||
|
||||
| Capa | Tecnologia |
|
||||
|---|---|
|
||||
| UI | Voila + ipywidgets |
|
||||
| Backend | Python 3.11+ (pandas, scikit-learn, pyodbc, psycopg2, sqlalchemy) |
|
||||
| BD aduanera | SQL Server (SCAII) |
|
||||
| BD DataStage | PostgreSQL 16 (en Docker) |
|
||||
| Reportes | openpyxl, python-docx |
|
||||
|
||||
## Requisitos del servidor
|
||||
|
||||
- Windows 10 / 11 / Server con PowerShell.
|
||||
- **Python 3.11+** con "Add to PATH" durante instalacion.
|
||||
- **Docker Desktop** (para el contenedor de Postgres).
|
||||
- **ODBC Driver 18 for SQL Server** (para conectar a SCAII).
|
||||
- Acceso de red al servidor SQL Server donde vive SCAII.
|
||||
|
||||
## Configuracion inicial
|
||||
|
||||
### 1. Variables de entorno
|
||||
|
||||
Copia `.env.example` a `.env` y completa los valores reales:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edita `.env` con tu editor favorito y completa:
|
||||
|
||||
- `DB_*` — credenciales del Postgres local (las que va a usar el contenedor).
|
||||
- `SCAII_*` — credenciales del SQL Server donde vive SCAII.
|
||||
|
||||
> Importante: `.env` NO se sube al repo (esta en `.gitignore`).
|
||||
|
||||
### 2. Levantar Postgres en Docker
|
||||
|
||||
```powershell
|
||||
cd app
|
||||
docker compose --env-file ..\.env up -d postgres-datastage
|
||||
```
|
||||
|
||||
Espera unos segundos. Verifica con:
|
||||
|
||||
```powershell
|
||||
docker ps
|
||||
```
|
||||
|
||||
Debes ver `postgres-datastage` con estatus `Up X seconds (healthy)`.
|
||||
|
||||
### 3. Aplicar el schema de las tablas Registro*
|
||||
|
||||
```powershell
|
||||
docker cp schema_registro.sql postgres-datastage:/tmp/schema.sql
|
||||
docker exec -it postgres-datastage psql -U servicemanager -d dbSat -f /tmp/schema.sql
|
||||
```
|
||||
|
||||
(Ajusta `servicemanager` y `dbSat` segun tu `.env`.)
|
||||
|
||||
Verifica:
|
||||
|
||||
```powershell
|
||||
docker exec -it postgres-datastage psql -U servicemanager -d dbSat -c "\dt"
|
||||
```
|
||||
|
||||
Debes ver 27 tablas: `Registro501`, `Registro551`, `Registro701`, ..., `base_numpartes`.
|
||||
|
||||
### 4. Arrancar la app
|
||||
|
||||
```powershell
|
||||
.\run_dev.bat
|
||||
```
|
||||
|
||||
La primera vez tarda 2-3 minutos (crea `.venv`, instala dependencias).
|
||||
|
||||
Cuando termine, abre el navegador en:
|
||||
|
||||
```
|
||||
http://127.0.0.1:8866/
|
||||
```
|
||||
|
||||
## Comandos utiles
|
||||
|
||||
```powershell
|
||||
# Estado del contenedor
|
||||
docker compose ps
|
||||
|
||||
# Logs en vivo
|
||||
docker compose logs -f postgres-datastage
|
||||
|
||||
# Detener Postgres
|
||||
docker compose stop
|
||||
|
||||
# Volver a iniciar
|
||||
docker compose start
|
||||
|
||||
# Eliminar contenedor (mantiene los datos)
|
||||
docker compose down
|
||||
|
||||
# Eliminar contenedor + DATOS (CUIDADO)
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
## Estructura del proyecto
|
||||
|
||||
```
|
||||
.
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
└── app/
|
||||
├── app.ipynb # notebook principal
|
||||
├── launcher.py # lanzador Voila
|
||||
├── run_dev.bat # script de arranque
|
||||
├── requirements.txt # deps Python
|
||||
├── docker-compose.yml # contenedor Postgres
|
||||
├── schema_registro.sql # DDL tablas Registro*
|
||||
├── Manual_*.docx # manuales de usuario
|
||||
└── generar_manual_*.py # generadores de manuales
|
||||
```
|
||||
|
||||
## Manuales
|
||||
|
||||
Los `.docx` en `app/` son los manuales de usuario por modulo:
|
||||
|
||||
- `Manual_GENPACT_V2.docx` — manual general (Conexion, Descargas, Analisis, NLP, KGS).
|
||||
- `Manual_CTM.docx` — modulo CTM.
|
||||
- `Manual_SaldosVencidos.docx` — modulo Saldos Vencidos.
|
||||
- `Manual_DataStage.docx` — modulo DataStage.
|
||||
- `Manual_EstructurasSCAII.docx` — modulo Estructuras SCAII.
|
||||
- `Manual_Valores.docx` — modulo Valores.
|
||||
|
||||
Para regenerarlos despues de editar el script correspondiente:
|
||||
|
||||
```powershell
|
||||
python app\generar_manual_valores.py
|
||||
# (y los demas generar_manual_*.py segun el caso)
|
||||
```
|
||||
|
||||
## Soporte
|
||||
|
||||
Para problemas o cambios, abre un PR en este repositorio.
|
||||
No hagas push directo a `main`.
|
||||
BIN
app/Manual_CTM.docx
Normal file
BIN
app/Manual_CTM.docx
Normal file
Binary file not shown.
BIN
app/Manual_DataStage.docx
Normal file
BIN
app/Manual_DataStage.docx
Normal file
Binary file not shown.
BIN
app/Manual_EstructurasSCAII.docx
Normal file
BIN
app/Manual_EstructurasSCAII.docx
Normal file
Binary file not shown.
BIN
app/Manual_GENPACT_V2.docx
Normal file
BIN
app/Manual_GENPACT_V2.docx
Normal file
Binary file not shown.
BIN
app/Manual_SaldosVencidos.docx
Normal file
BIN
app/Manual_SaldosVencidos.docx
Normal file
Binary file not shown.
BIN
app/Manual_Valores.docx
Normal file
BIN
app/Manual_Valores.docx
Normal file
Binary file not shown.
4701
app/app.ipynb
Normal file
4701
app/app.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
21
app/docker-compose.yml
Normal file
21
app/docker-compose.yml
Normal 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
645
app/generar_manual.py
Normal 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
497
app/generar_manual_ctm.py
Normal 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)}')
|
||||
303
app/generar_manual_datastage.py
Normal file
303
app/generar_manual_datastage.py
Normal 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}')
|
||||
437
app/generar_manual_estructuras.py
Normal file
437
app/generar_manual_estructuras.py
Normal 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}')
|
||||
503
app/generar_manual_saldos_vencidos.py
Normal file
503
app/generar_manual_saldos_vencidos.py
Normal 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}')
|
||||
388
app/generar_manual_valores.py
Normal file
388
app/generar_manual_valores.py
Normal 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
121
app/launcher.py
Normal 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
15
app/requirements.txt
Normal 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
97
app/run_dev.bat
Normal 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
558
app/schema_registro.sql
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user