Files
utilerias-recon-2/app/generar_manual_ctm.py
Ernesto Herrera a0081f54d2 chore: initial commit del deploy de utilerias-recon-2
Incluye:
- app.ipynb con las 10 pestanas (Conexion, Descargas, Analisis, NLP, KGS,
  CTM, Saldos Vencidos, DataStage, Estructuras SCAII, Valores).
- launcher.py + run_dev.bat para lanzar Voila localmente.
- docker-compose.yml para levantar Postgres 16 con healthcheck.
- schema_registro.sql con las 27 tablas (Registro501..Registro701,
  RegistroInci/Resumen/Sel, base_numpartes).
- requirements.txt con pandas, pyodbc, psycopg2-binary, sqlalchemy,
  scikit-learn, openpyxl, voila, ipywidgets, python-docx.
- 6 manuales de usuario (CTM, SaldosVencidos, DataStage, EstructurasSCAII,
  Valores, GENPACT_V2 general) en .docx.
- 6 generadores de manuales para regenerar los .docx tras editar.
- .gitignore que excluye .env, .venv, outputs generados y caches.
- README.md con instrucciones de setup en server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 08:18:13 -06:00

498 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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