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>
498 lines
23 KiB
Python
498 lines
23 KiB
Python
"""
|
||
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)}')
|