diff --git a/.env.example b/.env.example index 017aa79..de3fc3c 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,8 @@ # 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 +# DataStage corre embebido en SQLite (archivo datastage.db junto al .exe). +# No requiere configuracion. Para cambiar la ruta usa: DATASTAGE_DB=ruta\completa\datastage.db # Configuracion de conexion a SQL Server (SCAII) # SCAII_TRUSTED=yes -> usa Windows Authentication (ignora SCAII_USER/PASSWORD) diff --git a/app/app.ipynb b/app/app.ipynb index a6e1302..4d5da44 100644 --- a/app/app.ipynb +++ b/app/app.ipynb @@ -1,4718 +1,20139 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "imports", - "metadata": { - "tags": [ - "hide-input" - ] - }, - "outputs": [], - "source": [ - "import os, sys, time, datetime as _dt, re, warnings\n", - "warnings.filterwarnings('ignore')\n", - "import pandas as pd\n", - "import numpy as np\n", - "import pyodbc\n", - "import matplotlib.pyplot as plt\n", - "import matplotlib.ticker as mtick\n", - "import ipywidgets as W\n", - "from IPython.display import display, clear_output, HTML\n", - "from dotenv import load_dotenv\n", - "for env_path in ['.env', '../.env']:\n", - " if os.path.exists(env_path):\n", - " load_dotenv(env_path); break\n", - "SCAII_SERVER = os.getenv('SCAII_SERVER', 'localhost')\n", - "SCAII_DB = os.getenv('SCAII_DATABASE', ' GENPACT-CORRECCION')\n", - "SCAII_USER = os.getenv('SCAII_USER', 'sa')\n", - "SCAII_PASSWORD = os.getenv('SCAII_PASSWORD', 'Soluciones01')\n", - "\n", - "def _make_conn_str(db_name=None):\n", - " db_part = f'DATABASE={{{db_name}}};' if db_name else ''\n", - " return (f\"DRIVER={{ODBC Driver 18 for SQL Server}};\"\n", - " f\"SERVER={SCAII_SERVER};{db_part}\"\n", - " f\"UID={SCAII_USER};PWD={SCAII_PASSWORD};\"\n", - " f\"Encrypt=yes;TrustServerCertificate=yes;MARS_Connection=yes;\")\n", - "\n", - "scaii_conn = None\n", - "DB_ACTUAL = None\n", - "CONEXION_OK = False\n", - "CONEXION_MSG = ''\n", - "\n", - "def conectar_a_db(db_name=None):\n", - " global scaii_conn, DB_ACTUAL, CONEXION_OK, CONEXION_MSG\n", - " try:\n", - " if scaii_conn is not None:\n", - " try: scaii_conn.close()\n", - " except Exception: pass\n", - " scaii_conn = pyodbc.connect(_make_conn_str(db_name))\n", - " with scaii_conn.cursor() as cur:\n", - " cur.execute('SELECT DB_NAME()')\n", - " DB_ACTUAL = cur.fetchone()[0]\n", - " CONEXION_OK = True\n", - " CONEXION_MSG = f'Conectado a [{DB_ACTUAL}] @ {SCAII_SERVER}'\n", - " _state.clear()\n", - " return True\n", - " except Exception as e:\n", - " scaii_conn = None; DB_ACTUAL = None\n", - " CONEXION_OK = False; CONEXION_MSG = f'ERROR conexion: {e}'\n", - " return False\n", - "\n", - "def listar_databases():\n", - " if scaii_conn is None:\n", - " try:\n", - " tmp = pyodbc.connect(_make_conn_str('master'))\n", - " except Exception:\n", - " try: tmp = pyodbc.connect(_make_conn_str(None))\n", - " except Exception: return []\n", - " try:\n", - " df = pd.read_sql(\"SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 ORDER BY name\", tmp)\n", - " tmp.close()\n", - " return df['name'].tolist()\n", - " except Exception:\n", - " try: tmp.close()\n", - " except: pass\n", - " return []\n", - " try:\n", - " df = pd.read_sql(\"SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 ORDER BY name\", scaii_conn)\n", - " return df['name'].tolist()\n", - " except Exception:\n", - " return []\n", - "\n", - "EPOCH_CLARION = _dt.date(1801, 1, 1)\n", - "def to_clarion(d):\n", - " if d is None or pd.isna(d): return None\n", - " if isinstance(d, str):\n", - " try: d = pd.to_datetime(d).date()\n", - " except Exception: return None\n", - " elif isinstance(d, pd.Timestamp): d = d.date()\n", - " elif isinstance(d, _dt.datetime): d = d.date()\n", - " return (d - EPOCH_CLARION).days + 4\n", - "\n", - "_state = {}\n", - "\n", - "# === Helper de progreso ===\n", - "class _Progress:\n", - " \"\"\"Wrapper de IntProgress. Si el widget es None, los metodos son no-op.\"\"\"\n", - " def __init__(self, widget=None):\n", - " self.w = widget\n", - " def setup(self, total, desc=''):\n", - " if self.w is None: return\n", - " self.w.min = 0\n", - " self.w.max = max(1, int(total))\n", - " self.w.value = 0\n", - " self.w.bar_style = 'info'\n", - " self.w.description = (desc or '')[:40]\n", - " def step(self, n=1, desc=None):\n", - " if self.w is None: return\n", - " try: self.w.value = min(self.w.value + n, self.w.max)\n", - " except Exception: pass\n", - " if desc is not None: self.w.description = desc[:40]\n", - " def done(self, desc='Listo'):\n", - " if self.w is None: return\n", - " self.w.value = self.w.max\n", - " self.w.bar_style = 'success'\n", - " self.w.description = desc[:40]\n", - " def error(self, desc='Error'):\n", - " if self.w is None: return\n", - " self.w.bar_style = 'danger'\n", - " self.w.description = desc[:40]\n", - "\n", - "conectar_a_db(SCAII_DB)\n", - "# ============================================================\n", - "# Postgres (DataStage)\n", - "# Conexion opcional: si falla, la pestania DataStage se deshabilita\n", - "# ============================================================\n", - "try:\n", - " import psycopg2 as _psycopg2\n", - " from sqlalchemy import create_engine as _create_engine\n", - " _PSYCOPG2_OK = True\n", - "except Exception as _e_imp:\n", - " _PSYCOPG2_OK = False\n", - " _DATASTAGE_IMPORT_ERR = str(_e_imp)\n", - "\n", - "PG_CONFIG = {\n", - " 'host': os.getenv('DB_HOST', '127.0.0.1'),\n", - " 'port': os.getenv('DB_PORT', '5432'),\n", - " 'dbname': os.getenv('DB_NAME', 'dbSat'),\n", - " 'user': os.getenv('DB_USER', 'postgres'),\n", - " 'password': os.getenv('DB_PASSWORD', ''),\n", - "}\n", - "DATASTAGE_ROOT = os.getenv('DATASTAGE_ROOT', '')\n", - "\n", - "pg_engine = None\n", - "DATASTAGE_OK = False\n", - "DATASTAGE_MSG = ''\n", - "\n", - "def conectar_postgres():\n", - " global pg_engine, DATASTAGE_OK, DATASTAGE_MSG\n", - " if not _PSYCOPG2_OK:\n", - " DATASTAGE_OK = False\n", - " DATASTAGE_MSG = f'psycopg2 no instalado: {_DATASTAGE_IMPORT_ERR}'\n", - " return False\n", - " try:\n", - " url = (f\"postgresql+psycopg2://{PG_CONFIG['user']}:{PG_CONFIG['password']}\"\n", - " f\"@{PG_CONFIG['host']}:{PG_CONFIG['port']}/{PG_CONFIG['dbname']}\")\n", - " pg_engine = _create_engine(url, pool_pre_ping=True, pool_recycle=1800)\n", - " with pg_engine.connect() as c:\n", - " c.execute(__import__('sqlalchemy').text('SELECT 1'))\n", - " DATASTAGE_OK = True\n", - " DATASTAGE_MSG = f\"Postgres conectado: {PG_CONFIG['host']}:{PG_CONFIG['port']}/{PG_CONFIG['dbname']}\"\n", - " return True\n", - " except Exception as e:\n", - " pg_engine = None\n", - " DATASTAGE_OK = False\n", - " DATASTAGE_MSG = f'Postgres no disponible: {e}'\n", - " return False\n", - "\n", - "conectar_postgres()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "logic-descargas", - "metadata": { - "tags": [ - "hide-input" - ] - }, - "outputs": [], - "source": [ - "def _load_catalogos():\n", - " if 'df_boms_all' in _state: return\n", - " _state['df_facturas_NA'] = pd.read_sql(\n", - " \"SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO, ESTATUS FROM SFacExp WHERE ESTATUS='NA' ORDER BY FECHAFACTURA_ISO\", scaii_conn)\n", - " _state['df_partidas_all'] = pd.read_sql(\n", - " \"SELECT FACTURAEXPO, LINEA, NUMPARTE AS PT, CANTEXPO, ISNULL(PESONETO,0) AS PESONETO, ISNULL(PESONETOKGS,0) AS PESONETOKGS, MONTOIGIME FROM SPartidasExpo\", scaii_conn)\n", - " _state['df_boms_all'] = pd.read_sql(\"SELECT NUMPARTE AS PT, NUMPARTEBOM AS COMPONENTE_MP, CANTIDAD, UNIMED FROM SMatBOM\", scaii_conn)\n", - " _state['df_sustitutos_all']= pd.read_sql(\"SELECT NUMPARTE, NUMPARTESUSTITUTO AS COMPONENTE_ALTERNO, UNIDADMEDIDA1, UNIDADMEDIDA2 FROM SPartesSustitutos\", scaii_conn)\n", - " _state['df_partepais_all'] = pd.read_sql(\"SELECT FRACCION, PAIS, TIPOFRACCION, TASAIM FROM SPartePais\", scaii_conn)\n", - "\n", - "def _load_saldos_snapshot():\n", - " df = pd.read_sql(\"\"\"\n", - " SELECT NUMPARTE, FACTURAIMPO, PEDIMENTOIMPO, CLASE, PAISORIGEN, FRACCIONIMPO,\n", - " FECHAFACTURA_ISO AS FECHA_ENTRADA, FECHAVENC_ISO AS FECHA_EXPIRACION,\n", - " UMEXITENCIA AS UNIDAD_MEDIDA, CANTEXITENCIA AS CANT_LOTE_ORIG,\n", - " VALORIMPOMN, VALORIMPOME, PESONETO AS PESONETO_LOTE,\n", - " (CANTEXITENCIA - (CANTUSADA + CANTUSADADESP)) AS SALDO_DISPONIBLE\n", - " FROM SSaldoTem\n", - " WHERE (CANTEXITENCIA - (CANTUSADA + CANTUSADADESP)) > 0\n", - " \"\"\", scaii_conn)\n", - " df['UM_KEY'] = df['UNIDAD_MEDIDA'].fillna('').str.strip().str.upper()\n", - " return df\n", - "\n", - "def _consumir(saldos, idx_list, faltante, base_row, tipo, np_usado):\n", - " rows = []\n", - " for idx in idx_list:\n", - " if faltante <= 1e-9: break\n", - " disp = float(saldos.at[idx, 'SALDO_DISPONIBLE'])\n", - " if disp <= 1e-9: continue\n", - " consumo = min(faltante, disp)\n", - " cant_orig = float(saldos.at[idx, 'CANT_LOTE_ORIG'] or 0)\n", - " prop = (consumo / cant_orig) if cant_orig > 0 else 0.0\n", - " rows.append({**base_row,\n", - " 'NUMPARTE_USADO': np_usado, 'TIPO': tipo,\n", - " 'FACTURAIMPO_SALDO': saldos.at[idx, 'FACTURAIMPO'],\n", - " 'PEDIMENTOIMPO': saldos.at[idx, 'PEDIMENTOIMPO'],\n", - " 'CLASE': saldos.at[idx, 'CLASE'],\n", - " 'PAISMERCANCIA': saldos.at[idx, 'PAISORIGEN'],\n", - " 'FRACCION_SALDO': saldos.at[idx, 'FRACCIONIMPO'],\n", - " 'UNIMED_SALDO': saldos.at[idx, 'UNIDAD_MEDIDA'],\n", - " 'FECHA_ENTRADA': saldos.at[idx, 'FECHA_ENTRADA'],\n", - " 'FECHA_EXPIRACION': saldos.at[idx, 'FECHA_EXPIRACION'],\n", - " 'CANT_LOTE_ORIG': cant_orig,\n", - " 'CANT_DESCARGADA': consumo,\n", - " 'VALORMN': float(saldos.at[idx, 'VALORIMPOMN'] or 0) * prop,\n", - " 'VALORME': float(saldos.at[idx, 'VALORIMPOME'] or 0) * prop,\n", - " 'PESONETO_DESC': float(saldos.at[idx, 'PESONETO_LOTE'] or 0) * prop,\n", - " 'STATUS': 'OK'})\n", - " saldos.at[idx, 'SALDO_DISPONIBLE'] = disp - consumo\n", - " faltante -= consumo\n", - " return faltante, rows\n", - "\n", - "def _saldos_idx(saldos, numparte, um_keys, fecha_export):\n", - " um_match = saldos['UM_KEY'] == um_keys if isinstance(um_keys, str) else saldos['UM_KEY'].isin(um_keys)\n", - " mask = ((saldos['NUMPARTE'] == numparte) & um_match\n", - " & (saldos['SALDO_DISPONIBLE'] > 1e-9)\n", - " & (saldos['FECHA_ENTRADA'] <= fecha_export)\n", - " & (saldos['FECHA_EXPIRACION'] >= fecha_export))\n", - " return saldos[mask].sort_values('FECHA_ENTRADA').index.tolist()\n", - "\n", - "def _f(v):\n", - " if pd.isna(v): return None\n", - " if isinstance(v, str):\n", - " v = v.strip()\n", - " if v == '': return None\n", - " try: return float(v)\n", - " except ValueError: return None\n", - " try: return float(v)\n", - " except: return None\n", - "def _adv(v):\n", - " if pd.isna(v): return None\n", - " if isinstance(v, str):\n", - " v = v.strip()\n", - " if v == '': return None\n", - " try: return float(v)\n", - " except ValueError: return v\n", - " return v\n", - "def _s(v): return None if pd.isna(v) else v\n", - "def _f0(v):\n", - " r = _f(v); return 0.0 if r is None else r\n", - "\n", - "INSERT_DESC_SQL = \"\"\"INSERT INTO SDescargaT\n", - " (CONSECUTIVO, FACTEXPO, FACREFERENCIA, FACTIMPO, PEDIMENTOIMPO, PEDIMENTOEXPO, CLASE,\n", - " VALORMN, VALORME, PESONETO, PESOBRUTO, PAISMERCANCIA, FECHADESC, TIPOFRACCION,\n", - " NUMPARTE, CANTDESC, UNIMED, LINEAEXPO, PARTEORIGINAL, PORUTILERIA, TIPOMATEXPO,\n", - " MONTOIGI, ADVALOREMIMPO, TIPODESC)\n", - " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\"\"\"\n", - "UPDATE_FACEXP_SQL = \"UPDATE SFacExp SET ESTATUS='AC' WHERE FACTURAEXPO=?\"\n", - "UPDATE_SALDO_SQL = \"\"\"UPDATE SSaldoTem\n", - " SET CANTUSADA=ISNULL(CANTUSADA,0)+?, VALORUSADOMN=ISNULL(VALORUSADOMN,0)+?,\n", - " VALORUSADOME=ISNULL(VALORUSADOME,0)+?, PESOUSADO=ISNULL(PESOUSADO,0)+?,\n", - " PESOBRUTOUSADO=ISNULL(PESOBRUTOUSADO,0)+?\n", - " WHERE FACTURAIMPO=? AND NUMPARTE=? AND UMEXITENCIA=?\"\"\"\n", - "\n", - "def _build_descarga_df(modo_calc='STANDARD', facturas_df=None, partidas_df=None, prev_dict=None, progress=None):\n", - " prog = _Progress(progress)\n", - " _load_catalogos()\n", - " saldos = _load_saldos_snapshot()\n", - " f_use = facturas_df if facturas_df is not None else _state['df_facturas_NA']\n", - " p_use = partidas_df if partidas_df is not None else _state['df_partidas_all']\n", - " b_all, s_all, pp_all = _state['df_boms_all'], _state['df_sustitutos_all'], _state['df_partepais_all']\n", - " partidas_por_factura = p_use.groupby('FACTURAEXPO')\n", - " boms_por_pt = b_all.groupby('PT')\n", - " sustitutos_por_comp = s_all.groupby('NUMPARTE')\n", - " partida_montoigi = p_use.set_index(['FACTURAEXPO','LINEA'])['MONTOIGIME'].to_dict()\n", - " partepais_dict = pp_all.drop_duplicates(subset=['FRACCION','PAIS'], keep='last').set_index(['FRACCION','PAIS'])[['TIPOFRACCION','TASAIM']].to_dict('index')\n", - " prog.setup(len(f_use), 'Procesando facturas')\n", - " all_rows = []\n", - " for i, (_, f) in enumerate(f_use.iterrows()):\n", - " factura, fecha_export, pedimento_ex = f['FACTURAEXPO'], f['FECHAFACTURA_ISO'], f.get('PEDIMENTOEXPO')\n", - " if factura not in partidas_por_factura.groups:\n", - " prog.step(); continue\n", - " for _, p in partidas_por_factura.get_group(factura).iterrows():\n", - " if p['PT'] not in boms_por_pt.groups: continue\n", - " linea = int(p['LINEA']) if pd.notna(p['LINEA']) else 0\n", - " montoigi = partida_montoigi.get((factura, p['LINEA']))\n", - " peso_neto = float(p.get('PESONETO', 0) or 0)\n", - " for _, c in boms_por_pt.get_group(p['PT']).iterrows():\n", - " comp = c['COMPONENTE_MP']\n", - " unimed_bom = (c['UNIMED'] or '').strip().upper()\n", - " if modo_calc == 'KG_PCT':\n", - " cant_req_total = (float(c['CANTIDAD']) / 100.0) * peso_neto\n", - " else:\n", - " cant_req_total = float(p['CANTEXPO']) * float(c['CANTIDAD'])\n", - " cant_prev = 0.0\n", - " if prev_dict is not None:\n", - " cant_prev = float(prev_dict.get((factura, linea, comp), 0.0))\n", - " cant_pend = cant_req_total - cant_prev\n", - " if prev_dict is not None and cant_pend <= 1e-9: continue\n", - " cant_req = cant_pend if prev_dict is not None else cant_req_total\n", - " base_row = {'FACTURAEXPO': factura, 'FECHA_FACTURAEXPO': fecha_export, 'PEDIMENTOEXPO': pedimento_ex,\n", - " 'LINEA': linea, 'PT': p['PT'], 'CANT_PT': float(p['CANTEXPO']),\n", - " 'PESONETO_PARTIDA': peso_neto, 'PCT_BOM': float(c['CANTIDAD']),\n", - " 'MONTOIGI': montoigi, 'COMPONENTE_MP': comp, 'UNIMED_BOM': unimed_bom,\n", - " 'CANT_REQUERIDA': cant_req, 'CANT_PREV': cant_prev}\n", - " faltante = cant_req\n", - " faltante, r = _consumir(saldos, _saldos_idx(saldos, comp, unimed_bom, fecha_export), faltante, base_row, 'ORIGINAL', comp)\n", - " all_rows.extend(r)\n", - " if faltante > 1e-9 and comp in sustitutos_por_comp.groups:\n", - " for _, sub in sustitutos_por_comp.get_group(comp).iterrows():\n", - " if faltante <= 1e-9: break\n", - " um_keys = [str(u).strip().upper() for u in [sub.get('UNIDADMEDIDA1'), sub.get('UNIDADMEDIDA2')] if u and str(u).strip()]\n", - " if not um_keys: continue\n", - " faltante, r = _consumir(saldos, _saldos_idx(saldos, sub['COMPONENTE_ALTERNO'], um_keys, fecha_export),\n", - " faltante, base_row, 'SUSTITUTO', sub['COMPONENTE_ALTERNO'])\n", - " all_rows.extend(r)\n", - " if faltante > 1e-9:\n", - " all_rows.append({**base_row, 'NUMPARTE_USADO': None, 'TIPO': 'FALTANTE',\n", - " 'FACTURAIMPO_SALDO': None, 'PEDIMENTOIMPO': None, 'CLASE': None,\n", - " 'PAISMERCANCIA': None, 'FRACCION_SALDO': None, 'UNIMED_SALDO': None,\n", - " 'FECHA_ENTRADA': None, 'FECHA_EXPIRACION': None, 'CANT_LOTE_ORIG': None,\n", - " 'CANT_DESCARGADA': faltante, 'VALORMN': None, 'VALORME': None, 'PESONETO_DESC': None,\n", - " 'STATUS': 'FALTANTE'})\n", - " prog.step()\n", - " df = pd.DataFrame(all_rows)\n", - " if not df.empty:\n", - " def _pp(row):\n", - " info = partepais_dict.get((row['FRACCION_SALDO'], row['PAISMERCANCIA']))\n", - " if info is None: return pd.Series([None, None])\n", - " return pd.Series([info.get('TIPOFRACCION'), info.get('TASAIM')])\n", - " df[['TIPOFRACCION','ADVALOREMIMPO']] = df.apply(_pp, axis=1)\n", - " df['FECHADESC_CLARION'] = df['FECHA_FACTURAEXPO'].apply(to_clarion)\n", - " prog.done('Pronostico listo')\n", - " return df\n", - "\n", - "def _build_cobertura(df):\n", - " if df is None or df.empty or 'CANT_DESCARGADA' not in df.columns:\n", - " return pd.DataFrame(columns=['FACTURAEXPO','FECHA_FACTURAEXPO',\n", - " 'componentes_total','componentes_100pct','pct_promedio'])\n", - " df_d = df.copy()\n", - " df_d['cant_cubierta'] = df_d['CANT_DESCARGADA'].where(df_d['TIPO'].isin(['ORIGINAL','SUSTITUTO']), 0)\n", - " cob_comp = df_d.groupby(['FACTURAEXPO','FECHA_FACTURAEXPO','LINEA','PT','COMPONENTE_MP','UNIMED_BOM','CANT_REQUERIDA'],\n", - " as_index=False, dropna=False).agg(cant_cubierta=('cant_cubierta','sum'))\n", - " cob_comp['pct'] = ((cob_comp['cant_cubierta'] / cob_comp['CANT_REQUERIDA']).fillna(0) * 100).round(2).clip(upper=100)\n", - " cob_fact = cob_comp.groupby(['FACTURAEXPO','FECHA_FACTURAEXPO'], as_index=False, dropna=False).agg(\n", - " componentes_total=('COMPONENTE_MP','count'),\n", - " componentes_100pct=('pct', lambda s: (s >= 99.99).sum()),\n", - " pct_promedio=('pct','mean'))\n", - " cob_fact['pct_promedio'] = cob_fact['pct_promedio'].round(2)\n", - " return cob_fact\n", - "\n", - "def calcular_pronostico_paso8(progress=None):\n", - " df = _build_descarga_df(modo_calc='STANDARD', progress=progress)\n", - " cob = _build_cobertura(df)\n", - " _state['df_descarga_all'] = df\n", - " _state['cobertura_factura'] = cob\n", - " return df, cob\n", - "\n", - "def calcular_pronostico_paso12_kg(progress=None):\n", - " df = _build_descarga_df(modo_calc='KG_PCT', progress=progress)\n", - " cob = _build_cobertura(df)\n", - " _state['df_descarga_kg'] = df\n", - " _state['cobertura_factura_kg']= cob\n", - " return df, cob\n", - "\n", - "def _do_inserts(df_ins, dry_run, log, do_update_status=True, progress=None):\n", - " prog = _Progress(progress)\n", - " if df_ins.empty:\n", - " log('Nada que insertar.'); prog.done('Nada que insertar'); return 0, 0, 0, []\n", - " with scaii_conn.cursor() as cur:\n", - " cur.execute(\"SELECT ISNULL(MAX(CONSECUTIVO),0) FROM SDescargaT\")\n", - " next_consec = int(cur.fetchone()[0]) + 1\n", - " log(f'Proximo CONSECUTIVO: {next_consec}')\n", - " grupos = list(df_ins.groupby('FACTURAEXPO'))\n", - " prog.setup(len(grupos), 'Insertando facturas')\n", - " inserted, updated, saldos_upd, errores = 0, 0, 0, []\n", - " for factura, df_f in grupos:\n", - " cb, ib, sb = next_consec, inserted, saldos_upd\n", - " ok = True\n", - " try:\n", - " with scaii_conn.cursor() as cur:\n", - " for _, r in df_f.iterrows():\n", - " if not dry_run:\n", - " cur.execute(INSERT_DESC_SQL, (\n", - " next_consec, r['FACTURAEXPO'], r['FACTURAEXPO'],\n", - " r['FACTURAIMPO_SALDO'], _s(r['PEDIMENTOIMPO']),\n", - " _s(r['PEDIMENTOEXPO']), _s(r['CLASE']),\n", - " _f(r['VALORMN']), _f(r['VALORME']),\n", - " _f(r['PESONETO_DESC']), _f(r['PESONETO_DESC']),\n", - " _s(r['PAISMERCANCIA']),\n", - " int(r['FECHADESC_CLARION']) if pd.notna(r['FECHADESC_CLARION']) else None,\n", - " _s(r['TIPOFRACCION']), r['NUMPARTE_USADO'],\n", - " float(r['CANT_DESCARGADA']),\n", - " r['UNIMED_SALDO'] if pd.notna(r['UNIMED_SALDO']) else r['UNIMED_BOM'],\n", - " int(r['LINEA']) if pd.notna(r['LINEA']) else 0,\n", - " r['COMPONENTE_MP'], 1, '',\n", - " _f(r['MONTOIGI']), _adv(r['ADVALOREMIMPO']), '0 PARTE'))\n", - " cur.execute(UPDATE_SALDO_SQL, (\n", - " float(r['CANT_DESCARGADA']),\n", - " _f0(r['VALORMN']), _f0(r['VALORME']),\n", - " _f0(r['PESONETO_DESC']), _f0(r['PESONETO_DESC']),\n", - " r['FACTURAIMPO_SALDO'], r['NUMPARTE_USADO'],\n", - " r['UNIMED_SALDO'] if pd.notna(r['UNIMED_SALDO']) else r['UNIMED_BOM']))\n", - " saldos_upd += 1\n", - " next_consec += 1\n", - " inserted += 1\n", - " if not dry_run and do_update_status:\n", - " cur.execute(UPDATE_FACEXP_SQL, (factura,))\n", - " if not dry_run: scaii_conn.commit()\n", - " except Exception as e:\n", - " ok = False\n", - " if not dry_run: scaii_conn.rollback()\n", - " next_consec, inserted, saldos_upd = cb, ib, sb\n", - " errores.append((factura, str(e)))\n", - " if ok and do_update_status: updated += 1\n", - " prog.step()\n", - " prog.done(f'{inserted} insertados')\n", - " return inserted, updated, saldos_upd, errores\n", - "\n", - "def _ejecutar_descarga_NA(modo, dry_run, fecha_desde, fecha_hasta, log, key_df, key_cob, paso_label, progress=None):\n", - " if key_df not in _state:\n", - " log(f'ERROR: corre primero el pronostico del {paso_label}.'); return\n", - " df_da, cob = _state[key_df], _state[key_cob]\n", - " if modo == 'NATURAL':\n", - " elig = cob[cob['componentes_100pct'] == cob['componentes_total']]['FACTURAEXPO'].tolist()\n", - " else:\n", - " elig = cob['FACTURAEXPO'].tolist()\n", - " if fecha_desde or fecha_hasta:\n", - " df_cf = cob.copy()\n", - " df_cf['_f'] = pd.to_datetime(df_cf['FECHA_FACTURAEXPO'], errors='coerce')\n", - " if fecha_desde: df_cf = df_cf[df_cf['_f'] >= pd.to_datetime(fecha_desde)]\n", - " if fecha_hasta: df_cf = df_cf[df_cf['_f'] <= pd.to_datetime(fecha_hasta)]\n", - " rango = set(df_cf['FACTURAEXPO']); elig = [f for f in elig if f in rango]\n", - " df_na = pd.read_sql(\"SELECT FACTURAEXPO FROM SFacExp WHERE ESTATUS='NA'\", scaii_conn)\n", - " set_na = set(df_na['FACTURAEXPO'].astype(str).str.strip())\n", - " omitidas = [f for f in elig if str(f).strip() not in set_na]\n", - " elig = [f for f in elig if str(f).strip() in set_na]\n", - " if omitidas: log(f' Omitidas (ya AC): {len(omitidas)}')\n", - " log(f'Modo: {modo} | DRY_RUN: {dry_run} | Facturas elegibles: {len(elig)}')\n", - " mask = df_da['FACTURAEXPO'].isin(elig) & df_da['TIPO'].isin(['ORIGINAL','SUSTITUTO'])\n", - " df_ins = df_da[mask].copy()\n", - " log(f'Filas a insertar: {len(df_ins)}')\n", - " inserted, updated, saldos_upd, errores = _do_inserts(df_ins, dry_run, log, do_update_status=True, progress=progress)\n", - " log(f'\\n=== RESUMEN {paso_label} ({modo}, DRY_RUN={dry_run}) ===')\n", - " log(f' Insertados : {inserted}')\n", - " log(f' Updates SSaldoTem : {saldos_upd}')\n", - " log(f' Facturas a AC : {updated}')\n", - " log(f' Errores : {len(errores)}')\n", - " for f, e in errores[:5]: log(f' {f}: {e}')\n", - "\n", - "def ejecutar_paso9(modo, dry_run, fecha_desde=None, fecha_hasta=None, log=print, progress=None):\n", - " _ejecutar_descarga_NA(modo, dry_run, fecha_desde, fecha_hasta, log,\n", - " 'df_descarga_all', 'cobertura_factura', 'paso 9', progress=progress)\n", - "\n", - "def ejecutar_paso12_kg(modo, dry_run, fecha_desde=None, fecha_hasta=None, log=print, progress=None):\n", - " _ejecutar_descarga_NA(modo, dry_run, fecha_desde, fecha_hasta, log,\n", - " 'df_descarga_kg', 'cobertura_factura_kg', 'paso 12 (% KGS)', progress=progress)\n", - "\n", - "def _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, modo_calc, paso_label, progress=None):\n", - " prog = _Progress(progress)\n", - " prog.setup(1, 'Cargando facturas AC...')\n", - " sql_fact = \"SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO FROM SFacExp WHERE ESTATUS='AC'\"\n", - " params = []\n", - " if facturas_objetivo:\n", - " sql_fact += f\" AND FACTURAEXPO IN ({','.join(['?']*len(facturas_objetivo))})\"\n", - " params.extend(list(facturas_objetivo))\n", - " if fecha_desde:\n", - " sql_fact += \" AND FECHAFACTURA_ISO >= ?\"; params.append(fecha_desde)\n", - " if fecha_hasta:\n", - " sql_fact += \" AND FECHAFACTURA_ISO <= ?\"; params.append(fecha_hasta)\n", - " sql_fact += \" ORDER BY FECHAFACTURA_ISO\"\n", - " df_fact_ac = pd.read_sql(sql_fact, scaii_conn, params=params)\n", - " log(f'Facturas AC: {len(df_fact_ac)}')\n", - " if df_fact_ac.empty: prog.done('Sin facturas'); return\n", - " factura_list = df_fact_ac['FACTURAEXPO'].tolist()\n", - " prev_parts = []\n", - " for i in range(0, len(factura_list), 500):\n", - " chunk = factura_list[i:i+500]\n", - " ph = ','.join(['?']*len(chunk))\n", - " prev_parts.append(pd.read_sql(f\"\"\"\n", - " SELECT FACTEXPO AS FACTURAEXPO, LINEAEXPO AS LINEA, PARTEORIGINAL AS COMPONENTE_MP,\n", - " SUM(CANTDESC) AS CANT_PREV\n", - " FROM SDescargaT WHERE FACTEXPO IN ({ph})\n", - " GROUP BY FACTEXPO, LINEAEXPO, PARTEORIGINAL\n", - " \"\"\", scaii_conn, params=chunk))\n", - " df_desc_prev = pd.concat(prev_parts) if prev_parts else pd.DataFrame(columns=['FACTURAEXPO','LINEA','COMPONENTE_MP','CANT_PREV'])\n", - " df_desc_prev['LINEA'] = df_desc_prev['LINEA'].astype(int)\n", - " prev_dict = df_desc_prev.set_index(['FACTURAEXPO','LINEA','COMPONENTE_MP'])['CANT_PREV'].to_dict()\n", - " _load_catalogos()\n", - " df_comp = _build_descarga_df(modo_calc=modo_calc, facturas_df=df_fact_ac, prev_dict=prev_dict, progress=progress)\n", - " log(f'Filas calculadas: {len(df_comp)}')\n", - " if df_comp.empty: return\n", - " if modo_comp == 'NATURAL':\n", - " bad = set(df_comp[df_comp['TIPO']=='FALTANTE']['FACTURAEXPO'])\n", - " df_to_ins = df_comp[~df_comp['FACTURAEXPO'].isin(bad) & df_comp['TIPO'].isin(['ORIGINAL','SUSTITUTO'])]\n", - " log(f'Excluidas en NATURAL (con faltante): {len(bad)}')\n", - " else:\n", - " df_to_ins = df_comp[df_comp['TIPO'].isin(['ORIGINAL','SUSTITUTO'])]\n", - " log(f'Filas a insertar: {len(df_to_ins)}')\n", - " inserted, _, saldos_upd, errores = _do_inserts(df_to_ins, dry_run, log, do_update_status=False, progress=progress)\n", - " log(f'\\n=== RESUMEN {paso_label} ({modo_comp}, DRY_RUN={dry_run}) ===')\n", - " log(f' Insertados : {inserted}')\n", - " log(f' Updates SSaldoTem : {saldos_upd}')\n", - " log(f' Errores : {len(errores)}')\n", - " for f, e in errores[:5]: log(f' {f}: {e}')\n", - "\n", - "def ejecutar_paso10(modo_comp, dry_run, fecha_desde=None, fecha_hasta=None, facturas_objetivo=None, log=print, progress=None):\n", - " _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, 'STANDARD', 'paso 10', progress=progress)\n", - "\n", - "def ejecutar_paso12_complementaria_kg(modo_comp, dry_run, fecha_desde=None, fecha_hasta=None, facturas_objetivo=None, log=print, progress=None):\n", - " _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, 'KG_PCT', 'paso 12 complementaria (% KGS)', progress=progress)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "logic-analisis-y-nlp", - "metadata": { - "tags": [ - "hide-input" - ] - }, - "outputs": [], - "source": [ - "def cargar_analisis_saldos(progress=None):\n", - " prog = _Progress(progress)\n", - " prog.setup(4, 'Cargando SSaldoTem...')\n", - " df = pd.read_sql(\"\"\"\n", - " SELECT NUMPARTE, FACTURAIMPO, PEDIMENTOIMPO, FRACCIONIMPO, PAISORIGEN, CLASE, SECTOR, DESCRIPCIONE,\n", - " UMEXITENCIA AS UNIDAD_MEDIDA, FECHAFACTURA_ISO AS FECHA_ENTRADA, FECHAVENC_ISO AS FECHA_EXPIRACION,\n", - " CANTEXITENCIA AS CANT_LOTE, ISNULL(CANTUSADA,0) AS CANT_USADA, ISNULL(CANTUSADADESP,0) AS CANT_USADA_DESP,\n", - " (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_CANT,\n", - " ISNULL(VALORIMPOMN,0) AS VALOR_LOTE_MN, ISNULL(VALORIMPOME,0) AS VALOR_LOTE_ME,\n", - " ISNULL(VALORUSADOMN,0) AS VALOR_USADO_MN, ISNULL(VALORUSADOME,0) AS VALOR_USADO_ME,\n", - " (ISNULL(VALORIMPOMN,0)-ISNULL(VALORUSADOMN,0)) AS SALDO_VMN,\n", - " (ISNULL(VALORIMPOME,0)-ISNULL(VALORUSADOME,0)) AS SALDO_VME,\n", - " ISNULL(PESONETO,0) AS PESO_NETO_LOTE, ISNULL(PESOBRUTO,0) AS PESO_BRUTO_LOTE,\n", - " ISNULL(PESOUSADO,0) AS PESO_USADO, (ISNULL(PESONETO,0)-ISNULL(PESOUSADO,0)) AS SALDO_PESO_NETO\n", - " FROM SSaldoTem\n", - " \"\"\", scaii_conn)\n", - " prog.step(desc='Procesando fechas...')\n", - " df['FECHA_ENTRADA'] = pd.to_datetime(df['FECHA_ENTRADA'], errors='coerce')\n", - " df['ANIO_ENTRADA'] = df['FECHA_ENTRADA'].dt.year\n", - " _state['df_saldos_full'] = df\n", - " prog.step(desc='SSaldoTem listo')\n", - " return df\n", - "\n", - "def calcular_por_anio_saldos(df):\n", - " return (df[df['SALDO_CANT']>0].groupby('ANIO_ENTRADA', as_index=False, dropna=False)\n", - " .agg(lotes=('NUMPARTE','count'), partes_unicas=('NUMPARTE','nunique'),\n", - " saldo_cant=('SALDO_CANT','sum'), saldo_vmn=('SALDO_VMN','sum'),\n", - " saldo_vme=('SALDO_VME','sum'), saldo_peso_neto=('SALDO_PESO_NETO','sum'))\n", - " .sort_values('ANIO_ENTRADA'))\n", - "\n", - "def calcular_impo_expo_anio(progress=None):\n", - " prog = _Progress(progress)\n", - " prog.setup(3, 'Cargando IMPO...')\n", - " df_imp = pd.read_sql(\"\"\"\n", - " SELECT YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')) AS ANIO,\n", - " COUNT(*) AS partidas_impo, COUNT(DISTINCT NUMPARTE) AS partes_impo,\n", - " SUM(PESONETO) AS peso_neto_impo, SUM(VALORIMPOME) AS valor_me_impo\n", - " FROM SPartidasImpo WHERE FECHAFACTURA IS NOT NULL\n", - " GROUP BY YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01'))\n", - " \"\"\", scaii_conn)\n", - " prog.step(desc='Cargando EXPO...')\n", - " df_exp = pd.read_sql(\"\"\"\n", - " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", - " COUNT(*) AS partidas_expo, COUNT(DISTINCT p.NUMPARTE) AS partes_expo,\n", - " SUM(p.PESONETO) AS peso_neto_expo, SUM(p.VALORTOTALME) AS valor_me_expo\n", - " FROM SPartidasExpo p INNER JOIN SFacExp f ON f.FACTURAEXPO = p.FACTURAEXPO\n", - " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", - " GROUP BY YEAR(f.FECHAFACTURA_ISO)\n", - " \"\"\", scaii_conn)\n", - " prog.step(desc='Comparando...')\n", - " cmp = (df_imp.merge(df_exp, on='ANIO', how='outer').fillna(0).sort_values('ANIO').reset_index(drop=True))\n", - " cmp['ANIO'] = cmp['ANIO'].astype(int)\n", - " for c in ['partidas_impo','partes_impo','partidas_expo','partes_expo']: cmp[c] = cmp[c].astype(int)\n", - " cmp['dif_peso'] = (cmp['peso_neto_expo'] - cmp['peso_neto_impo']).round(2)\n", - " cmp['dif_valor_me'] = (cmp['valor_me_expo'] - cmp['valor_me_impo']).round(2)\n", - " cmp['ratio_peso_expo_impo'] = (cmp['peso_neto_expo'] / cmp['peso_neto_impo'].replace(0, np.nan)).round(4)\n", - " cmp['ratio_valor_expo_impo'] = (cmp['valor_me_expo'] / cmp['valor_me_impo'].replace(0, np.nan)).round(4)\n", - " prog.done('Comparativo listo')\n", - " return df_imp, df_exp, cmp\n", - "\n", - "\n", - "\n", - "def graficar_impo_expo(cmp):\n", - " plt.close('all')\n", - " fig, axes = plt.subplots(2, 1, figsize=(11, 8))\n", - " fig.suptitle('IMPO vs EXPO por a-o', fontsize=14, fontweight='bold', y=1.0)\n", - " x = cmp['ANIO'].astype(int).values\n", - " xpos = np.arange(len(x)); ancho = 0.4\n", - " cI, cE = '#1976D2', '#F57C00'\n", - " def lab(ax, bars, color, fmt='{:,.0f}'):\n", - " for b in bars:\n", - " h = b.get_height()\n", - " if h > 0:\n", - " ax.text(b.get_x()+b.get_width()/2, h, fmt.format(h),\n", - " ha='center', va='bottom', fontsize=7, color=color, rotation=90)\n", - " ax = axes[0]\n", - " b1 = ax.bar(xpos-ancho/2, cmp['peso_neto_impo'], ancho, label='IMPO', color=cI)\n", - " b2 = ax.bar(xpos+ancho/2, cmp['peso_neto_expo'], ancho, label='EXPO', color=cE)\n", - " ax.set_title('Peso neto'); ax.set_xticks(xpos); ax.set_xticklabels(x)\n", - " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", - " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", - " ax.set_ylim(top=ax.get_ylim()[1]*1.18); lab(ax, b1, cI); lab(ax, b2, cE)\n", - " ax = axes[1]\n", - " b1 = ax.bar(xpos-ancho/2, cmp['valor_me_impo'], ancho, label='IMPO', color=cI)\n", - " b2 = ax.bar(xpos+ancho/2, cmp['valor_me_expo'], ancho, label='EXPO', color=cE)\n", - " ax.set_title('Valor ME (USD)'); ax.set_xticks(xpos); ax.set_xticklabels(x); ax.set_xlabel('A-o')\n", - " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'${v:,.0f}'))\n", - " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", - " ax.set_ylim(top=ax.get_ylim()[1]*1.18); lab(ax, b1, cI, '${:,.0f}'); lab(ax, b2, cE, '${:,.0f}')\n", - " plt.tight_layout()\n", - " return fig\n", - "\n", - "def graficar_saldos_anio(por_anio):\n", - " plt.close('all')\n", - " fig, axes = plt.subplots(1, 2, figsize=(13, 5))\n", - " fig.suptitle('Saldo disponible por a-o de entrada', fontsize=13, fontweight='bold')\n", - " x = por_anio['ANIO_ENTRADA'].astype(int).astype(str).values\n", - " axes[0].bar(x, por_anio['saldo_cant'], color='#42A5F5')\n", - " axes[0].set_title('Cantidad'); axes[0].grid(axis='y', linestyle=':', alpha=0.5)\n", - " axes[0].yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", - " for i, v in enumerate(por_anio['saldo_cant'].values):\n", - " if v > 0: axes[0].text(i, v, f'{v:,.0f}', ha='center', va='bottom', fontsize=8, rotation=90)\n", - " axes[1].bar(x, por_anio['saldo_vmn'], color='#FFA726', label='MN', alpha=0.85)\n", - " axes[1].bar(x, por_anio['saldo_vme'], color='#7E57C2', label='ME', alpha=0.55)\n", - " axes[1].set_title('Valor (MN + ME)'); axes[1].legend(); axes[1].grid(axis='y', linestyle=':', alpha=0.5)\n", - " axes[1].yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'${v:,.0f}'))\n", - " plt.tight_layout()\n", - " return fig\n", - "\n", - "def calcular_pesos_por_anio(progress=None):\n", - " prog = _Progress(progress)\n", - " prog.setup(4, 'Cargando IMPO...')\n", - " df_imp = pd.read_sql(\"\"\"\n", - " SELECT YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')) AS ANIO,\n", - " SUM(PESONETO) AS peso_impo\n", - " FROM SPartidasImpo WHERE FECHAFACTURA IS NOT NULL\n", - " GROUP BY YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01'))\n", - " \"\"\", scaii_conn)\n", - " prog.step(desc='Cargando EXPO...')\n", - " df_exp = pd.read_sql(\"\"\"\n", - " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", - " SUM(p.PESONETO) AS peso_expo\n", - " FROM SPartidasExpo p INNER JOIN SFacExp f ON f.FACTURAEXPO = p.FACTURAEXPO\n", - " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", - " GROUP BY YEAR(f.FECHAFACTURA_ISO)\n", - " \"\"\", scaii_conn)\n", - " prog.step(desc='Cargando CONSUMIDO...')\n", - " df_cons = pd.read_sql(\"\"\"\n", - " SELECT YEAR(FECHAFACTURA_ISO) AS ANIO,\n", - " SUM(ISNULL(PESOUSADO,0)) AS peso_consumido\n", - " FROM SSaldoTem WHERE FECHAFACTURA_ISO IS NOT NULL\n", - " GROUP BY YEAR(FECHAFACTURA_ISO)\n", - " \"\"\", scaii_conn)\n", - " prog.step(desc='Cargando DESCARGAS...')\n", - " df_desc = pd.read_sql(\"\"\"\n", - " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", - " SUM(ISNULL(d.PESONETO,0)) AS peso_descargas\n", - " FROM SDescargaT d INNER JOIN SFacExp f ON f.FACTURAEXPO = d.FACTEXPO\n", - " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", - " GROUP BY YEAR(f.FECHAFACTURA_ISO)\n", - " \"\"\", scaii_conn)\n", - " cmp = (df_imp.merge(df_exp, on='ANIO', how='outer')\n", - " .merge(df_cons, on='ANIO', how='outer')\n", - " .merge(df_desc, on='ANIO', how='outer')\n", - " .fillna(0).sort_values('ANIO').reset_index(drop=True))\n", - " cmp['ANIO'] = cmp['ANIO'].astype(int)\n", - " for c in ['peso_impo','peso_expo','peso_consumido','peso_descargas']:\n", - " cmp[c] = cmp[c].round(2)\n", - " prog.done('Pesos por anio listos')\n", - " return cmp\n", - "\n", - "def graficar_pesos_anio(cmp):\n", - " plt.close('all')\n", - " fig, ax = plt.subplots(figsize=(13, 5.5))\n", - " fig.suptitle('Peso por a-o - IMPO / EXPO / CONSUMIDO / DESCARGAS', fontsize=13, fontweight='bold')\n", - " x = cmp['ANIO'].astype(int).values\n", - " xpos = np.arange(len(x)); ancho = 0.2\n", - " cI, cE, cC, cD = '#1976D2', '#F57C00', '#43A047', '#8E24AA'\n", - " b1 = ax.bar(xpos-1.5*ancho, cmp['peso_impo'], ancho, label='IMPO', color=cI)\n", - " b2 = ax.bar(xpos-0.5*ancho, cmp['peso_expo'], ancho, label='EXPO', color=cE)\n", - " b3 = ax.bar(xpos+0.5*ancho, cmp['peso_consumido'], ancho, label='CONSUMIDO', color=cC)\n", - " b4 = ax.bar(xpos+1.5*ancho, cmp['peso_descargas'], ancho, label='DESCARGAS', color=cD)\n", - " ax.set_xticks(xpos); ax.set_xticklabels(x); ax.set_xlabel('A-o')\n", - " ax.set_ylabel('Peso neto')\n", - " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", - " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", - " ax.set_ylim(top=ax.get_ylim()[1]*1.18)\n", - " def lab(bars, color):\n", - " for b in bars:\n", - " h = b.get_height()\n", - " if h > 0:\n", - " ax.text(b.get_x()+b.get_width()/2, h, f'{h:,.0f}',\n", - " ha='center', va='bottom', fontsize=6, color=color, rotation=90)\n", - " lab(b1, cI); lab(b2, cE); lab(b3, cC); lab(b4, cD)\n", - " plt.tight_layout()\n", - " return fig\n", - "\n", - "def calcular_cantidades_por_anio(progress=None):\n", - " prog = _Progress(progress)\n", - " prog.setup(2, 'Cargando cantidades IMPO...')\n", - " df_imp = pd.read_sql(\"\"\"\n", - " SELECT YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')) AS ANIO,\n", - " COUNT(*) AS partidas_impo,\n", - " SUM(CANTIMPO) AS cantidad_impo\n", - " FROM SPartidasImpo WHERE FECHAFACTURA IS NOT NULL\n", - " GROUP BY YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01'))\n", - " \"\"\", scaii_conn)\n", - " prog.step(desc='Cargando cantidades EXPO...')\n", - " df_exp = pd.read_sql(\"\"\"\n", - " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", - " COUNT(*) AS partidas_expo,\n", - " SUM(p.CANTEXPO) AS cantidad_expo\n", - " FROM SPartidasExpo p INNER JOIN SFacExp f ON f.FACTURAEXPO = p.FACTURAEXPO\n", - " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", - " GROUP BY YEAR(f.FECHAFACTURA_ISO)\n", - " \"\"\", scaii_conn)\n", - " cmp = (df_imp.merge(df_exp, on='ANIO', how='outer').fillna(0).sort_values('ANIO').reset_index(drop=True))\n", - " cmp['ANIO'] = cmp['ANIO'].astype(int)\n", - " for c in ['partidas_impo','partidas_expo']: cmp[c] = cmp[c].astype(int)\n", - " cmp['cantidad_impo'] = cmp['cantidad_impo'].round(2)\n", - " cmp['cantidad_expo'] = cmp['cantidad_expo'].round(2)\n", - " cmp['diferencia'] = (cmp['cantidad_expo'] - cmp['cantidad_impo']).round(2)\n", - " prog.done('Cantidades por a-o listas')\n", - " return cmp\n", - "\n", - "def graficar_cantidades_anio(cmp):\n", - " plt.close('all')\n", - " fig, ax = plt.subplots(figsize=(12, 5.5))\n", - " fig.suptitle('Cantidades por a-o - IMPO vs EXPO', fontsize=13, fontweight='bold')\n", - " x = cmp['ANIO'].astype(int).values\n", - " xpos = np.arange(len(x)); ancho = 0.4\n", - " cI, cE = '#1976D2', '#F57C00'\n", - " b1 = ax.bar(xpos-ancho/2, cmp['cantidad_impo'], ancho, label='IMPO', color=cI)\n", - " b2 = ax.bar(xpos+ancho/2, cmp['cantidad_expo'], ancho, label='EXPO', color=cE)\n", - " ax.set_xticks(xpos); ax.set_xticklabels(x); ax.set_xlabel('A-o')\n", - " ax.set_ylabel('Cantidad (suma de unidades, varias UM)')\n", - " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", - " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", - " ax.set_ylim(top=ax.get_ylim()[1]*1.18)\n", - " def lab(bars, color):\n", - " for b in bars:\n", - " h = b.get_height()\n", - " if h > 0:\n", - " ax.text(b.get_x()+b.get_width()/2, h, f'{h:,.0f}',\n", - " ha='center', va='bottom', fontsize=7, color=color, rotation=90)\n", - " lab(b1, cI); lab(b2, cE)\n", - " plt.tight_layout()\n", - " return fig\n", - "\n", - "def calcular_cantidades_por_anio_um(progress=None):\n", - " prog = _Progress(progress)\n", - " prog.setup(2, 'Cargando cantidades IMPO por UM...')\n", - " df_imp = pd.read_sql(\"\"\"\n", - " SELECT YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')) AS ANIO,\n", - " UPPER(LTRIM(RTRIM(UNIMED))) AS UM,\n", - " COUNT(*) AS partidas_impo,\n", - " SUM(CANTIMPO) AS cantidad_impo\n", - " FROM SPartidasImpo WHERE FECHAFACTURA IS NOT NULL\n", - " GROUP BY YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')),\n", - " UPPER(LTRIM(RTRIM(UNIMED)))\n", - " \"\"\", scaii_conn)\n", - " prog.step(desc='Cargando cantidades EXPO por UM...')\n", - " df_exp = pd.read_sql(\"\"\"\n", - " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", - " UPPER(LTRIM(RTRIM(p.UNIMED))) AS UM,\n", - " COUNT(*) AS partidas_expo,\n", - " SUM(p.CANTEXPO) AS cantidad_expo\n", - " FROM SPartidasExpo p INNER JOIN SFacExp f ON f.FACTURAEXPO = p.FACTURAEXPO\n", - " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", - " GROUP BY YEAR(f.FECHAFACTURA_ISO),\n", - " UPPER(LTRIM(RTRIM(p.UNIMED)))\n", - " \"\"\", scaii_conn)\n", - " cmp = (df_imp.merge(df_exp, on=['ANIO','UM'], how='outer').fillna(0)\n", - " .sort_values(['ANIO','UM']).reset_index(drop=True))\n", - " cmp['ANIO'] = cmp['ANIO'].astype(int)\n", - " cmp['UM'] = cmp['UM'].fillna('').astype(str)\n", - " for c in ['partidas_impo','partidas_expo']: cmp[c] = cmp[c].astype(int)\n", - " cmp['cantidad_impo'] = cmp['cantidad_impo'].round(2)\n", - " cmp['cantidad_expo'] = cmp['cantidad_expo'].round(2)\n", - " cmp['diferencia'] = (cmp['cantidad_expo'] - cmp['cantidad_impo']).round(2)\n", - " prog.done('Cantidades por a-o + UM listas')\n", - " return cmp\n", - "\n", - "def graficar_cantidades_anio_um(cmp, top_ums=None):\n", - " plt.close('all')\n", - " if top_ums is None:\n", - " # Top 4 UMs por volumen total\n", - " totales = cmp.groupby('UM')[['cantidad_impo','cantidad_expo']].sum().sum(axis=1).sort_values(ascending=False)\n", - " top_ums = totales.head(4).index.tolist()\n", - " filt = cmp[cmp['UM'].isin(top_ums)].copy()\n", - " if filt.empty:\n", - " fig, ax = plt.subplots(figsize=(10, 3))\n", - " ax.text(0.5, 0.5, 'Sin datos', ha='center', va='center')\n", - " ax.axis('off')\n", - " return fig\n", - " n = len(top_ums)\n", - " fig, axes = plt.subplots(n, 1, figsize=(12, 3.2*n), squeeze=False)\n", - " fig.suptitle('Cantidades por a-o (separado por UM, top {} UMs)'.format(n), fontsize=13, fontweight='bold', y=1.0)\n", - " for idx, um in enumerate(top_ums):\n", - " sub = filt[filt['UM'] == um].sort_values('ANIO')\n", - " ax = axes[idx][0]\n", - " x = sub['ANIO'].astype(int).astype(str).values\n", - " xpos = np.arange(len(x)); ancho = 0.4\n", - " b1 = ax.bar(xpos-ancho/2, sub['cantidad_impo'], ancho, label='IMPO', color='#1976D2')\n", - " b2 = ax.bar(xpos+ancho/2, sub['cantidad_expo'], ancho, label='EXPO', color='#F57C00')\n", - " ax.set_title(f'UM = {um or \"(sin UM)\"}', fontsize=11, fontweight='bold')\n", - " ax.set_xticks(xpos); ax.set_xticklabels(x)\n", - " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", - " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", - " ax.set_ylim(top=ax.get_ylim()[1]*1.18 if ax.get_ylim()[1] > 0 else 1)\n", - " def lab(bars, color):\n", - " for b in bars:\n", - " h = b.get_height()\n", - " if h > 0:\n", - " ax.text(b.get_x()+b.get_width()/2, h, f'{h:,.0f}',\n", - " ha='center', va='bottom', fontsize=7, color=color, rotation=90)\n", - " lab(b1, '#1976D2'); lab(b2, '#F57C00')\n", - " plt.tight_layout()\n", - " return fig\n", - "\n", - "def exportar_excel_analisis(df, por_anio, df_imp, df_exp, cmp, cmp_pesos=None, cmp_cantidades=None, cmp_cantidades_um=None):\n", - " out = f'analisis_ssaldotem_{_dt.datetime.now().strftime(\"%Y%m%d_%H%M%S\")}.xlsx'\n", - " with pd.ExcelWriter(out, engine='openpyxl') as w:\n", - " df.to_excel(w, sheet_name='SSaldoTem_Detalle', index=False)\n", - " por_anio.to_excel(w, sheet_name='Por_Anio', index=False)\n", - " df_imp.to_excel(w, sheet_name='IMPO_x_Anio', index=False)\n", - " df_exp.to_excel(w, sheet_name='EXPO_x_Anio', index=False)\n", - " cmp.to_excel(w, sheet_name='IMPO_vs_EXPO', index=False)\n", - " if cmp_pesos is not None and not cmp_pesos.empty:\n", - " cmp_pesos.to_excel(w, sheet_name='Pesos_x_Anio', index=False)\n", - " if cmp_cantidades is not None and not cmp_cantidades.empty:\n", - " cmp_cantidades.to_excel(w, sheet_name='Cantidades_x_Anio', index=False)\n", - " if cmp_cantidades_um is not None and not cmp_cantidades_um.empty:\n", - " cmp_cantidades_um.to_excel(w, sheet_name='Cantidades_Anio_UM', index=False)\n", - " return os.path.abspath(out)\n", - "\n", - "def generar_sustitutos_nlp(min_sim=0.80, top_n=3, dry_run=True, log=print, progress=None):\n", - " from sklearn.feature_extraction.text import TfidfVectorizer\n", - " from sklearn.metrics.pairwise import cosine_similarity\n", - " prog = _Progress(progress)\n", - " prog.setup(5, 'Cargando catalogo SPartes...')\n", - " df_spartes = pd.read_sql(\"\"\"\n", - " SELECT NUMPARTE, DESCRIPCIONE, FRACCION, UNIMED FROM SPartes\n", - " WHERE DESCRIPCIONE IS NOT NULL AND LTRIM(RTRIM(DESCRIPCIONE)) <> ''\n", - " \"\"\", scaii_conn).drop_duplicates(subset='NUMPARTE').reset_index(drop=True)\n", - " log(f'Catalogo SPartes: {len(df_spartes):,}')\n", - " prog.step(desc='Cargando componentes BOM...')\n", - " df_comp = pd.read_sql(\"SELECT DISTINCT NUMPARTEBOM AS NUMPARTE FROM SMatBOM WHERE NUMPARTEBOM IS NOT NULL\", scaii_conn)\n", - " df_comp = df_comp.merge(df_spartes, on='NUMPARTE', how='left')\n", - " df_comp = df_comp[df_comp['DESCRIPCIONE'].notna()].reset_index(drop=True)\n", - " log(f'Componentes con descripcion: {len(df_comp):,}')\n", - " if df_comp.empty:\n", - " prog.done('Sin componentes'); log('Sin componentes para procesar.'); return\n", - " prog.step(desc='Vectorizando TF-IDF...')\n", - " def limpiar(t):\n", - " if pd.isna(t) or str(t).strip() == '': return ''\n", - " t = re.sub(r'[^\\w\\s]', ' ', str(t).upper().strip())\n", - " return re.sub(r'\\s+', ' ', t).strip()\n", - " def texto(row): return f\"{limpiar(row['DESCRIPCIONE'])} {str(row['UNIMED'] or '').upper().strip()}\".strip()\n", - " df_spartes['texto'] = df_spartes.apply(texto, axis=1)\n", - " df_comp['texto'] = df_comp.apply(texto, axis=1)\n", - " vec = TfidfVectorizer(ngram_range=(1,2), sublinear_tf=True, min_df=1, max_features=80000)\n", - " vec.fit(pd.concat([df_spartes['texto'], df_comp['texto']], ignore_index=True))\n", - " cat_mat = vec.transform(df_spartes['texto'])\n", - " upper = df_spartes['NUMPARTE'].astype(str).str.upper().values\n", - " # Reset progreso para el calculo de similitud por lote\n", - " total_batches = max(1, (len(df_comp) + 199) // 200)\n", - " prog.setup(total_batches, 'Calculando similitud...')\n", - " rows = []\n", - " BATCH = 200\n", - " for s in range(0, len(df_comp), BATCH):\n", - " e = min(s+BATCH, len(df_comp))\n", - " bm = vec.transform(df_comp.iloc[s:e]['texto'])\n", - " sims = cosine_similarity(bm, cat_mat)\n", - " for j, (_, comp) in enumerate(df_comp.iloc[s:e].iterrows()):\n", - " sr = sims[j].copy()\n", - " sr[upper == str(comp['NUMPARTE']).upper()] = 0.0\n", - " order = sr.argsort()[::-1]\n", - " rank = 1\n", - " for idx in order:\n", - " if sr[idx] < min_sim or rank > top_n: break\n", - " sust = df_spartes.iloc[idx]\n", - " rows.append({\n", - " 'NUMPARTE': str(comp['NUMPARTE']).strip(),\n", - " 'NUMPARTESUSTITUTO': str(sust['NUMPARTE']).strip(),\n", - " 'UNIMED_COMP': str(comp['UNIMED'] or '').strip(),\n", - " 'UNIMED_SUST': str(sust['UNIMED'] or '').strip(),\n", - " 'SIMILITUD': round(float(sr[idx]),4), 'RANK': rank,\n", - " })\n", - " rank += 1\n", - " prog.step(desc=f'Similitud {e}/{len(df_comp)}')\n", - " df_sust = pd.DataFrame(rows)\n", - " log(f'Sustitutos calculados: {len(df_sust):,}')\n", - " df_exist = pd.read_sql(\"SELECT NUMPARTE, NUMPARTESUSTITUTO FROM SPartesSustitutos\", scaii_conn)\n", - " pares = set(zip(df_exist['NUMPARTE'].astype(str).str.strip(),\n", - " df_exist['NUMPARTESUSTITUTO'].astype(str).str.strip()))\n", - " df_sust['_dup'] = df_sust.apply(lambda r: (r['NUMPARTE'], r['NUMPARTESUSTITUTO']) in pares, axis=1)\n", - " df_nuevos = df_sust[~df_sust['_dup']].drop(columns='_dup').reset_index(drop=True)\n", - " log(f' Duplicados (omitir): {df_sust[\"_dup\"].sum():,}')\n", - " log(f' NUEVOS a insertar: {len(df_nuevos):,}')\n", - " _state['df_nuevos_sust'] = df_nuevos\n", - " if df_nuevos.empty or dry_run:\n", - " if dry_run: log('DRY_RUN=True. Cambia para insertar.')\n", - " prog.done(f'{len(df_nuevos)} nuevos'); return\n", - " INS = \"\"\"INSERT INTO SPartesSustitutos\n", - " (NUMPARTE, NUMPARTESUSTITUTO, FACTORCONVERSION, UNIDADMEDIDA1, UNIDADMEDIDA2)\n", - " VALUES (?, ?, 0, ?, ?)\"\"\"\n", - " cur = scaii_conn.cursor()\n", - " total_lotes = max(1, (len(df_nuevos) + 499) // 500)\n", - " prog.setup(total_lotes, 'Insertando lotes...')\n", - " inserted = 0\n", - " for s in range(0, len(df_nuevos), 500):\n", - " e = min(s+500, len(df_nuevos))\n", - " params = [(r['NUMPARTE'], r['NUMPARTESUSTITUTO'],\n", - " r['UNIMED_COMP'] or r['UNIMED_SUST'],\n", - " r['UNIMED_SUST'] or r['UNIMED_COMP'])\n", - " for _, r in df_nuevos.iloc[s:e].iterrows()]\n", - " try:\n", - " cur.executemany(INS, params)\n", - " scaii_conn.commit()\n", - " inserted += len(params)\n", - " except Exception as e2:\n", - " scaii_conn.rollback()\n", - " log(f' ERROR lote {s}-{e}: {e2}')\n", - " prog.step()\n", - " log(f'Insertados: {inserted:,}')\n", - " prog.done(f'{inserted} insertados')\n", - "\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "logic-ctm", - "metadata": { - "tags": [ - "hide-input" - ] - }, - "outputs": [], - "source": [ - "# ============================================================\n", - "# CTM - Reasignacion de descargas de Cambio de Regimen a CTM\n", - "# ============================================================\n", - "\n", - "def cargar_excel_mapping_ctm(file_bytes_or_path):\n", - " \"\"\"Carga el Excel del cliente y normaliza.\n", - " Espera columnas: 'Facturas CTM', 'PEDIMENTO COMPLETO', 'PATENTE', 'ADUANA',\n", - " 'PEDIMENTO', 'Operacion', 'Clave de pedimento'.\n", - " Una sola celda 'Facturas CTM' puede traer varias facturas separadas por coma.\n", - " \"\"\"\n", - " import io\n", - " if isinstance(file_bytes_or_path, (bytes, bytearray)):\n", - " df = pd.read_excel(io.BytesIO(file_bytes_or_path))\n", - " else:\n", - " df = pd.read_excel(file_bytes_or_path)\n", - " # Normalizar nombres de columnas (acentos / mayusc)\n", - " norm = {c: c.strip() for c in df.columns}\n", - " df.rename(columns=norm, inplace=True)\n", - " col_ctm = next((c for c in df.columns if 'CTM' in c.upper() and 'FACTURA' in c.upper()), None)\n", - " col_ped = next((c for c in df.columns if 'PEDIMENTO' in c.upper() and 'COMPLETO' in c.upper()), None)\n", - " if col_ctm is None or col_ped is None:\n", - " raise ValueError('El Excel debe tener columnas Facturas CTM y PEDIMENTO COMPLETO')\n", - " # Expandir CTM separadas por coma\n", - " rows = []\n", - " for _, r in df.iterrows():\n", - " ctm_cell = str(r[col_ctm]) if pd.notna(r[col_ctm]) else ''\n", - " ped_cell = str(r[col_ped]) if pd.notna(r[col_ped]) else ''\n", - " if not ctm_cell.strip() or not ped_cell.strip():\n", - " continue\n", - " for ctm in [c.strip() for c in ctm_cell.split(',') if c.strip()]:\n", - " rows.append({'FACTURA_CTM': ctm, 'PEDIMENTO': ped_cell.strip()})\n", - " mapping = pd.DataFrame(rows).drop_duplicates().reset_index(drop=True)\n", - " return mapping, df\n", - "\n", - "def listar_facturas_ctm():\n", - " \"\"\"Devuelve facturas TIPOFACTURA='CTM' con su estatus.\"\"\"\n", - " return pd.read_sql(\"\"\"\n", - " SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO, ESTATUS, TIPOFACTURA\n", - " FROM SFacExp WHERE TIPOFACTURA='CTM'\n", - " ORDER BY FECHAFACTURA_ISO\n", - " \"\"\", scaii_conn)\n", - "\n", - "def cargar_partidas_ctm_explotadas():\n", - " \"\"\"Partidas CTM con explosion BOM cuando TIPOMAT IN ('PT','SE').\n", - " Una fila por (FACTURA_CTM, LINEA, componente_final).\"\"\"\n", - " return pd.read_sql(\"\"\"\n", - " SELECT PE.FACTURAEXPO AS FACTURA_CTM, PE.LINEA, PE.NUMPARTE AS PT_O_MP,\n", - " PE.CANTEXPO, PE.UNIMED AS UNIMED_PE, PA.TIPOMAT,\n", - " CASE\n", - " WHEN (SELECT COUNT(*) FROM SMatBOM X WHERE X.NUMPARTE = PE.NUMPARTE) = 0 THEN NULL\n", - " WHEN PA.TIPOMAT IN ('PT','SE') THEN B.NUMPARTEBOM\n", - " ELSE PE.NUMPARTE\n", - " END AS COMPONENTE,\n", - " CASE\n", - " WHEN (SELECT COUNT(*) FROM SMatBOM X WHERE X.NUMPARTE = PE.NUMPARTE) = 0 THEN 0\n", - " WHEN PA.TIPOMAT IN ('PT','SE') THEN B.CANTIDAD\n", - " ELSE 1\n", - " END AS CANT_BOM,\n", - " B.UNIMED AS UNIMED_BOM,\n", - " CASE\n", - " WHEN (SELECT COUNT(*) FROM SMatBOM X WHERE X.NUMPARTE = PE.NUMPARTE) = 0 THEN 0\n", - " WHEN PA.TIPOMAT IN ('PT','SE') THEN (B.CANTIDAD * PE.CANTEXPO)\n", - " ELSE PE.CANTEXPO\n", - " END AS CANT_REQUERIDA\n", - " FROM SPartidasExpo PE\n", - " LEFT JOIN SFacExp FE ON FE.FACTURAEXPO = PE.FACTURAEXPO\n", - " LEFT JOIN SPartes PA ON PA.NUMPARTE = PE.NUMPARTE\n", - " LEFT JOIN SMatBOM B ON B.NUMPARTE = PE.NUMPARTE\n", - " WHERE FE.TIPOFACTURA = 'CTM'\n", - " ORDER BY PE.FACTURAEXPO, PE.LINEA, B.NUMPARTEBOM\n", - " \"\"\", scaii_conn)\n", - "\n", - "def cargar_pool_descargas_cr():\n", - " \"\"\"Pool de descargas existentes en facturas de Cambio de Regimen.\n", - " Estas son las candidatas a reasignarse a las CTM.\"\"\"\n", - " return pd.read_sql(\"\"\"\n", - " SELECT D.CONSECUTIVO, D.FACTEXPO AS FACTURA_CR, D.NUMPARTE, D.PARTEORIGINAL,\n", - " D.CANTDESC, D.UNIMED, D.PEDIMENTOIMPO, D.PEDIMENTOEXPO,\n", - " D.FECHADESC, D.LINEAEXPO, D.FACTIMPO, PA.TIPOMAT,\n", - " DATEADD(DAY, D.FECHADESC - 4, '1801-01-01') AS FECHA_DESC_ISO\n", - " FROM SDescargaT D\n", - " INNER JOIN SFacExp FE ON FE.FACTURAEXPO = D.FACTEXPO\n", - " LEFT JOIN SPartes PA ON PA.NUMPARTE = D.NUMPARTE\n", - " WHERE FE.ESCAMBIOREGIMEN = 'S'\n", - " ORDER BY D.FECHADESC, D.CONSECUTIVO\n", - " \"\"\", scaii_conn)\n", - "\n", - "def cargar_sustitutos_dict():\n", - " \"\"\"Devuelve dict {numparte_original: [lista_sustitutos]}.\"\"\"\n", - " df = pd.read_sql('SELECT NUMPARTE, NUMPARTESUSTITUTO FROM SPartesSustitutos', scaii_conn)\n", - " d = {}\n", - " for _, r in df.iterrows():\n", - " d.setdefault(str(r['NUMPARTE']).strip(), []).append(str(r['NUMPARTESUSTITUTO']).strip())\n", - " return d\n", - "\n", - "def analizar_ctm(df_mapping=None, progress=None):\n", - " \"\"\"Analisis Paso A: para cada partida CTM, busca matches en el pool CR.\n", - " Aplica PEPS (FECHA_DESC ascendente). Si df_mapping no es None, prioriza\n", - " descargas cuyo PEDIMENTOIMPO contenga el numero de pedimento mapeado.\n", - "\n", - " Retorna dos DataFrames:\n", - " plan : una fila por descarga CR que se tomaria (o por faltante)\n", - " resumen : agregado por (FACTURA_CTM, LINEA, COMPONENTE)\n", - " \"\"\"\n", - " prog = _Progress(progress)\n", - " prog.setup(4, 'Cargando facturas CTM...')\n", - " df_part = cargar_partidas_ctm_explotadas()\n", - " prog.step(desc='Cargando pool CR...')\n", - " df_pool = cargar_pool_descargas_cr()\n", - " prog.step(desc='Cargando sustitutos...')\n", - " sust = cargar_sustitutos_dict()\n", - "\n", - " # Dict de pedimentos mapeados por factura CTM (si hay mapping)\n", - " map_dict = {}\n", - " if df_mapping is not None and not df_mapping.empty:\n", - " for _, r in df_mapping.iterrows():\n", - " map_dict.setdefault(str(r['FACTURA_CTM']).strip(),\n", - " set()).add(str(r['PEDIMENTO']).strip())\n", - "\n", - " # Pool mutable: usar saldo virtual por consecutivo\n", - " pool = df_pool.copy()\n", - " pool['SALDO_DISPONIBLE'] = pool['CANTDESC']\n", - "\n", - " # Construir indice rapido: pool por (NUMPARTE) y por (PARTEORIGINAL)\n", - " pool_by_np = pool.groupby('NUMPARTE').groups\n", - " pool_by_po = pool.groupby('PARTEORIGINAL').groups\n", - "\n", - " prog.step(desc='Matching CTM vs CR...')\n", - " plan_rows = []\n", - " total = len(df_part)\n", - " prog.setup(max(total, 1), 'Matching')\n", - " for i, p in df_part.iterrows():\n", - " factura_ctm = p['FACTURA_CTM']\n", - " linea = p['LINEA']\n", - " comp = p['COMPONENTE']\n", - " cant_req = float(p['CANT_REQUERIDA'] or 0)\n", - " unimed = p['UNIMED_BOM'] or p['UNIMED_PE'] or ''\n", - " if cant_req <= 0 or not comp:\n", - " plan_rows.append({\n", - " 'FACTURA_CTM': factura_ctm, 'LINEA': linea, 'PT_O_MP': p['PT_O_MP'],\n", - " 'TIPOMAT': p['TIPOMAT'], 'COMPONENTE': comp, 'CANT_REQUERIDA': cant_req,\n", - " 'UNIMED': unimed, 'STATUS': 'SIN_REQUERIMIENTO', 'CONSECUTIVO_CR': None,\n", - " 'FACTURA_CR': None, 'NUMPARTE_CR': None, 'CANT_DISPONIBLE_CR': 0,\n", - " 'CANT_A_TOMAR': 0, 'FECHA_DESC_CR': None, 'PEDIMENTOIMPO_CR': None,\n", - " 'PRIORIDAD_MAPPING': False,\n", - " })\n", - " prog.step()\n", - " continue\n", - "\n", - " # Candidatos: COMPONENTE directo + sustitutos del COMPONENTE\n", - " candidatos_np = [comp] + sust.get(comp, [])\n", - " # Buscar en pool donde NUMPARTE o PARTEORIGINAL coincida con algun candidato\n", - " idx_match = set()\n", - " for cand in candidatos_np:\n", - " if cand in pool_by_np: idx_match.update(pool_by_np[cand])\n", - " if cand in pool_by_po: idx_match.update(pool_by_po[cand])\n", - " sub = pool.loc[list(idx_match)].copy() if idx_match else pool.iloc[0:0].copy()\n", - " if not sub.empty:\n", - " # Marcar prioridad si su pedimento esta mapeado para esta CTM\n", - " peds_target = map_dict.get(str(factura_ctm).strip(), set())\n", - " def _matches_ped(p_imp):\n", - " if not peds_target: return False\n", - " s = str(p_imp or '')\n", - " # Buscar coincidencia parcial del numero de pedimento mapeado\n", - " for pt in peds_target:\n", - " if pt in s or s in pt: return True\n", - " return False\n", - " sub['PRIORIDAD'] = sub['PEDIMENTOIMPO'].apply(_matches_ped)\n", - " # Ordenar: prioridad descendente, despues fecha ascendente PEPS\n", - " sub = sub.sort_values(['PRIORIDAD','FECHADESC','CONSECUTIVO'], ascending=[False, True, True])\n", - " sub = sub[sub['SALDO_DISPONIBLE'] > 1e-9]\n", - "\n", - " faltante = cant_req\n", - " for idx in sub.index:\n", - " if faltante <= 1e-9: break\n", - " disp = float(pool.at[idx, 'SALDO_DISPONIBLE'])\n", - " if disp <= 1e-9: continue\n", - " toma = min(faltante, disp)\n", - " plan_rows.append({\n", - " 'FACTURA_CTM': factura_ctm, 'LINEA': linea, 'PT_O_MP': p['PT_O_MP'],\n", - " 'TIPOMAT': p['TIPOMAT'], 'COMPONENTE': comp, 'CANT_REQUERIDA': cant_req,\n", - " 'UNIMED': unimed, 'STATUS': 'ASIGNADO',\n", - " 'CONSECUTIVO_CR': int(pool.at[idx, 'CONSECUTIVO']),\n", - " 'FACTURA_CR': pool.at[idx, 'FACTURA_CR'],\n", - " 'NUMPARTE_CR': pool.at[idx, 'NUMPARTE'],\n", - " 'PARTEORIGINAL_CR': pool.at[idx, 'PARTEORIGINAL'],\n", - " 'CANT_DISPONIBLE_CR': disp,\n", - " 'CANT_A_TOMAR': toma,\n", - " 'FECHA_DESC_CR': pool.at[idx, 'FECHA_DESC_ISO'],\n", - " 'PEDIMENTOIMPO_CR': pool.at[idx, 'PEDIMENTOIMPO'],\n", - " 'PRIORIDAD_MAPPING': bool(sub.at[idx, 'PRIORIDAD']) if 'PRIORIDAD' in sub.columns else False,\n", - " })\n", - " pool.at[idx, 'SALDO_DISPONIBLE'] = disp - toma\n", - " faltante -= toma\n", - " if faltante > 1e-9:\n", - " plan_rows.append({\n", - " 'FACTURA_CTM': factura_ctm, 'LINEA': linea, 'PT_O_MP': p['PT_O_MP'],\n", - " 'TIPOMAT': p['TIPOMAT'], 'COMPONENTE': comp, 'CANT_REQUERIDA': cant_req,\n", - " 'UNIMED': unimed, 'STATUS': 'FALTANTE', 'CONSECUTIVO_CR': None,\n", - " 'FACTURA_CR': None, 'NUMPARTE_CR': None, 'PARTEORIGINAL_CR': None,\n", - " 'CANT_DISPONIBLE_CR': 0, 'CANT_A_TOMAR': faltante,\n", - " 'FECHA_DESC_CR': None, 'PEDIMENTOIMPO_CR': None,\n", - " 'PRIORIDAD_MAPPING': False,\n", - " })\n", - " prog.step()\n", - "\n", - " plan = pd.DataFrame(plan_rows)\n", - " if plan.empty:\n", - " prog.done('Sin partidas')\n", - " return plan, plan\n", - " # Resumen por (FACTURA_CTM, LINEA, COMPONENTE)\n", - " resumen = (plan.groupby(['FACTURA_CTM','LINEA','COMPONENTE','UNIMED','CANT_REQUERIDA'], as_index=False, dropna=False)\n", - " .agg(cubierto=('CANT_A_TOMAR', lambda s: s[plan.loc[s.index, 'STATUS']=='ASIGNADO'].sum()),\n", - " faltante=('CANT_A_TOMAR', lambda s: s[plan.loc[s.index, 'STATUS']=='FALTANTE'].sum()),\n", - " filas_cr_usadas=('CONSECUTIVO_CR', lambda s: s.notna().sum())))\n", - " resumen['pct_cobertura'] = ((resumen['cubierto'] / resumen['CANT_REQUERIDA']).fillna(0)*100).round(2).clip(upper=100)\n", - " prog.done('Analisis CTM listo')\n", - " _state['ctm_plan'] = plan\n", - " _state['ctm_resumen'] = resumen\n", - " return plan, resumen\n", - "\n", - "def exportar_excel_ctm(plan, resumen):\n", - " out = f'analisis_ctm_{_dt.datetime.now().strftime(\"%Y%m%d_%H%M%S\")}.xlsx'\n", - " with pd.ExcelWriter(out, engine='openpyxl') as w:\n", - " resumen.to_excel(w, sheet_name='Resumen', index=False)\n", - " plan.to_excel(w, sheet_name='Plan_Detalle', index=False)\n", - " plan[plan['STATUS']=='FALTANTE'].to_excel(w, sheet_name='Faltantes', index=False)\n", - " return os.path.abspath(out)\n", - "def generar_plantilla_excel_ctm():\n", - " \"\"\"Crea un Excel de ejemplo con el formato esperado.\"\"\"\n", - " out = f'plantilla_ctm_{_dt.datetime.now().strftime(\"%Y%m%d_%H%M%S\")}.xlsx'\n", - " df = pd.DataFrame([\n", - " {'Facturas CTM': 'AAU112023RFR0481, NIS112023RFR0035',\n", - " 'PEDIMENTO COMPLETO': '75-3076-4021492',\n", - " 'PATENTE': 3076, 'ADUANA': 75, 'PEDIMENTO': 4021492,\n", - " 'Operacion': 'Importacion', 'Clave de pedimento': 'F4'},\n", - " {'Facturas CTM': 'AAU122023RFR0482',\n", - " 'PEDIMENTO COMPLETO': '75-3076-4033174',\n", - " 'PATENTE': 3076, 'ADUANA': 75, 'PEDIMENTO': 4033174,\n", - " 'Operacion': 'Importacion', 'Clave de pedimento': 'F4'},\n", - " {'Facturas CTM': 'AAU062024RFR0488,NIS062024RFR0030',\n", - " 'PEDIMENTO COMPLETO': '75-3076-4133436',\n", - " 'PATENTE': 3076, 'ADUANA': 75, 'PEDIMENTO': 4133436,\n", - " 'Operacion': 'Importacion', 'Clave de pedimento': 'F4'},\n", - " ])\n", - " df.to_excel(out, index=False)\n", - " return os.path.abspath(out)\n", - "\n", - "def ejecutar_reasignacion_ctm(modo='NATURAL', dry_run=True, log=print, progress=None):\n", - " \"\"\"Paso B - Ejecuta el plan generado por analizar_ctm.\n", - " - Cambia FACTEXPO en SDescargaT (caso total) o divide la fila (caso parcial).\n", - " - Inserta fila espejo en SDescargaM cada vez que algo se asigna a la CTM.\n", - " - Actualiza SFacExp: ESTATUS='AC', APLICADESCMANUAL='S', CANT_PARTIDAS=count.\n", - " Modos: NATURAL (solo CTMs 100% cubiertas) | DIRIGIDA (todas con asignaciones).\n", - " Todo en transaccion atomica por factura CTM.\n", - " \"\"\"\n", - " prog = _Progress(progress)\n", - " if 'ctm_plan' not in _state or _state['ctm_plan'].empty:\n", - " log('ERROR: corre primero \"Analizar CTM\" para generar el plan.')\n", - " return\n", - " plan = _state['ctm_plan']; resumen = _state.get('ctm_resumen')\n", - " plan_asignado = plan[plan['STATUS'] == 'ASIGNADO'].copy()\n", - " if plan_asignado.empty:\n", - " log('No hay filas ASIGNADAS en el plan.'); return\n", - "\n", - " if modo == 'NATURAL':\n", - " if resumen is None or resumen.empty:\n", - " log('Sin resumen para evaluar NATURAL. Aborto.'); return\n", - " elig = [f for f, g in resumen.groupby('FACTURA_CTM') if (g['pct_cobertura'] >= 99.99).all()]\n", - " plan_asignado = plan_asignado[plan_asignado['FACTURA_CTM'].isin(elig)]\n", - " log(f'Modo NATURAL: {len(elig)} facturas CTM elegibles (cobertura 100%)')\n", - " else:\n", - " log('Modo DIRIGIDA: procesa todas las facturas con asignaciones')\n", - "\n", - " log(f'Filas a procesar: {len(plan_asignado):,}')\n", - " if plan_asignado.empty: prog.done('Nada que procesar'); return\n", - "\n", - " with scaii_conn.cursor() as cur:\n", - " cur.execute('SELECT ISNULL(MAX(CONSECUTIVO),0) FROM SDescargaM')\n", - " next_m = int(cur.fetchone()[0]) + 1\n", - " log(f'Proximo CONSECUTIVO SDescargaM: {next_m}')\n", - "\n", - " INSERT_M = (\"INSERT INTO SDescargaM (CONSECUTIVO, CONSECUTIVOEXPO, FACTURAEXPO, LINEA, \"\n", - " \"NUMPARTE, CLASE, CANTIDAD, UNIMED, FACTURAIMPO, NUMPARTEMP, \"\n", - " \"VALORIMPOMN, VALORIMPOME, PAIS, PESONETO, PESOBRUTO, \"\n", - " \"TIPOFRACCION, SECTOR, FACTURADEF, PROCEDENCIA, FACTURA, \"\n", - " \"TIPODESPERDICIO, ORDENVENTA, TOMARSALDOBASEALPT) \"\n", - " \"VALUES (\" + ','.join(['?']*23) + \")\")\n", - "\n", - " UPDATE_SFACEXP = (\"UPDATE SFacExp SET ESTATUS='AC', APLICADESCMANUAL='S', \"\n", - " \"CANT_PARTIDAS = (SELECT COUNT(*) FROM SPartidasExpo \"\n", - " \"WHERE FACTURAEXPO = SFacExp.FACTURAEXPO) WHERE FACTURAEXPO=?\")\n", - "\n", - " facturas = plan_asignado['FACTURA_CTM'].unique()\n", - " prog.setup(len(facturas), 'Procesando CTMs')\n", - " ins_m = stat = 0\n", - " errores = []\n", - "\n", - " for factura_ctm in facturas:\n", - " filas_factura = plan_asignado[plan_asignado['FACTURA_CTM'] == factura_ctm]\n", - " try:\n", - " with scaii_conn.cursor() as cur:\n", - " cur.execute(\"SELECT CONSECUTIVO FROM SFacExp WHERE FACTURAEXPO=?\", factura_ctm)\n", - " _rf = cur.fetchone()\n", - " consec_factura_ctm = int(_rf[0]) if _rf and _rf[0] is not None else 0\n", - " for _, fila in filas_factura.iterrows():\n", - " consec_cr = int(fila['CONSECUTIVO_CR'])\n", - " cant_tomar = float(fila['CANT_A_TOMAR'])\n", - " linea_ctm = int(fila['LINEA']) if pd.notna(fila['LINEA']) else 0\n", - " numparte_pt_ctm = str(fila['PT_O_MP']).strip() if pd.notna(fila['PT_O_MP']) else None\n", - " cur.execute(\"SELECT FACTIMPO, CLASE, CANTDESC, UNIMED, VALORMN, VALORME, \"\n", - " \"PESONETO, PESOBRUTO, PAISMERCANCIA, TIPOFRACCION, SECTOR, \"\n", - " \"PARTEORIGINAL, ORDENVENTA \"\n", - " \"FROM SDescargaT WHERE CONSECUTIVO=?\", consec_cr)\n", - " r = cur.fetchone()\n", - " if r is None:\n", - " log(f' WARN consec {consec_cr} ya no existe; se salta'); continue\n", - " cantdesc_actual = float(r.CANTDESC or 0)\n", - " if cantdesc_actual <= 1e-9:\n", - " log(f' WARN consec {consec_cr} tiene CANTDESC=0; se salta'); continue\n", - " p = 1.0 if cant_tomar >= cantdesc_actual else (cant_tomar / cantdesc_actual)\n", - "\n", - " def esc(val):\n", - " v = float(val or 0); return v * p\n", - "\n", - " m_cantidad = esc(r.CANTDESC); m_vmn = esc(r.VALORMN); m_vme = esc(r.VALORME)\n", - " m_pneto = esc(r.PESONETO); m_pbruto = esc(r.PESOBRUTO)\n", - "\n", - " # SOLO INSERT espejo en SDescargaM. SDescargaT no se toca.\n", - " if not dry_run:\n", - " cur.execute(INSERT_M,\n", - " next_m, consec_factura_ctm, factura_ctm, linea_ctm,\n", - " numparte_pt_ctm, r.CLASE, m_cantidad, r.UNIMED,\n", - " r.FACTIMPO, r.PARTEORIGINAL,\n", - " m_vmn, m_vme, r.PAISMERCANCIA, m_pneto, m_pbruto,\n", - " r.TIPOFRACCION, r.SECTOR,\n", - " '', 'TEM', r.FACTIMPO, 'N', r.ORDENVENTA, '')\n", - " next_m += 1; ins_m += 1\n", - "\n", - " if not dry_run:\n", - " cur.execute(UPDATE_SFACEXP, factura_ctm)\n", - " stat += 1\n", - " if not dry_run:\n", - " scaii_conn.commit()\n", - " except Exception as e:\n", - " if not dry_run: scaii_conn.rollback()\n", - " errores.append((factura_ctm, str(e)))\n", - " log(f' ERROR {factura_ctm}: {e}')\n", - " prog.step()\n", - "\n", - " prog.done(f'{ins_m} mirror ops')\n", - " log(f'\\n=== RESUMEN Paso B ({modo}, DRY_RUN={dry_run}) ===')\n", - " log(f' SDescargaM insertadas (espejo) : {ins_m:,}')\n", - " log(f' Facturas CTM -> ESTATUS=AC + APLICADESCMANUAL=S + CANT_PARTIDAS: {stat:,}')\n", - " log(f' SDescargaT NO se modifica (solo se usa como referencia)')\n", - " log(f' Errores : {len(errores):,}')\n", - " for f, e in errores[:5]:\n", - " log(f' {f}: {e}')\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "logic-saldos-vencidos", - "metadata": {}, - "outputs": [], - "source": [ - "# Fix completo para SaldosVencidos: usa PK de 6 campos en SSaldoTem\n", - "# y match por PAISMERCANCIA en SDescargaT.\n", - "\n", - "# Las funciones aqui REEMPLAZAN las de la celda logic-saldos-vencidos.\n", - "\n", - "import io as _io_sv\n", - "import datetime as _dt_sv\n", - "\n", - "def cargar_excel_saldos_vencidos(path):\n", - " \"\"\"Lee Excel con 3 columnas: FACTURAIMPO, CANTIDAD_SALDO, FRACCION_IMPO.\"\"\"\n", - " df = pd.read_excel(path, dtype=str)\n", - " norm = {c: c.strip().upper().replace(' ', '_') for c in df.columns}\n", - " df = df.rename(columns=norm)\n", - " aliases = {\n", - " 'FACTURAIMPO': ['FACTURAIMPO', 'FACTURA_IMPO', 'FACTURA'],\n", - " 'CANTIDAD_SALDO': ['CANTIDAD_SALDO', 'CANT_SALDO', 'CANTIDAD', 'SALDO'],\n", - " 'FRACCION_IMPO': ['FRACCION_IMPO', 'FRACCIONIMPO', 'FRACCION'],\n", - " }\n", - " out = {}\n", - " for std, opts in aliases.items():\n", - " for o in opts:\n", - " if o in df.columns:\n", - " out[std] = df[o]; break\n", - " if std not in out:\n", - " raise ValueError(f'Falta la columna {std} (acepta: {opts})')\n", - " df2 = pd.DataFrame(out)\n", - " df2['FACTURAIMPO'] = df2['FACTURAIMPO'].astype(str).str.strip()\n", - " df2['FRACCION_IMPO'] = df2['FRACCION_IMPO'].astype(str).str.strip()\n", - " df2['CANTIDAD_SALDO'] = pd.to_numeric(df2['CANTIDAD_SALDO'], errors='coerce').fillna(0)\n", - " df2 = df2[df2['CANTIDAD_SALDO'] > 0]\n", - " return df2.reset_index(drop=True)\n", - "\n", - "\n", - "def generar_plantilla_excel_saldos_vencidos():\n", - " df = pd.DataFrame([\n", - " {'FACTURAIMPO': 'F1234567', 'CANTIDAD_SALDO': 100.0, 'FRACCION_IMPO': '85044010'},\n", - " {'FACTURAIMPO': 'F1234568', 'CANTIDAD_SALDO': 50.5, 'FRACCION_IMPO': '85044010'},\n", - " {'FACTURAIMPO': 'F1234569', 'CANTIDAD_SALDO': 25.0, 'FRACCION_IMPO': '73181500'},\n", - " ])\n", - " buf = _io_sv.BytesIO()\n", - " with pd.ExcelWriter(buf, engine='openpyxl') as w:\n", - " df.to_excel(w, sheet_name='SaldosVencidos', index=False)\n", - " buf.seek(0)\n", - " return buf.read()\n", - "\n", - "\n", - "# PK de 6 campos para identificar univocamente una fila de SSaldoTem\n", - "_PK_SALDO = ['FACTURAIMPO', 'PEDIMENTOIMPO', 'FRACCIONIMPO', 'NUMPARTE', 'UMEXITENCIA', 'PAISORIGEN']\n", - "\n", - "# Llave de match con SDescargaT (las descargas no traen PEDIMENTOIMPO/FRACCIONIMPO\n", - "# necesariamente alineados con SSaldoTem; matchamos por los 4 campos que SI son comparables).\n", - "_MATCH_DESC = ['FACTIMPO', 'NUMPARTE', 'UNIMED', 'PAISMERCANCIA']\n", - "\n", - "\n", - "def _enriquecer_saldos_con_tasas(df):\n", - " if df.empty: return df\n", - " if 'CANTIDAD_SALDO' in df.columns:\n", - " df['SALDO_APLICABLE'] = df[['CANTIDAD_SALDO', 'SALDO_DISPONIBLE']].min(axis=1)\n", - " else:\n", - " df['SALDO_APLICABLE'] = df['SALDO_DISPONIBLE']\n", - " denom = df['CANTEXITENCIA'].replace(0, np.nan)\n", - " df['TASA_VMN'] = (df['VALORIMPOMN'] / denom).fillna(0)\n", - " df['TASA_VME'] = (df['VALORIMPOME'] / denom).fillna(0)\n", - " df['TASA_PNETO'] = (df['PESONETO'] / denom).fillna(0)\n", - " df['TASA_PBRUTO'] = (df['PESOBRUTO'] / denom).fillna(0)\n", - " # Normalizar claves a string strip\n", - " for k in _PK_SALDO:\n", - " if k in df.columns:\n", - " df[k] = df[k].astype(str).str.strip()\n", - " return df.reset_index(drop=True)\n", - "\n", - "\n", - "def cargar_saldos_vencidos_ssaldotem(df_excel, fecha_ini, fecha_fin):\n", - " \"\"\"Modo Excel: filtra SSaldoTem por rango FECHAFACTURA_ISO y matchea con Excel\n", - " por (FACTURAIMPO, FRACCIONIMPO). Trae los 6 campos de PK.\"\"\"\n", - " sql = \"\"\"\n", - " SELECT FACTURAIMPO, PEDIMENTOIMPO, FRACCIONIMPO, NUMPARTE, UMEXITENCIA, PAISORIGEN,\n", - " FECHAFACTURA_ISO, FECHAVENC_ISO,\n", - " CANTEXITENCIA,\n", - " ISNULL(CANTUSADA,0) AS CANTUSADA,\n", - " ISNULL(CANTUSADADESP,0) AS CANTUSADADESP,\n", - " (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_DISPONIBLE,\n", - " ISNULL(VALORIMPOMN,0) AS VALORIMPOMN,\n", - " ISNULL(VALORIMPOME,0) AS VALORIMPOME,\n", - " ISNULL(PESONETO,0) AS PESONETO,\n", - " ISNULL(PESOBRUTO,0) AS PESOBRUTO\n", - " FROM SSaldoTem\n", - " WHERE FECHAFACTURA_ISO BETWEEN ? AND ?\n", - " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", - " \"\"\"\n", - " df = pd.read_sql(sql, scaii_conn, params=(fecha_ini, fecha_fin))\n", - " if df.empty: return df\n", - " pares = df_excel[['FACTURAIMPO', 'FRACCION_IMPO', 'CANTIDAD_SALDO']].copy()\n", - " pares = pares.rename(columns={'FRACCION_IMPO': 'FRACCIONIMPO'})\n", - " pares['FACTURAIMPO'] = pares['FACTURAIMPO'].astype(str).str.strip()\n", - " pares['FRACCIONIMPO'] = pares['FRACCIONIMPO'].astype(str).str.strip()\n", - " df['FACTURAIMPO'] = df['FACTURAIMPO'].astype(str).str.strip()\n", - " df['FRACCIONIMPO'] = df['FRACCIONIMPO'].astype(str).str.strip()\n", - " df = df.merge(pares, on=['FACTURAIMPO', 'FRACCIONIMPO'], how='inner')\n", - " return _enriquecer_saldos_con_tasas(df)\n", - "\n", - "\n", - "def cargar_saldos_vencidos_auto(fecha_ini, fecha_fin, fecha_corte=None):\n", - " \"\"\"Modo Automatico: SALDO_DISPONIBLE > 0 + FECHAVENC_ISO < fecha_corte (default hoy).\"\"\"\n", - " if fecha_corte is None:\n", - " fecha_corte = _dt_sv.date.today().isoformat()\n", - " sql = \"\"\"\n", - " SELECT FACTURAIMPO, PEDIMENTOIMPO, FRACCIONIMPO, NUMPARTE, UMEXITENCIA, PAISORIGEN,\n", - " FECHAFACTURA_ISO, FECHAVENC_ISO,\n", - " CANTEXITENCIA,\n", - " ISNULL(CANTUSADA,0) AS CANTUSADA,\n", - " ISNULL(CANTUSADADESP,0) AS CANTUSADADESP,\n", - " (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_DISPONIBLE,\n", - " ISNULL(VALORIMPOMN,0) AS VALORIMPOMN,\n", - " ISNULL(VALORIMPOME,0) AS VALORIMPOME,\n", - " ISNULL(PESONETO,0) AS PESONETO,\n", - " ISNULL(PESOBRUTO,0) AS PESOBRUTO\n", - " FROM SSaldoTem\n", - " WHERE FECHAFACTURA_ISO BETWEEN ? AND ?\n", - " AND FECHAVENC_ISO IS NOT NULL\n", - " AND FECHAVENC_ISO < ?\n", - " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", - " \"\"\"\n", - " df = pd.read_sql(sql, scaii_conn, params=(fecha_ini, fecha_fin, fecha_corte))\n", - " return _enriquecer_saldos_con_tasas(df)\n", - "\n", - "\n", - "def cargar_descargas_candidatas_sv(df_saldos):\n", - " \"\"\"SDescargaT por las FACTIMPO involucradas; trae PAISMERCANCIA para match completo.\"\"\"\n", - " if df_saldos.empty: return pd.DataFrame()\n", - " facturas = df_saldos['FACTURAIMPO'].astype(str).str.strip().unique().tolist()\n", - " if not facturas: return pd.DataFrame()\n", - " out = []\n", - " LOTE = 1000\n", - " for i in range(0, len(facturas), LOTE):\n", - " sub = facturas[i:i+LOTE]\n", - " placeholders = ','.join(['?'] * len(sub))\n", - " sql = f\"\"\"\n", - " SELECT CONSECUTIVO, FACTIMPO, NUMPARTE, UNIMED, PAISMERCANCIA, FACTEXPO,\n", - " ISNULL(CANTDESC,0) AS CANTDESC\n", - " FROM SDescargaT\n", - " WHERE FACTIMPO IN ({placeholders})\n", - " AND ISNULL(CANTDESC,0) > 0\n", - " \"\"\"\n", - " out.append(pd.read_sql(sql, scaii_conn, params=sub))\n", - " df = pd.concat(out, ignore_index=True) if out else pd.DataFrame()\n", - " if df.empty: return df\n", - " for k in ['FACTIMPO', 'NUMPARTE', 'UNIMED', 'PAISMERCANCIA']:\n", - " df[k] = df[k].fillna('').astype(str).str.strip()\n", - " return df\n", - "\n", - "\n", - "def _prorratear_saldos(df_saldos, df_desc, prog):\n", - " \"\"\"Motor de prorrateo. Match con descargas por (FACTIMPO, NUMPARTE, UNIMED, PAISMERCANCIA).\n", - " Cada fila del plan propaga los 6 campos PK del saldo.\"\"\"\n", - " plan_rows = []\n", - " resumen_rows = []\n", - " desc_by_key = {}\n", - " if not df_desc.empty:\n", - " for key, g in df_desc.groupby(_MATCH_DESC):\n", - " desc_by_key[key] = g\n", - " for _, s in df_saldos.iterrows():\n", - " fimpo = str(s['FACTURAIMPO']).strip()\n", - " pedimpo = str(s['PEDIMENTOIMPO']).strip()\n", - " fraccion = str(s['FRACCIONIMPO']).strip()\n", - " numparte = str(s['NUMPARTE']).strip()\n", - " um = str(s['UMEXITENCIA']).strip()\n", - " pais = str(s['PAISORIGEN']).strip()\n", - " candidatas = desc_by_key.get((fimpo, numparte, um, pais))\n", - " base_resumen = {\n", - " 'FACTURAIMPO': fimpo, 'PEDIMENTOIMPO': pedimpo, 'FRACCIONIMPO': fraccion,\n", - " 'NUMPARTE': numparte, 'UMEXITENCIA': um, 'PAISORIGEN': pais,\n", - " 'FECHAFACTURA_ISO': s.get('FECHAFACTURA_ISO'),\n", - " 'FECHAVENC_ISO': s.get('FECHAVENC_ISO'),\n", - " 'SALDO_APLICABLE': float(s['SALDO_APLICABLE']),\n", - " }\n", - " if candidatas is None or candidatas.empty:\n", - " resumen_rows.append({**base_resumen, 'CANT_DESCARGAS': 0, 'STATUS': 'SIN_DESCARGAS'})\n", - " continue\n", - " total_cantdesc = float(candidatas['CANTDESC'].sum())\n", - " if total_cantdesc <= 1e-9:\n", - " resumen_rows.append({**base_resumen, 'CANT_DESCARGAS': 0, 'STATUS': 'CANTDESC_CERO'})\n", - " continue\n", - " saldo_apl = float(s['SALDO_APLICABLE'])\n", - " tasa_vmn = float(s['TASA_VMN'])\n", - " tasa_vme = float(s['TASA_VME'])\n", - " tasa_pn = float(s['TASA_PNETO'])\n", - " tasa_pb = float(s['TASA_PBRUTO'])\n", - " for _, d in candidatas.iterrows():\n", - " prop = float(d['CANTDESC']) / total_cantdesc\n", - " pc = saldo_apl * prop\n", - " plan_rows.append({\n", - " 'FACTURAIMPO': fimpo, 'PEDIMENTOIMPO': pedimpo, 'FRACCIONIMPO': fraccion,\n", - " 'NUMPARTE': numparte, 'UMEXITENCIA': um, 'PAISORIGEN': pais,\n", - " 'CONSECUTIVO_DESC': int(d['CONSECUTIVO']),\n", - " 'FACTEXPO': d['FACTEXPO'],\n", - " 'CANTDESC_ACTUAL': float(d['CANTDESC']),\n", - " 'PROPORCION': prop,\n", - " 'PORCION_CANT': pc,\n", - " 'PORCION_VMN': pc * tasa_vmn,\n", - " 'PORCION_VME': pc * tasa_vme,\n", - " 'PORCION_PNETO': pc * tasa_pn,\n", - " 'PORCION_PBRUTO': pc * tasa_pb,\n", - " })\n", - " resumen_rows.append({**base_resumen, 'CANT_DESCARGAS': len(candidatas), 'STATUS': 'PRORRATEADO'})\n", - " if prog is not None: prog.done('Analisis listo')\n", - " return pd.DataFrame(plan_rows), pd.DataFrame(resumen_rows)\n", - "\n", - "\n", - "def analizar_saldos_vencidos(df_excel, fecha_ini, fecha_fin, progress=None):\n", - " prog = _Progress(progress)\n", - " prog.setup(3, 'Cargando saldos SSaldoTem...')\n", - " df_saldos = cargar_saldos_vencidos_ssaldotem(df_excel, fecha_ini, fecha_fin)\n", - " prog.step(desc=f'Saldos matched: {len(df_saldos)}')\n", - " df_desc = cargar_descargas_candidatas_sv(df_saldos)\n", - " prog.step(desc=f'Descargas candidatas: {len(df_desc)}')\n", - " return _prorratear_saldos(df_saldos, df_desc, prog)\n", - "\n", - "\n", - "def analizar_saldos_vencidos_auto(fecha_ini, fecha_fin, fecha_corte=None, progress=None):\n", - " prog = _Progress(progress)\n", - " prog.setup(3, 'Buscando saldos vencidos en SSaldoTem...')\n", - " df_saldos = cargar_saldos_vencidos_auto(fecha_ini, fecha_fin, fecha_corte)\n", - " prog.step(desc=f'Saldos vencidos: {len(df_saldos)}')\n", - " df_desc = cargar_descargas_candidatas_sv(df_saldos)\n", - " prog.step(desc=f'Descargas candidatas: {len(df_desc)}')\n", - " return _prorratear_saldos(df_saldos, df_desc, prog)\n", - "\n", - "\n", - "def exportar_excel_saldos_vencidos(plan, resumen, ruta):\n", - " with pd.ExcelWriter(ruta, engine='openpyxl') as w:\n", - " if not plan.empty: plan.to_excel(w, sheet_name='Plan_Detalle', index=False)\n", - " if not resumen.empty: resumen.to_excel(w, sheet_name='Resumen', index=False)\n", - "\n", - "\n", - "def ejecutar_saldos_vencidos(plan, dry_run=True, progress=None, log=print):\n", - " \"\"\"Paso B: UPDATE SDescargaT por descarga + UPDATE SSaldoTem por saldo.\n", - " Usa los 6 campos PK del saldo para identificar univocamente la fila.\"\"\"\n", - " assert isinstance(dry_run, bool), 'dry_run debe ser bool'\n", - " prog = _Progress(progress)\n", - " if plan is None or plan.empty:\n", - " log('ERROR: plan vacio, corre primero \"Analizar\".')\n", - " return\n", - " UPD_DESC = \"\"\"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 = ?\"\"\"\n", - " SEL_SALDO = \"\"\"SELECT (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0))\n", - " FROM SSaldoTem\n", - " WHERE FACTURAIMPO=? AND PEDIMENTOIMPO=? AND FRACCIONIMPO=?\n", - " AND NUMPARTE=? AND UMEXITENCIA=? AND PAISORIGEN=?\"\"\"\n", - " UPD_SALDO = \"\"\"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 PEDIMENTOIMPO=? AND FRACCIONIMPO=?\n", - " AND NUMPARTE=? AND UMEXITENCIA=? AND PAISORIGEN=?\"\"\"\n", - " saldos = plan[_PK_SALDO].drop_duplicates().reset_index(drop=True)\n", - " prog.setup(len(saldos), 'Procesando saldos')\n", - " upd_desc = upd_saldo = errores = capados = 0\n", - " err_list = []\n", - " for _, s in saldos.iterrows():\n", - " pk = (s['FACTURAIMPO'], s['PEDIMENTOIMPO'], s['FRACCIONIMPO'],\n", - " s['NUMPARTE'], s['UMEXITENCIA'], s['PAISORIGEN'])\n", - " etiqueta = f\"{pk[0]}/{pk[1]}/{pk[2]}/{pk[3]}/{pk[4]}/{pk[5]}\"\n", - " mask = (plan['FACTURAIMPO']==pk[0]) & (plan['PEDIMENTOIMPO']==pk[1]) & \\\n", - " (plan['FRACCIONIMPO']==pk[2]) & (plan['NUMPARTE']==pk[3]) & \\\n", - " (plan['UMEXITENCIA']==pk[4]) & (plan['PAISORIGEN']==pk[5])\n", - " fil = plan[mask].copy()\n", - " if fil.empty: prog.step(); continue\n", - " sum_c = float(fil['PORCION_CANT'].sum())\n", - " try:\n", - " with scaii_conn.cursor() as cur:\n", - " cur.execute(SEL_SALDO, *pk)\n", - " row = cur.fetchone()\n", - " if row is None:\n", - " log(f' WARN saldo {etiqueta} no existe; se salta')\n", - " prog.step(); continue\n", - " disp = float(row[0] or 0)\n", - " if sum_c > disp + 1e-6:\n", - " factor = disp / sum_c if sum_c > 0 else 0\n", - " log(f' CAPEO {etiqueta}: sum={sum_c:.4f} > disp={disp:.4f} (factor={factor:.4f})')\n", - " for col in ['PORCION_CANT','PORCION_VMN','PORCION_VME','PORCION_PNETO','PORCION_PBRUTO']:\n", - " fil[col] = fil[col] * factor\n", - " capados += 1\n", - " sum_c = float(fil['PORCION_CANT'].sum())\n", - " sum_vmn = float(fil['PORCION_VMN'].sum())\n", - " sum_vme = float(fil['PORCION_VME'].sum())\n", - " sum_pn = float(fil['PORCION_PNETO'].sum())\n", - " sum_pb = float(fil['PORCION_PBRUTO'].sum())\n", - " if sum_c <= 1e-9:\n", - " log(f' SKIP {etiqueta}: factor=0, no hay nada que aplicar')\n", - " prog.step(); continue\n", - " for _, p in fil.iterrows():\n", - " if not dry_run:\n", - " cur.execute(UPD_DESC,\n", - " float(p['PORCION_CANT']), float(p['PORCION_VMN']), float(p['PORCION_VME']),\n", - " float(p['PORCION_PNETO']), float(p['PORCION_PBRUTO']),\n", - " int(p['CONSECUTIVO_DESC']))\n", - " upd_desc += 1\n", - " if not dry_run:\n", - " cur.execute(UPD_SALDO, sum_c, sum_vmn, sum_vme, sum_pn, sum_pb, *pk)\n", - " upd_saldo += 1\n", - " if not dry_run: scaii_conn.commit()\n", - " except Exception as e:\n", - " if not dry_run: scaii_conn.rollback()\n", - " errores += 1\n", - " err_list.append((etiqueta, str(e)))\n", - " log(f' ERROR {etiqueta}: {e}')\n", - " prog.step()\n", - " prog.done(f'{upd_desc} desc / {upd_saldo} saldos')\n", - " log(f'\\n=== RESUMEN Saldos Vencidos (DRY_RUN={dry_run}) ===')\n", - " log(f' SDescargaT actualizadas: {upd_desc:,}')\n", - " log(f' SSaldoTem actualizadas: {upd_saldo:,}')\n", - " log(f' Saldos capeados al 100%: {capados:,}')\n", - " log(f' Errores : {errores:,}')\n", - " for f, e in err_list[:5]:\n", - " log(f' {f}: {e}')\n", - "\n", - "\n", - "def cargar_saldos_vencidos_por_anio(fecha_corte=None, eje='VENCIMIENTO'):\n", - " if fecha_corte is None:\n", - " fecha_corte = _dt_sv.date.today().isoformat()\n", - " col_fecha = 'FECHAFACTURA_ISO' if eje == 'FACTURA' else 'FECHAVENC_ISO'\n", - " alias_anio = 'ANIO_FACTURA' if eje == 'FACTURA' else 'ANIO_VENC'\n", - " sql = f\"\"\"\n", - " SELECT YEAR(CAST({col_fecha} AS DATE)) AS {alias_anio},\n", - " COUNT(*) AS LOTES,\n", - " COUNT(DISTINCT FACTURAIMPO) AS FACTURAS,\n", - " SUM(CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_CANT,\n", - " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", - " CASE WHEN CANTEXITENCIA > 0\n", - " THEN (ISNULL(VALORIMPOMN,0) * 1.0 / CANTEXITENCIA)\n", - " ELSE 0 END) AS SALDO_VMN,\n", - " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", - " CASE WHEN CANTEXITENCIA > 0\n", - " THEN (ISNULL(VALORIMPOME,0) * 1.0 / CANTEXITENCIA)\n", - " ELSE 0 END) AS SALDO_VME\n", - " FROM SSaldoTem\n", - " WHERE FECHAVENC_ISO IS NOT NULL\n", - " AND FECHAVENC_ISO < ?\n", - " AND {col_fecha} IS NOT NULL\n", - " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", - " GROUP BY YEAR(CAST({col_fecha} AS DATE))\n", - " ORDER BY {alias_anio}\n", - " \"\"\"\n", - " return pd.read_sql(sql, scaii_conn, params=(fecha_corte,))\n", - "\n", - "\n", - "def graficar_saldos_vencidos_por_anio(df, metric='SALDO_VMN'):\n", - " import matplotlib.ticker as _mtick\n", - " if df is None or df.empty:\n", - " print('Sin datos para graficar.')\n", - " return\n", - " fig, ax = plt.subplots(figsize=(10, 5))\n", - " col_x = 'ANIO_FACTURA' if 'ANIO_FACTURA' in df.columns else 'ANIO_VENC'\n", - " x = df[col_x].astype(int).astype(str).tolist()\n", - " y = df[metric].astype(float).tolist()\n", - " bars = ax.bar(x, y, color='#1565C0', edgecolor='#0D47A1')\n", - " for i, b in enumerate(bars):\n", - " lotes = int(df.iloc[i]['LOTES'])\n", - " ax.text(b.get_x() + b.get_width()/2, b.get_height(),\n", - " f'{lotes:,} lotes', ha='center', va='bottom', fontsize=9, color='#333')\n", - " titulos = {\n", - " 'SALDO_VMN': 'Saldos vencidos por anio - Valor MN (pesos)',\n", - " 'SALDO_VME': 'Saldos vencidos por anio - Valor ME (dolares)',\n", - " 'SALDO_CANT': 'Saldos vencidos por anio - Cantidad disponible',\n", - " 'LOTES': 'Saldos vencidos por anio - Cantidad de lotes',\n", - " }\n", - " ax.set_title(titulos.get(metric, f'Saldos vencidos por anio - {metric}'),\n", - " fontsize=13, fontweight='bold', color='#0D47A1')\n", - " ax.set_xlabel('Anio')\n", - " ax.set_ylabel(metric)\n", - " ax.yaxis.set_major_formatter(_mtick.FuncFormatter(lambda v, _: f'{v:,.0f}'))\n", - " ax.grid(axis='y', linestyle='--', alpha=0.5)\n", - " plt.tight_layout()\n", - " plt.show()\n", - " display(df.assign(\n", - " SALDO_CANT=df['SALDO_CANT'].round(2),\n", - " SALDO_VMN=df['SALDO_VMN'].round(2),\n", - " SALDO_VME=df['SALDO_VME'].round(2),\n", - " ))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "logic-valores", - "metadata": {}, - "outputs": [], - "source": [ - "# =============================================================\n", - "# VALORES - Ajuste de VALORTOTALME / VALORTOTALMN en SPartidasExpo\n", - "# Prorrateo proporcional al valor actual de cada partida.\n", - "# =============================================================\n", - "\n", - "import io as _io_val\n", - "\n", - "def cargar_excel_valores(path):\n", - " \"\"\"Lee Excel con 3 columnas: PEDIMENTO, VALOR_ME, VALOR_MN.\"\"\"\n", - " df = pd.read_excel(path, dtype=str)\n", - " norm = {c: c.strip().upper().replace(' ', '_') for c in df.columns}\n", - " df = df.rename(columns=norm)\n", - " aliases = {\n", - " 'PEDIMENTO': ['PEDIMENTO', 'PEDIMENTOEXPO', 'PEDIMENTO_EXPO'],\n", - " 'VALOR_ME': ['VALOR_ME', 'VALORME', 'VALOR_M_E', 'VALORTOTALME'],\n", - " 'VALOR_MN': ['VALOR_MN', 'VALORMN', 'VALOR_M_N', 'VALORTOTALMN'],\n", - " }\n", - " out = {}\n", - " for std, opts in aliases.items():\n", - " for o in opts:\n", - " if o in df.columns:\n", - " out[std] = df[o]; break\n", - " if std not in out:\n", - " raise ValueError(f'Falta la columna {std} (acepta: {opts})')\n", - " df2 = pd.DataFrame(out)\n", - " df2['PEDIMENTO'] = df2['PEDIMENTO'].astype(str).str.strip()\n", - " df2['VALOR_ME'] = pd.to_numeric(df2['VALOR_ME'], errors='coerce').fillna(0)\n", - " df2['VALOR_MN'] = pd.to_numeric(df2['VALOR_MN'], errors='coerce').fillna(0)\n", - " df2 = df2[df2['PEDIMENTO'] != ''].reset_index(drop=True)\n", - " return df2\n", - "\n", - "\n", - "def generar_plantilla_excel_valores():\n", - " df = pd.DataFrame([\n", - " {'PEDIMENTO': '07-3429-4015540', 'VALOR_ME': 12345.67, 'VALOR_MN': 234567.89},\n", - " {'PEDIMENTO': '07-3429-4015541', 'VALOR_ME': 8000.00, 'VALOR_MN': 152000.00},\n", - " ])\n", - " buf = _io_val.BytesIO()\n", - " with pd.ExcelWriter(buf, engine='openpyxl') as w:\n", - " df.to_excel(w, sheet_name='Valores', index=False)\n", - " buf.seek(0)\n", - " return buf.read()\n", - "\n", - "\n", - "def cargar_partidas_expo_pedimento(pedimento, db=None, esquema=None):\n", - " \"\"\"Trae partidas SPartidasExpo de un pedimento via SFacExp.\n", - " Si se pasan db y esquema, usa 3-part naming [db].[esquema].SPartidasExpo\n", - " para apuntar a otra BD de la misma instancia SQL Server.\"\"\"\n", - " if db and esquema:\n", - " prefix = f'[{db}].[{esquema}]'\n", - " else:\n", - " prefix = ''\n", - " pa = f'{prefix}.SPartidasExpo' if prefix else 'SPartidasExpo'\n", - " fa = f'{prefix}.SFacExp' if prefix else 'SFacExp'\n", - " sql = f\"\"\"\n", - " SELECT se.FACTURAEXPO, se.LINEA, spe.TIPOCAMBIO,\n", - " ISNULL(se.VALORTOTALME,0) AS VALORTOTALME,\n", - " ISNULL(se.VALORTOTALMN,0) AS VALORTOTALMN,\n", - " ISNULL(se.COSTOUNITARIOME,0) AS COSTOUNITARIOME,\n", - " ISNULL(se.CANTEXPO,0) AS CANTEXPO\n", - " FROM {pa} se\n", - " INNER JOIN {fa} spe ON spe.FACTURAEXPO = se.FACTURAEXPO\n", - " WHERE spe.PEDIMENTOEXPO = ?\n", - " ORDER BY se.FACTURAEXPO, se.LINEA\n", - " \"\"\"\n", - " return pd.read_sql(sql, scaii_conn, params=(str(pedimento).strip(),))\n", - "\n", - "\n", - "def analizar_valores(df_excel, modo='APLICAR_SIEMPRE', umbral_pct=50.0,\n", - " usar_shelter=False, progress=None, log=print):\n", - " \"\"\"Paso A: arma el plan de prorrateo. Devuelve (plan, resumen).\n", - " plan: una fila por partida con AJUSTE_ME, AJUSTE_MN, NUEVO_ME, NUEVO_MN.\n", - " resumen: una fila por pedimento con STATUS, factor, diferencia, etc.\n", - "\n", - " modo='APLICAR_SIEMPRE': escala todo sin importar la magnitud.\n", - " modo='USAR_UMBRAL': si |factor - 1| > umbral_pct/100, marca FUERA_DE_UMBRAL y no se aplica.\n", - " \"\"\"\n", - " prog = _Progress(progress)\n", - " if df_excel.empty:\n", - " log('Excel vacio.'); return pd.DataFrame(), pd.DataFrame()\n", - " # Localizar pedimentos en otras BDs si se pidio shelter.\n", - " ubic_map = {} # pedimento -> [(db, esquema), ...]\n", - " if usar_shelter:\n", - " try:\n", - " log('Buscando pedimentos en todas las BDs de la instancia...')\n", - " reporte, _, _ = buscar_pedimentos_en_bds(df_excel, progress=progress, log=log)\n", - " # Reconstruir mapping db/esquema (uno por (pedimento, BD))\n", - " sub = pd.read_sql(\"\"\"\n", - " SELECT name FROM sys.databases\n", - " WHERE database_id > 4 AND state_desc = 'ONLINE' AND HAS_DBACCESS(name) = 1\n", - " \"\"\", scaii_conn)\n", - " sub_set = set(sub['name'].astype(str).tolist())\n", - " for _, rr in reporte.iterrows():\n", - " if not rr['BasesEncontradas']: continue\n", - " ped = str(rr['PEDIMENTO']).strip()\n", - " for tok in str(rr['BasesEncontradas']).split(','):\n", - " tok = tok.strip()\n", - " if '.' in tok:\n", - " db, esq = tok.rsplit('.', 1)\n", - " if db in sub_set:\n", - " ubic_map.setdefault(ped, []).append((db, esq))\n", - " except Exception as e:\n", - " log(f'WARN shelter fallo: {e}. Usando BD actual.')\n", - " ubic_map = {}\n", - " prog.setup(len(df_excel), 'Analizando pedimentos...')\n", - " plan_rows = []\n", - " resumen_rows = []\n", - " umb = float(umbral_pct) / 100.0\n", - " for _, r in df_excel.iterrows():\n", - " pedimento = str(r['PEDIMENTO']).strip()\n", - " v_me_esp = float(r['VALOR_ME'] or 0)\n", - " v_mn_esp = float(r['VALOR_MN'] or 0)\n", - " # Lista de (db, esquema) a procesar para este pedimento\n", - " if usar_shelter:\n", - " destinos = ubic_map.get(pedimento, [])\n", - " if not destinos:\n", - " resumen_rows.append({'PEDIMENTO': pedimento, 'BaseDeDatos': '', 'Esquema': '',\n", - " 'SUM_ME_ACTUAL': 0, 'SUM_MN_ACTUAL': 0,\n", - " 'VALOR_ME_ESPERADO': v_me_esp, 'VALOR_MN_ESPERADO': v_mn_esp,\n", - " 'PARTIDAS': 0, 'FACTOR_ME': 0, 'FACTOR_MN': 0,\n", - " 'STATUS': 'NO_ENCONTRADO_EN_BDS'})\n", - " prog.step(desc=pedimento[:30]); continue\n", - " else:\n", - " destinos = [(None, None)]\n", - " # Procesar cada destino\n", - " for (db_dest, esq_dest) in destinos:\n", - " try:\n", - " df_part = cargar_partidas_expo_pedimento(pedimento, db=db_dest, esquema=esq_dest)\n", - " except Exception as e:\n", - " log(f' ERROR query {pedimento} [{db_dest or DB_ACTUAL}]: {e}')\n", - " resumen_rows.append({'PEDIMENTO': pedimento,\n", - " 'BaseDeDatos': db_dest or DB_ACTUAL, 'Esquema': esq_dest or '',\n", - " 'SUM_ME_ACTUAL': 0, 'SUM_MN_ACTUAL': 0,\n", - " 'VALOR_ME_ESPERADO': v_me_esp, 'VALOR_MN_ESPERADO': v_mn_esp,\n", - " 'PARTIDAS': 0, 'FACTOR_ME': 0, 'FACTOR_MN': 0,\n", - " 'STATUS': f'ERROR: {e}'})\n", - " continue\n", - " if df_part.empty:\n", - " resumen_rows.append({'PEDIMENTO': pedimento,\n", - " 'BaseDeDatos': db_dest or DB_ACTUAL, 'Esquema': esq_dest or '',\n", - " 'SUM_ME_ACTUAL': 0, 'SUM_MN_ACTUAL': 0,\n", - " 'VALOR_ME_ESPERADO': v_me_esp, 'VALOR_MN_ESPERADO': v_mn_esp,\n", - " 'PARTIDAS': 0, 'FACTOR_ME': 0, 'FACTOR_MN': 0,\n", - " 'STATUS': 'SIN_PARTIDAS'})\n", - " continue\n", - " sum_me = float(df_part['VALORTOTALME'].astype(float).sum())\n", - " sum_mn = float(df_part['VALORTOTALMN'].astype(float).sum())\n", - " if abs(sum_me) < 1e-9 and v_me_esp > 0:\n", - " status = 'SIN_BASE_ME'\n", - " elif abs(sum_mn) < 1e-9 and v_mn_esp > 0:\n", - " status = 'SIN_BASE_MN'\n", - " else:\n", - " f_me_chk = (v_me_esp / sum_me) if sum_me > 1e-9 else 0.0\n", - " f_mn_chk = (v_mn_esp / sum_mn) if sum_mn > 1e-9 else 0.0\n", - " if modo == 'USAR_UMBRAL':\n", - " if abs(f_me_chk - 1.0) > umb or abs(f_mn_chk - 1.0) > umb:\n", - " status = 'FUERA_DE_UMBRAL'\n", - " else:\n", - " status = 'AJUSTAR'\n", - " else:\n", - " status = 'AJUSTAR'\n", - "\n", - " f_me = (v_me_esp / sum_me) if sum_me > 1e-9 else 0.0\n", - " f_mn = (v_mn_esp / sum_mn) if sum_mn > 1e-9 else 0.0\n", - "\n", - " if status == 'AJUSTAR':\n", - " df_part = df_part.copy()\n", - " df_part['NUEVO_ME'] = (df_part['VALORTOTALME'].astype(float) * f_me).round(6)\n", - " df_part['NUEVO_MN'] = (df_part['VALORTOTALMN'].astype(float) * f_mn).round(6)\n", - " diff_me = round(v_me_esp - df_part['NUEVO_ME'].sum(), 6)\n", - " diff_mn = round(v_mn_esp - df_part['NUEVO_MN'].sum(), 6)\n", - " if abs(diff_me) > 1e-9:\n", - " idx_last = df_part.index[-1]\n", - " df_part.at[idx_last, 'NUEVO_ME'] = round(df_part.at[idx_last, 'NUEVO_ME'] + diff_me, 6)\n", - " if abs(diff_mn) > 1e-9:\n", - " idx_last = df_part.index[-1]\n", - " df_part.at[idx_last, 'NUEVO_MN'] = round(df_part.at[idx_last, 'NUEVO_MN'] + diff_mn, 6)\n", - " df_part['AJUSTE_ME'] = (df_part['NUEVO_ME'] - df_part['VALORTOTALME'].astype(float)).round(6)\n", - " df_part['AJUSTE_MN'] = (df_part['NUEVO_MN'] - df_part['VALORTOTALMN'].astype(float)).round(6)\n", - " df_part['PEDIMENTO'] = pedimento\n", - " for _, p in df_part.iterrows():\n", - " plan_rows.append({\n", - " 'PEDIMENTO': pedimento,\n", - " 'BaseDeDatos': db_dest or DB_ACTUAL,\n", - " 'Esquema': esq_dest or '',\n", - " 'FACTURAEXPO': p['FACTURAEXPO'],\n", - " 'LINEA': int(p['LINEA']),\n", - " 'CANTEXPO': float(p['CANTEXPO']),\n", - " 'TIPOCAMBIO': float(p['TIPOCAMBIO'] or 0),\n", - " 'VALORTOTALME_ACTUAL': float(p['VALORTOTALME']),\n", - " 'VALORTOTALMN_ACTUAL': float(p['VALORTOTALMN']),\n", - " 'AJUSTE_ME': float(p['AJUSTE_ME']),\n", - " 'AJUSTE_MN': float(p['AJUSTE_MN']),\n", - " 'NUEVO_ME': float(p['NUEVO_ME']),\n", - " 'NUEVO_MN': float(p['NUEVO_MN']),\n", - " })\n", - "\n", - " resumen_rows.append({\n", - " 'PEDIMENTO': pedimento,\n", - " 'BaseDeDatos': db_dest or DB_ACTUAL,\n", - " 'Esquema': esq_dest or '',\n", - " 'SUM_ME_ACTUAL': round(sum_me, 6),\n", - " 'SUM_MN_ACTUAL': round(sum_mn, 6),\n", - " 'VALOR_ME_ESPERADO': v_me_esp,\n", - " 'VALOR_MN_ESPERADO': v_mn_esp,\n", - " 'PARTIDAS': len(df_part),\n", - " 'FACTOR_ME': round(f_me, 6),\n", - " 'FACTOR_MN': round(f_mn, 6),\n", - " 'STATUS': status,\n", - " })\n", - " prog.step(desc=pedimento[:30])\n", - " prog.done('Analisis listo')\n", - " return pd.DataFrame(plan_rows), pd.DataFrame(resumen_rows)\n", - "\n", - "\n", - "def exportar_excel_valores(plan, resumen, ruta):\n", - " with pd.ExcelWriter(ruta, engine='openpyxl') as w:\n", - " if not plan.empty: plan.to_excel(w, sheet_name='Plan_Detalle', index=False)\n", - " if not resumen.empty: resumen.to_excel(w, sheet_name='Resumen', index=False)\n", - "\n", - "\n", - "def ejecutar_valores(plan, dry_run=True, aplicar_vtmn=True, aplicar_mptemp=False, progress=None, log=print):\n", - " \"\"\"Paso B: UPDATE SPartidasExpo por cada partida del plan, usando 3-part\n", - " naming si la fila trae BaseDeDatos/Esquema (cuando se uso shelter).\n", - " Transaccion por (pedimento, BD) rollback si alguna partida falla.\"\"\"\n", - " assert isinstance(dry_run, bool), 'dry_run debe ser bool'\n", - " prog = _Progress(progress)\n", - " if plan is None or plan.empty:\n", - " log('ERROR: plan vacio, corre primero \"Analizar\".')\n", - " return\n", - " # Si el plan no trae las columnas (compat), las agregamos vacias\n", - " if 'BaseDeDatos' not in plan.columns:\n", - " plan = plan.copy(); plan['BaseDeDatos'] = ''\n", - " if 'Esquema' not in plan.columns:\n", - " plan = plan.copy(); plan['Esquema'] = ''\n", - " # Agrupar por (pedimento, BD, esquema)\n", - " grupos = plan.groupby(['PEDIMENTO', 'BaseDeDatos', 'Esquema'], dropna=False)\n", - " prog.setup(len(grupos), 'Procesando pedimentos')\n", - " upd = errores = 0\n", - " err_list = []\n", - " for (ped, db, esq), fil in grupos:\n", - " if db and esq:\n", - " target = f'[{db}].[{esq}].SPartidasExpo'\n", - " else:\n", - " target = 'SPartidasExpo'\n", - " sets, params_tmpl = ['VALORTOTALME = ?'], ['ME']\n", - " if aplicar_vtmn:\n", - " sets.append('VALORTOTALMN = ?'); params_tmpl.append('MN')\n", - " if aplicar_mptemp:\n", - " sets.append('ValorMPTempMN = ?'); params_tmpl.append('MN')\n", - " upd_sql = f\"UPDATE {target} SET {', '.join(sets)} WHERE FACTURAEXPO = ? AND LINEA = ?\"\n", - " try:\n", - " with scaii_conn.cursor() as cur:\n", - " for _, p in fil.iterrows():\n", - " if not dry_run:\n", - " vals = []\n", - " for t in params_tmpl:\n", - " vals.append(float(p['NUEVO_ME']) if t == 'ME' else float(p['NUEVO_MN']))\n", - " vals.extend([p['FACTURAEXPO'], int(p['LINEA'])])\n", - " cur.execute(upd_sql, *vals)\n", - " upd += 1\n", - " if not dry_run: scaii_conn.commit()\n", - " except Exception as e:\n", - " if not dry_run: scaii_conn.rollback()\n", - " errores += 1\n", - " etq = f'{ped} [{db or DB_ACTUAL}]' if db else str(ped)\n", - " err_list.append((etq, str(e)))\n", - " log(f' ERROR {etq}: {e}')\n", - " prog.step(desc=str(ped)[:30])\n", - " prog.done(f'{upd} partidas')\n", - " bds = plan['BaseDeDatos'].replace('', pd.NA).dropna().unique().tolist()\n", - " log(f'\\n=== RESUMEN Valores (DRY_RUN={dry_run}) ===')\n", - " log(f' Partidas actualizadas: {upd:,}')\n", - " log(f' Pedimentos procesados: {plan[\"PEDIMENTO\"].nunique():,}')\n", - " log(f' BDs tocadas : {bds if bds else \"(solo la actual)\"}')\n", - " log(f' Errores : {errores:,}')\n", - " for f, e in err_list[:5]:\n", - " log(f' {f}: {e}')\n", - "\n", - "\n", - "# =============================================================\n", - "# SHELTER - Busca pedimentos en todas las BDs de la instancia\n", - "# (todas las que tengan SPedimentos.PEDIMENTO)\n", - "# =============================================================\n", - "\n", - "def buscar_pedimentos_en_bds(df_excel, progress=None, log=print):\n", - " \"\"\"Recorre sys.databases, detecta BDs con SPedimentos.PEDIMENTO y busca\n", - " los pedimentos del Excel en cada una. Devuelve (reporte, resumen_bd, faltantes).\"\"\"\n", - " prog = _Progress(progress)\n", - " if df_excel is None or df_excel.empty:\n", - " log('Excel vacio.')\n", - " return pd.DataFrame(), pd.DataFrame(), []\n", - " pedimentos = [str(p).strip() for p in df_excel['PEDIMENTO'].astype(str).tolist() if str(p).strip()]\n", - " if not pedimentos:\n", - " log('Sin pedimentos validos.')\n", - " return pd.DataFrame(), pd.DataFrame(), []\n", - "\n", - " # 1) BDs online accesibles\n", - " try:\n", - " df_dbs = pd.read_sql(\"\"\"\n", - " SELECT name FROM sys.databases\n", - " WHERE database_id > 4 AND state_desc = 'ONLINE' AND HAS_DBACCESS(name) = 1\n", - " ORDER BY name\n", - " \"\"\", scaii_conn)\n", - " except Exception as e:\n", - " log(f'ERROR listando BDs: {e}')\n", - " return pd.DataFrame(), pd.DataFrame(), []\n", - " log(f'BDs online accesibles: {len(df_dbs)}')\n", - " if df_dbs.empty:\n", - " return pd.DataFrame(), pd.DataFrame(), pedimentos\n", - "\n", - " # 2) Por cada BD, detectar SPedimentos.PEDIMENTO + buscar\n", - " prog.setup(len(df_dbs), 'Recorriendo BDs...')\n", - " rows = []\n", - " LOTE = 1000\n", - " for _, r in df_dbs.iterrows():\n", - " db = str(r['name'])\n", - " try:\n", - " sql_check = f\"\"\"\n", - " SELECT s.name AS Esquema\n", - " FROM [{db}].sys.tables t\n", - " JOIN [{db}].sys.schemas s ON s.schema_id = t.schema_id\n", - " JOIN [{db}].sys.columns c ON c.object_id = t.object_id\n", - " WHERE t.name = 'SPedimentos' AND c.name = 'PEDIMENTO'\n", - " \"\"\"\n", - " df_check = pd.read_sql(sql_check, scaii_conn)\n", - " except Exception as e:\n", - " log(f' [{db}] saltada (metadata): {e}')\n", - " prog.step(desc=db[:30]); continue\n", - " if df_check.empty:\n", - " prog.step(desc=db[:30]); continue\n", - " for _, ec in df_check.iterrows():\n", - " esquema = str(ec['Esquema'])\n", - " for i in range(0, len(pedimentos), LOTE):\n", - " sub = pedimentos[i:i+LOTE]\n", - " placeholders = ','.join(['?'] * len(sub))\n", - " sql_search = (f\"SELECT DISTINCT PEDIMENTO \"\n", - " f\"FROM [{db}].[{esquema}].SPedimentos \"\n", - " f\"WHERE PEDIMENTO IN ({placeholders})\")\n", - " try:\n", - " df_found = pd.read_sql(sql_search, scaii_conn, params=tuple(sub))\n", - " for _, f in df_found.iterrows():\n", - " rows.append({\n", - " 'PEDIMENTO': str(f['PEDIMENTO']).strip(),\n", - " 'BaseDeDatos': db,\n", - " 'Esquema': esquema,\n", - " })\n", - " except Exception as e:\n", - " log(f' [{db}].[{esquema}] saltada (busqueda): {e}')\n", - " break\n", - " prog.step(desc=db[:30])\n", - " df_result = pd.DataFrame(rows)\n", - "\n", - " # 3) Reporte por pedimento\n", - " df_in = pd.DataFrame({'PEDIMENTO': pedimentos}).drop_duplicates().reset_index(drop=True)\n", - " if df_result.empty:\n", - " reporte = df_in.assign(BasesEncontradas='', NumDBs=0)\n", - " else:\n", - " df_result['Ubicacion'] = df_result['BaseDeDatos'] + '.' + df_result['Esquema']\n", - " agg = (df_result.groupby('PEDIMENTO', as_index=False)\n", - " .agg(BasesEncontradas=('Ubicacion', lambda s: ', '.join(sorted(set(s)))),\n", - " NumDBs=('Ubicacion', 'nunique')))\n", - " reporte = df_in.merge(agg, on='PEDIMENTO', how='left')\n", - " reporte['BasesEncontradas'] = reporte['BasesEncontradas'].fillna('')\n", - " reporte['NumDBs'] = reporte['NumDBs'].fillna(0).astype(int)\n", - "\n", - " # 4) Resumen por BD\n", - " if df_result.empty:\n", - " resumen_bd = pd.DataFrame(columns=['BaseDeDatos', 'Esquema', 'PedimentosEncontrados'])\n", - " else:\n", - " resumen_bd = (df_result.groupby(['BaseDeDatos', 'Esquema'])\n", - " .size().reset_index(name='PedimentosEncontrados')\n", - " .sort_values('PedimentosEncontrados', ascending=False))\n", - "\n", - " # 5) Faltantes\n", - " faltantes = reporte.loc[reporte['NumDBs'] == 0, 'PEDIMENTO'].tolist()\n", - "\n", - " prog.done(f'{len(df_result)} hits en {df_result[\"BaseDeDatos\"].nunique() if not df_result.empty else 0} BDs')\n", - " return reporte, resumen_bd, faltantes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "logic-datastage", - "metadata": {}, - "outputs": [], - "source": [ - "# =============================================================\n", - "# DATASTAGE - Carga de archivos .asc en Postgres (tablas Registro)\n", - "# Migrado de CONSULTORIA-IMMEX_old/HOME/DATASTAGE/index.php\n", - "# y de carga_datastage_dual_db.ipynb.\n", - "# =============================================================\n", - "\n", - "import re as _re_ds\n", - "import csv as _csv_ds\n", - "from pathlib import Path as _Path_ds\n", - "\n", - "_TIPOS_NUMERICOS_PG = {\n", - " 'numeric', 'decimal', 'real', 'double precision',\n", - " 'integer', 'smallint', 'bigint', 'float',\n", - "}\n", - "\n", - "_BATCH_DS = 100\n", - "\n", - "\n", - "def _ds_conn():\n", - " \"\"\"Devuelve una conexion psycopg2 fresca a partir de PG_CONFIG.\"\"\"\n", - " if not DATASTAGE_OK or not _PSYCOPG2_OK:\n", - " raise RuntimeError(DATASTAGE_MSG or 'Postgres no disponible')\n", - " return _psycopg2.connect(\n", - " host=PG_CONFIG['host'], port=PG_CONFIG['port'], dbname=PG_CONFIG['dbname'],\n", - " user=PG_CONFIG['user'], password=PG_CONFIG['password'])\n", - "\n", - "\n", - "def extraer_tipo_registro(nombre_archivo):\n", - " \"\"\"'725199_501.asc' -> '501' | '725199_Inci.asc' -> 'Inci'.\"\"\"\n", - " m = _re_ds.search(r'_(\\d{3})\\.asc$', nombre_archivo, flags=_re_ds.IGNORECASE)\n", - " if m: return m.group(1)\n", - " m = _re_ds.search(r'_(\\w+)\\.asc$', nombre_archivo, flags=_re_ds.IGNORECASE)\n", - " if m: return m.group(1)\n", - " return None\n", - "\n", - "\n", - "def obtener_esquema_tabla(conn, tabla):\n", - " \"\"\"Columnas y tipos de una tabla de Postgres (information_schema).\"\"\"\n", - " q = \"\"\"\n", - " SELECT column_name, data_type\n", - " FROM information_schema.columns\n", - " WHERE table_name = %s\n", - " ORDER BY ordinal_position\n", - " \"\"\"\n", - " with conn.cursor() as cur:\n", - " cur.execute(q, (tabla,))\n", - " rows = cur.fetchall()\n", - " return [{'column_name': r[0], 'data_type': r[1]} for r in rows]\n", - "\n", - "\n", - "def listar_tablas_registro(conn):\n", - " \"\"\"Tablas que empiezan con 'Registro' (case-insensitive).\"\"\"\n", - " q = \"\"\"\n", - " SELECT table_name\n", - " FROM information_schema.tables\n", - " WHERE table_schema = 'public'\n", - " AND lower(table_name) LIKE 'registro%'\n", - " ORDER BY table_name\n", - " \"\"\"\n", - " with conn.cursor() as cur:\n", - " cur.execute(q)\n", - " return [r[0] for r in cur.fetchall()]\n", - "\n", - "\n", - "def listar_archivos_asc(ruta_raiz):\n", - " \"\"\"Devuelve lista de Path con todos los .asc encontrados.\n", - " Soporta dos layouts:\n", - " - ruta/2020/*.asc, ruta/2021/*.asc, ... (estructura DATASTAGE_HONDA)\n", - " - ruta/*.asc (carpeta plana)\n", - " Ignora subcarpetas de meses para evitar duplicados con la estructura HONDA.\"\"\"\n", - " raiz = _Path_ds(ruta_raiz)\n", - " if not raiz.exists() or not raiz.is_dir():\n", - " return []\n", - " archivos = []\n", - " # Modo HONDA: solo .asc directos en subcarpetas que sean YYYY\n", - " subdirs_anio = [d for d in raiz.iterdir() if d.is_dir() and d.name.isdigit()]\n", - " if subdirs_anio:\n", - " for d in sorted(subdirs_anio):\n", - " for f in sorted(d.glob('*.asc')):\n", - " if f.is_file():\n", - " archivos.append(f)\n", - " # Modo plano: .asc directos en la raiz\n", - " for f in sorted(raiz.glob('*.asc')):\n", - " if f.is_file():\n", - " archivos.append(f)\n", - " return archivos\n", - "\n", - "\n", - "def _limpiar_valor(valor, tipo_dato):\n", - " v = (valor or '').strip()\n", - " # Filtrar NUL (Postgres lo rechaza) y otros caracteres de control\n", - " v = v.replace('\\x00', '').replace('\\r', '')\n", - " if v == '': return None\n", - " if tipo_dato in _TIPOS_NUMERICOS_PG:\n", - " v = v.replace(',', '.')\n", - " v = _re_ds.sub(r'[^0-9.\\-]', '', v)\n", - " if v in ('', '-'): return None\n", - " return v\n", - "\n", - "\n", - "def cargar_archivo_asc(conn, ruta_archivo):\n", - " \"\"\"Carga un .asc en su tabla Registro. Usa SAVEPOINT por archivo:\n", - " un error no rompe los archivos anteriores. Devuelve dict con resultado.\"\"\"\n", - " p = _Path_ds(ruta_archivo)\n", - " nombre = p.name\n", - " registro = extraer_tipo_registro(nombre)\n", - " if registro is None:\n", - " return {'archivo': nombre, 'tabla': None, 'filas': 0,\n", - " 'estatus': 'SKIP', 'mensaje': 'No se reconoce la estructura'}\n", - " tabla = f'Registro{registro}'\n", - " columnas = obtener_esquema_tabla(conn, tabla)\n", - " if not columnas:\n", - " return {'archivo': nombre, 'tabla': tabla, 'filas': 0,\n", - " 'estatus': 'SKIP', 'mensaje': f\"Tabla '{tabla}' no existe\"}\n", - " nombres_cols = [c['column_name'] for c in columnas]\n", - " tipos_cols = {c['column_name']: c['data_type'] for c in columnas}\n", - " n = len(nombres_cols)\n", - " cols_sql = ', '.join(f'\"{c}\"' for c in nombres_cols)\n", - " placeholders = ', '.join(['%s'] * n)\n", - " insert_sql = f'INSERT INTO \"{tabla}\" ({cols_sql}) VALUES ({placeholders})'\n", - " filas_ins = 0\n", - " with conn.cursor() as cur:\n", - " cur.execute('SAVEPOINT archivo_sp')\n", - " try:\n", - " with open(p, 'r', encoding='latin-1') as f:\n", - " reader = _csv_ds.reader(f, delimiter='|')\n", - " next(reader, None) # header\n", - " batch = []\n", - " for fila in reader:\n", - " fila = fila[:n]\n", - " while len(fila) < n:\n", - " fila.append('')\n", - " valores = [_limpiar_valor(v, tipos_cols[nombres_cols[i]])\n", - " for i, v in enumerate(fila)]\n", - " batch.append(tuple(valores))\n", - " if len(batch) >= _BATCH_DS:\n", - " with conn.cursor() as cur:\n", - " _psycopg2.extras.execute_batch(cur, insert_sql, batch)\n", - " filas_ins += len(batch)\n", - " batch = []\n", - " if batch:\n", - " with conn.cursor() as cur:\n", - " _psycopg2.extras.execute_batch(cur, insert_sql, batch)\n", - " filas_ins += len(batch)\n", - " with conn.cursor() as cur:\n", - " cur.execute('RELEASE SAVEPOINT archivo_sp')\n", - " return {'archivo': nombre, 'tabla': tabla, 'filas': filas_ins,\n", - " 'estatus': 'OK', 'mensaje': f'{filas_ins:,} filas insertadas'}\n", - " except Exception as e:\n", - " with conn.cursor() as cur:\n", - " cur.execute('ROLLBACK TO SAVEPOINT archivo_sp')\n", - " return {'archivo': nombre, 'tabla': tabla, 'filas': filas_ins,\n", - " 'estatus': 'ERROR', 'mensaje': str(e)}\n", - "\n", - "\n", - "def cargar_directorio_datastage(ruta_raiz, progress=None, log=print):\n", - " \"\"\"Itera todos los .asc encontrados y carga en su tabla Registro.\n", - " Devuelve DataFrame con resultado por archivo.\"\"\"\n", - " prog = _Progress(progress)\n", - " archivos = listar_archivos_asc(ruta_raiz)\n", - " if not archivos:\n", - " log(f'No se encontraron .asc en: {ruta_raiz}')\n", - " return pd.DataFrame()\n", - " log(f'Encontrados {len(archivos):,} archivos .asc')\n", - " prog.setup(len(archivos), 'Cargando .asc')\n", - " # psycopg2.extras se importa lazy\n", - " import psycopg2.extras\n", - " _psycopg2.extras = psycopg2.extras\n", - " conn = _ds_conn()\n", - " conn.autocommit = False\n", - " resultados = []\n", - " ok = sk = er = 0\n", - " total_filas = 0\n", - " try:\n", - " for p in archivos:\n", - " r = cargar_archivo_asc(conn, p)\n", - " resultados.append(r)\n", - " if r['estatus'] == 'OK':\n", - " ok += 1; total_filas += r['filas']\n", - " elif r['estatus'] == 'SKIP':\n", - " sk += 1\n", - " else:\n", - " er += 1\n", - " log(f\" ERROR {r['archivo']}: {r['mensaje']}\")\n", - " prog.step(desc=f\"{r['estatus']} {r['archivo'][:25]}\")\n", - " conn.commit()\n", - " except Exception as e:\n", - " conn.rollback()\n", - " log(f'EXCEPCION GLOBAL: {e}')\n", - " finally:\n", - " conn.close()\n", - " prog.done(f'{ok} OK / {sk} SKIP / {er} ERR')\n", - " log(f'\\n=== RESUMEN DataStage ===')\n", - " log(f' Archivos OK : {ok:,}')\n", - " log(f' Archivos SKIP : {sk:,}')\n", - " log(f' Archivos ERROR : {er:,}')\n", - " log(f' Filas insertadas: {total_filas:,}')\n", - " return pd.DataFrame(resultados)\n", - "\n", - "\n", - "def previsualizar_archivos_ds(ruta_raiz):\n", - " \"\"\"Devuelve DataFrame con archivos detectados + tabla destino + tamanio.\"\"\"\n", - " archivos = listar_archivos_asc(ruta_raiz)\n", - " rows = []\n", - " for p in archivos:\n", - " reg = extraer_tipo_registro(p.name)\n", - " rows.append({\n", - " 'archivo': p.name,\n", - " 'tabla_destino': f'Registro{reg}' if reg else '(no detectado)',\n", - " 'tamanio_kb': round(p.stat().st_size / 1024, 1),\n", - " 'ruta': str(p),\n", - " })\n", - " return pd.DataFrame(rows)\n", - "\n", - "\n", - "def truncar_tablas_registro(progress=None, log=print):\n", - " \"\"\"TRUNCATE en todas las tablas Registro*. Devuelve dict {tabla: filas_antes}.\"\"\"\n", - " prog = _Progress(progress)\n", - " conn = _ds_conn()\n", - " conn.autocommit = False\n", - " resultado = {}\n", - " try:\n", - " with conn.cursor() as cur:\n", - " cur.execute(\"\"\"\n", - " SELECT table_name FROM information_schema.tables\n", - " WHERE table_schema = 'public'\n", - " AND lower(table_name) LIKE 'registro%'\n", - " ORDER BY table_name\n", - " \"\"\")\n", - " tablas = [r[0] for r in cur.fetchall()]\n", - " if not tablas:\n", - " log('No hay tablas Registro* en Postgres.')\n", - " return resultado\n", - " prog.setup(len(tablas), 'Truncando tablas')\n", - " for t in tablas:\n", - " with conn.cursor() as cur:\n", - " cur.execute(f'SELECT COUNT(*) FROM \"{t}\"')\n", - " antes = cur.fetchone()[0]\n", - " cur.execute(f'TRUNCATE TABLE \"{t}\"')\n", - " resultado[t] = antes\n", - " log(f' TRUNCATE {t}: {antes:,} filas eliminadas')\n", - " prog.step(desc=t[:30])\n", - " conn.commit()\n", - " prog.done(f'{len(tablas)} tablas truncadas')\n", - " log(f'\\nTotal: {sum(resultado.values()):,} filas eliminadas en {len(tablas)} tablas.')\n", - " except Exception as e:\n", - " conn.rollback()\n", - " log(f'ERROR: {e}')\n", - " prog.error('Error')\n", - " finally:\n", - " conn.close()\n", - " return resultado\n", - "\n", - "\n", - "def estadisticas_tablas_registro(progress=None):\n", - " \"\"\"Cuenta filas por tabla Registro*. Devuelve DataFrame con columnas: tabla, filas.\"\"\"\n", - " prog = _Progress(progress)\n", - " conn = _ds_conn()\n", - " try:\n", - " with conn.cursor() as cur:\n", - " cur.execute(\"\"\"\n", - " SELECT table_name FROM information_schema.tables\n", - " WHERE table_schema = 'public'\n", - " AND lower(table_name) LIKE 'registro%'\n", - " ORDER BY table_name\n", - " \"\"\")\n", - " tablas = [r[0] for r in cur.fetchall()]\n", - " if not tablas:\n", - " return pd.DataFrame(columns=['tabla', 'filas'])\n", - " prog.setup(len(tablas), 'Contando filas')\n", - " rows = []\n", - " for t in tablas:\n", - " with conn.cursor() as cur:\n", - " cur.execute(f'SELECT COUNT(*) FROM \"{t}\"')\n", - " n = cur.fetchone()[0]\n", - " rows.append({'tabla': t, 'filas': n})\n", - " prog.step(desc=t[:30])\n", - " prog.done('Listo')\n", - " return pd.DataFrame(rows)\n", - " finally:\n", - " conn.close()\n", - "\n", - "\n", - "def obtener_muestra_tabla(tabla, limit=100, offset=0):\n", - " \"\"\"Devuelve hasta `limit` filas de la tabla (con OFFSET) como DataFrame.\"\"\"\n", - " conn = _ds_conn()\n", - " try:\n", - " sql = f'SELECT * FROM \"{tabla}\" LIMIT %s OFFSET %s'\n", - " return pd.read_sql(sql, conn, params=(limit, offset))\n", - " finally:\n", - " conn.close()\n", - "\n", - "\n", - "# ============================================================\n", - "# REPORTES DataStage (migrados de PHP a Postgres)\n", - "# ============================================================\n", - "\n", - "def _ds_format_pedimento_sql(yy_col, sec_col, pat_col, ped_col):\n", - " \"\"\"Construye PEDIMENTO formato YY-SS-PPPP-NNNNNNN en SQL Postgres.\"\"\"\n", - " return (f\"(RIGHT(LEFT({yy_col}::text, 4), 2) || '-' \"\n", - " f\"|| LEFT({sec_col}, 2) || '-' \"\n", - " f\"|| {pat_col} || '-' \"\n", - " f\"|| {ped_col})\")\n", - "\n", - "\n", - "def cat_pedimentos_ds(fecha_ini, fecha_fin):\n", - " \"\"\"Estructura CAT Pedimentos. Toma Registro501 en el rango y marca si fue\n", - " rectificado (existe en Registro701 como pedimento anterior).\n", - " Migrado de generar_estructuracat.php.\"\"\"\n", - " sql = \"\"\"\n", - " WITH RECURSIVE historial_rect AS (\n", - " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", - " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", - " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", - " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", - " FROM \"Registro701\" R7\n", - " UNION ALL\n", - " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", - " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", - " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", - " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", - " FROM \"Registro701\" R7\n", - " INNER JOIN historial_rect HR ON\n", - " (RIGHT(LEFT(R7.\"FechaOperacionAnterior\"::text, 4), 2) || '-' ||\n", - " LEFT(R7.\"SeccionAduaneraAnterior\", 2) || '-' ||\n", - " R7.\"PatenteAnterior\" || '-' || R7.\"PedimentoAnterior\")\n", - " =\n", - " (RIGHT(LEFT(HR.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(HR.\"SeccionAduanera\", 2) || '-' ||\n", - " HR.\"Patente\" || '-' || HR.\"Pedimento\")\n", - " )\n", - " SELECT\n", - " (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\") AS \"PEDIMENTO\",\n", - " CASE WHEN Q1.\"TipoOperacion\"::text = '1' THEN 'I'\n", - " WHEN Q1.\"TipoOperacion\"::text = '2' THEN 'E'\n", - " ELSE 'Otro' END AS \"TIPO PEDIMENTO\",\n", - " CASE WHEN EXISTS (\n", - " SELECT 1 FROM historial_rect H\n", - " WHERE (RIGHT(LEFT(H.\"FechaOperacionAnterior\"::text, 4), 2) || '-' ||\n", - " LEFT(H.\"SeccionAduaneraAnterior\", 2) || '-' ||\n", - " H.\"PatenteAnterior\" || '-' || H.\"PedimentoAnterior\")\n", - " = (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", - " ) THEN 'R1' ELSE Q1.\"ClaveDocumento\" END AS \"CLAVE PEDIMENTO\",\n", - " Q1.\"FechaPagoReal\" AS \"FECHA PAGO\",\n", - " Q1.\"SeccionAduaneraEntrada\" AS \"SECCION ADUANERA\",\n", - " Q1.\"MedioTransporteEntrada_Salida\" AS \"MEDIO TRANSPORTE ENTRADA\",\n", - " Q1.\"MedioTransporteArribo\" AS \"MEDIO TRANSPORTE ARRIBO\",\n", - " Q1.\"MedioTransporteSalida\" AS \"MEDIO TRANSPORTE SALIDA\",\n", - " CASE WHEN EXISTS (\n", - " SELECT 1 FROM historial_rect H\n", - " WHERE (RIGHT(LEFT(H.\"FechaOperacionAnterior\"::text, 4), 2) || '-' ||\n", - " LEFT(H.\"SeccionAduaneraAnterior\", 2) || '-' ||\n", - " H.\"PatenteAnterior\" || '-' || H.\"PedimentoAnterior\")\n", - " = (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", - " ) THEN 'Si' ELSE 'No' END AS \"SE RECTIFICO\",\n", - " (SELECT (RIGHT(LEFT(H.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(H.\"SeccionAduanera\", 2) || '-' ||\n", - " H.\"Patente\" || '-' || H.\"Pedimento\")\n", - " FROM historial_rect H\n", - " WHERE (RIGHT(LEFT(H.\"FechaOperacionAnterior\"::text, 4), 2) || '-' ||\n", - " LEFT(H.\"SeccionAduaneraAnterior\", 2) || '-' ||\n", - " H.\"PatenteAnterior\" || '-' || H.\"PedimentoAnterior\")\n", - " = (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", - " ORDER BY H.\"FechaPagoReal\" DESC LIMIT 1) AS \"PEDIMENTO RECTIFICADO\",\n", - " Q1.\"TotalSeguros\" AS \"SEGUROS\",\n", - " Q1.\"TotalEmbalajes\" AS \"EMBALAJES\",\n", - " Q1.\"TotalIncrementables\" AS \"OTROS INCREMENTALES\"\n", - " FROM \"Registro501\" Q1\n", - " WHERE Q1.\"FechaPagoReal\" BETWEEN %s AND %s\n", - " ORDER BY Q1.\"FechaPagoReal\"\n", - " \"\"\"\n", - " return pd.read_sql(sql, pg_engine, params=(fecha_ini, fecha_fin))\n", - "\n", - "\n", - "def cat_pedimentos_rect_ds(fecha_ini, fecha_fin):\n", - " \"\"\"Estructura CAT Pedimentos Rectificados. Desde Registro701 con tipo de\n", - " operacion tomado de Registro501 que lo origino.\n", - " Migrado de generar_estructuracatScaf.php.\"\"\"\n", - " sql = \"\"\"\n", - " SELECT\n", - " (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\") AS \"PEDIMENTO\",\n", - " COALESCE(Q2.\"TipoOperacion\"::text, 'Desconocido') AS \"TIPO PEDIMENTO\",\n", - " Q1.\"ClaveDocumento\" AS \"CLAVE PEDIMENTO\",\n", - " Q1.\"FechaPagoReal\" AS \"FECHA PAGO\",\n", - " Q1.\"SeccionAduanera\" AS \"SECCION ADUANERA\",\n", - " CASE WHEN Q3.\"Pedimento\" IS NOT NULL THEN 'SI' ELSE 'NO' END AS \"SE RECTIFICO\",\n", - " COALESCE(\n", - " (RIGHT(LEFT(Q3.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q3.\"SeccionAduanera\", 2) || '-' ||\n", - " Q3.\"Patente\" || '-' || Q3.\"Pedimento\"), '') AS \"PEDIMENTO RECTIFICADO\",\n", - " Q2.\"MedioTransporteEntrada_Salida\" AS \"MEDIO TRANSPORTE ENTRADA\",\n", - " Q2.\"MedioTransporteArribo\" AS \"MEDIO TRANSPORTE ARRIBO\",\n", - " Q2.\"MedioTransporteSalida\" AS \"MEDIO TRANSPORTE SALIDA\",\n", - " Q2.\"TotalSeguros\" AS \"SEGUROS\",\n", - " Q2.\"TotalEmbalajes\" AS \"EMBALAJES\",\n", - " Q2.\"TotalIncrementables\" AS \"OTROS INCREMENTALES\"\n", - " FROM \"Registro701\" Q1\n", - " LEFT JOIN \"Registro501\" Q2 ON\n", - " (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", - " =\n", - " (RIGHT(LEFT(Q2.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q2.\"SeccionAduanera\", 2) || '-' ||\n", - " Q2.\"Patente\" || '-' || Q2.\"Pedimento\")\n", - " LEFT JOIN \"Registro701\" Q3 ON\n", - " (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", - " =\n", - " (RIGHT(LEFT(Q3.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q3.\"SeccionAduanera\", 2) || '-' ||\n", - " Q3.\"Patente\" || '-' || Q3.\"PedimentoAnterior\")\n", - " WHERE Q1.\"FechaPagoReal\" BETWEEN %s AND %s\n", - " ORDER BY Q1.\"FechaPagoReal\"\n", - " \"\"\"\n", - " return pd.read_sql(sql, pg_engine, params=(fecha_ini, fecha_fin))\n", - "\n", - "\n", - "def rectificados_ds(fecha_ini=None, fecha_fin=None, search=''):\n", - " \"\"\"Lista pedimentos de Registro501 que tienen rectificacion en Registro701.\n", - " Filtros opcionales por rango de fechas y por texto libre.\n", - " Migrado de rectificados.php.\"\"\"\n", - " sql = \"\"\"\n", - " SELECT R501.\"Patente\", R501.\"Pedimento\", R501.\"SeccionAduanera\",\n", - " R501.\"ClaveDocumento\", R501.\"FechaPagoReal\"\n", - " FROM \"Registro501\" R501\n", - " WHERE EXISTS (\n", - " SELECT 1 FROM \"Registro701\" R701\n", - " WHERE R701.\"PedimentoAnterior\" = R501.\"Pedimento\"\n", - " AND R701.\"PatenteAnterior\" = R501.\"Patente\"\n", - " AND R701.\"SeccionAduaneraAnterior\"= R501.\"SeccionAduanera\"\n", - " AND EXTRACT(YEAR FROM R501.\"FechaPagoReal\"::date)\n", - " = EXTRACT(YEAR FROM R701.\"FechaOperacionAnterior\"::date)\n", - " )\n", - " \"\"\"\n", - " params = []\n", - " if fecha_ini and fecha_fin:\n", - " sql += ' AND R501.\"FechaPagoReal\" BETWEEN %s AND %s'\n", - " params += [fecha_ini, fecha_fin]\n", - " if search:\n", - " sql += (' AND (R501.\"Pedimento\" LIKE %s OR R501.\"Patente\" LIKE %s '\n", - " 'OR R501.\"ClaveDocumento\" LIKE %s)')\n", - " like = f'%{search}%'\n", - " params += [like, like, like]\n", - " sql += ' ORDER BY R501.\"FechaPagoReal\" DESC'\n", - " return pd.read_sql(sql, pg_engine, params=tuple(params))\n", - "\n", - "\n", - "def historial_rectificaciones_ds(patente, pedimento, seccion_aduanera, anio_operacion):\n", - " \"\"\"Cadena recursiva de rectificaciones para un pedimento dado.\n", - " Migrado de obtener_historial.php.\"\"\"\n", - " sql = \"\"\"\n", - " WITH RECURSIVE historial AS (\n", - " SELECT R7.\"Patente\", R7.\"Pedimento\", R7.\"SeccionAduanera\",\n", - " R7.\"ClaveDocumento\", R7.\"FechaPago\", R7.\"PedimentoAnterior\",\n", - " R7.\"PatenteAnterior\", R7.\"SeccionAduaneraAnterior\",\n", - " R7.\"DocumentoAnterior\", R7.\"FechaOperacionAnterior\",\n", - " R7.\"FechaPagoReal\"\n", - " FROM \"Registro701\" R7\n", - " WHERE R7.\"PedimentoAnterior\" = %s\n", - " AND R7.\"PatenteAnterior\" = %s\n", - " AND R7.\"SeccionAduaneraAnterior\" = %s\n", - " AND EXTRACT(YEAR FROM R7.\"FechaOperacionAnterior\"::date) = %s\n", - " UNION ALL\n", - " SELECT R7.\"Patente\", R7.\"Pedimento\", R7.\"SeccionAduanera\",\n", - " R7.\"ClaveDocumento\", R7.\"FechaPago\", R7.\"PedimentoAnterior\",\n", - " R7.\"PatenteAnterior\", R7.\"SeccionAduaneraAnterior\",\n", - " R7.\"DocumentoAnterior\", R7.\"FechaOperacionAnterior\",\n", - " R7.\"FechaPagoReal\"\n", - " FROM \"Registro701\" R7\n", - " INNER JOIN historial HR ON\n", - " R7.\"PedimentoAnterior\" = HR.\"Pedimento\"\n", - " AND R7.\"PatenteAnterior\" = HR.\"Patente\"\n", - " AND R7.\"SeccionAduaneraAnterior\" = HR.\"SeccionAduanera\"\n", - " AND EXTRACT(YEAR FROM R7.\"FechaOperacionAnterior\"::date)\n", - " = EXTRACT(YEAR FROM HR.\"FechaOperacionAnterior\"::date)\n", - " )\n", - " SELECT * FROM historial ORDER BY \"FechaPagoReal\" ASC\n", - " \"\"\"\n", - " return pd.read_sql(sql, pg_engine, params=(\n", - " str(pedimento), str(patente), str(seccion_aduanera), int(anio_operacion)))\n", - "\n", - "\n", - "def exportar_df_a_excel(df, nombre_prefijo):\n", - " \"\"\"Guarda DataFrame en xlsx con timestamp y devuelve la ruta.\"\"\"\n", - " import datetime as _dt\n", - " ts = _dt.datetime.now().strftime('%Y%m%d_%H%M%S')\n", - " ruta = os.path.join(os.getcwd(), f'{nombre_prefijo}_{ts}.xlsx')\n", - " df.to_excel(ruta, index=False)\n", - " return ruta\n", - "\n", - "\n", - "def encabezado_facturas_ds(fecha_ini, fecha_fin, tipo_op):\n", - " \"\"\"Encabezado de facturas (Impo o Expo). Migrado de\n", - " generar_estructura_factImpo.php y generar_estructura_factExpo.php.\n", - " tipo_op: 1 = Impo, 2 = Expo.\n", - " Filtra Registro501 por TipoOperacion + rango FechaPagoReal y excluye\n", - " los pedimentos que ya fueron rectificados (existen en Registro701).\"\"\"\n", - " if int(tipo_op) not in (1, 2):\n", - " raise ValueError(\"tipo_op debe ser 1 (Impo) o 2 (Expo)\")\n", - " sql = \"\"\"\n", - " WITH RECURSIVE historial_rect AS (\n", - " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", - " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", - " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", - " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", - " FROM \"Registro701\" R7\n", - " UNION ALL\n", - " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", - " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", - " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", - " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", - " FROM \"Registro701\" R7\n", - " INNER JOIN historial_rect HR ON\n", - " (RIGHT(LEFT(R7.\"FechaOperacionAnterior\"::text, 4), 2) || '-' ||\n", - " LEFT(R7.\"SeccionAduaneraAnterior\", 2) || '-' ||\n", - " R7.\"PatenteAnterior\" || '-' || R7.\"PedimentoAnterior\")\n", - " =\n", - " (RIGHT(LEFT(HR.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(HR.\"SeccionAduanera\", 2) || '-' ||\n", - " HR.\"Patente\" || '-' || HR.\"Pedimento\")\n", - " )\n", - " SELECT\n", - " (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\") AS \"PEDIMENTO\",\n", - " '1' AS \"REMESA\",\n", - " CASE WHEN EXISTS (\n", - " SELECT 1 FROM historial_rect H\n", - " WHERE (RIGHT(LEFT(H.\"FechaOperacionAnterior\"::text, 4), 2) || '-' ||\n", - " LEFT(H.\"SeccionAduaneraAnterior\", 2) || '-' ||\n", - " H.\"PatenteAnterior\" || '-' || H.\"PedimentoAnterior\")\n", - " = (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", - " ) THEN Q1.\"Pedimento\" || '-' || 'R1'\n", - " ELSE Q1.\"Pedimento\" || '-' || Q1.\"ClaveDocumento\"\n", - " END AS \"NUMERO FACTURA\",\n", - " Q1.\"FechaPagoReal\" AS \"FECHA FACTURA\",\n", - " Q1.\"TipoCambio\" AS \"TIPO DE CAMBIO\",\n", - " '1' AS \"CLAVE PROVEEDOR\",\n", - " '8' AS \"CLAVE VENDIDO A\",\n", - " '8' AS \"CLAVE ENVIADO A\",\n", - " Q1.\"Patente\" AS \"AGENTE ADUANAL\",\n", - " '' AS \"CLAVE TRANSPORTISTA\",\n", - " '' AS \"NOMBRE CONDUCTOR\",\n", - " '' AS \"TIPO TRANSPORTE\",\n", - " '' AS \"NUMERO TRANSPORTE\",\n", - " 'ME' AS \"TIPO MONEDA\",\n", - " 'USD' AS \"CLAVE MONEDA\",\n", - " '' AS \"FLETES\",\n", - " '' AS \"VALORE SEGUROS\",\n", - " '' AS \"SEGUROS\",\n", - " '' AS \"EMBALAJES\",\n", - " '' AS \"OTROS INCREMENTALES\",\n", - " '' AS \"CLAVE INTERCOM\",\n", - " '' AS \"PRECINTO\",\n", - " Q1.\"FechaPagoReal\" AS \"FECHA EMISION\",\n", - " 'KILOS' AS \"TIPO PESO\",\n", - " '' AS \"E-DOCUMENT\",\n", - " '' AS \"NUM.OPERACION\",\n", - " Q1.\"SeccionAduanera\" AS \"ADUANA DE CRUCE\",\n", - " '' AS \"OBSERVACIONES E\",\n", - " '' AS \"LOCALIZACION\"\n", - " FROM \"Registro501\" Q1\n", - " WHERE Q1.\"TipoOperacion\"::text = %s\n", - " AND Q1.\"FechaPagoReal\" BETWEEN %s AND %s\n", - " AND NOT EXISTS (\n", - " SELECT 1 FROM \"Registro701\" R7\n", - " WHERE (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", - " = (RIGHT(LEFT(R7.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(R7.\"SeccionAduaneraAnterior\", 2) || '-' ||\n", - " R7.\"PatenteAnterior\" || '-' || R7.\"Pedimento\")\n", - " )\n", - " ORDER BY Q1.\"FechaPagoReal\" ASC\n", - " \"\"\"\n", - " return pd.read_sql(sql, pg_engine, params=(str(int(tipo_op)), fecha_ini, fecha_fin))\n", - "\n", - "\n", - "def tipo_cambio_ds(fecha_ini, fecha_fin):\n", - " \"\"\"Estructura Tipo de Cambio 501. Migrado de generar_estructura_tipo_cambio.php.\n", - " Devuelve PEDIMENTO, ClaveDocumento, TipoCambio, FechaPagoReal y una columna\n", - " INCONSISTENTE = True cuando existen distintos TipoCambio para la misma fecha.\"\"\"\n", - " sql = \"\"\"\n", - " SELECT\n", - " (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\") AS \"PEDIMENTO\",\n", - " Q1.\"ClaveDocumento\" AS \"CLAVE DOCUMENTO\",\n", - " Q1.\"TipoCambio\" AS \"TIPO CAMBIO\",\n", - " Q1.\"FechaPagoReal\" AS \"FECHA PAGO REAL\"\n", - " FROM \"Registro501\" Q1\n", - " WHERE Q1.\"FechaPagoReal\" BETWEEN %s AND %s\n", - " ORDER BY Q1.\"FechaPagoReal\" ASC\n", - " \"\"\"\n", - " df = pd.read_sql(sql, pg_engine, params=(fecha_ini, fecha_fin))\n", - " if df.empty:\n", - " df['INCONSISTENTE'] = []\n", - " return df\n", - " # Inconsistencia: fechas (formato d/m/Y) donde hay > 1 valor distinto de TIPO CAMBIO\n", - " fechas_norm = pd.to_datetime(df['FECHA PAGO REAL']).dt.strftime('%d/%m/%Y')\n", - " distintos = df.assign(_fnorm=fechas_norm).groupby('_fnorm')['TIPO CAMBIO'].nunique()\n", - " fechas_incon = set(distintos[distintos > 1].index)\n", - " df['INCONSISTENTE'] = fechas_norm.isin(fechas_incon)\n", - " return df\n", - "\n", - "\n", - "def exportar_tipo_cambio_excel(df, ruta):\n", - " \"\"\"Exporta tipo_cambio_ds() a xlsx con celdas TIPO CAMBIO en rojo cuando\n", - " INCONSISTENTE=True. Usa openpyxl.\"\"\"\n", - " from openpyxl import Workbook\n", - " from openpyxl.styles import PatternFill, Alignment, Font\n", - " wb = Workbook()\n", - " ws = wb.active\n", - " ws.title = 'TipoCambio'\n", - " columnas_out = ['PEDIMENTO', 'CLAVE DOCUMENTO', 'TIPO CAMBIO', 'FECHA PAGO REAL']\n", - " # Header\n", - " for j, h in enumerate(columnas_out, start=1):\n", - " c = ws.cell(row=1, column=j, value=h)\n", - " c.alignment = Alignment(horizontal='center')\n", - " c.font = Font(bold=True)\n", - " rojo = PatternFill(start_color='FFFF0000', end_color='FFFF0000', fill_type='solid')\n", - " fblanca = Font(color='FFFFFFFF')\n", - " for i, fila in enumerate(df.itertuples(index=False), start=2):\n", - " d = fila._asdict() if hasattr(fila, '_asdict') else dict(zip(df.columns, fila))\n", - " ws.cell(row=i, column=1, value=d.get('PEDIMENTO')).alignment = Alignment(horizontal='center')\n", - " ws.cell(row=i, column=2, value=d.get('CLAVE DOCUMENTO')).alignment = Alignment(horizontal='center')\n", - " c_tc = ws.cell(row=i, column=3, value=d.get('TIPO CAMBIO'))\n", - " c_tc.alignment = Alignment(horizontal='center')\n", - " if d.get('INCONSISTENTE'):\n", - " c_tc.fill = rojo\n", - " c_tc.font = fblanca\n", - " fpr = d.get('FECHA PAGO REAL')\n", - " try:\n", - " fpr = pd.to_datetime(fpr).strftime('%d/%m/%Y')\n", - " except Exception:\n", - " fpr = str(fpr)\n", - " ws.cell(row=i, column=4, value=fpr).alignment = Alignment(horizontal='center')\n", - " wb.save(ruta)\n", - " return ruta\n", - "\n", - "\n", - "# ============================================================\n", - "# PARTIDAS - Catalogo de NUMPARTES y asignacion por similitud\n", - "# ============================================================\n", - "\n", - "_UM_MAP_551 = {\n", - " 1: 'KGS', 2: 'GR', 3: 'CM', 4: 'CM2', 5: 'BD FT', 6: 'PZA',\n", - " 7: 'CBZA',8: 'LT', 9: 'PAR', 12:'JGO',14:'TON', 17:'DEC',\n", - " 18:'CIEN',19:'DOCE',20:'CAJA',21:'BTL',22:'CARAT'\n", - "}\n", - "\n", - "\n", - "def _ds_limpiar_texto(t):\n", - " \"\"\"Mismo limpiador que usa el NLP de sustitutos.\"\"\"\n", - " if pd.isna(t) or str(t).strip() == '': return ''\n", - " s = _re_ds.sub(r'[^\\w\\s]', ' ', str(t).upper().strip())\n", - " return _re_ds.sub(r'\\s+', ' ', s).strip()\n", - "\n", - "\n", - "def crear_tabla_base_numpartes():\n", - " \"\"\"DDL idempotente para la tabla del catalogo del cliente.\"\"\"\n", - " conn = _ds_conn()\n", - " conn.autocommit = True\n", - " try:\n", - " with conn.cursor() as cur:\n", - " cur.execute(\"\"\"\n", - " CREATE TABLE IF NOT EXISTS base_numpartes (\n", - " numparte TEXT PRIMARY KEY,\n", - " descripcion TEXT,\n", - " unimed TEXT,\n", - " fraccion TEXT\n", - " )\n", - " \"\"\")\n", - " finally:\n", - " conn.close()\n", - "\n", - "\n", - "def cargar_excel_base_numpartes(path, log=print):\n", - " \"\"\"Lee Excel con columnas NUMPARTE, DESCRIPCION, UNIDAD DE MEDIDA, FRACCION.\n", - " Acepta variantes y hace UPSERT (acumular).\"\"\"\n", - " crear_tabla_base_numpartes()\n", - " df = pd.read_excel(path, dtype=str)\n", - " norm = {c: c.strip().upper().replace(' ', '_') for c in df.columns}\n", - " df = df.rename(columns=norm)\n", - " aliases = {\n", - " 'NUMPARTE': ['NUMPARTE', 'NUM_PARTE', 'NUMERO_DE_PARTE', 'NUMERO_PARTE', 'PARTE'],\n", - " 'DESCRIPCION': ['DESCRIPCION', 'DESCRIPCION_PARTE', 'DESC', 'DESCRIPCIONE'],\n", - " 'UNIMED': ['UNIDAD_DE_MEDIDA', 'UNIMED', 'UM', 'UNIDAD'],\n", - " 'FRACCION': ['FRACCION', 'FRACCION_ARANCELARIA', 'FRACC'],\n", - " }\n", - " out = {}\n", - " for std, opts in aliases.items():\n", - " for o in opts:\n", - " if o in df.columns:\n", - " out[std] = df[o]; break\n", - " if std not in out and std != 'FRACCION':\n", - " raise ValueError(f'Falta la columna {std} (acepta: {opts})')\n", - " if std not in out:\n", - " out[std] = ''\n", - " df2 = pd.DataFrame(out)\n", - " for c in df2.columns:\n", - " df2[c] = df2[c].fillna('').astype(str).str.strip()\n", - " df2 = df2[df2['NUMPARTE'] != ''].drop_duplicates(subset='NUMPARTE').reset_index(drop=True)\n", - " log(f'Filas validas en el Excel: {len(df2):,}')\n", - " conn = _ds_conn()\n", - " conn.autocommit = False\n", - " upserted = 0\n", - " try:\n", - " with conn.cursor() as cur:\n", - " for _, r in df2.iterrows():\n", - " cur.execute(\"\"\"\n", - " INSERT INTO base_numpartes (numparte, descripcion, unimed, fraccion)\n", - " VALUES (%s, %s, %s, %s)\n", - " ON CONFLICT (numparte) DO UPDATE SET\n", - " descripcion = EXCLUDED.descripcion,\n", - " unimed = EXCLUDED.unimed,\n", - " fraccion = EXCLUDED.fraccion\n", - " \"\"\", (r['NUMPARTE'], r['DESCRIPCION'], r['UNIMED'], r['FRACCION']))\n", - " upserted += 1\n", - " conn.commit()\n", - " except Exception as e:\n", - " conn.rollback()\n", - " log(f'ERROR: {e}')\n", - " finally:\n", - " conn.close()\n", - " log(f'Upsert completado: {upserted:,} filas')\n", - " return upserted\n", - "\n", - "\n", - "def listar_base_numpartes(limit=500):\n", - " \"\"\"Devuelve DataFrame con el catalogo actual.\"\"\"\n", - " crear_tabla_base_numpartes()\n", - " conn = _ds_conn()\n", - " try:\n", - " return pd.read_sql(\n", - " 'SELECT numparte, descripcion, unimed, fraccion FROM base_numpartes '\n", - " 'ORDER BY numparte LIMIT %s', conn, params=(int(limit),))\n", - " finally:\n", - " conn.close()\n", - "\n", - "\n", - "def truncar_base_numpartes(log=print):\n", - " \"\"\"TRUNCATE base_numpartes. Devuelve filas eliminadas.\"\"\"\n", - " crear_tabla_base_numpartes()\n", - " conn = _ds_conn()\n", - " conn.autocommit = False\n", - " try:\n", - " with conn.cursor() as cur:\n", - " cur.execute('SELECT COUNT(*) FROM base_numpartes')\n", - " n = cur.fetchone()[0]\n", - " cur.execute('TRUNCATE TABLE base_numpartes')\n", - " conn.commit()\n", - " log(f'base_numpartes truncada: {n:,} filas eliminadas')\n", - " return n\n", - " except Exception as e:\n", - " conn.rollback()\n", - " log(f'ERROR: {e}')\n", - " return 0\n", - " finally:\n", - " conn.close()\n", - "\n", - "\n", - "def _ds_um_sigla(num_um):\n", - " \"\"\"Mapea codigo numerico de UM del 551 a sigla. Si ya es string, devuelve uppercase.\"\"\"\n", - " if pd.isna(num_um): return ''\n", - " try:\n", - " n = int(num_um)\n", - " return _UM_MAP_551.get(n, str(num_um).strip().upper())\n", - " except (ValueError, TypeError):\n", - " return str(num_um).strip().upper()\n", - "\n", - "\n", - "def _cargar_partidas_551(fecha_ini, fecha_fin, tipo_op):\n", - " \"\"\"Carga partidas del Registro551 en el rango con LATERAL JOIN a Registro501\n", - " para obtener ClaveDocumento y TipoCambio (toma una sola fila aunque haya\n", - " duplicados en 501), y EXISTS Registro701 para flag rectificado.\"\"\"\n", - " sql = \"\"\"\n", - " SELECT\n", - " Q1.\"Patente\" AS patente,\n", - " Q1.\"Pedimento\" AS pedimento,\n", - " Q1.\"SeccionAduanera\" AS seccion_aduanera,\n", - " Q1.\"Fraccion\" AS fraccion,\n", - " Q1.\"SecuenciaFraccion\" AS secuencia_fraccion,\n", - " Q1.\"DescripcionMercancia\" AS descripcion_mercancia,\n", - " Q1.\"PrecioUnitario\" AS precio_unitario,\n", - " Q1.\"ValorAduana\" AS valor_aduana,\n", - " Q1.\"ValorComercial\" AS valor_comercial,\n", - " Q1.\"ValorDolares\" AS valor_dolares,\n", - " Q1.\"ValorAgregado\" AS valor_agregado,\n", - " Q1.\"CantidadUMComercial\" AS cantidad_um_comercial,\n", - " Q1.\"UnidadMedidaComercial\" AS unidad_medida_comercial,\n", - " Q1.\"CantidadUMTarifa\" AS cantidad_um_tarifa,\n", - " Q1.\"UnidadMedidaTarifa\" AS unidad_medida_tarifa,\n", - " Q1.\"MetodoValorizacion\" AS metodo_valorizacion,\n", - " Q1.\"PaisOrigenDestino\" AS pais_origen_destino,\n", - " Q1.\"ClaveDocumento\" AS clave_documento_551,\n", - " Q1.\"FechaPagoReal\" AS fecha_pago_real,\n", - " Q1.\"TipoOperacion\" AS tipo_operacion,\n", - " R501.clave_documento_501,\n", - " R501.tipo_cambio_501,\n", - " CASE WHEN EXISTS (\n", - " SELECT 1 FROM \"Registro701\" R7\n", - " WHERE (RIGHT(LEFT(Q1.\"FechaPagoReal\"::text, 4), 2) || '-' ||\n", - " LEFT(Q1.\"SeccionAduanera\", 2) || '-' ||\n", - " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", - " = (RIGHT(LEFT(R7.\"FechaOperacionAnterior\"::text, 4), 2) || '-' ||\n", - " LEFT(R7.\"SeccionAduaneraAnterior\", 2) || '-' ||\n", - " R7.\"PatenteAnterior\" || '-' || R7.\"PedimentoAnterior\")\n", - " ) THEN 1 ELSE 0 END AS rectificado\n", - " FROM \"Registro551\" Q1\n", - " LEFT JOIN LATERAL (\n", - " SELECT R501i.\"ClaveDocumento\" AS clave_documento_501,\n", - " R501i.\"TipoCambio\" AS tipo_cambio_501\n", - " FROM \"Registro501\" R501i\n", - " WHERE R501i.\"Patente\" = Q1.\"Patente\"\n", - " AND R501i.\"Pedimento\" = Q1.\"Pedimento\"\n", - " AND R501i.\"SeccionAduanera\" = Q1.\"SeccionAduanera\"\n", - " LIMIT 1\n", - " ) R501 ON TRUE\n", - " WHERE Q1.\"TipoOperacion\"::text = %s\n", - " AND Q1.\"FechaPagoReal\" BETWEEN %s AND %s\n", - " ORDER BY Q1.\"FechaPagoReal\", Q1.\"Patente\", Q1.\"Pedimento\", Q1.\"SecuenciaFraccion\"\n", - " \"\"\"\n", - " df = pd.read_sql(sql, pg_engine, params=(str(int(tipo_op)), fecha_ini, fecha_fin))\n", - " # Dedupe defensivo: si el Registro551 se cargo N veces, no multiplicar partidas\n", - " antes = len(df)\n", - " df = df.drop_duplicates(\n", - " subset=['patente', 'pedimento', 'seccion_aduanera', 'fraccion', 'secuencia_fraccion'],\n", - " keep='first').reset_index(drop=True)\n", - " dups = antes - len(df)\n", - " if dups > 0:\n", - " print(f' [INFO] Se omitieron {dups:,} filas duplicadas del Registro551 '\n", - " f'(misma Patente+Pedimento+SeccionAduanera+Fraccion+SecuenciaFraccion).')\n", - " return df\n", - "\n", - "\n", - "def asignar_numpartes_551(fecha_ini, fecha_fin, tipo_op, umbral_sim=0.80,\n", - " progress=None, log=print):\n", - " \"\"\"Genera la Estructura de Partidas a partir de Registro551, asignando NUMPARTE\n", - " por similitud contra base_numpartes y agrupando huerfanas con 'MP-R'.\"\"\"\n", - " assert int(tipo_op) in (1, 2), 'tipo_op debe ser 1 (Impo) o 2 (Expo)'\n", - " from sklearn.feature_extraction.text import TfidfVectorizer\n", - " from sklearn.metrics.pairwise import cosine_similarity\n", - " prog = _Progress(progress)\n", - " prog.setup(5, 'Cargando Registro551...')\n", - " df = _cargar_partidas_551(fecha_ini, fecha_fin, tipo_op)\n", - " log(f'Partidas Registro551 ({\"IMPO\" if int(tipo_op)==1 else \"EXPO\"}): {len(df):,}')\n", - " if df.empty:\n", - " prog.done('Sin datos'); return df\n", - " prog.step(desc='Preparando textos...')\n", - "\n", - " df['um_sigla'] = df['unidad_medida_comercial'].apply(_ds_um_sigla)\n", - " df['fraccion4'] = df['fraccion'].fillna('').astype(str).str[:4]\n", - " df['desc_norm'] = df['descripcion_mercancia'].apply(_ds_limpiar_texto)\n", - " df['NUMERO_PARTE'] = ''\n", - " df['MATCH_TIPO'] = ''\n", - "\n", - " try:\n", - " df_base = listar_base_numpartes(limit=10_000_000)\n", - " except Exception as e:\n", - " log(f'WARN cargando base_numpartes: {e}')\n", - " df_base = pd.DataFrame(columns=['numparte','descripcion','unimed','fraccion'])\n", - " if not df_base.empty:\n", - " df_base['fraccion4'] = df_base['fraccion'].fillna('').astype(str).str[:4]\n", - " df_base['um_sigla'] = df_base['unimed'].fillna('').astype(str).str.upper().str.strip()\n", - " df_base['desc_norm'] = df_base['descripcion'].apply(_ds_limpiar_texto)\n", - " log(f'Catalogo base_numpartes: {len(df_base):,}')\n", - "\n", - " secuenciales_por_f4 = {}\n", - " grupos = list(df.groupby(['fraccion4', 'um_sigla'], dropna=False))\n", - " total_grupos = len(grupos)\n", - " prog.setup(total_grupos, 'Procesando grupos...')\n", - "\n", - " for procesados, ((f4, um), g) in enumerate(grupos, start=1):\n", - " idxs_g = list(g.index)\n", - " textos_g = g['desc_norm'].tolist()\n", - " base_sub = df_base[(df_base['fraccion4'] == f4) & (df_base['um_sigla'] == um)] if not df_base.empty else df_base\n", - " sin_base_idx = []\n", - " if not base_sub.empty and any(t for t in base_sub['desc_norm'].tolist()):\n", - " try:\n", - " vec = TfidfVectorizer(ngram_range=(1,2), sublinear_tf=True,\n", - " min_df=1, max_features=20000)\n", - " vec.fit(pd.concat([base_sub['desc_norm'],\n", - " pd.Series(textos_g)], ignore_index=True))\n", - " base_mat = vec.transform(base_sub['desc_norm'].tolist())\n", - " grp_mat = vec.transform(textos_g)\n", - " sims = cosine_similarity(grp_mat, base_mat)\n", - " for j, idx in enumerate(idxs_g):\n", - " best_j = int(sims[j].argmax())\n", - " best_s = float(sims[j][best_j])\n", - " if best_s >= umbral_sim and textos_g[j]:\n", - " df.at[idx, 'NUMERO_PARTE'] = str(base_sub.iloc[best_j]['numparte'])\n", - " df.at[idx, 'MATCH_TIPO'] = f'BASE ({best_s:.2f})'\n", - " else:\n", - " sin_base_idx.append(idx)\n", - " except Exception as e:\n", - " log(f' WARN TF-IDF base en grupo ({f4},{um}): {e}')\n", - " sin_base_idx.extend(idxs_g)\n", - " else:\n", - " sin_base_idx.extend(idxs_g)\n", - "\n", - " if sin_base_idx:\n", - " textos_h = [df.at[i, 'desc_norm'] for i in sin_base_idx]\n", - " no_vacios = [(i, t) for i, t in zip(sin_base_idx, textos_h) if t]\n", - " if no_vacios:\n", - " idxs_h = [x[0] for x in no_vacios]\n", - " txts_h = [x[1] for x in no_vacios]\n", - " try:\n", - " vec = TfidfVectorizer(ngram_range=(1,2), sublinear_tf=True,\n", - " min_df=1, max_features=20000)\n", - " mat = vec.fit_transform(txts_h)\n", - " sims = cosine_similarity(mat, mat)\n", - " parent = list(range(len(idxs_h)))\n", - " def _find(x):\n", - " while parent[x] != x:\n", - " parent[x] = parent[parent[x]]; x = parent[x]\n", - " return x\n", - " for a in range(len(idxs_h)):\n", - " for b in range(a+1, len(idxs_h)):\n", - " if sims[a][b] >= umbral_sim:\n", - " ra, rb = _find(a), _find(b)\n", - " if ra != rb: parent[rb] = ra\n", - " grupos_loc = {}\n", - " for a in range(len(idxs_h)):\n", - " grupos_loc.setdefault(_find(a), []).append(idxs_h[a])\n", - " for miembros in grupos_loc.values():\n", - " secuenciales_por_f4[f4] = secuenciales_por_f4.get(f4, 0) + 1\n", - " nuevo_np = f\"MP{f4 or 'XXXX'}-R{secuenciales_por_f4[f4]:03d}\"\n", - " for idx in miembros:\n", - " df.at[idx, 'NUMERO_PARTE'] = nuevo_np\n", - " df.at[idx, 'MATCH_TIPO'] = 'AUTO'\n", - " except Exception as e:\n", - " log(f' WARN union-find grupo ({f4},{um}): {e}')\n", - " for idx in idxs_h:\n", - " secuenciales_por_f4[f4] = secuenciales_por_f4.get(f4, 0) + 1\n", - " df.at[idx, 'NUMERO_PARTE'] = f\"MP{f4 or 'XXXX'}-R{secuenciales_por_f4[f4]:03d}\"\n", - " df.at[idx, 'MATCH_TIPO'] = 'AUTO'\n", - " for idx in sin_base_idx:\n", - " if df.at[idx, 'NUMERO_PARTE'] == '':\n", - " secuenciales_por_f4[f4] = secuenciales_por_f4.get(f4, 0) + 1\n", - " df.at[idx, 'NUMERO_PARTE'] = f\"MP{f4 or 'XXXX'}-R{secuenciales_por_f4[f4]:03d}\"\n", - " df.at[idx, 'MATCH_TIPO'] = 'AUTO_SINDESC'\n", - " prog.step(desc=f'{procesados}/{total_grupos}')\n", - "\n", - " df['anio_corto'] = pd.to_datetime(df['fecha_pago_real'], errors='coerce') .dt.year.astype(str).str[-2:]\n", - " df['ped_full'] = (df['anio_corto'] + '-' + df['seccion_aduanera'].astype(str).str[:2]\n", - " + '-' + df['patente'].astype(str).str.zfill(4)\n", - " + '-' + df['pedimento'].astype(str).str.zfill(7))\n", - " df['clave_eff'] = df.apply(\n", - " lambda r: 'R1' if r['rectificado'] == 1\n", - " else (r['clave_documento_501'] or r['clave_documento_551'] or ''),\n", - " axis=1)\n", - " df['factura'] = df['pedimento'].astype(str).str.zfill(7).str[-7:] + '-' + df['clave_eff'].fillna('')\n", - " df['linea_seq'] = df.groupby('factura').cumcount() + 1\n", - "\n", - " prog.done('Asignacion completa')\n", - " cnt = df['MATCH_TIPO'].apply(lambda s: 'BASE' if str(s).startswith('BASE') else s).value_counts().to_dict()\n", - " log(f'Distribucion de match: {cnt}')\n", - "\n", - " # Costo unitario:\n", - " # Impo: ValorDolares / CantidadUMComercial\n", - " # Expo: (ValorDolares - ValorAgregado * TipoCambio) / CantidadUMComercial\n", - " _vd = pd.to_numeric(df['valor_dolares'], errors='coerce').fillna(0.0)\n", - " _va = pd.to_numeric(df['valor_agregado'], errors='coerce').fillna(0.0)\n", - " _tc = pd.to_numeric(df['tipo_cambio_501'], errors='coerce').fillna(0.0)\n", - " _qty = pd.to_numeric(df['cantidad_um_comercial'], errors='coerce').replace(0, np.nan)\n", - " if int(tipo_op) == 1:\n", - " df['costo_unitario_calc'] = (_vd / _qty).round(6)\n", - " else:\n", - " df['costo_unitario_calc'] = ((_vd - (_va * _tc)) / _qty).round(6)\n", - " df['costo_unitario_calc'] = df['costo_unitario_calc'].fillna(0.0)\n", - "\n", - " out = pd.DataFrame({\n", - " 'NUMERO FACTURA': df['factura'],\n", - " 'FECHA PAGO REAL': df['fecha_pago_real'],\n", - " 'LINEA': df['linea_seq'],\n", - " 'NUMERO DE PARTE': df['NUMERO_PARTE'],\n", - " 'CANTIDAD IMPORTADA': df['cantidad_um_comercial'],\n", - " 'UNIDAD DE MEDIDA': df['um_sigla'],\n", - " 'COSTO UNITARIO': df['costo_unitario_calc'],\n", - " 'PESO NETO': '',\n", - " 'PESO BRUTO': '',\n", - " 'CANTIDAD BULTOS': df['cantidad_um_tarifa'],\n", - " 'CLAVE BULTOS': df['unidad_medida_tarifa'],\n", - " 'PAIS ORIGEN': df['pais_origen_destino'],\n", - " 'FRACCION ARANCELARIA': df['fraccion'],\n", - " 'PREFERENCIA ARANCELARIA':'',\n", - " 'SECTOR': '',\n", - " 'FRACCION AMERICANA': '',\n", - " 'ORDEN DE COMPRA': '',\n", - " 'METODO DE VALORACION': df['metodo_valorizacion'],\n", - " 'NUMERO DE GUIA': '',\n", - " 'NUMERO DE ENTRADA': '',\n", - " 'CLIENTE': '',\n", - " 'FORMA DE PAGO': '',\n", - " 'MONTO IGI': df['valor_aduana'],\n", - " 'LOCALIZACION': '',\n", - " 'PERMISO RO': '',\n", - " 'LINEA RO': '',\n", - " 'VALOR TOTAL': df['valor_comercial'],\n", - " 'LOTE': df['linea_seq'],\n", - " 'INFORMACION ADICIONAL': df['secuencia_fraccion'],\n", - " 'CANTIDAD AUXILIAR': '',\n", - " 'U.M. AUXILIAR': '',\n", - " 'NUMERO DE ENTRADA 2': '',\n", - " 'MATCH_TIPO': df['MATCH_TIPO'],\n", - " })\n", - " return out" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ui", - "metadata": {}, - "outputs": [], - "source": [ - "header = W.HTML()\n", - "def _refresh_header():\n", - " color = '#1565C0' if CONEXION_OK else '#C62828'\n", - " header.value = (\n", - " f'
'\n", - " f'

Sistema de Utilerias 2.0 SCAII

'\n", - " f'
{CONEXION_MSG}
'\n", - " )\n", - "_refresh_header()\n", - "OUT_STYLE = {'border':'1px solid #ddd','padding':'8px','min_height':'120px'}\n", - "BAR_LAYOUT = {'width':'600px', 'height':'25px'}\n", - "BAR_STYLE = {'description_width':'170px'}\n", - "\n", - "def _mkbar(desc='Listo'):\n", - " return W.IntProgress(value=0, min=0, max=1, description=desc, layout=BAR_LAYOUT, style=BAR_STYLE, bar_style='')\n", - "\n", - "# ----- Tab 0: Conexion -----\n", - "out_conn = W.Output(layout=OUT_STYLE)\n", - "db_dropdown = W.Dropdown(options=[], description='Base de datos:', layout={'width':'500px'}, style={'description_width':'120px'})\n", - "btn_refresh = W.Button(description='Refrescar lista', icon='refresh', layout={'width':'180px'})\n", - "btn_conectar = W.Button(description='Conectar a esta DB', button_style='primary', icon='plug', layout={'width':'220px'})\n", - "lbl_actual = W.HTML()\n", - "\n", - "def _refresh_lista():\n", - " with out_conn:\n", - " clear_output()\n", - " print('Cargando lista de bases de datos...')\n", - " try: dbs = listar_databases()\n", - " except Exception as e: print(f'ERROR listando DBs: {e}'); return\n", - " if not dbs: print('No se pudieron obtener bases de datos. Revisa credenciales y permisos.')\n", - " else:\n", - " print(f'Encontradas {len(dbs)} bases:')\n", - " for db in dbs: print(f' - {db}')\n", - " db_dropdown.options = dbs\n", - " if DB_ACTUAL and DB_ACTUAL in dbs: db_dropdown.value = DB_ACTUAL\n", - " lbl_actual.value = f'DB actual: {DB_ACTUAL or \"sin conexion\"}'\n", - "btn_refresh.on_click(lambda _: _refresh_lista())\n", - "\n", - "def _on_conectar(_):\n", - " with out_conn:\n", - " clear_output()\n", - " if not db_dropdown.value: print('Selecciona una base de datos.'); return\n", - " target = db_dropdown.value\n", - " print(f'Conectando a [{target}]...')\n", - " ok = conectar_a_db(target)\n", - " if ok: print(f'OK. Cache reseteado. Ahora todas las pestanas usan [{DB_ACTUAL}].')\n", - " else: print(f'FALLO: {CONEXION_MSG}')\n", - " _refresh_header()\n", - " lbl_actual.value = f'DB actual: {DB_ACTUAL or \"sin conexion\"}'\n", - "btn_conectar.on_click(_on_conectar)\n", - "\n", - "tab_conn = W.VBox([\n", - " W.HTML('

Conexion a SQL Server

'\n", - " f'

Server: {SCAII_SERVER} | Usuario: {SCAII_USER}

'),\n", - " lbl_actual,\n", - " W.HTML('

Selecciona la base de datos. El cambio aplica a todas las pestanas; '\n", - " 'el cache de pronostico/analisis se resetea al cambiar.

'),\n", - " W.HBox([db_dropdown, btn_refresh]),\n", - " btn_conectar, out_conn,\n", - "])\n", - "_refresh_lista()\n", - "\n", - "# ----- Tab 1: Descargas -----\n", - "out_pron = W.Output(layout=OUT_STYLE); bar_pron = _mkbar('Pronostico')\n", - "out_p9 = W.Output(layout=OUT_STYLE); bar_p9 = _mkbar('Paso 9')\n", - "out_p10 = W.Output(layout=OUT_STYLE); bar_p10 = _mkbar('Paso 10')\n", - "btn_pron = W.Button(description='Calcular pronostico (paso 8)', button_style='primary', icon='play', layout={'width':'260px'})\n", - "modo9 = W.Dropdown(options=['NATURAL','DIRIGIDA'], value='NATURAL', description='Modo:', layout={'width':'250px'})\n", - "dry9 = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", - "fd9 = W.Text(placeholder='YYYY-MM-DD', description='Desde:', layout={'width':'250px'})\n", - "fh9 = W.Text(placeholder='YYYY-MM-DD', description='Hasta:', layout={'width':'250px'})\n", - "btn_p9 = W.Button(description='Ejecutar paso 9 (NA->AC)', button_style='warning', icon='check', layout={'width':'260px'})\n", - "modo10 = W.Dropdown(options=['DIRIGIDA','NATURAL'], value='DIRIGIDA', description='Modo:', layout={'width':'250px'})\n", - "dry10 = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", - "fd10 = W.Text(placeholder='YYYY-MM-DD', description='Desde:', layout={'width':'250px'})\n", - "fh10 = W.Text(placeholder='YYYY-MM-DD', description='Hasta:', layout={'width':'250px'})\n", - "facts_obj= W.Text(placeholder=\"factura1,factura2 (opcional)\", description='Facturas:', layout={'width':'480px'})\n", - "btn_p10 = W.Button(description='Ejecutar paso 10 (complementaria)', button_style='warning', icon='plus-square', layout={'width':'320px'})\n", - "\n", - "def _on_pron(_):\n", - " with out_pron:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})'); print('Cargando catalogos y calculando...'); t0 = time.time()\n", - " calcular_pronostico_paso8(progress=bar_pron)\n", - " cob = _state['cobertura_factura']; df = _state['df_descarga_all']\n", - " print(f'Tiempo: {time.time()-t0:.1f}s | Filas: {len(df):,} | Facturas: {len(cob):,}')\n", - " n100 = (cob['componentes_100pct'] == cob['componentes_total']).sum()\n", - " print(f' 100% cobertura: {n100:,} | parcial: {len(cob)-n100:,}')\n", - "btn_pron.on_click(_on_pron)\n", - "\n", - "def _on_p9(_):\n", - " with out_p9:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " ejecutar_paso9(modo9.value, dry9.value, fd9.value or None, fh9.value or None, log=print, progress=bar_p9)\n", - "btn_p9.on_click(_on_p9)\n", - "\n", - "def _on_p10(_):\n", - " with out_p10:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " fobj = [f.strip() for f in (facts_obj.value or '').split(',') if f.strip()] or None\n", - " ejecutar_paso10(modo10.value, dry10.value, fd10.value or None, fh10.value or None, fobj, log=print, progress=bar_p10)\n", - "btn_p10.on_click(_on_p10)\n", - "\n", - "tab_desc = W.VBox([\n", - " W.HTML('

Paso 8 - Pronostico de cobertura

'),\n", - " btn_pron, bar_pron, out_pron,\n", - " W.HTML('

Paso 9 - Descargas pendientes (NA - AC)

'),\n", - " W.HBox([modo9, dry9]), W.HBox([fd9, fh9]), btn_p9, bar_p9, out_p9,\n", - " W.HTML('

Paso 10 - Complementaria (sobre facturas AC)

'),\n", - " W.HBox([modo10, dry10]), W.HBox([fd10, fh10]), facts_obj, btn_p10, bar_p10, out_p10,\n", - "])\n", - "\n", - "# ----- Tab 2: Analisis Saldos -----\n", - "out_an_log = W.Output(layout=OUT_STYLE)\n", - "out_an_anio = W.Output()\n", - "out_an_imp_exp= W.Output()\n", - "out_an_pesos = W.Output()\n", - "out_an_cant = W.Output()\n", - "out_an_cant_um = W.Output()\n", - "bar_an = _mkbar('Analisis')\n", - "btn_an = W.Button(description='Cargar y analizar SSaldoTem', button_style='primary', icon='database', layout={'width':'280px'})\n", - "btn_an_xlsx = W.Button(description='Exportar Excel', button_style='success', icon='file-excel-o', layout={'width':'200px'})\n", - "\n", - "def _on_an(_):\n", - " with out_an_log:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " print('Cargando SSaldoTem...'); t0 = time.time()\n", - " df = cargar_analisis_saldos(progress=bar_an)\n", - " print(f' {len(df):,} lotes | tiempo: {time.time()-t0:.1f}s')\n", - " por_anio = calcular_por_anio_saldos(df); _state['por_anio_saldos'] = por_anio\n", - " print(f' Saldo cant: {df[\"SALDO_CANT\"].sum():,.2f}')\n", - " print(f' Saldo MN: ${df[\"SALDO_VMN\"].sum():,.2f}')\n", - " print(f' Saldo ME: ${df[\"SALDO_VME\"].sum():,.2f}')\n", - " print('Calculando IMPO vs EXPO...')\n", - " df_imp, df_exp, cmp = calcular_impo_expo_anio(progress=bar_an)\n", - " _state['df_imp'], _state['df_exp'], _state['cmp_impo_expo'] = df_imp, df_exp, cmp\n", - " print(f' IMPO a-os: {len(df_imp)} | EXPO a-os: {len(df_exp)} | Comparativo: {len(cmp)}')\n", - " print('Calculando pesos IMPO/EXPO/SALDO por a-o...')\n", - " cmp_pesos = calcular_pesos_por_anio(progress=bar_an)\n", - " _state['cmp_pesos_anio'] = cmp_pesos\n", - " print(f' Comparativo pesos: {len(cmp_pesos)} a-os')\n", - " print('Calculando cantidades IMPO/EXPO por a-o...')\n", - " cmp_cant = calcular_cantidades_por_anio(progress=bar_an)\n", - " _state['cmp_cantidades_anio'] = cmp_cant\n", - " print(f' Comparativo cantidades: {len(cmp_cant)} a-os')\n", - " print('Calculando cantidades IMPO/EXPO por a-o + UM...')\n", - " cmp_cant_um = calcular_cantidades_por_anio_um(progress=bar_an)\n", - " _state['cmp_cantidades_anio_um'] = cmp_cant_um\n", - " print(f' Comparativo cantidades por UM: {len(cmp_cant_um)} filas')\n", - " with out_an_anio:\n", - " clear_output()\n", - " display(HTML('

Saldo disponible por a-o (de entrada del lote)

'))\n", - " display(_state['por_anio_saldos'])\n", - " if not _state['por_anio_saldos'].empty:\n", - " fig = graficar_saldos_anio(_state['por_anio_saldos']); display(fig); plt.close(fig)\n", - " with out_an_imp_exp:\n", - " clear_output()\n", - " display(HTML('

IMPO vs EXPO por a-o

'))\n", - " display(_state['cmp_impo_expo'])\n", - " if not _state['cmp_impo_expo'].empty:\n", - " fig = graficar_impo_expo(_state['cmp_impo_expo']); display(fig); plt.close(fig)\n", - " with out_an_pesos:\n", - " clear_output()\n", - " display(HTML('

Peso por a-o - IMPO / EXPO / CONSUMIDO / DESCARGAS

'))\n", - " display(_state['cmp_pesos_anio'])\n", - " if not _state['cmp_pesos_anio'].empty:\n", - " fig = graficar_pesos_anio(_state['cmp_pesos_anio']); display(fig); plt.close(fig)\n", - " with out_an_cant:\n", - " clear_output()\n", - " display(HTML('

Cantidades por a-o - IMPO vs EXPO

'))\n", - " display(_state['cmp_cantidades_anio'])\n", - " if not _state['cmp_cantidades_anio'].empty:\n", - " fig = graficar_cantidades_anio(_state['cmp_cantidades_anio']); display(fig); plt.close(fig)\n", - " with out_an_cant_um:\n", - " clear_output()\n", - " display(HTML('

Cantidades por a-o + UM (separado por unidad de medida)

'))\n", - " display(_state['cmp_cantidades_anio_um'])\n", - " if not _state['cmp_cantidades_anio_um'].empty:\n", - " fig = graficar_cantidades_anio_um(_state['cmp_cantidades_anio_um']); display(fig); plt.close(fig)\n", - "btn_an.on_click(_on_an)\n", - "\n", - "def _on_an_xlsx(_):\n", - " with out_an_log:\n", - " if 'df_saldos_full' not in _state: print('Corre primero \"Cargar y analizar\".'); return\n", - " path = exportar_excel_analisis(_state['df_saldos_full'], _state['por_anio_saldos'],\n", - " _state['df_imp'], _state['df_exp'], _state['cmp_impo_expo'],\n", - " _state.get('cmp_pesos_anio'),\n", - " _state.get('cmp_cantidades_anio'),\n", - " _state.get('cmp_cantidades_anio_um'))\n", - " print(f'Excel: {path}')\n", - "btn_an_xlsx.on_click(_on_an_xlsx)\n", - "\n", - "tab_an = W.VBox([\n", - " W.HTML('

An-lisis SSaldoTem + IMPO vs EXPO

'),\n", - " W.HBox([btn_an, btn_an_xlsx]), bar_an, out_an_log, out_an_anio, out_an_imp_exp, out_an_pesos, out_an_cant, out_an_cant_um,\n", - "])\n", - "\n", - "# ----- Tab 3: Sustitutos NLP -----\n", - "out_nlp = W.Output(layout=OUT_STYLE); bar_nlp = _mkbar('Sustitutos NLP')\n", - "min_sim = W.FloatSlider(value=0.80, min=0.5, max=1.0, step=0.05, description='Min similitud:', readout_format='.0%', layout={'width':'380px'})\n", - "top_n = W.IntSlider(value=3, min=1, max=10, step=1, description='Top-N:', layout={'width':'380px'})\n", - "dry_nlp = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", - "btn_nlp = W.Button(description='Generar sustitutos NLP', button_style='primary', icon='magic', layout={'width':'260px'})\n", - "\n", - "def _on_nlp(_):\n", - " with out_nlp:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " try:\n", - " generar_sustitutos_nlp(min_sim.value, top_n.value, dry_nlp.value, log=print, progress=bar_nlp)\n", - " if 'df_nuevos_sust' in _state and not _state['df_nuevos_sust'].empty:\n", - " print('\\nMuestra de los primeros 10:')\n", - " display(_state['df_nuevos_sust'].head(10))\n", - " except Exception as e: print(f'ERROR: {e}')\n", - "btn_nlp.on_click(_on_nlp)\n", - "\n", - "tab_nlp = W.VBox([\n", - " W.HTML('

Sustitutos NLP - TF-IDF + coseno

'),\n", - " min_sim, top_n, dry_nlp, btn_nlp, bar_nlp, out_nlp,\n", - "])\n", - "\n", - "# ----- Tab 4: Descarga % KGS (paso 12) -----\n", - "out_pron12 = W.Output(layout=OUT_STYLE); bar_pron12 = _mkbar('Pronostico KG')\n", - "out_p12 = W.Output(layout=OUT_STYLE); bar_p12 = _mkbar('Paso 12')\n", - "out_p12c = W.Output(layout=OUT_STYLE); bar_p12c = _mkbar('Paso 12 comp')\n", - "btn_pron12 = W.Button(description='Calcular pronostico KG', button_style='primary', icon='play', layout={'width':'260px'})\n", - "modo12 = W.Dropdown(options=['NATURAL','DIRIGIDA'], value='NATURAL', description='Modo:', layout={'width':'250px'})\n", - "dry12 = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", - "fd12 = W.Text(placeholder='YYYY-MM-DD', description='Desde:', layout={'width':'250px'})\n", - "fh12 = W.Text(placeholder='YYYY-MM-DD', description='Hasta:', layout={'width':'250px'})\n", - "btn_p12 = W.Button(description='Ejecutar paso 12 (NA->AC)', button_style='warning', icon='check', layout={'width':'260px'})\n", - "modo12c = W.Dropdown(options=['DIRIGIDA','NATURAL'], value='DIRIGIDA', description='Modo:', layout={'width':'250px'})\n", - "dry12c = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", - "fd12c = W.Text(placeholder='YYYY-MM-DD', description='Desde:', layout={'width':'250px'})\n", - "fh12c = W.Text(placeholder='YYYY-MM-DD', description='Hasta:', layout={'width':'250px'})\n", - "facts_obj12= W.Text(placeholder=\"factura1,factura2 (opcional)\", description='Facturas:', layout={'width':'480px'})\n", - "btn_p12c = W.Button(description='Ejecutar complementaria KG', button_style='warning', icon='plus-square', layout={'width':'320px'})\n", - "\n", - "def _on_pron12(_):\n", - " with out_pron12:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})'); print('Calculando pronostico KG...'); t0 = time.time()\n", - " calcular_pronostico_paso12_kg(progress=bar_pron12)\n", - " cob = _state['cobertura_factura_kg']; df = _state['df_descarga_kg']\n", - " print(f'Tiempo: {time.time()-t0:.1f}s | Filas: {len(df):,} | Facturas: {len(cob):,}')\n", - " n100 = (cob['componentes_100pct'] == cob['componentes_total']).sum()\n", - " print(f' 100% cobertura: {n100:,} | parcial: {len(cob)-n100:,}')\n", - "btn_pron12.on_click(_on_pron12)\n", - "\n", - "def _on_p12(_):\n", - " with out_p12:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " ejecutar_paso12_kg(modo12.value, dry12.value, fd12.value or None, fh12.value or None, log=print, progress=bar_p12)\n", - "btn_p12.on_click(_on_p12)\n", - "\n", - "def _on_p12c(_):\n", - " with out_p12c:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " fobj = [f.strip() for f in (facts_obj12.value or '').split(',') if f.strip()] or None\n", - " ejecutar_paso12_complementaria_kg(modo12c.value, dry12c.value, fd12c.value or None, fh12c.value or None, fobj, log=print, progress=bar_p12c)\n", - "btn_p12c.on_click(_on_p12c)\n", - "\n", - "tab_kg = W.VBox([\n", - " W.HTML('

Paso 12 - Descarga por % de KGS

'\n", - " '

'\n", - " 'cant_req = (BOM.CANTIDAD / 100) - PESONETO de la partida. Mismo PEPS, UM y sustitutos.

'),\n", - " btn_pron12, bar_pron12, out_pron12,\n", - " W.HTML('

Paso 12 - Descargas pendientes KG (NA - AC)

'),\n", - " W.HBox([modo12, dry12]), W.HBox([fd12, fh12]), btn_p12, bar_p12, out_p12,\n", - " W.HTML('

Paso 12 - Complementaria KG (sobre facturas AC)

'),\n", - " W.HBox([modo12c, dry12c]), W.HBox([fd12c, fh12c]), facts_obj12, btn_p12c, bar_p12c, out_p12c,\n", - "])\n", - "\n", - "\n", - "# ----- Tab 5: CTM (Reasignacion de descargas) -----\n", - "out_ctm_log = W.Output(layout=OUT_STYLE)\n", - "out_ctm_tabla = W.Output()\n", - "bar_ctm = _mkbar('Analisis CTM')\n", - "upload_ctm = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n", - "chk_use_mapping= W.Checkbox(value=True, description='Usar mapping para priorizar')\n", - "btn_plantilla = W.Button(description='Descargar plantilla Excel', button_style='info', icon='download', layout={'width':'260px'})\n", - "btn_ctm_analizar = W.Button(description='Analizar CTM (sin escribir)', button_style='primary', icon='search', layout={'width':'280px'})\n", - "btn_ctm_xlsx = W.Button(description='Exportar Excel completo', button_style='success', icon='file-excel-o', layout={'width':'240px'})\n", - "\n", - "_html_formato = '''\n", - "
\n", - "Formato esperado del Excel del cliente\n", - "

La primera hoja del archivo debe contener al menos estas columnas (solo las dos primeras son obligatorias):

\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
ColumnaEjemploObligatoria
Facturas CTMAAU112023RFR0481, NIS112023RFR0035SI
PEDIMENTO COMPLETO75-3076-4021492SI
PATENTE3076no
ADUANA75no
PEDIMENTO4021492no
OperacionImportacionno
Clave de pedimentoF4no
\n", - "

La celda Facturas CTM puede traer varias facturas separadas por coma; la herramienta las separa automaticamente.

\n", - "
\n", - "'''\n", - "\n", - "def _on_plantilla(_):\n", - " with out_ctm_log:\n", - " clear_output()\n", - " path = generar_plantilla_excel_ctm()\n", - " display(HTML(f'
'\n", - " f'Plantilla generada
'\n", - " f'{path}
'\n", - " f'Abre el archivo, agrega tus datos, guarda y luego subelo con el boton \"Subir Excel\".
'))\n", - "btn_plantilla.on_click(_on_plantilla)\n", - "\n", - "def _on_ctm_analizar(_):\n", - " with out_ctm_log:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " mapping_df = None\n", - " if chk_use_mapping.value and len(upload_ctm.value) > 0:\n", - " try:\n", - " if isinstance(upload_ctm.value, dict):\n", - " fname = list(upload_ctm.value.keys())[0]\n", - " file_bytes = upload_ctm.value[fname]['content']\n", - " else:\n", - " file_bytes = upload_ctm.value[0]['content']\n", - " mapping_df, raw = cargar_excel_mapping_ctm(file_bytes)\n", - " _state['ctm_mapping'] = mapping_df\n", - " print(f'Excel cargado: {len(mapping_df):,} relaciones CTM<->Pedimento')\n", - " except Exception as e:\n", - " print(f'WARN cargando Excel: {e} (se procesa sin mapping)')\n", - " mapping_df = None\n", - " elif chk_use_mapping.value:\n", - " print('Sin Excel cargado; se procesa sin prioridad de mapping.')\n", - " print('Analizando facturas CTM...')\n", - " plan, resumen = analizar_ctm(df_mapping=mapping_df, progress=bar_ctm)\n", - " print(f'Plan: {len(plan):,} filas | Resumen: {len(resumen):,} combinaciones')\n", - " if not resumen.empty:\n", - " asignados = (plan['STATUS']=='ASIGNADO').sum()\n", - " faltantes = (plan['STATUS']=='FALTANTE').sum()\n", - " print(f' Filas ASIGNADAS: {asignados:,}')\n", - " print(f' Filas FALTANTE : {faltantes:,}')\n", - " display(HTML('

En pantalla se muestran solo las primeras 50 filas del plan. '\n", - " 'Para ver el detalle completo presiona Exportar Excel completo.

'))\n", - " with out_ctm_tabla:\n", - " clear_output()\n", - " if 'ctm_resumen' in _state and not _state['ctm_resumen'].empty:\n", - " display(HTML('

Resumen CTM (por factura + linea + componente)

'))\n", - " display(_state['ctm_resumen'])\n", - " display(HTML('

Plan detalle (primeras 50 filas)

'))\n", - " display(_state['ctm_plan'].head(50))\n", - "btn_ctm_analizar.on_click(_on_ctm_analizar)\n", - "\n", - "def _on_ctm_xlsx(_):\n", - " with out_ctm_log:\n", - " if 'ctm_plan' not in _state:\n", - " display(HTML('
Corre primero Analizar CTM.
'))\n", - " return\n", - " path = exportar_excel_ctm(_state['ctm_plan'], _state['ctm_resumen'])\n", - " display(HTML(f'
'\n", - " f'Excel generado con el detalle completo
'\n", - " f'{path}
'\n", - " f'Contiene tres hojas: Resumen, Plan_Detalle (todas las filas, no solo 50) y Faltantes.
'))\n", - "btn_ctm_xlsx.on_click(_on_ctm_xlsx)\n", - "\n", - "# ---- Paso B - Ejecucion (CTM) ----\n", - "modo_ctm = W.Dropdown(options=['NATURAL','DIRIGIDA'], value='NATURAL', description='Modo:', layout={'width':'250px'})\n", - "dry_ctm = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n", - "btn_ctm_ejecutar = W.Button(description='Ejecutar reasignacion (Paso B)', button_style='warning', icon='play', layout={'width':'300px'})\n", - "bar_ctm_b = _mkbar('Ejecucion CTM')\n", - "out_ctm_ejec = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ctm_ejecutar(_):\n", - " with out_ctm_ejec:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " ejecutar_reasignacion_ctm(modo=modo_ctm.value, dry_run=dry_ctm.value, log=print, progress=bar_ctm_b)\n", - "btn_ctm_ejecutar.on_click(_on_ctm_ejecutar)\n", - "tab_ctm = W.VBox([\n", - " W.HTML('

CTM - Reasignacion de descargas desde Cambio de Regimen

'\n", - " '

'\n", - " 'Analiza las facturas CTM pendientes y propone una asignacion de descargas '\n", - " 'tomadas del pool existente del modulo de Cambio de Regimen. Esta pestana '\n", - " 'es solo lectura (Paso A): no escribe en la base de datos.

'),\n", - " W.HTML(_html_formato),\n", - " btn_plantilla,\n", - " W.HTML('

Sube tu Excel con el mapeo Facturas CTM <-> Pedimento F4:

'),\n", - " W.HBox([upload_ctm, chk_use_mapping]),\n", - " W.HBox([btn_ctm_analizar, btn_ctm_xlsx]), bar_ctm,\n", - " out_ctm_log, out_ctm_tabla,\n", - " W.HTML('

Paso B - Ejecutar reasignacion en la base de datos

'\n", - " '

Toma el plan calculado arriba y aplica los cambios en SDescargaT y SFacExp. Corre primero con DRY_RUN activado para revisar.

'),\n", - " W.HBox([modo_ctm, dry_ctm]),\n", - " btn_ctm_ejecutar, bar_ctm_b,\n", - " out_ctm_ejec,\n", - "])\n", - "\n", - "\n", - "# ===== Tab 7: Saldos Vencidos (Utileria Forma 5) =====\n", - "upload_sv = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n", - "btn_plantilla_sv = W.Button(description='Descargar plantilla Excel', button_style='info', icon='download', layout={'width':'250px'})\n", - "w_fecha_ini_sv = W.DatePicker(description='Fecha inicio:', value=None, layout={'width':'260px'})\n", - "w_fecha_fin_sv = W.DatePicker(description='Fecha fin:', value=None, layout={'width':'260px'})\n", - "btn_sv_analizar = W.Button(description='Analizar (sin escribir)', button_style='primary', icon='search', layout={'width':'280px'})\n", - "btn_sv_xlsx = W.Button(description='Exportar Excel completo', button_style='info', icon='download', layout={'width':'280px'})\n", - "bar_sv = _mkbar('Analisis Saldos Vencidos')\n", - "out_sv_log = W.Output(layout=OUT_STYLE)\n", - "out_sv_tabla = W.Output(layout=OUT_STYLE)\n", - "\n", - "_html_formato_sv = '''\n", - "
\n", - "Formato esperado del Excel (3 columnas)\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
ColumnaEjemplo
FACTURAIMPOF1234567
CANTIDAD_SALDO100.0
FRACCION_IMPO85044010
\n", - "

Las fechas inicio/fin filtran SSaldoTem por FECHAFACTURA_ISO.

\n", - "
\n", - "'''\n", - "\n", - "def _on_plantilla_sv(_):\n", - " with out_sv_log:\n", - " clear_output()\n", - " bts = generar_plantilla_excel_saldos_vencidos()\n", - " path = os.path.join(os.getcwd(), 'plantilla_saldos_vencidos.xlsx')\n", - " with open(path, 'wb') as f: f.write(bts)\n", - " display(HTML(f'
'\n", - " f'Plantilla generada
'\n", - " f'{path}
'))\n", - "btn_plantilla_sv.on_click(_on_plantilla_sv)\n", - "\n", - "def _on_sv_analizar(_):\n", - " with out_sv_log:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " if len(upload_sv.value) == 0:\n", - " print('ERROR: sube un Excel primero.'); return\n", - " if w_fecha_ini_sv.value is None or w_fecha_fin_sv.value is None:\n", - " print('ERROR: define fecha inicio y fecha fin.'); return\n", - " try:\n", - " if isinstance(upload_sv.value, dict):\n", - " fname = list(upload_sv.value.keys())[0]\n", - " file_bytes = upload_sv.value[fname]['content']\n", - " else:\n", - " file_bytes = upload_sv.value[0]['content']\n", - " tmp_path = os.path.join(os.getcwd(), '_upload_sv.xlsx')\n", - " with open(tmp_path, 'wb') as f: f.write(file_bytes)\n", - " df_excel = cargar_excel_saldos_vencidos(tmp_path)\n", - " print(f'Excel cargado: {len(df_excel):,} filas')\n", - " except Exception as e:\n", - " print(f'ERROR cargando Excel: {e}'); return\n", - " print('Analizando saldos vencidos...')\n", - " plan, resumen = analizar_saldos_vencidos(df_excel, str(w_fecha_ini_sv.value), str(w_fecha_fin_sv.value), progress=bar_sv)\n", - " _state['sv_plan'] = plan\n", - " _state['sv_resumen'] = resumen\n", - " print(f'Plan: {len(plan):,} filas | Resumen: {len(resumen):,} saldos')\n", - " if not resumen.empty:\n", - " ok = (resumen['STATUS']=='PRORRATEADO').sum()\n", - " sin = (resumen['STATUS']=='SIN_DESCARGAS').sum()\n", - " cz = (resumen['STATUS']=='CANTDESC_CERO').sum()\n", - " print(f' Saldos PRORRATEADOS : {ok:,}')\n", - " print(f' Saldos SIN_DESCARGAS: {sin:,}')\n", - " print(f' Saldos CANTDESC_CERO: {cz:,}')\n", - " with out_sv_tabla:\n", - " clear_output()\n", - " if 'sv_resumen' in _state and not _state['sv_resumen'].empty:\n", - " display(HTML('

Resumen Saldos Vencidos

'))\n", - " display(_state['sv_resumen'])\n", - " display(HTML('

Plan detalle (primeras 50 filas)

'))\n", - " display(_state['sv_plan'].head(50))\n", - "btn_sv_analizar.on_click(_on_sv_analizar)\n", - "\n", - "def _on_sv_xlsx(_):\n", - " with out_sv_log:\n", - " if 'sv_plan' not in _state:\n", - " display(HTML('
Corre primero Analizar.
'))\n", - " return\n", - " path = os.path.join(os.getcwd(), 'plan_saldos_vencidos.xlsx')\n", - " exportar_excel_saldos_vencidos(_state['sv_plan'], _state['sv_resumen'], path)\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{path}
'))\n", - "btn_sv_xlsx.on_click(_on_sv_xlsx)\n", - "\n", - "dry_sv = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n", - "btn_sv_ejecutar = W.Button(description='Ejecutar prorrateo (Paso B)', button_style='warning', icon='play', layout={'width':'300px'})\n", - "bar_sv_b = _mkbar('Ejecucion Saldos Vencidos')\n", - "out_sv_ejec = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_sv_ejecutar(_):\n", - " with out_sv_ejec:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " if 'sv_plan' not in _state:\n", - " print('ERROR: corre primero Analizar.'); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " ejecutar_saldos_vencidos(_state['sv_plan'], dry_run=dry_sv.value, log=print, progress=bar_sv_b)\n", - "btn_sv_ejecutar.on_click(_on_sv_ejecutar)\n", - "\n", - "tab_sv = W.VBox([\n", - " W.HTML('

Saldos Vencidos - Utileria Forma 5

'\n", - " '

'\n", - " 'Prorratea masivamente saldos IMPO vencidos entre las descargas EXPO que comparten '\n", - " '(FACTURAIMPO, NUMPARTE, UMEXITENCIA). El Paso A es solo lectura; el Paso B aplica '\n", - " 'UPDATE en SDescargaT y SSaldoTem (no toca SFacExp).

'),\n", - " W.HTML(_html_formato_sv),\n", - " btn_plantilla_sv,\n", - " W.HTML('

Sube tu Excel y define el rango de fechas (FECHAFACTURA_ISO):

'),\n", - " W.HBox([upload_sv]),\n", - " W.HBox([w_fecha_ini_sv, w_fecha_fin_sv]),\n", - " W.HBox([btn_sv_analizar, btn_sv_xlsx]), bar_sv,\n", - " out_sv_log, out_sv_tabla,\n", - " W.HTML('

Paso B - Ejecutar prorrateo en la base de datos

'\n", - " '

UPDATE en cada SDescargaT matched + UPDATE en SSaldoTem (CANTUSADA, VALORUSADOMN/ME, PESOUSADO, PESOBRUTOUSADO). Corre primero con DRY_RUN activado.

'),\n", - " W.HBox([dry_sv]),\n", - " btn_sv_ejecutar, bar_sv_b,\n", - " out_sv_ejec,\n", - "])\n", - "\n", - "\n", - "# ===== Saldos Vencidos - Modo Automatico (sin Excel) =====\n", - "w_fecha_ini_sv2 = W.DatePicker(description='Fecha inicio:', value=None, layout={'width':'260px'})\n", - "w_fecha_fin_sv2 = W.DatePicker(description='Fecha fin:', value=None, layout={'width':'260px'})\n", - "btn_sv2_analizar = W.Button(description='Buscar saldos vencidos', button_style='primary', icon='search', layout={'width':'280px'})\n", - "btn_sv2_xlsx = W.Button(description='Exportar Excel completo', button_style='info', icon='download', layout={'width':'280px'})\n", - "bar_sv2 = _mkbar('Busqueda automatica')\n", - "out_sv2_log = W.Output(layout=OUT_STYLE)\n", - "out_sv2_tabla = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_sv2_analizar(_):\n", - " with out_sv2_log:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " if w_fecha_ini_sv2.value is None or w_fecha_fin_sv2.value is None:\n", - " print('ERROR: define fecha inicio y fecha fin.'); return\n", - " import datetime as _dt\n", - " hoy = _dt.date.today().isoformat()\n", - " print(f'Buscando saldos en SSaldoTem con FECHAFACTURA_ISO entre {w_fecha_ini_sv2.value} y {w_fecha_fin_sv2.value}')\n", - " print(f' y FECHAVENC_ISO < {hoy} (vencidos a hoy)')\n", - " plan, resumen = analizar_saldos_vencidos_auto(\n", - " str(w_fecha_ini_sv2.value), str(w_fecha_fin_sv2.value), fecha_corte=hoy, progress=bar_sv2)\n", - " _state['sv2_plan'] = plan\n", - " _state['sv2_resumen'] = resumen\n", - " print(f'Plan: {len(plan):,} filas | Resumen: {len(resumen):,} saldos vencidos')\n", - " if not resumen.empty:\n", - " ok = (resumen['STATUS']=='PRORRATEADO').sum()\n", - " sin = (resumen['STATUS']=='SIN_DESCARGAS').sum()\n", - " cz = (resumen['STATUS']=='CANTDESC_CERO').sum()\n", - " print(f' Saldos PRORRATEADOS : {ok:,}')\n", - " print(f' Saldos SIN_DESCARGAS: {sin:,}')\n", - " print(f' Saldos CANTDESC_CERO: {cz:,}')\n", - " with out_sv2_tabla:\n", - " clear_output()\n", - " if 'sv2_resumen' in _state and not _state['sv2_resumen'].empty:\n", - " display(HTML('

Resumen Saldos Vencidos (busqueda automatica)

'))\n", - " display(_state['sv2_resumen'])\n", - " display(HTML('

Plan detalle (primeras 50 filas)

'))\n", - " display(_state['sv2_plan'].head(50))\n", - "btn_sv2_analizar.on_click(_on_sv2_analizar)\n", - "\n", - "def _on_sv2_xlsx(_):\n", - " with out_sv2_log:\n", - " if 'sv2_plan' not in _state:\n", - " display(HTML('
Corre primero Buscar saldos vencidos.
'))\n", - " return\n", - " path = os.path.join(os.getcwd(), 'plan_saldos_vencidos_auto.xlsx')\n", - " exportar_excel_saldos_vencidos(_state['sv2_plan'], _state['sv2_resumen'], path)\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{path}
'))\n", - "btn_sv2_xlsx.on_click(_on_sv2_xlsx)\n", - "\n", - "dry_sv2 = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n", - "btn_sv2_ejecutar = W.Button(description='Ejecutar prorrateo (Paso B)', button_style='warning', icon='play', layout={'width':'300px'})\n", - "bar_sv2_b = _mkbar('Ejecucion Saldos Vencidos (auto)')\n", - "out_sv2_ejec = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_sv2_ejecutar(_):\n", - " with out_sv2_ejec:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " if 'sv2_plan' not in _state:\n", - " print('ERROR: corre primero Buscar saldos vencidos.'); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " ejecutar_saldos_vencidos(_state['sv2_plan'], dry_run=dry_sv2.value, log=print, progress=bar_sv2_b)\n", - "btn_sv2_ejecutar.on_click(_on_sv2_ejecutar)\n", - "\n", - "tab_sv.children = tuple(list(tab_sv.children) + [\n", - " W.HTML('

Modo Automatico - sin Excel

'\n", - " '

'\n", - " 'Define el rango de fechas (filtra FECHAFACTURA_ISO) y el sistema busca automaticamente '\n", - " 'los saldos con FECHAVENC_ISO < hoy (vencidos) y SALDO_DISPONIBLE > 0. '\n", - " 'Aplica el mismo prorrateo y los mismos UPDATEs que el modo Excel.

'),\n", - " W.HBox([w_fecha_ini_sv2, w_fecha_fin_sv2]),\n", - " W.HBox([btn_sv2_analizar, btn_sv2_xlsx]), bar_sv2,\n", - " out_sv2_log, out_sv2_tabla,\n", - " W.HTML('

Paso B - Ejecutar prorrateo automatico

'\n", - " '

Mismos UPDATEs: SDescargaT por descarga + SSaldoTem por saldo. Corre primero con DRY_RUN activado.

'),\n", - " W.HBox([dry_sv2]),\n", - " btn_sv2_ejecutar, bar_sv2_b,\n", - " out_sv2_ejec,\n", - "])\n", - "\n", - "\n", - "# ===== Saldos Vencidos - Grafica por anio =====\n", - "w_sv_metric = W.Dropdown(\n", - " options=[('Valor MN','SALDO_VMN'),('Valor ME','SALDO_VME'),\n", - " ('Cantidad','SALDO_CANT'),('Lotes','LOTES')],\n", - " value='SALDO_VMN', description='Metrica:', layout={'width':'260px'})\n", - "w_sv_eje = W.Dropdown(\n", - " options=[('Anio Vencimiento','VENCIMIENTO'),('Anio Factura','FACTURA')],\n", - " value='VENCIMIENTO', description='Eje X:', layout={'width':'260px'})\n", - "btn_sv_grafica = W.Button(description='Ver grafica saldos vencidos por anio',\n", - " button_style='primary', icon='bar-chart', layout={'width':'320px'})\n", - "out_sv_grafica = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_sv_grafica(_):\n", - " with out_sv_grafica:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " import datetime as _dt\n", - " hoy = _dt.date.today().isoformat()\n", - " print(f'Vencidos a {hoy} (FECHAVENC_ISO < hoy y SALDO_DISPONIBLE > 0)')\n", - " df = cargar_saldos_vencidos_por_anio(fecha_corte=hoy, eje=w_sv_eje.value)\n", - " if df.empty:\n", - " print('No hay saldos vencidos.'); return\n", - " print(f'Anios con saldos vencidos: {len(df)}')\n", - " graficar_saldos_vencidos_por_anio(df, metric=w_sv_metric.value)\n", - "btn_sv_grafica.on_click(_on_sv_grafica)\n", - "\n", - "tab_sv.children = tuple(list(tab_sv.children) + [\n", - " W.HTML('

Saldos vencidos por anio (vista general)

'\n", - " '

'\n", - " 'Resumen global de saldos en SSaldoTem cuyo FECHAVENC_ISO < hoy y '\n", - " 'SALDO_DISPONIBLE > 0, agrupados por anio de vencimiento. '\n", - " 'No depende del rango de fechas de arriba.

'),\n", - " W.HBox([w_sv_metric, w_sv_eje, btn_sv_grafica]),\n", - " out_sv_grafica,\n", - "])\n", - "\n", - "\n", - "# ===== Tab 8: DataStage (Subir .asc a Postgres) =====\n", - "w_ds_ruta = W.Text(\n", - " value=DATASTAGE_ROOT or '', placeholder=r'C:\\ruta\\DATASTAGE_HONDA',\n", - " description='Carpeta:', layout={'width':'650px'},\n", - " style={'description_width':'80px'})\n", - "btn_ds_listar = W.Button(description='Listar archivos', button_style='info', icon='search', layout={'width':'200px'})\n", - "btn_ds_cargar = W.Button(description='Cargar todos a Postgres', button_style='warning', icon='upload', layout={'width':'260px'})\n", - "btn_ds_tablas = W.Button(description='Ver tablas Registro en Postgres', button_style='', icon='database', layout={'width':'280px'})\n", - "bar_ds = _mkbar('DataStage')\n", - "out_ds_lista = W.Output(layout=OUT_STYLE)\n", - "out_ds_log = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _ds_msg_conexion():\n", - " if not DATASTAGE_OK:\n", - " display(HTML(f'
'\n", - " f'Postgres no disponible.
'\n", - " f'{DATASTAGE_MSG}
'\n", - " f'Revisa que el contenedor postgres-datastage este levantado y que el .env tenga DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD.
'))\n", - " return False\n", - " display(HTML(f'
{DATASTAGE_MSG}
'))\n", - " return True\n", - "\n", - "def _on_ds_listar(_):\n", - " with out_ds_lista:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " ruta = (w_ds_ruta.value or '').strip()\n", - " if not ruta:\n", - " print('ERROR: define la ruta de la carpeta con los .asc')\n", - " return\n", - " df = previsualizar_archivos_ds(ruta)\n", - " if df.empty:\n", - " print(f'No se encontraron .asc en: {ruta}')\n", - " return\n", - " print(f'Archivos detectados: {len(df):,}')\n", - " display(df.head(200))\n", - " if len(df) > 200:\n", - " print(f'(mostrando 200 de {len(df)})')\n", - " # Resumen por tabla destino\n", - " resumen = (df.groupby('tabla_destino', as_index=False)\n", - " .agg(archivos=('archivo','count'),\n", - " tamanio_kb=('tamanio_kb','sum')))\n", - " display(HTML('

Resumen por tabla destino

'))\n", - " display(resumen)\n", - "btn_ds_listar.on_click(_on_ds_listar)\n", - "\n", - "def _on_ds_cargar(_):\n", - " with out_ds_log:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " ruta = (w_ds_ruta.value or '').strip()\n", - " if not ruta:\n", - " print('ERROR: define la ruta de la carpeta con los .asc')\n", - " return\n", - " print(f'Iniciando carga desde: {ruta}')\n", - " df_res = cargar_directorio_datastage(ruta, progress=bar_ds, log=print)\n", - " if not df_res.empty:\n", - " _state['ds_resultados'] = df_res\n", - " display(HTML('

Resultado por archivo

'))\n", - " display(df_res)\n", - "btn_ds_cargar.on_click(_on_ds_cargar)\n", - "\n", - "def _on_ds_tablas(_):\n", - " with out_ds_lista:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " try:\n", - " with _ds_conn() as c:\n", - " tablas = listar_tablas_registro(c)\n", - " if not tablas:\n", - " print('No hay tablas Registro* en Postgres. Migra el schema primero.')\n", - " return\n", - " print(f'Tablas Registro en Postgres: {len(tablas)}')\n", - " df = pd.DataFrame({'tabla': tablas})\n", - " display(df)\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_tablas.on_click(_on_ds_tablas)\n", - "\n", - "tab_datastage = W.VBox([\n", - " W.HTML('

DataStage - Subir archivos .asc a Postgres

'\n", - " '

'\n", - " 'Carga masiva de archivos .asc del DataStage en sus tablas Registro<NNN>. '\n", - " 'Migrado del modulo PHP HOME/DATASTAGE. La estructura de los archivos '\n", - " 'se infiere del nombre (_NNN.ascRegistroNNN); '\n", - " 'el separador es |, el encoding es latin-1, y se omite el header.

'),\n", - " W.HTML('
'\n", - " 'Estructura esperada de la carpeta'\n", - " '
    '\n", - " '
  • Layout HONDA: RAIZ/2020/*.asc, RAIZ/2021/*.asc, ... (solo .asc directos, sin entrar a subcarpetas de meses)
  • '\n", - " '
  • Layout plano: RAIZ/*.asc
  • '\n", - " '
'\n", - " '

Tablas destino deben existir previamente en Postgres. Esta pestania no crea tablas.

'\n", - " '
'),\n", - " W.HBox([w_ds_ruta]),\n", - " W.HBox([btn_ds_listar, btn_ds_tablas, btn_ds_cargar]), bar_ds,\n", - " out_ds_lista,\n", - " W.HTML('

Log de carga

'),\n", - " out_ds_log,\n", - "])\n", - "\n", - "\n", - "\n", - "\n", - "# ===== DataStage: Limpiar y Estadisticas =====\n", - "chk_ds_confirmar_truncate = W.Checkbox(\n", - " value=False, description='Confirmo limpiar TODAS las tablas Registro*',\n", - " indent=False, layout={'width':'420px'})\n", - "btn_ds_truncate = W.Button(description='Limpiar todas las tablas',\n", - " button_style='danger', icon='trash', layout={'width':'250px'})\n", - "btn_ds_stats = W.Button(description='Ver estadisticas',\n", - " button_style='primary', icon='chart-bar', layout={'width':'200px'})\n", - "w_ds_tabla_sel = W.Dropdown(options=[], description='Tabla:',\n", - " layout={'width':'320px'})\n", - "w_ds_limit = W.IntText(value=100, description='Filas:',\n", - " layout={'width':'180px'}, style={'description_width':'60px'})\n", - "btn_ds_ver_datos = W.Button(description='Ver datos',\n", - " button_style='info', icon='eye', layout={'width':'180px'})\n", - "btn_ds_export = W.Button(description='Exportar a Excel',\n", - " button_style='', icon='file-excel', layout={'width':'200px'})\n", - "bar_ds_stats = _mkbar('Estadisticas')\n", - "out_ds_stats = W.Output(layout=OUT_STYLE)\n", - "out_ds_datos = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ds_truncate(_):\n", - " with out_ds_log:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not chk_ds_confirmar_truncate.value:\n", - " display(HTML('
Marca el checkbox de confirmacion antes de ejecutar.
'))\n", - " return\n", - " print('Truncando todas las tablas Registro*...')\n", - " res = truncar_tablas_registro(progress=bar_ds, log=print)\n", - " chk_ds_confirmar_truncate.value = False\n", - " if res:\n", - " df = pd.DataFrame([{'tabla': k, 'filas_eliminadas': v} for k, v in res.items()])\n", - " display(HTML('

Resultado del TRUNCATE

'))\n", - " display(df)\n", - "btn_ds_truncate.on_click(_on_ds_truncate)\n", - "\n", - "def _on_ds_stats(_):\n", - " with out_ds_stats:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " df = estadisticas_tablas_registro(progress=bar_ds_stats)\n", - " if df.empty:\n", - " print('No hay tablas Registro* en Postgres.')\n", - " return\n", - " total_filas = df['filas'].sum()\n", - " total_tablas = len(df)\n", - " total_con_datos = (df['filas'] > 0).sum()\n", - " display(HTML(\n", - " f'
'\n", - " f'Total: '\n", - " f'{total_tablas} tablas | {total_con_datos} con datos | '\n", - " f'{total_filas:,} filas en total
'))\n", - " df_show = df.copy()\n", - " df_show['filas'] = df_show['filas'].apply(lambda n: f'{n:,}')\n", - " display(df_show)\n", - " _state['ds_stats'] = df\n", - " # Llenar dropdown para visor\n", - " tablas_con_datos = df[df['filas'] > 0]['tabla'].tolist()\n", - " w_ds_tabla_sel.options = tablas_con_datos\n", - "btn_ds_stats.on_click(_on_ds_stats)\n", - "\n", - "def _on_ds_ver_datos(_):\n", - " with out_ds_datos:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " tabla = w_ds_tabla_sel.value\n", - " if not tabla:\n", - " print('Selecciona una tabla primero (corre \"Ver estadisticas\" antes).')\n", - " return\n", - " limit = max(1, int(w_ds_limit.value or 100))\n", - " try:\n", - " df = obtener_muestra_tabla(tabla, limit=limit, offset=0)\n", - " display(HTML(f'

{tabla} (primeras {limit})

'))\n", - " display(df)\n", - " _state['ds_muestra'] = df\n", - " _state['ds_muestra_tabla'] = tabla\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_ver_datos.on_click(_on_ds_ver_datos)\n", - "\n", - "def _on_ds_export(_):\n", - " with out_ds_datos:\n", - " if 'ds_muestra' not in _state or _state['ds_muestra'].empty:\n", - " display(HTML('
No hay datos cargados. Corre \"Ver datos\" primero.
'))\n", - " return\n", - " tabla = _state.get('ds_muestra_tabla', 'tabla')\n", - " ruta = os.path.join(os.getcwd(), f'{tabla}.xlsx')\n", - " _state['ds_muestra'].to_excel(ruta, index=False)\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{ruta}
'))\n", - "btn_ds_export.on_click(_on_ds_export)\n", - "\n", - "tab_datastage.children = tuple(list(tab_datastage.children) + [\n", - " W.HTML('

Estadisticas de tablas Registro*

'\n", - " '

Cuenta de filas por tabla. Equivalente al modulo PHP datastage.php: '\n", - " 'permite ver totales y explorar el contenido cargado.

'),\n", - " W.HBox([btn_ds_stats]), bar_ds_stats,\n", - " out_ds_stats,\n", - " W.HTML('
Explorar contenido
'),\n", - " W.HBox([w_ds_tabla_sel, w_ds_limit, btn_ds_ver_datos, btn_ds_export]),\n", - " out_ds_datos,\n", - " W.HTML('

Zona peligrosa

'\n", - " '

TRUNCATE TABLE en todas las tablas Registro*. '\n", - " 'Elimina TODAS las filas (no se puede deshacer). Util para reiniciar la carga desde cero.

'),\n", - " W.HBox([chk_ds_confirmar_truncate]),\n", - " W.HBox([btn_ds_truncate]),\n", - "])\n", - "\n", - "\n", - "\n", - "\n", - "# ===== DataStage: Reportes de Pedimentos =====\n", - "def _ds_report_block(titulo_html, fi_widget, ff_widget, btn_widget,\n", - " btn_export_widget, out_widget):\n", - " return W.VBox([\n", - " W.HTML(titulo_html),\n", - " W.HBox([fi_widget, ff_widget, btn_widget, btn_export_widget]),\n", - " out_widget,\n", - " ])\n", - "\n", - "# --- 1) Estructura CAT Pedimentos ---\n", - "w_ds_cat_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", - "w_ds_cat_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", - "btn_ds_cat_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", - "btn_ds_cat_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", - "out_ds_cat = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ds_cat(_):\n", - " with out_ds_cat:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not w_ds_cat_fi.value or not w_ds_cat_ff.value:\n", - " print('Define fecha inicio y fecha fin.'); return\n", - " print(f'Consultando CAT Pedimentos {w_ds_cat_fi.value} a {w_ds_cat_ff.value}...')\n", - " try:\n", - " df = cat_pedimentos_ds(str(w_ds_cat_fi.value), str(w_ds_cat_ff.value))\n", - " print(f'Filas: {len(df):,}')\n", - " _state['ds_cat_df'] = df\n", - " display(df.head(200))\n", - " if len(df) > 200:\n", - " print(f'(mostrando 200 de {len(df)})')\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_cat_run.on_click(_on_ds_cat)\n", - "\n", - "def _on_ds_cat_export(_):\n", - " with out_ds_cat:\n", - " if 'ds_cat_df' not in _state or _state['ds_cat_df'].empty:\n", - " display(HTML('
Corre Generar primero.
'))\n", - " return\n", - " ruta = exportar_df_a_excel(_state['ds_cat_df'], 'Estructura_CAT_Pedimentos')\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{ruta}
'))\n", - "btn_ds_cat_export.on_click(_on_ds_cat_export)\n", - "\n", - "# --- 2) Estructura CAT Pedimentos Rectificados ---\n", - "w_ds_catr_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", - "w_ds_catr_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", - "btn_ds_catr_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", - "btn_ds_catr_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", - "out_ds_catr = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ds_catr(_):\n", - " with out_ds_catr:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not w_ds_catr_fi.value or not w_ds_catr_ff.value:\n", - " print('Define fecha inicio y fecha fin.'); return\n", - " print(f'Consultando CAT Pedimentos Rectificados {w_ds_catr_fi.value} a {w_ds_catr_ff.value}...')\n", - " try:\n", - " df = cat_pedimentos_rect_ds(str(w_ds_catr_fi.value), str(w_ds_catr_ff.value))\n", - " print(f'Filas: {len(df):,}')\n", - " _state['ds_catr_df'] = df\n", - " display(df.head(200))\n", - " if len(df) > 200:\n", - " print(f'(mostrando 200 de {len(df)})')\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_catr_run.on_click(_on_ds_catr)\n", - "\n", - "def _on_ds_catr_export(_):\n", - " with out_ds_catr:\n", - " if 'ds_catr_df' not in _state or _state['ds_catr_df'].empty:\n", - " display(HTML('
Corre Generar primero.
'))\n", - " return\n", - " ruta = exportar_df_a_excel(_state['ds_catr_df'], 'Estructura_CAT_Pedimentos_Rectificados')\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{ruta}
'))\n", - "btn_ds_catr_export.on_click(_on_ds_catr_export)\n", - "\n", - "# --- 3) Rastreo Rectificaciones (con historial recursivo) ---\n", - "w_ds_rect_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", - "w_ds_rect_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", - "w_ds_rect_search = W.Text(description='Buscar:', placeholder='Pedimento, patente o clave',\n", - " layout={'width':'320px'}, style={'description_width':'70px'})\n", - "btn_ds_rect_run = W.Button(description='Listar', button_style='primary', icon='search', layout={'width':'120px'})\n", - "btn_ds_rect_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", - "out_ds_rect = W.Output(layout=OUT_STYLE)\n", - "\n", - "w_ds_hist_pat = W.Text(description='Patente:', layout={'width':'220px'}, style={'description_width':'80px'})\n", - "w_ds_hist_ped = W.Text(description='Pedimento:', layout={'width':'260px'}, style={'description_width':'80px'})\n", - "w_ds_hist_sec = W.Text(description='Seccion Ad.:', layout={'width':'220px'}, style={'description_width':'80px'})\n", - "w_ds_hist_anio = W.IntText(value=2024, description='Anio:', layout={'width':'150px'}, style={'description_width':'60px'})\n", - "btn_ds_hist_run = W.Button(description='Ver historial', button_style='info', icon='clock-o', layout={'width':'180px'})\n", - "out_ds_hist = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ds_rect(_):\n", - " with out_ds_rect:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " try:\n", - " fi = str(w_ds_rect_fi.value) if w_ds_rect_fi.value else None\n", - " ff = str(w_ds_rect_ff.value) if w_ds_rect_ff.value else None\n", - " df = rectificados_ds(fi, ff, (w_ds_rect_search.value or '').strip())\n", - " print(f'Pedimentos rectificados encontrados: {len(df):,}')\n", - " _state['ds_rect_df'] = df\n", - " display(df.head(200))\n", - " if len(df) > 200:\n", - " print(f'(mostrando 200 de {len(df)})')\n", - " display(HTML('Tip: copia Patente / Pedimento / SeccionAduanera / Anio a la seccion de abajo para ver el historial recursivo.'))\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_rect_run.on_click(_on_ds_rect)\n", - "\n", - "def _on_ds_rect_export(_):\n", - " with out_ds_rect:\n", - " if 'ds_rect_df' not in _state or _state['ds_rect_df'].empty:\n", - " display(HTML('
Corre Listar primero.
'))\n", - " return\n", - " ruta = exportar_df_a_excel(_state['ds_rect_df'], 'Pedimentos_Rectificados')\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{ruta}
'))\n", - "btn_ds_rect_export.on_click(_on_ds_rect_export)\n", - "\n", - "def _on_ds_hist(_):\n", - " with out_ds_hist:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not (w_ds_hist_pat.value and w_ds_hist_ped.value and w_ds_hist_sec.value):\n", - " print('Completa Patente, Pedimento, Seccion Aduanera y Anio.'); return\n", - " try:\n", - " df = historial_rectificaciones_ds(\n", - " w_ds_hist_pat.value.strip(),\n", - " w_ds_hist_ped.value.strip(),\n", - " w_ds_hist_sec.value.strip(),\n", - " int(w_ds_hist_anio.value))\n", - " print(f'Filas en cadena: {len(df):,}')\n", - " display(df)\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_hist_run.on_click(_on_ds_hist)\n", - "\n", - "tab_estructuras = W.VBox([\n", - " W.HTML('

Estructuras SCAII - Reportes de Pedimentos

'\n", - " '

Migracion de las opciones del menu PHP Pedimentos: '\n", - " 'CAT Pedimentos, CAT Pedimentos Rectificados y Rastreo de Rectificaciones (con historial recursivo). '\n", - " 'Las consultas se ejecutan sobre Postgres (tablas Registro501 y Registro701).

'),\n", - " _ds_report_block(\n", - " '
Estructura CAT Pedimentos
'\n", - " '

Pedimentos de Registro501 en el rango, indicando si fueron rectificados.

',\n", - " w_ds_cat_fi, w_ds_cat_ff, btn_ds_cat_run, btn_ds_cat_export, out_ds_cat),\n", - " _ds_report_block(\n", - " '
Estructura CAT Pedimentos Rectificados
'\n", - " '

Pedimentos rectificados de Registro701 en el rango, '\n", - " 'enlazados con el tipo de operacion de Registro501.

',\n", - " w_ds_catr_fi, w_ds_catr_ff, btn_ds_catr_run, btn_ds_catr_export, out_ds_catr),\n", - " W.HTML('
Rastreo de Rectificaciones
'\n", - " '

Pedimentos de Registro501 que fueron rectificados, con filtros y opcion de ver la cadena historica.

'),\n", - " W.HBox([w_ds_rect_fi, w_ds_rect_ff, w_ds_rect_search]),\n", - " W.HBox([btn_ds_rect_run, btn_ds_rect_export]),\n", - " out_ds_rect,\n", - " W.HTML('
Historial recursivo de un pedimento
'\n", - " '

Equivalente al modal obtener_historial.php.

'),\n", - " W.HBox([w_ds_hist_pat, w_ds_hist_ped, w_ds_hist_sec, w_ds_hist_anio]),\n", - " W.HBox([btn_ds_hist_run]),\n", - " out_ds_hist,\n", - "])\n", - "\n", - "\n", - "\n", - "\n", - "# --- 4) Encabezado Facturas Importacion ---\n", - "w_ds_fimpo_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", - "w_ds_fimpo_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", - "btn_ds_fimpo_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", - "btn_ds_fimpo_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", - "out_ds_fimpo = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ds_fimpo(_):\n", - " with out_ds_fimpo:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not w_ds_fimpo_fi.value or not w_ds_fimpo_ff.value:\n", - " print('Define fecha inicio y fecha fin.'); return\n", - " print(f'Consultando facturas IMPO {w_ds_fimpo_fi.value} a {w_ds_fimpo_ff.value}...')\n", - " try:\n", - " df = encabezado_facturas_ds(str(w_ds_fimpo_fi.value), str(w_ds_fimpo_ff.value), 1)\n", - " print(f'Filas: {len(df):,}')\n", - " _state['ds_fimpo_df'] = df\n", - " display(df.head(200))\n", - " if len(df) > 200:\n", - " print(f'(mostrando 200 de {len(df)})')\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_fimpo_run.on_click(_on_ds_fimpo)\n", - "\n", - "def _on_ds_fimpo_export(_):\n", - " with out_ds_fimpo:\n", - " if 'ds_fimpo_df' not in _state or _state['ds_fimpo_df'].empty:\n", - " display(HTML('
Corre Generar primero.
')); return\n", - " ruta = exportar_df_a_excel(_state['ds_fimpo_df'], 'Estructura_facturasImpo_501')\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{ruta}
'))\n", - "btn_ds_fimpo_export.on_click(_on_ds_fimpo_export)\n", - "\n", - "# --- 5) Encabezado Facturas Exportacion ---\n", - "w_ds_fexpo_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", - "w_ds_fexpo_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", - "btn_ds_fexpo_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", - "btn_ds_fexpo_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", - "out_ds_fexpo = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ds_fexpo(_):\n", - " with out_ds_fexpo:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not w_ds_fexpo_fi.value or not w_ds_fexpo_ff.value:\n", - " print('Define fecha inicio y fecha fin.'); return\n", - " print(f'Consultando facturas EXPO {w_ds_fexpo_fi.value} a {w_ds_fexpo_ff.value}...')\n", - " try:\n", - " df = encabezado_facturas_ds(str(w_ds_fexpo_fi.value), str(w_ds_fexpo_ff.value), 2)\n", - " print(f'Filas: {len(df):,}')\n", - " _state['ds_fexpo_df'] = df\n", - " display(df.head(200))\n", - " if len(df) > 200:\n", - " print(f'(mostrando 200 de {len(df)})')\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_fexpo_run.on_click(_on_ds_fexpo)\n", - "\n", - "def _on_ds_fexpo_export(_):\n", - " with out_ds_fexpo:\n", - " if 'ds_fexpo_df' not in _state or _state['ds_fexpo_df'].empty:\n", - " display(HTML('
Corre Generar primero.
')); return\n", - " ruta = exportar_df_a_excel(_state['ds_fexpo_df'], 'Estructura_facturasExpo_501')\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{ruta}
'))\n", - "btn_ds_fexpo_export.on_click(_on_ds_fexpo_export)\n", - "\n", - "tab_estructuras.children = tuple(list(tab_estructuras.children) + [\n", - " W.HTML('

Encabezado de Facturas

'\n", - " '

Genera el encabezado de facturas de importacion (TipoOperacion=1) o exportacion '\n", - " '(TipoOperacion=2) excluyendo los pedimentos que ya fueron rectificados.

'),\n", - " _ds_report_block(\n", - " '
Encabezado Facturas Importacion
'\n", - " '

Pedimentos IMPO de Registro501 en el rango (excluye rectificados).

',\n", - " w_ds_fimpo_fi, w_ds_fimpo_ff, btn_ds_fimpo_run, btn_ds_fimpo_export, out_ds_fimpo),\n", - " _ds_report_block(\n", - " '
Encabezado Facturas Exportacion
'\n", - " '

Pedimentos EXPO de Registro501 en el rango (excluye rectificados).

',\n", - " w_ds_fexpo_fi, w_ds_fexpo_ff, btn_ds_fexpo_run, btn_ds_fexpo_export, out_ds_fexpo),\n", - "])\n", - "\n", - "\n", - "\n", - "\n", - "# --- 6) Estructura Tipo de Cambio 501 ---\n", - "w_ds_tc_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", - "w_ds_tc_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", - "btn_ds_tc_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", - "btn_ds_tc_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'200px'})\n", - "out_ds_tc = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ds_tc(_):\n", - " with out_ds_tc:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not w_ds_tc_fi.value or not w_ds_tc_ff.value:\n", - " print('Define fecha inicio y fecha fin.'); return\n", - " print(f'Consultando Tipo de Cambio {w_ds_tc_fi.value} a {w_ds_tc_ff.value}...')\n", - " try:\n", - " df = tipo_cambio_ds(str(w_ds_tc_fi.value), str(w_ds_tc_ff.value))\n", - " n_incon = int(df['INCONSISTENTE'].sum()) if not df.empty else 0\n", - " n_fechas_incon = df.loc[df['INCONSISTENTE'], 'FECHA PAGO REAL'].astype(str).str[:10].nunique() if n_incon else 0\n", - " print(f'Filas: {len(df):,} | Filas con TipoCambio inconsistente: {n_incon:,} ({n_fechas_incon} fechas distintas)')\n", - " _state['ds_tc_df'] = df\n", - " # Mostrar con celdas resaltadas (Styler)\n", - " styler = (df.head(200).style\n", - " .apply(lambda r: ['background-color:#FF0000;color:white;font-weight:bold' if r['INCONSISTENTE'] and c == 'TIPO CAMBIO' else ''\n", - " for c in df.columns], axis=1))\n", - " display(styler)\n", - " if len(df) > 200:\n", - " print(f'(mostrando 200 de {len(df)})')\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_tc_run.on_click(_on_ds_tc)\n", - "\n", - "def _on_ds_tc_export(_):\n", - " with out_ds_tc:\n", - " if 'ds_tc_df' not in _state or _state['ds_tc_df'].empty:\n", - " display(HTML('
Corre Generar primero.
')); return\n", - " import datetime as _dt_xc\n", - " ts = _dt_xc.datetime.now().strftime('%Y%m%d_%H%M%S')\n", - " ruta = os.path.join(os.getcwd(), f'Estructura_Tipo_Cambio_501_{ts}.xlsx')\n", - " exportar_tipo_cambio_excel(_state['ds_tc_df'], ruta)\n", - " display(HTML(f'
'\n", - " f'Excel generado (celdas con inconsistencia en rojo)
'\n", - " f'{ruta}
'))\n", - "btn_ds_tc_export.on_click(_on_ds_tc_export)\n", - "\n", - "tab_estructuras.children = tuple(list(tab_estructuras.children) + [\n", - " W.HTML('

Tipo de Cambio

'\n", - " '

Estructura de Tipo de Cambio del Registro501. '\n", - " 'Detecta automaticamente inconsistencias: fechas que tienen mas de un valor distinto '\n", - " 'de TIPO CAMBIO en sus pedimentos (resaltadas en rojo).

'),\n", - " _ds_report_block(\n", - " '
Estructura Tipo de Cambio 501
'\n", - " '

Pedimentos de Registro501 en el rango con su Tipo de Cambio.

',\n", - " w_ds_tc_fi, w_ds_tc_ff, btn_ds_tc_run, btn_ds_tc_export, out_ds_tc),\n", - "])\n", - "\n", - "\n", - "\n", - "\n", - "# ===== Catalogo base de NUMPARTES =====\n", - "upload_basenp = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n", - "btn_basenp_cargar = W.Button(description='Cargar a base (upsert)', button_style='primary', icon='upload', layout={'width':'260px'})\n", - "btn_basenp_ver = W.Button(description='Ver base actual', button_style='info', icon='database', layout={'width':'200px'})\n", - "chk_basenp_confirmar = W.Checkbox(value=False, description='Confirmo TRUNCATE de base_numpartes',\n", - " indent=False, layout={'width':'380px'})\n", - "btn_basenp_truncar = W.Button(description='Limpiar base', button_style='danger', icon='trash', layout={'width':'180px'})\n", - "out_basenp = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_basenp_cargar(_):\n", - " with out_basenp:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if len(upload_basenp.value) == 0:\n", - " print('Sube un Excel primero.'); return\n", - " try:\n", - " if isinstance(upload_basenp.value, dict):\n", - " fname = list(upload_basenp.value.keys())[0]\n", - " fb = upload_basenp.value[fname]['content']\n", - " else:\n", - " fb = upload_basenp.value[0]['content']\n", - " tmp = os.path.join(os.getcwd(), '_upload_basenp.xlsx')\n", - " with open(tmp, 'wb') as f: f.write(fb)\n", - " n = cargar_excel_base_numpartes(tmp, log=print)\n", - " print(f'OK. {n} filas cargadas/actualizadas.')\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_basenp_cargar.on_click(_on_basenp_cargar)\n", - "\n", - "def _on_basenp_ver(_):\n", - " with out_basenp:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " try:\n", - " df = listar_base_numpartes(limit=500)\n", - " print(f'base_numpartes: {len(df):,} filas (max 500 mostradas)')\n", - " display(df)\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_basenp_ver.on_click(_on_basenp_ver)\n", - "\n", - "def _on_basenp_truncar(_):\n", - " with out_basenp:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not chk_basenp_confirmar.value:\n", - " display(HTML('
Marca el checkbox de confirmacion antes de ejecutar.
')); return\n", - " try:\n", - " n = truncar_base_numpartes(log=print)\n", - " chk_basenp_confirmar.value = False\n", - " print(f'OK. {n} filas eliminadas.')\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_basenp_truncar.on_click(_on_basenp_truncar)\n", - "\n", - "\n", - "# ===== Estructura de Partidas Impo =====\n", - "w_ds_pimpo_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", - "w_ds_pimpo_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", - "w_ds_pimpo_umb = W.FloatSlider(value=0.80, min=0.50, max=1.00, step=0.05,\n", - " description='Umbral sim:', readout_format='.2f', layout={'width':'380px'})\n", - "btn_ds_pimpo_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", - "btn_ds_pimpo_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", - "bar_ds_pimpo = _mkbar('Partidas IMPO')\n", - "out_ds_pimpo = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ds_pimpo(_):\n", - " with out_ds_pimpo:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not w_ds_pimpo_fi.value or not w_ds_pimpo_ff.value:\n", - " print('Define fecha inicio y fecha fin.'); return\n", - " try:\n", - " df = asignar_numpartes_551(str(w_ds_pimpo_fi.value), str(w_ds_pimpo_ff.value),\n", - " 1, float(w_ds_pimpo_umb.value),\n", - " progress=bar_ds_pimpo, log=print)\n", - " print(f'Filas: {len(df):,}')\n", - " _state['ds_pimpo_df'] = df\n", - " display(df.head(200))\n", - " if len(df) > 200:\n", - " print(f'(mostrando 200 de {len(df)})')\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_pimpo_run.on_click(_on_ds_pimpo)\n", - "\n", - "def _on_ds_pimpo_export(_):\n", - " with out_ds_pimpo:\n", - " if 'ds_pimpo_df' not in _state or _state['ds_pimpo_df'].empty:\n", - " display(HTML('
Corre Generar primero.
')); return\n", - " ruta = exportar_df_a_excel(_state['ds_pimpo_df'], 'Estructura_Partidas_Impo')\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{ruta}
'))\n", - "btn_ds_pimpo_export.on_click(_on_ds_pimpo_export)\n", - "\n", - "\n", - "# ===== Estructura de Partidas Expo =====\n", - "w_ds_pexpo_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", - "w_ds_pexpo_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", - "w_ds_pexpo_umb = W.FloatSlider(value=0.80, min=0.50, max=1.00, step=0.05,\n", - " description='Umbral sim:', readout_format='.2f', layout={'width':'380px'})\n", - "btn_ds_pexpo_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", - "btn_ds_pexpo_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", - "bar_ds_pexpo = _mkbar('Partidas EXPO')\n", - "out_ds_pexpo = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_ds_pexpo(_):\n", - " with out_ds_pexpo:\n", - " clear_output()\n", - " if not _ds_msg_conexion(): return\n", - " if not w_ds_pexpo_fi.value or not w_ds_pexpo_ff.value:\n", - " print('Define fecha inicio y fecha fin.'); return\n", - " try:\n", - " df = asignar_numpartes_551(str(w_ds_pexpo_fi.value), str(w_ds_pexpo_ff.value),\n", - " 2, float(w_ds_pexpo_umb.value),\n", - " progress=bar_ds_pexpo, log=print)\n", - " print(f'Filas: {len(df):,}')\n", - " _state['ds_pexpo_df'] = df\n", - " display(df.head(200))\n", - " if len(df) > 200:\n", - " print(f'(mostrando 200 de {len(df)})')\n", - " except Exception as e:\n", - " print(f'ERROR: {e}')\n", - "btn_ds_pexpo_run.on_click(_on_ds_pexpo)\n", - "\n", - "def _on_ds_pexpo_export(_):\n", - " with out_ds_pexpo:\n", - " if 'ds_pexpo_df' not in _state or _state['ds_pexpo_df'].empty:\n", - " display(HTML('
Corre Generar primero.
')); return\n", - " ruta = exportar_df_a_excel(_state['ds_pexpo_df'], 'Estructura_Partidas_Expo')\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{ruta}
'))\n", - "btn_ds_pexpo_export.on_click(_on_ds_pexpo_export)\n", - "\n", - "\n", - "tab_estructuras.children = tuple(list(tab_estructuras.children) + [\n", - " W.HTML('

Catalogo base de NUMPARTES

'\n", - " '

Catalogo del cliente con columnas NUMPARTE, DESCRIPCION, UNIDAD DE MEDIDA, FRACCION. '\n", - " 'Se usa para asignar NUMPARTE a las partidas del Registro551 por similitud. La subida hace upsert (acumula).

'),\n", - " W.HBox([upload_basenp, btn_basenp_cargar, btn_basenp_ver]),\n", - " W.HBox([chk_basenp_confirmar, btn_basenp_truncar]),\n", - " out_basenp,\n", - " W.HTML('

Estructura de Partidas

'\n", - " '

Genera la estructura de partidas a partir de Registro551. '\n", - " 'Asigna NUMPARTE buscando primero en el catalogo (TF-IDF + cosine sobre descripcion, con fraccion 4d y UM exactas); '\n", - " 'las que no encuentran match se agrupan entre si por similitud y reciben NUMPARTE auto MP<F4>-R<NNN>.

'),\n", - " W.HTML('
Partidas Importacion
'),\n", - " W.HBox([w_ds_pimpo_fi, w_ds_pimpo_ff, w_ds_pimpo_umb]),\n", - " W.HBox([btn_ds_pimpo_run, btn_ds_pimpo_export]), bar_ds_pimpo,\n", - " out_ds_pimpo,\n", - " W.HTML('
Partidas Exportacion
'),\n", - " W.HBox([w_ds_pexpo_fi, w_ds_pexpo_ff, w_ds_pexpo_umb]),\n", - " W.HBox([btn_ds_pexpo_run, btn_ds_pexpo_export]), bar_ds_pexpo,\n", - " out_ds_pexpo,\n", - "])\n", - "\n", - "\n", - "\n", - "\n", - "# ===== Tab Valores: Ajuste de VALORTOTALME / VALORTOTALMN =====\n", - "upload_val = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n", - "btn_plantilla_val = W.Button(description='Descargar plantilla', button_style='info', icon='download', layout={'width':'220px'})\n", - "w_val_modo = W.RadioButtons(\n", - " options=[('Aplicar siempre', 'APLICAR_SIEMPRE'), ('Usar umbral %', 'USAR_UMBRAL')],\n", - " value='APLICAR_SIEMPRE', description='Modo:', layout={'width':'320px'})\n", - "chk_val_shelter = W.Checkbox(value=False,\n", - " description='Buscar en todas las BDs (shelter)', indent=False,\n", - " layout={'width':'380px'})\n", - "w_val_umbral = W.FloatSlider(value=50.0, min=1.0, max=500.0, step=1.0,\n", - " description='Umbral %:', readout_format='.0f', layout={'width':'380px'})\n", - "btn_val_analizar = W.Button(description='Analizar', button_style='primary', icon='search', layout={'width':'180px'})\n", - "btn_val_xlsx = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'200px'})\n", - "bar_val = _mkbar('Valores')\n", - "out_val_log = W.Output(layout=OUT_STYLE)\n", - "out_val_tabla = W.Output(layout=OUT_STYLE)\n", - "\n", - "_html_formato_val = ('
'\n", - " 'Formato esperado del Excel (3 columnas)'\n", - " ''\n", - " ''\n", - " ''\n", - " ''\n", - " ''\n", - " '
ColumnaEjemplo
PEDIMENTO07-3429-4015540
VALOR_ME12345.67
VALOR_MN234567.89
'\n", - " '

Por cada pedimento se buscan sus partidas de exportacion y se prorratean los valores hasta cuadrar al 100%.

'\n", - " '
')\n", - "\n", - "def _on_plantilla_val(_):\n", - " with out_val_log:\n", - " clear_output()\n", - " bts = generar_plantilla_excel_valores()\n", - " path = os.path.join(os.getcwd(), 'plantilla_valores.xlsx')\n", - " with open(path, 'wb') as f: f.write(bts)\n", - " display(HTML(f'
'\n", - " f'Plantilla generada
'\n", - " f'{path}
'))\n", - "btn_plantilla_val.on_click(_on_plantilla_val)\n", - "\n", - "def _on_val_analizar(_):\n", - " with out_val_log:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " if len(upload_val.value) == 0:\n", - " print('ERROR: sube un Excel primero.'); return\n", - " try:\n", - " if isinstance(upload_val.value, dict):\n", - " fname = list(upload_val.value.keys())[0]\n", - " fb = upload_val.value[fname]['content']\n", - " else:\n", - " fb = upload_val.value[0]['content']\n", - " tmp = os.path.join(os.getcwd(), '_upload_valores.xlsx')\n", - " with open(tmp, 'wb') as f: f.write(fb)\n", - " df_excel = cargar_excel_valores(tmp)\n", - " print(f'Excel cargado: {len(df_excel):,} pedimentos')\n", - " except Exception as e:\n", - " print(f'ERROR cargando Excel: {e}'); return\n", - " plan, resumen = analizar_valores(df_excel,\n", - " modo=w_val_modo.value, umbral_pct=float(w_val_umbral.value),\n", - " usar_shelter=bool(chk_val_shelter.value),\n", - " progress=bar_val, log=print)\n", - " _state['val_plan'] = plan\n", - " _state['val_resumen'] = resumen\n", - " if not resumen.empty:\n", - " cnt = resumen['STATUS'].value_counts().to_dict()\n", - " print(f'Resumen: {cnt}')\n", - " with out_val_tabla:\n", - " clear_output()\n", - " if 'val_resumen' in _state and not _state['val_resumen'].empty:\n", - " display(HTML('

Resumen por pedimento

'))\n", - " display(_state['val_resumen'])\n", - " if 'val_plan' in _state and not _state['val_plan'].empty:\n", - " display(HTML('

Plan detalle (primeras 50 filas)

'))\n", - " display(_state['val_plan'].head(50))\n", - "btn_val_analizar.on_click(_on_val_analizar)\n", - "\n", - "def _on_val_xlsx(_):\n", - " with out_val_log:\n", - " if 'val_plan' not in _state:\n", - " display(HTML('
Corre Analizar primero.
')); return\n", - " path = os.path.join(os.getcwd(), 'plan_valores.xlsx')\n", - " exportar_excel_valores(_state['val_plan'], _state['val_resumen'], path)\n", - " display(HTML(f'
'\n", - " f'Excel generado
'\n", - " f'{path}
'))\n", - "btn_val_xlsx.on_click(_on_val_xlsx)\n", - "\n", - "dry_val = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n", - "chk_val_vtmn = W.Checkbox(value=True, description='Aplicar a VALORTOTALMN', indent=False, layout={'width':'280px'})\n", - "chk_val_mptemp = W.Checkbox(value=False, description='Aplicar a ValorMPTempMN', indent=False, layout={'width':'280px'})\n", - "btn_val_ejecutar = W.Button(description='Ejecutar ajuste (Paso B)', button_style='warning', icon='play', layout={'width':'280px'})\n", - "bar_val_b = _mkbar('Ejecucion Valores')\n", - "out_val_ejec = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_val_ejecutar(_):\n", - " with out_val_ejec:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " if 'val_plan' not in _state:\n", - " print('ERROR: corre primero Analizar.'); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " ejecutar_valores(_state['val_plan'], dry_run=dry_val.value,\n", - " aplicar_vtmn=bool(chk_val_vtmn.value),\n", - " aplicar_mptemp=bool(chk_val_mptemp.value),\n", - " log=print, progress=bar_val_b)\n", - "btn_val_ejecutar.on_click(_on_val_ejecutar)\n", - "\n", - "tab_valores = W.VBox([\n", - " W.HTML('

Valores - Ajuste de VALORTOTALME / VALORTOTALMN

'\n", - " '

'\n", - " 'Sube un Excel con los valores ME y MN esperados por pedimento. La herramienta '\n", - " 'busca las partidas de exportacion de cada pedimento y prorratea los valores '\n", - " 'proporcionalmente al actual de cada una hasta cuadrar 100%.

'),\n", - " W.HTML(_html_formato_val),\n", - " btn_plantilla_val,\n", - " W.HTML('

Sube tu Excel:

'),\n", - " W.HBox([upload_val]),\n", - " W.HBox([w_val_modo]),\n", - " W.HBox([chk_val_shelter]),\n", - " W.HBox([w_val_umbral]),\n", - " W.HBox([btn_val_analizar, btn_val_xlsx]), bar_val,\n", - " out_val_log, out_val_tabla,\n", - " W.HTML('

Paso B - Ejecutar UPDATE en SPartidasExpo

'\n", - " '

UPDATE VALORTOTALME y VALORTOTALMN por (FACTURAEXPO, LINEA). '\n", - " 'Corre primero con DRY_RUN activado.

'),\n", - " W.HBox([dry_val]),\n", - " W.HTML('Columnas MN a actualizar:'),\n", - " W.HBox([chk_val_vtmn, chk_val_mptemp]),\n", - " btn_val_ejecutar, bar_val_b,\n", - " out_val_ejec,\n", - "])\n", - "\n", - "\n", - "\n", - "\n", - "# ===== Shelter: buscar pedimentos en todas las BDs =====\n", - "btn_val_shelter = W.Button(description='Buscar pedimentos en todas las BDs',\n", - " button_style='info', icon='search-plus', layout={'width':'320px'})\n", - "bar_val_shelter = _mkbar('Shelter')\n", - "out_val_shelter = W.Output(layout=OUT_STYLE)\n", - "\n", - "def _on_val_shelter(_):\n", - " with out_val_shelter:\n", - " clear_output()\n", - " if not CONEXION_OK: print(CONEXION_MSG); return\n", - " print(f'(DB actual: {DB_ACTUAL})')\n", - " if len(upload_val.value) == 0:\n", - " print('ERROR: sube un Excel primero.'); return\n", - " try:\n", - " if isinstance(upload_val.value, dict):\n", - " fname = list(upload_val.value.keys())[0]\n", - " fb = upload_val.value[fname]['content']\n", - " else:\n", - " fb = upload_val.value[0]['content']\n", - " tmp = os.path.join(os.getcwd(), '_upload_valores.xlsx')\n", - " with open(tmp, 'wb') as f: f.write(fb)\n", - " df_excel = cargar_excel_valores(tmp)\n", - " print(f'Excel: {len(df_excel):,} pedimentos')\n", - " except Exception as e:\n", - " print(f'ERROR Excel: {e}'); return\n", - " reporte, resumen_bd, faltantes = buscar_pedimentos_en_bds(\n", - " df_excel, progress=bar_val_shelter, log=print)\n", - " _state['val_shelter_reporte'] = reporte\n", - " _state['val_shelter_resumen_bd'] = resumen_bd\n", - " _state['val_shelter_faltantes'] = faltantes\n", - " encontrados = len(reporte) - len(faltantes) if not reporte.empty else 0\n", - " print(f'Encontrados: {encontrados} | Faltantes: {len(faltantes)}')\n", - " if not reporte.empty:\n", - " display(HTML('

Donde esta cada pedimento

'))\n", - " display(reporte)\n", - " if not resumen_bd.empty:\n", - " display(HTML('

Resumen por base de datos

'))\n", - " display(resumen_bd)\n", - " if faltantes:\n", - " display(HTML('

No encontrados en ninguna BD

'))\n", - " display(pd.DataFrame({'PEDIMENTO': faltantes}))\n", - "btn_val_shelter.on_click(_on_val_shelter)\n", - "\n", - "tab_valores.children = tuple(list(tab_valores.children) + [\n", - " W.HTML('

Shelter - Buscar pedimentos en otras BDs

'\n", - " '

'\n", - " 'Recorre todas las bases de datos de la instancia SQL Server (las que el usuario '\n", - " 'puede acceder y tienen la tabla SPedimentos.PEDIMENTO) y reporta en '\n", - " 'cual(es) base(s) vive cada pedimento del Excel. Solo es diagnostico no '\n", - " 'modifica nada. Util cuando el cliente tiene varias BDs y no sabes a cual '\n", - " 'apuntar el ajuste.

'),\n", - " W.HBox([btn_val_shelter]), bar_val_shelter,\n", - " out_val_shelter,\n", - "])\n", - "\n", - "\n", - "tabs = W.Tab(children=[tab_conn, tab_desc, tab_an, tab_nlp, tab_kg, tab_ctm, tab_sv, tab_datastage, tab_estructuras, tab_valores])\n", - "tabs.set_title(0, 'Conexion')\n", - "tabs.set_title(1, 'Descargas')\n", - "tabs.set_title(2, 'Analisis Saldos')\n", - "tabs.set_title(3, 'Sustitutos NLP')\n", - "tabs.set_title(4, 'Descarga % KGS')\n", - "tabs.set_title(5, 'CTM')\n", - "tabs.set_title(6, 'Saldos Vencidos')\n", - "tabs.set_title(7, 'DataStage')\n", - "tabs.set_title(8, 'Estructuras SCAII')\n", - "tabs.set_title(9, 'Valores')\n", - "display(header, tabs)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.11" - } + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "o", + "s", + ",", + " ", + "s", + "y", + "s", + ",", + " ", + "t", + "i", + "m", + "e", + ",", + " ", + "d", + "a", + "t", + "e", + "t", + "i", + "m", + "e", + " ", + "a", + "s", + " ", + "_", + "d", + "t", + ",", + " ", + "r", + "e", + ",", + " ", + "w", + "a", + "r", + "n", + "i", + "n", + "g", + "s", + "\n", + "w", + "a", + "r", + "n", + "i", + "n", + "g", + "s", + ".", + "f", + "i", + "l", + "t", + "e", + "r", + "w", + "a", + "r", + "n", + "i", + "n", + "g", + "s", + "(", + "'", + "i", + "g", + "n", + "o", + "r", + "e", + "'", + ")", + "\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "p", + "a", + "n", + "d", + "a", + "s", + " ", + "a", + "s", + " ", + "p", + "d", + "\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "n", + "u", + "m", + "p", + "y", + " ", + "a", + "s", + " ", + "n", + "p", + "\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "p", + "y", + "o", + "d", + "b", + "c", + "\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "m", + "a", + "t", + "p", + "l", + "o", + "t", + "l", + "i", + "b", + ".", + "p", + "y", + "p", + "l", + "o", + "t", + " ", + "a", + "s", + " ", + "p", + "l", + "t", + "\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "m", + "a", + "t", + "p", + "l", + "o", + "t", + "l", + "i", + "b", + ".", + "t", + "i", + "c", + "k", + "e", + "r", + " ", + "a", + "s", + " ", + "m", + "t", + "i", + "c", + "k", + "\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "i", + "p", + "y", + "w", + "i", + "d", + "g", + "e", + "t", + "s", + " ", + "a", + "s", + " ", + "W", + "\n", + "f", + "r", + "o", + "m", + " ", + "I", + "P", + "y", + "t", + "h", + "o", + "n", + ".", + "d", + "i", + "s", + "p", + "l", + "a", + "y", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "d", + "i", + "s", + "p", + "l", + "a", + "y", + ",", + " ", + "c", + "l", + "e", + "a", + "r", + "_", + "o", + "u", + "t", + "p", + "u", + "t", + ",", + " ", + "H", + "T", + "M", + "L", + "\n", + "f", + "r", + "o", + "m", + " ", + "d", + "o", + "t", + "e", + "n", + "v", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "l", + "o", + "a", + "d", + "_", + "d", + "o", + "t", + "e", + "n", + "v", + "\n", + "f", + "o", + "r", + " ", + "e", + "n", + "v", + "_", + "p", + "a", + "t", + "h", + " ", + "i", + "n", + " ", + "[", + "'", + ".", + "e", + "n", + "v", + "'", + ",", + " ", + "'", + ".", + ".", + "/", + ".", + "e", + "n", + "v", + "'", + "]", + ":", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + ".", + "e", + "x", + "i", + "s", + "t", + "s", + "(", + "e", + "n", + "v", + "_", + "p", + "a", + "t", + "h", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "o", + "a", + "d", + "_", + "d", + "o", + "t", + "e", + "n", + "v", + "(", + "e", + "n", + "v", + "_", + "p", + "a", + "t", + "h", + ")", + ";", + " ", + "b", + "r", + "e", + "a", + "k", + "\n", + "S", + "C", + "A", + "I", + "I", + "_", + "S", + "E", + "R", + "V", + "E", + "R", + " ", + " ", + " ", + "=", + " ", + "o", + "s", + ".", + "g", + "e", + "t", + "e", + "n", + "v", + "(", + "'", + "S", + "C", + "A", + "I", + "I", + "_", + "S", + "E", + "R", + "V", + "E", + "R", + "'", + ",", + " ", + "'", + "l", + "o", + "c", + "a", + "l", + "h", + "o", + "s", + "t", + "'", + ")", + "\n", + "S", + "C", + "A", + "I", + "I", + "_", + "D", + "B", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "=", + " ", + "o", + "s", + ".", + "g", + "e", + "t", + "e", + "n", + "v", + "(", + "'", + "S", + "C", + "A", + "I", + "I", + "_", + "D", + "A", + "T", + "A", + "B", + "A", + "S", + "E", + "'", + ",", + " ", + "'", + " ", + "G", + "E", + "N", + "P", + "A", + "C", + "T", + "-", + "C", + "O", + "R", + "R", + "E", + "C", + "C", + "I", + "O", + "N", + "'", + ")", + "\n", + "S", + "C", + "A", + "I", + "I", + "_", + "T", + "R", + "U", + "S", + "T", + "E", + "D", + " ", + " ", + "=", + " ", + "o", + "s", + ".", + "g", + "e", + "t", + "e", + "n", + "v", + "(", + "'", + "S", + "C", + "A", + "I", + "I", + "_", + "T", + "R", + "U", + "S", + "T", + "E", + "D", + "'", + ",", + " ", + "'", + "n", + "o", + "'", + ")", + ".", + "s", + "t", + "r", + "i", + "p", + "(", + ")", + ".", + "l", + "o", + "w", + "e", + "r", + "(", + ")", + " ", + "=", + "=", + " ", + "'", + "y", + "e", + "s", + "'", + "\n", + "S", + "C", + "A", + "I", + "I", + "_", + "U", + "S", + "E", + "R", + " ", + " ", + " ", + " ", + " ", + "=", + " ", + "o", + "s", + ".", + "g", + "e", + "t", + "e", + "n", + "v", + "(", + "'", + "S", + "C", + "A", + "I", + "I", + "_", + "U", + "S", + "E", + "R", + "'", + ",", + " ", + "'", + "s", + "a", + "'", + ")", + "\n", + "S", + "C", + "A", + "I", + "I", + "_", + "P", + "A", + "S", + "S", + "W", + "O", + "R", + "D", + " ", + "=", + " ", + "o", + "s", + ".", + "g", + "e", + "t", + "e", + "n", + "v", + "(", + "'", + "S", + "C", + "A", + "I", + "I", + "_", + "P", + "A", + "S", + "S", + "W", + "O", + "R", + "D", + "'", + ",", + " ", + "'", + "'", + ")", + "\n", + "\n", + "d", + "e", + "f", + " ", + "_", + "m", + "a", + "k", + "e", + "_", + "c", + "o", + "n", + "n", + "_", + "s", + "t", + "r", + "(", + "d", + "b", + "_", + "n", + "a", + "m", + "e", + "=", + "N", + "o", + "n", + "e", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "d", + "b", + "_", + "p", + "a", + "r", + "t", + " ", + "=", + " ", + "f", + "'", + "D", + "A", + "T", + "A", + "B", + "A", + "S", + "E", + "=", + "{", + "{", + "{", + "d", + "b", + "_", + "n", + "a", + "m", + "e", + "}", + "}", + "}", + ";", + "'", + " ", + "i", + "f", + " ", + "d", + "b", + "_", + "n", + "a", + "m", + "e", + " ", + "e", + "l", + "s", + "e", + " ", + "'", + "'", + "\n", + " ", + " ", + " ", + " ", + "a", + "u", + "t", + "h", + " ", + "=", + " ", + "'", + "T", + "r", + "u", + "s", + "t", + "e", + "d", + "_", + "C", + "o", + "n", + "n", + "e", + "c", + "t", + "i", + "o", + "n", + "=", + "y", + "e", + "s", + ";", + "'", + " ", + "i", + "f", + " ", + "S", + "C", + "A", + "I", + "I", + "_", + "T", + "R", + "U", + "S", + "T", + "E", + "D", + " ", + "e", + "l", + "s", + "e", + " ", + "f", + "'", + "U", + "I", + "D", + "=", + "{", + "S", + "C", + "A", + "I", + "I", + "_", + "U", + "S", + "E", + "R", + "}", + ";", + "P", + "W", + "D", + "=", + "{", + "S", + "C", + "A", + "I", + "I", + "_", + "P", + "A", + "S", + "S", + "W", + "O", + "R", + "D", + "}", + ";", + "'", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "(", + "f", + "\"", + "D", + "R", + "I", + "V", + "E", + "R", + "=", + "{", + "{", + "O", + "D", + "B", + "C", + " ", + "D", + "r", + "i", + "v", + "e", + "r", + " ", + "1", + "8", + " ", + "f", + "o", + "r", + " ", + "S", + "Q", + "L", + " ", + "S", + "e", + "r", + "v", + "e", + "r", + "}", + "}", + ";", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "S", + "E", + "R", + "V", + "E", + "R", + "=", + "{", + "S", + "C", + "A", + "I", + "I", + "_", + "S", + "E", + "R", + "V", + "E", + "R", + "}", + ";", + "{", + "d", + "b", + "_", + "p", + "a", + "r", + "t", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "{", + "a", + "u", + "t", + "h", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "E", + "n", + "c", + "r", + "y", + "p", + "t", + "=", + "y", + "e", + "s", + ";", + "T", + "r", + "u", + "s", + "t", + "S", + "e", + "r", + "v", + "e", + "r", + "C", + "e", + "r", + "t", + "i", + "f", + "i", + "c", + "a", + "t", + "e", + "=", + "y", + "e", + "s", + ";", + "M", + "A", + "R", + "S", + "_", + "C", + "o", + "n", + "n", + "e", + "c", + "t", + "i", + "o", + "n", + "=", + "y", + "e", + "s", + ";", + "\"", + ")", + "\n", + "\n", + "s", + "c", + "a", + "i", + "i", + "_", + "c", + "o", + "n", + "n", + " ", + "=", + " ", + "N", + "o", + "n", + "e", + "\n", + "D", + "B", + "_", + "A", + "C", + "T", + "U", + "A", + "L", + " ", + " ", + "=", + " ", + "N", + "o", + "n", + "e", + "\n", + "C", + "O", + "N", + "E", + "X", + "I", + "O", + "N", + "_", + "O", + "K", + " ", + "=", + " ", + "F", + "a", + "l", + "s", + "e", + "\n", + "C", + "O", + "N", + "E", + "X", + "I", + "O", + "N", + "_", + "M", + "S", + "G", + " ", + "=", + " ", + "'", + "'", + "\n", + "\n", + "d", + "e", + "f", + " ", + "c", + "o", + "n", + "e", + "c", + "t", + "a", + "r", + "_", + "a", + "_", + "d", + "b", + "(", + "d", + "b", + "_", + "n", + "a", + "m", + "e", + "=", + "N", + "o", + "n", + "e", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "g", + "l", + "o", + "b", + "a", + "l", + " ", + "s", + "c", + "a", + "i", + "i", + "_", + "c", + "o", + "n", + "n", + ",", + " ", + "D", + "B", + "_", + "A", + "C", + "T", + "U", + "A", + "L", + ",", + " ", + "C", + "O", + "N", + "E", + "X", + "I", + "O", + "N", + "_", + "O", + "K", + ",", + " ", + "C", + "O", + "N", + "E", + "X", + "I", + "O", + "N", + "_", + "M", + "S", + "G", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "s", + "c", + "a", + "i", + "i", + "_", + "c", + "o", + "n", + "n", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "N", + "o", + "n", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + " ", + "s", + "c", + "a", + "i", + "i", + "_", + "c", + "o", + "n", + "n", + ".", + "c", + "l", + "o", + "s", + "e", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "E", + "x", + "c", + "e", + "p", + "t", + "i", + "o", + "n", + ":", + " ", + "p", + "a", + "s", + "s", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "c", + "a", + "i", + "i", + "_", + "c", + "o", + "n", + "n", + " ", + "=", + " ", + "p", + "y", + "o", + "d", + "b", + "c", + ".", + "c", + "o", + "n", + "n", + "e", + "c", + "t", + "(", + "_", + "m", + "a", + "k", + "e", + "_", + "c", + "o", + "n", + "n", + "_", + "s", + "t", + "r", + "(", + "d", + "b", + "_", + "n", + "a", + "m", + "e", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "s", + "c", + "a", + "i", + "i", + "_", + "c", + "o", + "n", + "n", + ".", + "c", + "u", + "r", + "s", + "o", + "r", + "(", + ")", + " ", + "a", + "s", + " ", + "c", + "u", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "c", + "u", + "r", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "e", + "(", + "'", + "S", + "E", + "L", + "E", + "C", + "T", + " ", + "D", + "B", + "_", + "N", + "A", + "M", + "E", + "(", + ")", + "'", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "D", + "B", + "_", + "A", + "C", + "T", + "U", + "A", + "L", + " ", + "=", + " ", + "c", + "u", + "r", + ".", + "f", + "e", + "t", + "c", + "h", + "o", + "n", + "e", + "(", + ")", + "[", + "0", + "]", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "C", + "O", + "N", + "E", + "X", + "I", + "O", + "N", + "_", + "O", + "K", + " ", + "=", + " ", + "T", + "r", + "u", + "e", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "C", + "O", + "N", + "E", + "X", + "I", + "O", + "N", + "_", + "M", + "S", + "G", + " ", + "=", + " ", + "f", + "'", + "C", + "o", + "n", + "e", + "c", + "t", + "a", + "d", + "o", + " ", + "a", + " ", + "[", + "{", + "D", + "B", + "_", + "A", + "C", + "T", + "U", + "A", + "L", + "}", + "]", + " ", + "@", + " ", + "{", + "S", + "C", + "A", + "I", + "I", + "_", + "S", + "E", + "R", + "V", + "E", + "R", + "}", + "'", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "s", + "t", + "a", + "t", + "e", + ".", + "c", + "l", + "e", + "a", + "r", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "T", + "r", + "u", + "e", + "\n", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "E", + "x", + "c", + "e", + "p", + "t", + "i", + "o", + "n", + " ", + "a", + "s", + " ", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "c", + "a", + "i", + "i", + "_", + "c", + "o", + "n", + "n", + " ", + "=", + " ", + "N", + "o", + "n", + "e", + ";", + " ", + "D", + "B", + "_", + "A", + "C", + "T", + "U", + "A", + "L", + " ", + "=", + " ", + "N", + "o", + "n", + "e", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "C", + "O", + "N", + "E", + "X", + "I", + "O", + "N", + "_", + "O", + "K", + " ", + "=", + " ", + "F", + "a", + "l", + "s", + "e", + ";", + " ", + "C", + "O", + "N", + "E", + "X", + "I", + "O", + "N", + "_", + "M", + "S", + "G", + " ", + "=", + " ", + "f", + "'", + "E", + "R", + "R", + "O", + "R", + " ", + "c", + "o", + "n", + "e", + "x", + "i", + "o", + "n", + ":", + " ", + "{", + "e", + "}", + "'", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "F", + "a", + "l", + "s", + "e", + "\n", + "\n", + "d", + "e", + "f", + " ", + "l", + "i", + "s", + "t", + "a", + "r", + "_", + "d", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "s", + "(", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "s", + "c", + "a", + "i", + "i", + "_", + "c", + "o", + "n", + "n", + " ", + "i", + "s", + " ", + "N", + "o", + "n", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "t", + "m", + "p", + " ", + "=", + " ", + "p", + "y", + "o", + "d", + "b", + "c", + ".", + "c", + "o", + "n", + "n", + "e", + "c", + "t", + "(", + "_", + "m", + "a", + "k", + "e", + "_", + "c", + "o", + "n", + "n", + "_", + "s", + "t", + "r", + "(", + "'", + "m", + "a", + "s", + "t", + "e", + "r", + "'", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "E", + "x", + "c", + "e", + "p", + "t", + "i", + "o", + "n", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + " ", + "t", + "m", + "p", + " ", + "=", + " ", + "p", + "y", + "o", + "d", + "b", + "c", + ".", + "c", + "o", + "n", + "n", + "e", + "c", + "t", + "(", + "_", + "m", + "a", + "k", + "e", + "_", + "c", + "o", + "n", + "n", + "_", + "s", + "t", + "r", + "(", + "N", + "o", + "n", + "e", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "E", + "x", + "c", + "e", + "p", + "t", + "i", + "o", + "n", + ":", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "[", + "]", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "d", + "f", + " ", + "=", + " ", + "p", + "d", + ".", + "r", + "e", + "a", + "d", + "_", + "s", + "q", + "l", + "(", + "\"", + "S", + "E", + "L", + "E", + "C", + "T", + " ", + "n", + "a", + "m", + "e", + " ", + "F", + "R", + "O", + "M", + " ", + "s", + "y", + "s", + ".", + "d", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "s", + " ", + "W", + "H", + "E", + "R", + "E", + " ", + "d", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "_", + "i", + "d", + " ", + ">", + " ", + "4", + " ", + "A", + "N", + "D", + " ", + "s", + "t", + "a", + "t", + "e", + " ", + "=", + " ", + "0", + " ", + "O", + "R", + "D", + "E", + "R", + " ", + "B", + "Y", + " ", + "n", + "a", + "m", + "e", + "\"", + ",", + " ", + "t", + "m", + "p", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "t", + "m", + "p", + ".", + "c", + "l", + "o", + "s", + "e", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "d", + "f", + "[", + "'", + "n", + "a", + "m", + "e", + "'", + "]", + ".", + "t", + "o", + "l", + "i", + "s", + "t", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "E", + "x", + "c", + "e", + "p", + "t", + "i", + "o", + "n", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + " ", + "t", + "m", + "p", + ".", + "c", + "l", + "o", + "s", + "e", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + ":", + " ", + "p", + "a", + "s", + "s", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "[", + "]", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "d", + "f", + " ", + "=", + " ", + "p", + "d", + ".", + "r", + "e", + "a", + "d", + "_", + "s", + "q", + "l", + "(", + "\"", + "S", + "E", + "L", + "E", + "C", + "T", + " ", + "n", + "a", + "m", + "e", + " ", + "F", + "R", + "O", + "M", + " ", + "s", + "y", + "s", + ".", + "d", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "s", + " ", + "W", + "H", + "E", + "R", + "E", + " ", + "d", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "_", + "i", + "d", + " ", + ">", + " ", + "4", + " ", + "A", + "N", + "D", + " ", + "s", + "t", + "a", + "t", + "e", + " ", + "=", + " ", + "0", + " ", + "O", + "R", + "D", + "E", + "R", + " ", + "B", + "Y", + " ", + "n", + "a", + "m", + "e", + "\"", + ",", + " ", + "s", + "c", + "a", + "i", + "i", + "_", + "c", + "o", + "n", + "n", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "d", + "f", + "[", + "'", + "n", + "a", + "m", + "e", + "'", + "]", + ".", + "t", + "o", + "l", + "i", + "s", + "t", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "E", + "x", + "c", + "e", + "p", + "t", + "i", + "o", + "n", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "[", + "]", + "\n", + "\n", + "E", + "P", + "O", + "C", + "H", + "_", + "C", + "L", + "A", + "R", + "I", + "O", + "N", + " ", + "=", + " ", + "_", + "d", + "t", + ".", + "d", + "a", + "t", + "e", + "(", + "1", + "8", + "0", + "1", + ",", + " ", + "1", + ",", + " ", + "1", + ")", + "\n", + "d", + "e", + "f", + " ", + "t", + "o", + "_", + "c", + "l", + "a", + "r", + "i", + "o", + "n", + "(", + "d", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "d", + " ", + "i", + "s", + " ", + "N", + "o", + "n", + "e", + " ", + "o", + "r", + " ", + "p", + "d", + ".", + "i", + "s", + "n", + "a", + "(", + "d", + ")", + ":", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "N", + "o", + "n", + "e", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "i", + "s", + "i", + "n", + "s", + "t", + "a", + "n", + "c", + "e", + "(", + "d", + ",", + " ", + "s", + "t", + "r", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + " ", + "d", + " ", + "=", + " ", + "p", + "d", + ".", + "t", + "o", + "_", + "d", + "a", + "t", + "e", + "t", + "i", + "m", + "e", + "(", + "d", + ")", + ".", + "d", + "a", + "t", + "e", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "E", + "x", + "c", + "e", + "p", + "t", + "i", + "o", + "n", + ":", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "N", + "o", + "n", + "e", + "\n", + " ", + " ", + " ", + " ", + "e", + "l", + "i", + "f", + " ", + "i", + "s", + "i", + "n", + "s", + "t", + "a", + "n", + "c", + "e", + "(", + "d", + ",", + " ", + "p", + "d", + ".", + "T", + "i", + "m", + "e", + "s", + "t", + "a", + "m", + "p", + ")", + ":", + " ", + "d", + " ", + "=", + " ", + "d", + ".", + "d", + "a", + "t", + "e", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "e", + "l", + "i", + "f", + " ", + "i", + "s", + "i", + "n", + "s", + "t", + "a", + "n", + "c", + "e", + "(", + "d", + ",", + " ", + "_", + "d", + "t", + ".", + "d", + "a", + "t", + "e", + "t", + "i", + "m", + "e", + ")", + ":", + " ", + "d", + " ", + "=", + " ", + "d", + ".", + "d", + "a", + "t", + "e", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "(", + "d", + " ", + "-", + " ", + "E", + "P", + "O", + "C", + "H", + "_", + "C", + "L", + "A", + "R", + "I", + "O", + "N", + ")", + ".", + "d", + "a", + "y", + "s", + " ", + "+", + " ", + "4", + "\n", + "\n", + "_", + "s", + "t", + "a", + "t", + "e", + " ", + "=", + " ", + "{", + "}", + "\n", + "\n", + "#", + " ", + "=", + "=", + "=", + " ", + "H", + "e", + "l", + "p", + "e", + "r", + " ", + "d", + "e", + " ", + "p", + "r", + "o", + "g", + "r", + "e", + "s", + "o", + " ", + "=", + "=", + "=", + "\n", + "c", + "l", + "a", + "s", + "s", + " ", + "_", + "P", + "r", + "o", + "g", + "r", + "e", + "s", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + "\"", + "\"", + "\"", + "W", + "r", + "a", + "p", + "p", + "e", + "r", + " ", + "d", + "e", + " ", + "I", + "n", + "t", + "P", + "r", + "o", + "g", + "r", + "e", + "s", + "s", + ".", + " ", + "S", + "i", + " ", + "e", + "l", + " ", + "w", + "i", + "d", + "g", + "e", + "t", + " ", + "e", + "s", + " ", + "N", + "o", + "n", + "e", + ",", + " ", + "l", + "o", + "s", + " ", + "m", + "e", + "t", + "o", + "d", + "o", + "s", + " ", + "s", + "o", + "n", + " ", + "n", + "o", + "-", + "o", + "p", + ".", + "\"", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "_", + "_", + "i", + "n", + "i", + "t", + "_", + "_", + "(", + "s", + "e", + "l", + "f", + ",", + " ", + "w", + "i", + "d", + "g", + "e", + "t", + "=", + "N", + "o", + "n", + "e", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + " ", + "=", + " ", + "w", + "i", + "d", + "g", + "e", + "t", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "s", + "e", + "t", + "u", + "p", + "(", + "s", + "e", + "l", + "f", + ",", + " ", + "t", + "o", + "t", + "a", + "l", + ",", + " ", + "d", + "e", + "s", + "c", + "=", + "'", + "'", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + " ", + "i", + "s", + " ", + "N", + "o", + "n", + "e", + ":", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "m", + "i", + "n", + " ", + "=", + " ", + "0", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "m", + "a", + "x", + " ", + "=", + " ", + "m", + "a", + "x", + "(", + "1", + ",", + " ", + "i", + "n", + "t", + "(", + "t", + "o", + "t", + "a", + "l", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "v", + "a", + "l", + "u", + "e", + " ", + "=", + " ", + "0", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "b", + "a", + "r", + "_", + "s", + "t", + "y", + "l", + "e", + " ", + "=", + " ", + "'", + "i", + "n", + "f", + "o", + "'", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "d", + "e", + "s", + "c", + "r", + "i", + "p", + "t", + "i", + "o", + "n", + " ", + "=", + " ", + "(", + "d", + "e", + "s", + "c", + " ", + "o", + "r", + " ", + "'", + "'", + ")", + "[", + ":", + "4", + "0", + "]", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "s", + "t", + "e", + "p", + "(", + "s", + "e", + "l", + "f", + ",", + " ", + "n", + "=", + "1", + ",", + " ", + "d", + "e", + "s", + "c", + "=", + "N", + "o", + "n", + "e", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + " ", + "i", + "s", + " ", + "N", + "o", + "n", + "e", + ":", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "v", + "a", + "l", + "u", + "e", + " ", + "=", + " ", + "m", + "i", + "n", + "(", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "v", + "a", + "l", + "u", + "e", + " ", + "+", + " ", + "n", + ",", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "m", + "a", + "x", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "E", + "x", + "c", + "e", + "p", + "t", + "i", + "o", + "n", + ":", + " ", + "p", + "a", + "s", + "s", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "d", + "e", + "s", + "c", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "N", + "o", + "n", + "e", + ":", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "d", + "e", + "s", + "c", + "r", + "i", + "p", + "t", + "i", + "o", + "n", + " ", + "=", + " ", + "d", + "e", + "s", + "c", + "[", + ":", + "4", + "0", + "]", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "d", + "o", + "n", + "e", + "(", + "s", + "e", + "l", + "f", + ",", + " ", + "d", + "e", + "s", + "c", + "=", + "'", + "L", + "i", + "s", + "t", + "o", + "'", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + " ", + "i", + "s", + " ", + "N", + "o", + "n", + "e", + ":", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "v", + "a", + "l", + "u", + "e", + " ", + "=", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "m", + "a", + "x", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "b", + "a", + "r", + "_", + "s", + "t", + "y", + "l", + "e", + " ", + "=", + " ", + "'", + "s", + "u", + "c", + "c", + "e", + "s", + "s", + "'", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "d", + "e", + "s", + "c", + "r", + "i", + "p", + "t", + "i", + "o", + "n", + " ", + "=", + " ", + "d", + "e", + "s", + "c", + "[", + ":", + "4", + "0", + "]", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "e", + "r", + "r", + "o", + "r", + "(", + "s", + "e", + "l", + "f", + ",", + " ", + "d", + "e", + "s", + "c", + "=", + "'", + "E", + "r", + "r", + "o", + "r", + "'", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + " ", + "i", + "s", + " ", + "N", + "o", + "n", + "e", + ":", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "b", + "a", + "r", + "_", + "s", + "t", + "y", + "l", + "e", + " ", + "=", + " ", + "'", + "d", + "a", + "n", + "g", + "e", + "r", + "'", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "w", + ".", + "d", + "e", + "s", + "c", + "r", + "i", + "p", + "t", + "i", + "o", + "n", + " ", + "=", + " ", + "d", + "e", + "s", + "c", + "[", + ":", + "4", + "0", + "]", + "\n", + "\n", + "c", + "o", + "n", + "e", + "c", + "t", + "a", + "r", + "_", + "a", + "_", + "d", + "b", + "(", + "S", + "C", + "A", + "I", + "I", + "_", + "D", + "B", + ")", + "\n", + "#", + " ", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "\n", + "#", + " ", + "D", + "a", + "t", + "a", + "S", + "t", + "a", + "g", + "e", + " ", + "-", + " ", + "S", + "Q", + "L", + "i", + "t", + "e", + " ", + "l", + "o", + "c", + "a", + "l", + " ", + "(", + "s", + "u", + "s", + "t", + "i", + "t", + "u", + "y", + "e", + " ", + "P", + "o", + "s", + "t", + "g", + "r", + "e", + "s", + " ", + "e", + "n", + " ", + "D", + "o", + "c", + "k", + "e", + "r", + ")", + "\n", + "#", + " ", + "E", + "l", + " ", + "a", + "r", + "c", + "h", + "i", + "v", + "o", + " ", + ".", + "d", + "b", + " ", + "s", + "e", + " ", + "c", + "r", + "e", + "a", + " ", + "j", + "u", + "n", + "t", + "o", + " ", + "a", + "l", + " ", + ".", + "e", + "x", + "e", + " ", + "e", + "n", + " ", + "e", + "l", + " ", + "p", + "r", + "i", + "m", + "e", + "r", + " ", + "a", + "r", + "r", + "a", + "n", + "q", + "u", + "e", + ".", + "\n", + "#", + " ", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "=", + "\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "s", + "q", + "l", + "i", + "t", + "e", + "3", + " ", + "a", + "s", + " ", + "_", + "s", + "q", + "l", + "i", + "t", + "e", + "3", + "\n", + "f", + "r", + "o", + "m", + " ", + "s", + "q", + "l", + "a", + "l", + "c", + "h", + "e", + "m", + "y", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "c", + "r", + "e", + "a", + "t", + "e", + "_", + "e", + "n", + "g", + "i", + "n", + "e", + " ", + "a", + "s", + " ", + "_", + "c", + "r", + "e", + "a", + "t", + "e", + "_", + "e", + "n", + "g", + "i", + "n", + "e", + ",", + " ", + "t", + "e", + "x", + "t", + " ", + "a", + "s", + " ", + "_", + "s", + "a", + "_", + "t", + "e", + "x", + "t", + "\n", + "\n", + "\n", + "d", + "e", + "f", + " ", + "_", + "d", + "a", + "t", + "a", + "s", + "t", + "a", + "g", + "e", + "_", + "d", + "b", + "_", + "p", + "a", + "t", + "h", + "(", + ")", + " ", + "-", + ">", + " ", + "s", + "t", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "g", + "e", + "t", + "a", + "t", + "t", + "r", + "(", + "s", + "y", + "s", + ",", + " ", + "'", + "f", + "r", + "o", + "z", + "e", + "n", + "'", + ",", + " ", + "F", + "a", + "l", + "s", + "e", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "b", + "a", + "s", + "e", + " ", + "=", + " ", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + ".", + "d", + "i", + "r", + "n", + "a", + "m", + "e", + "(", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ")", + "\n", + " ", + " ", + " ", + " ", + "e", + "l", + "s", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "b", + "a", + "s", + "e", + " ", + "=", + " ", + "o", + "s", + ".", + "g", + "e", + "t", + "c", + "w", + "d", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + ".", + "j", + "o", + "i", + "n", + "(", + "b", + "a", + "s", + "e", + ",", + " ", + "'", + "d", + "a", + "t", + "a", + "s", + "t", + "a", + "g", + "e", + ".", + "d", + "b", + "'", + ")", + "\n", + "\n", + "\n", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "D", + "B", + " ", + " ", + " ", + "=", + " ", + "o", + "s", + ".", + "g", + "e", + "t", + "e", + "n", + "v", + "(", + "'", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "D", + "B", + "'", + ",", + " ", + "_", + "d", + "a", + "t", + "a", + "s", + "t", + "a", + "g", + "e", + "_", + "d", + "b", + "_", + "p", + "a", + "t", + "h", + "(", + ")", + ")", + "\n", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "R", + "O", + "O", + "T", + " ", + "=", + " ", + "o", + "s", + ".", + "g", + "e", + "t", + "e", + "n", + "v", + "(", + "'", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "R", + "O", + "O", + "T", + "'", + ",", + " ", + "'", + "'", + ")", + "\n", + "\n", + "#", + " ", + "S", + "c", + "h", + "e", + "m", + "a", + " ", + "e", + "m", + "b", + "e", + "b", + "i", + "d", + "o", + ":", + " ", + "2", + "7", + " ", + "t", + "a", + "b", + "l", + "a", + "s", + " ", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "<", + "N", + "N", + "N", + ">", + " ", + "+", + " ", + "b", + "a", + "s", + "e", + "_", + "n", + "u", + "m", + "p", + "a", + "r", + "t", + "e", + "s", + ".", + "\n", + "#", + " ", + "A", + "u", + "t", + "o", + "-", + "g", + "e", + "n", + "e", + "r", + "a", + "d", + "o", + " ", + "d", + "e", + "s", + "d", + "e", + " ", + "s", + "c", + "h", + "e", + "m", + "a", + "_", + "r", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + ".", + "s", + "q", + "l", + " ", + "(", + "P", + "o", + "s", + "t", + "g", + "r", + "e", + "s", + ")", + " ", + "c", + "o", + "n", + "v", + "e", + "r", + "t", + "i", + "d", + "o", + " ", + "a", + " ", + "S", + "Q", + "L", + "i", + "t", + "e", + ".", + "\n", + "_", + "S", + "C", + "H", + "E", + "M", + "A", + "_", + "S", + "Q", + "L", + "I", + "T", + "E", + " ", + "=", + " ", + "\"", + "\"", + "\"", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "0", + "1", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "O", + "p", + "e", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "D", + "o", + "c", + "u", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "E", + "n", + "t", + "r", + "a", + "d", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "u", + "r", + "p", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "y", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "R", + "f", + "c", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "u", + "r", + "p", + "A", + "g", + "e", + "n", + "t", + "e", + "A", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "C", + "a", + "m", + "b", + "i", + "o", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "o", + "t", + "a", + "l", + "F", + "l", + "e", + "t", + "e", + "s", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "o", + "t", + "a", + "l", + "S", + "e", + "g", + "u", + "r", + "o", + "s", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "o", + "t", + "a", + "l", + "E", + "m", + "b", + "a", + "l", + "a", + "j", + "e", + "s", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "o", + "t", + "a", + "l", + "I", + "n", + "c", + "r", + "e", + "m", + "e", + "n", + "t", + "a", + "b", + "l", + "e", + "s", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "o", + "t", + "a", + "l", + "D", + "e", + "d", + "u", + "c", + "i", + "b", + "l", + "e", + "s", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "s", + "o", + "B", + "r", + "u", + "t", + "o", + "M", + "e", + "r", + "c", + "a", + "n", + "c", + "i", + "a", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "e", + "d", + "i", + "o", + "T", + "r", + "a", + "n", + "s", + "p", + "o", + "r", + "t", + "e", + "S", + "a", + "l", + "i", + "d", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "e", + "d", + "i", + "o", + "T", + "r", + "a", + "n", + "s", + "p", + "o", + "r", + "t", + "e", + "A", + "r", + "r", + "i", + "b", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "e", + "d", + "i", + "o", + "T", + "r", + "a", + "n", + "s", + "p", + "o", + "r", + "t", + "e", + "E", + "n", + "t", + "r", + "a", + "d", + "a", + "_", + "S", + "a", + "l", + "i", + "d", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "D", + "e", + "s", + "t", + "i", + "n", + "o", + "M", + "e", + "r", + "c", + "a", + "n", + "c", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "o", + "m", + "b", + "r", + "e", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "y", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "a", + "l", + "l", + "e", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "y", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "I", + "n", + "t", + "e", + "r", + "i", + "o", + "r", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "y", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "E", + "x", + "t", + "e", + "r", + "i", + "o", + "r", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "y", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "P", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "y", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "u", + "n", + "i", + "c", + "i", + "p", + "i", + "o", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "y", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "E", + "n", + "t", + "i", + "d", + "a", + "d", + "F", + "e", + "d", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "y", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "i", + "s", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "y", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "R", + "e", + "c", + "e", + "p", + "c", + "i", + "o", + "n", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "0", + "2", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "R", + "f", + "c", + "T", + "r", + "a", + "n", + "s", + "p", + "o", + "r", + "t", + "i", + "s", + "t", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "u", + "r", + "p", + "T", + "r", + "a", + "n", + "s", + "p", + "o", + "r", + "t", + "i", + "s", + "t", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "o", + "m", + "b", + "r", + "e", + "T", + "r", + "a", + "n", + "s", + "p", + "o", + "r", + "t", + "i", + "s", + "t", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "i", + "s", + "T", + "r", + "a", + "n", + "s", + "p", + "o", + "r", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "d", + "e", + "n", + "t", + "i", + "f", + "i", + "c", + "a", + "d", + "o", + "r", + "T", + "r", + "a", + "n", + "s", + "p", + "o", + "r", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "0", + "3", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "e", + "r", + "o", + "G", + "u", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "G", + "u", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "0", + "4", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "C", + "o", + "n", + "t", + "e", + "n", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "C", + "o", + "n", + "t", + "e", + "n", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "0", + "5", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "F", + "a", + "c", + "t", + "u", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "e", + "r", + "o", + "F", + "a", + "c", + "t", + "u", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "e", + "r", + "m", + "i", + "n", + "o", + "F", + "a", + "c", + "t", + "u", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "o", + "n", + "e", + "d", + "a", + "F", + "a", + "c", + "t", + "u", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "a", + "l", + "o", + "r", + "D", + "o", + "l", + "a", + "r", + "e", + "s", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "a", + "l", + "o", + "r", + "M", + "o", + "n", + "e", + "d", + "a", + "E", + "x", + "t", + "r", + "a", + "n", + "j", + "e", + "r", + "a", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "i", + "s", + "F", + "a", + "c", + "t", + "u", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "E", + "n", + "t", + "i", + "d", + "a", + "d", + "F", + "e", + "d", + "F", + "a", + "c", + "t", + "u", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "n", + "d", + "e", + "n", + "t", + "F", + "i", + "s", + "c", + "a", + "l", + "P", + "r", + "o", + "v", + "e", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "r", + "o", + "v", + "e", + "e", + "d", + "o", + "r", + "M", + "e", + "r", + "c", + "a", + "n", + "c", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "a", + "l", + "l", + "e", + "P", + "r", + "o", + "v", + "e", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "I", + "n", + "t", + "e", + "r", + "i", + "o", + "r", + "P", + "r", + "o", + "v", + "e", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "E", + "x", + "t", + "e", + "r", + "i", + "o", + "r", + "P", + "r", + "o", + "v", + "e", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "p", + "P", + "r", + "o", + "v", + "e", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "u", + "n", + "i", + "c", + "i", + "p", + "i", + "o", + "P", + "r", + "o", + "v", + "e", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "0", + "6", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "F", + "e", + "c", + "h", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "O", + "p", + "e", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "V", + "a", + "l", + "i", + "d", + "a", + "c", + "i", + "o", + "n", + "P", + "a", + "g", + "o", + "R", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "0", + "7", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "C", + "a", + "s", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "d", + "e", + "n", + "t", + "i", + "f", + "i", + "c", + "a", + "d", + "o", + "r", + "C", + "a", + "s", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "o", + "m", + "p", + "l", + "e", + "m", + "e", + "n", + "t", + "o", + "C", + "a", + "s", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "V", + "a", + "l", + "i", + "d", + "a", + "c", + "i", + "o", + "n", + "P", + "a", + "g", + "o", + "R", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "0", + "8", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "n", + "s", + "t", + "i", + "t", + "u", + "c", + "i", + "o", + "n", + "E", + "m", + "i", + "s", + "o", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "e", + "r", + "o", + "C", + "u", + "e", + "n", + "t", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "o", + "l", + "i", + "o", + "C", + "o", + "n", + "s", + "t", + "a", + "n", + "c", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "C", + "o", + "n", + "s", + "t", + "a", + "n", + "c", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "C", + "u", + "e", + "n", + "t", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "G", + "a", + "r", + "a", + "n", + "t", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "a", + "l", + "o", + "r", + "U", + "n", + "i", + "t", + "a", + "r", + "i", + "o", + "T", + "i", + "t", + "u", + "l", + "o", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "o", + "t", + "a", + "l", + "G", + "a", + "r", + "a", + "n", + "t", + "i", + "a", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "a", + "n", + "t", + "i", + "d", + "a", + "d", + "U", + "n", + "i", + "d", + "a", + "d", + "e", + "s", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "t", + "u", + "l", + "o", + "s", + "A", + "s", + "i", + "g", + "n", + "a", + "d", + "o", + "s", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "0", + "9", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "a", + "s", + "a", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "c", + "i", + "o", + "n", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "T", + "a", + "s", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "1", + "0", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "o", + "r", + "m", + "a", + "P", + "a", + "g", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "m", + "p", + "o", + "r", + "t", + "e", + "P", + "a", + "g", + "o", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "1", + "1", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "O", + "b", + "s", + "e", + "r", + "v", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "O", + "b", + "s", + "e", + "r", + "v", + "a", + "c", + "i", + "o", + "n", + "e", + "s", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "V", + "a", + "l", + "i", + "d", + "a", + "c", + "i", + "o", + "n", + "P", + "a", + "g", + "o", + "R", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "1", + "2", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "A", + "d", + "u", + "a", + "n", + "a", + "l", + "O", + "r", + "i", + "g", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "O", + "r", + "i", + "g", + "i", + "n", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "D", + "e", + "s", + "p", + "O", + "r", + "i", + "g", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "D", + "o", + "c", + "u", + "m", + "e", + "n", + "t", + "o", + "O", + "r", + "i", + "g", + "i", + "n", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "O", + "p", + "e", + "r", + "a", + "c", + "i", + "o", + "n", + "O", + "r", + "i", + "g", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "O", + "r", + "i", + "g", + "i", + "n", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "U", + "n", + "i", + "d", + "a", + "d", + "M", + "e", + "d", + "i", + "d", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "e", + "r", + "c", + "a", + "n", + "c", + "i", + "a", + "D", + "e", + "s", + "c", + "a", + "r", + "g", + "a", + "d", + "a", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "2", + "0", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "n", + "d", + "e", + "n", + "t", + "F", + "i", + "s", + "c", + "a", + "l", + "D", + "e", + "s", + "t", + "i", + "n", + "a", + "t", + "a", + "r", + "i", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "o", + "m", + "b", + "r", + "e", + "D", + "e", + "s", + "t", + "i", + "n", + "a", + "t", + "a", + "r", + "i", + "o", + "M", + "e", + "r", + "c", + "a", + "n", + "c", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "a", + "l", + "l", + "e", + "D", + "e", + "s", + "t", + "i", + "n", + "a", + "t", + "a", + "r", + "i", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "I", + "n", + "t", + "e", + "r", + "i", + "o", + "r", + "D", + "e", + "s", + "t", + "i", + "n", + "a", + "t", + "a", + "r", + "i", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "E", + "x", + "t", + "e", + "r", + "i", + "o", + "r", + "D", + "e", + "s", + "t", + "i", + "n", + "a", + "t", + "a", + "r", + "i", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "p", + "D", + "e", + "s", + "t", + "i", + "n", + "a", + "t", + "a", + "r", + "i", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "u", + "n", + "i", + "c", + "p", + "i", + "o", + "D", + "e", + "s", + "t", + "i", + "n", + "a", + "t", + "a", + "r", + "i", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "i", + "s", + "D", + "e", + "s", + "t", + "i", + "n", + "a", + "t", + "a", + "r", + "i", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "5", + "1", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "u", + "b", + "d", + "i", + "v", + "i", + "s", + "i", + "o", + "n", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "D", + "e", + "s", + "c", + "r", + "i", + "p", + "c", + "i", + "o", + "n", + "M", + "e", + "r", + "c", + "a", + "n", + "c", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "r", + "e", + "c", + "i", + "o", + "U", + "n", + "i", + "t", + "a", + "r", + "i", + "o", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "a", + "l", + "o", + "r", + "A", + "d", + "u", + "a", + "n", + "a", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "a", + "l", + "o", + "r", + "C", + "o", + "m", + "e", + "r", + "c", + "i", + "a", + "l", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "a", + "l", + "o", + "r", + "D", + "o", + "l", + "a", + "r", + "e", + "s", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "a", + "n", + "t", + "i", + "d", + "a", + "d", + "U", + "M", + "C", + "o", + "m", + "e", + "r", + "c", + "i", + "a", + "l", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "U", + "n", + "i", + "d", + "a", + "d", + "M", + "e", + "d", + "i", + "d", + "a", + "C", + "o", + "m", + "e", + "r", + "c", + "i", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "a", + "n", + "t", + "i", + "d", + "a", + "d", + "U", + "M", + "T", + "a", + "r", + "i", + "f", + "a", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "U", + "n", + "i", + "d", + "a", + "d", + "M", + "e", + "d", + "i", + "d", + "a", + "T", + "a", + "r", + "i", + "f", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "a", + "l", + "o", + "r", + "A", + "g", + "r", + "e", + "g", + "a", + "d", + "o", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "V", + "i", + "n", + "c", + "u", + "l", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "e", + "t", + "o", + "d", + "o", + "V", + "a", + "l", + "o", + "r", + "i", + "z", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "o", + "d", + "i", + "g", + "o", + "M", + "e", + "r", + "c", + "a", + "n", + "c", + "i", + "a", + "P", + "r", + "o", + "d", + "u", + "c", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "a", + "r", + "c", + "a", + "M", + "e", + "r", + "c", + "a", + "n", + "c", + "i", + "a", + "P", + "r", + "o", + "d", + "u", + "c", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "o", + "d", + "e", + "l", + "o", + "M", + "e", + "r", + "c", + "a", + "n", + "c", + "i", + "a", + "P", + "r", + "o", + "d", + "u", + "c", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "i", + "s", + "O", + "r", + "i", + "g", + "e", + "n", + "D", + "e", + "s", + "t", + "i", + "n", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "i", + "s", + "C", + "o", + "m", + "p", + "r", + "a", + "d", + "o", + "r", + "V", + "e", + "n", + "d", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "E", + "n", + "t", + "i", + "d", + "a", + "d", + "F", + "e", + "d", + "O", + "r", + "i", + "g", + "e", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "E", + "n", + "t", + "i", + "d", + "a", + "d", + "F", + "e", + "d", + "D", + "e", + "s", + "t", + "i", + "n", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "E", + "n", + "t", + "i", + "d", + "a", + "d", + "F", + "e", + "d", + "C", + "o", + "m", + "p", + "r", + "a", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "E", + "n", + "t", + "i", + "d", + "a", + "d", + "F", + "e", + "d", + "V", + "e", + "n", + "d", + "e", + "d", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "O", + "p", + "e", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "D", + "o", + "c", + "u", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "5", + "2", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "i", + "n", + "N", + "u", + "m", + "e", + "r", + "o", + "S", + "e", + "r", + "i", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "K", + "i", + "l", + "o", + "m", + "e", + "t", + "r", + "a", + "j", + "e", + "V", + "e", + "h", + "i", + "c", + "u", + "l", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "5", + "3", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "P", + "e", + "r", + "m", + "i", + "s", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "i", + "r", + "m", + "a", + "D", + "e", + "s", + "c", + "a", + "r", + "g", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "e", + "r", + "o", + "P", + "e", + "r", + "m", + "i", + "s", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "a", + "l", + "o", + "r", + "C", + "o", + "m", + "e", + "r", + "c", + "i", + "a", + "l", + "D", + "o", + "l", + "a", + "r", + "e", + "s", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "a", + "n", + "t", + "i", + "d", + "a", + "d", + "M", + "U", + "M", + "T", + "a", + "r", + "i", + "f", + "a", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "5", + "4", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "C", + "a", + "s", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "d", + "e", + "n", + "t", + "i", + "f", + "i", + "c", + "a", + "d", + "o", + "r", + "C", + "a", + "s", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "o", + "m", + "p", + "l", + "e", + "m", + "e", + "n", + "t", + "o", + "C", + "a", + "s", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "5", + "5", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "n", + "s", + "t", + "i", + "t", + "u", + "c", + "i", + "o", + "n", + "E", + "m", + "i", + "s", + "o", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "e", + "r", + "o", + "C", + "u", + "e", + "n", + "t", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "o", + "l", + "i", + "o", + "C", + "o", + "n", + "s", + "t", + "a", + "n", + "c", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "C", + "o", + "n", + "s", + "t", + "a", + "n", + "c", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "G", + "a", + "r", + "a", + "n", + "t", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "V", + "a", + "l", + "o", + "r", + "U", + "n", + "i", + "t", + "a", + "r", + "i", + "o", + "T", + "i", + "t", + "u", + "l", + "o", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "o", + "t", + "a", + "l", + "G", + "a", + "r", + "a", + "n", + "t", + "i", + "a", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "a", + "n", + "t", + "i", + "d", + "a", + "d", + "U", + "n", + "i", + "d", + "a", + "d", + "e", + "s", + "M", + "e", + "d", + "i", + "d", + "a", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "t", + "u", + "l", + "o", + "s", + "A", + "s", + "i", + "g", + "n", + "a", + "d", + "o", + "s", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "5", + "6", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "a", + "s", + "a", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "c", + "i", + "o", + "n", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "T", + "a", + "s", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "5", + "7", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "o", + "r", + "m", + "a", + "P", + "a", + "g", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "m", + "p", + "o", + "r", + "t", + "e", + "P", + "a", + "g", + "o", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "5", + "5", + "8", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "O", + "b", + "s", + "e", + "r", + "v", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "O", + "b", + "s", + "e", + "r", + "v", + "a", + "c", + "i", + "o", + "n", + "e", + "s", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "7", + "0", + "1", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "D", + "o", + "c", + "u", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "A", + "n", + "t", + "e", + "r", + "i", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "A", + "n", + "t", + "e", + "r", + "i", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "A", + "n", + "t", + "e", + "r", + "i", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "D", + "o", + "c", + "u", + "m", + "e", + "n", + "t", + "o", + "A", + "n", + "t", + "e", + "r", + "i", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "O", + "p", + "e", + "r", + "a", + "c", + "i", + "o", + "n", + "A", + "n", + "t", + "e", + "r", + "i", + "o", + "r", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "O", + "r", + "i", + "g", + "i", + "n", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "A", + "d", + "u", + "a", + "n", + "a", + "l", + "O", + "r", + "i", + "g", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "D", + "e", + "s", + "p", + "O", + "r", + "i", + "g", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "7", + "0", + "2", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "o", + "r", + "m", + "a", + "P", + "a", + "g", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "I", + "m", + "p", + "o", + "r", + "t", + "e", + "P", + "a", + "g", + "o", + "\"", + " ", + "N", + "U", + "M", + "E", + "R", + "I", + "C", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "P", + "a", + "g", + "o", + "R", + "e", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "I", + "n", + "c", + "i", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "o", + "n", + "s", + "e", + "c", + "u", + "t", + "i", + "v", + "o", + "R", + "e", + "m", + "e", + "s", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "e", + "r", + "o", + "S", + "e", + "l", + "e", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "I", + "n", + "i", + "c", + "i", + "o", + "R", + "e", + "c", + "o", + "n", + "o", + "c", + "i", + "m", + "i", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "H", + "o", + "r", + "a", + "I", + "n", + "i", + "c", + "i", + "o", + "R", + "e", + "c", + "o", + "n", + "o", + "c", + "i", + "m", + "i", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "F", + "i", + "n", + "R", + "e", + "c", + "o", + "n", + "o", + "c", + "i", + "m", + "i", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "H", + "o", + "r", + "a", + "F", + "i", + "n", + "R", + "e", + "c", + "o", + "n", + "o", + "c", + "i", + "m", + "i", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "u", + "e", + "n", + "c", + "i", + "a", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "D", + "o", + "c", + "u", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "O", + "p", + "e", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "G", + "r", + "a", + "d", + "o", + "I", + "n", + "c", + "i", + "d", + "e", + "n", + "c", + "i", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "S", + "e", + "l", + "e", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "R", + "e", + "s", + "u", + "m", + "e", + "n", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "o", + "l", + "i", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "R", + "F", + "C", + "o", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "A", + "d", + "u", + "a", + "n", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "_", + "I", + "n", + "i", + "c", + "i", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "_", + "F", + "i", + "n", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "_", + "E", + "j", + "e", + "c", + "u", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "o", + "t", + "a", + "l", + "_", + "F", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + "e", + "s", + "\"", + " ", + "I", + "N", + "T", + "E", + "G", + "E", + "R", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "o", + "t", + "a", + "l", + "_", + "C", + "o", + "n", + "t", + "r", + "i", + "b", + "u", + "c", + "i", + "o", + "n", + "e", + "s", + "\"", + " ", + "I", + "N", + "T", + "E", + "G", + "E", + "R", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "\"", + "R", + "e", + "g", + "i", + "s", + "t", + "r", + "o", + "S", + "e", + "l", + "\"", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "a", + "t", + "e", + "n", + "t", + "e", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "P", + "e", + "d", + "i", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "c", + "c", + "i", + "o", + "n", + "A", + "d", + "u", + "a", + "n", + "e", + "r", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "o", + "n", + "s", + "e", + "c", + "u", + "t", + "i", + "v", + "o", + "R", + "e", + "m", + "e", + "s", + "a", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "N", + "u", + "m", + "e", + "r", + "o", + "S", + "e", + "l", + "e", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "F", + "e", + "c", + "h", + "a", + "S", + "e", + "l", + "e", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "H", + "o", + "r", + "a", + "S", + "e", + "l", + "e", + "c", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "S", + "e", + "m", + "a", + "f", + "o", + "r", + "o", + "F", + "i", + "s", + "c", + "a", + "l", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "C", + "l", + "a", + "v", + "e", + "D", + "o", + "c", + "u", + "m", + "e", + "n", + "t", + "o", + "\"", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "\"", + "T", + "i", + "p", + "o", + "O", + "p", + "e", + "r", + "a", + "c", + "i", + "o", + "n", + "\"", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\n", + "C", + "R", + "E", + "A", + "T", + "E", + " ", + "T", + "A", + "B", + "L", + "E", + " ", + "I", + "F", + " ", + "N", + "O", + "T", + " ", + "E", + "X", + "I", + "S", + "T", + "S", + " ", + "b", + "a", + "s", + "e", + "_", + "n", + "u", + "m", + "p", + "a", + "r", + "t", + "e", + "s", + " ", + "(", + "\n", + " ", + " ", + " ", + " ", + "n", + "u", + "m", + "p", + "a", + "r", + "t", + "e", + " ", + "T", + "E", + "X", + "T", + " ", + "P", + "R", + "I", + "M", + "A", + "R", + "Y", + " ", + "K", + "E", + "Y", + ",", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "s", + "c", + "r", + "i", + "p", + "c", + "i", + "o", + "n", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "u", + "n", + "i", + "m", + "e", + "d", + " ", + "T", + "E", + "X", + "T", + ",", + "\n", + " ", + " ", + " ", + " ", + "f", + "r", + "a", + "c", + "c", + "i", + "o", + "n", + " ", + "T", + "E", + "X", + "T", + "\n", + ")", + ";", + "\n", + "\"", + "\"", + "\"", + "\n", + "\n", + "p", + "g", + "_", + "e", + "n", + "g", + "i", + "n", + "e", + " ", + " ", + " ", + " ", + " ", + "=", + " ", + "N", + "o", + "n", + "e", + " ", + " ", + " ", + "#", + " ", + "S", + "Q", + "L", + "A", + "l", + "c", + "h", + "e", + "m", + "y", + " ", + "e", + "n", + "g", + "i", + "n", + "e", + " ", + "(", + "n", + "o", + "m", + "b", + "r", + "e", + " ", + "l", + "e", + "g", + "a", + "d", + "o", + ",", + " ", + "a", + "h", + "o", + "r", + "a", + " ", + "a", + "p", + "u", + "n", + "t", + "a", + " ", + "a", + " ", + "S", + "Q", + "L", + "i", + "t", + "e", + ")", + "\n", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "O", + "K", + " ", + " ", + "=", + " ", + "F", + "a", + "l", + "s", + "e", + "\n", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "M", + "S", + "G", + " ", + "=", + " ", + "'", + "'", + "\n", + "\n", + "\n", + "d", + "e", + "f", + " ", + "_", + "i", + "n", + "i", + "t", + "_", + "s", + "c", + "h", + "e", + "m", + "a", + "_", + "s", + "q", + "l", + "i", + "t", + "e", + "(", + "d", + "b", + "_", + "p", + "a", + "t", + "h", + ":", + " ", + "s", + "t", + "r", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "\"", + "\"", + "\"", + "C", + "r", + "e", + "a", + " ", + "l", + "a", + "s", + " ", + "t", + "a", + "b", + "l", + "a", + "s", + " ", + "s", + "i", + " ", + "n", + "o", + " ", + "e", + "x", + "i", + "s", + "t", + "e", + "n", + ".", + " ", + "I", + "d", + "e", + "m", + "p", + "o", + "t", + "e", + "n", + "t", + "e", + ".", + "\"", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "c", + "o", + "n", + "n", + " ", + "=", + " ", + "_", + "s", + "q", + "l", + "i", + "t", + "e", + "3", + ".", + "c", + "o", + "n", + "n", + "e", + "c", + "t", + "(", + "d", + "b", + "_", + "p", + "a", + "t", + "h", + ")", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "c", + "o", + "n", + "n", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "e", + "s", + "c", + "r", + "i", + "p", + "t", + "(", + "_", + "S", + "C", + "H", + "E", + "M", + "A", + "_", + "S", + "Q", + "L", + "I", + "T", + "E", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "c", + "o", + "n", + "n", + ".", + "c", + "o", + "m", + "m", + "i", + "t", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "f", + "i", + "n", + "a", + "l", + "l", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "c", + "o", + "n", + "n", + ".", + "c", + "l", + "o", + "s", + "e", + "(", + ")", + "\n", + "\n", + "\n", + "d", + "e", + "f", + " ", + "c", + "o", + "n", + "e", + "c", + "t", + "a", + "r", + "_", + "p", + "o", + "s", + "t", + "g", + "r", + "e", + "s", + "(", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "\"", + "\"", + "\"", + "C", + "o", + "n", + "e", + "c", + "t", + "a", + " ", + "a", + " ", + "l", + "a", + " ", + "b", + "a", + "s", + "e", + " ", + "S", + "Q", + "L", + "i", + "t", + "e", + " ", + "l", + "o", + "c", + "a", + "l", + " ", + "d", + "e", + " ", + "D", + "a", + "t", + "a", + "S", + "t", + "a", + "g", + "e", + ".", + " ", + "A", + "u", + "t", + "o", + "-", + "c", + "r", + "e", + "a", + " ", + "s", + "c", + "h", + "e", + "m", + "a", + " ", + "e", + "n", + " ", + "e", + "l", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "m", + "e", + "r", + " ", + "a", + "r", + "r", + "a", + "n", + "q", + "u", + "e", + ".", + " ", + "M", + "a", + "n", + "t", + "i", + "e", + "n", + "e", + " ", + "e", + "l", + " ", + "n", + "o", + "m", + "b", + "r", + "e", + " ", + "p", + "o", + "r", + " ", + "c", + "o", + "m", + "p", + "a", + "t", + "i", + "b", + "i", + "l", + "i", + "d", + "a", + "d", + " ", + "c", + "o", + "n", + " ", + "c", + "o", + "d", + "i", + "g", + "o", + " ", + "l", + "e", + "g", + "a", + "d", + "o", + ".", + "\"", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "g", + "l", + "o", + "b", + "a", + "l", + " ", + "p", + "g", + "_", + "e", + "n", + "g", + "i", + "n", + "e", + ",", + " ", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "O", + "K", + ",", + " ", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "M", + "S", + "G", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "n", + "u", + "e", + "v", + "o", + " ", + "=", + " ", + "n", + "o", + "t", + " ", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + ".", + "e", + "x", + "i", + "s", + "t", + "s", + "(", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "D", + "B", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "i", + "n", + "i", + "t", + "_", + "s", + "c", + "h", + "e", + "m", + "a", + "_", + "s", + "q", + "l", + "i", + "t", + "e", + "(", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "D", + "B", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "p", + "g", + "_", + "e", + "n", + "g", + "i", + "n", + "e", + " ", + "=", + " ", + "_", + "c", + "r", + "e", + "a", + "t", + "e", + "_", + "e", + "n", + "g", + "i", + "n", + "e", + "(", + "f", + "'", + "s", + "q", + "l", + "i", + "t", + "e", + ":", + "/", + "/", + "/", + "{", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "D", + "B", + "}", + "'", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "p", + "g", + "_", + "e", + "n", + "g", + "i", + "n", + "e", + ".", + "c", + "o", + "n", + "n", + "e", + "c", + "t", + "(", + ")", + " ", + "a", + "s", + " ", + "c", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "c", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "e", + "(", + "_", + "s", + "a", + "_", + "t", + "e", + "x", + "t", + "(", + "'", + "S", + "E", + "L", + "E", + "C", + "T", + " ", + "1", + "'", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "O", + "K", + " ", + " ", + "=", + " ", + "T", + "r", + "u", + "e", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "M", + "S", + "G", + " ", + "=", + " ", + "f", + "'", + "S", + "Q", + "L", + "i", + "t", + "e", + " ", + "D", + "a", + "t", + "a", + "S", + "t", + "a", + "g", + "e", + ":", + " ", + "{", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "D", + "B", + "}", + "'", + " ", + "+", + " ", + "(", + "'", + " ", + "(", + "B", + "D", + " ", + "n", + "u", + "e", + "v", + "a", + ")", + "'", + " ", + "i", + "f", + " ", + "n", + "u", + "e", + "v", + "o", + " ", + "e", + "l", + "s", + "e", + " ", + "'", + "'", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "T", + "r", + "u", + "e", + "\n", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "E", + "x", + "c", + "e", + "p", + "t", + "i", + "o", + "n", + " ", + "a", + "s", + " ", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "p", + "g", + "_", + "e", + "n", + "g", + "i", + "n", + "e", + " ", + " ", + " ", + " ", + " ", + "=", + " ", + "N", + "o", + "n", + "e", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "O", + "K", + " ", + " ", + "=", + " ", + "F", + "a", + "l", + "s", + "e", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "D", + "A", + "T", + "A", + "S", + "T", + "A", + "G", + "E", + "_", + "M", + "S", + "G", + " ", + "=", + " ", + "f", + "'", + "S", + "Q", + "L", + "i", + "t", + "e", + " ", + "n", + "o", + " ", + "d", + "i", + "s", + "p", + "o", + "n", + "i", + "b", + "l", + "e", + ":", + " ", + "{", + "e", + "}", + "'", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "F", + "a", + "l", + "s", + "e", + "\n", + "\n", + "\n", + "c", + "o", + "n", + "e", + "c", + "t", + "a", + "r", + "_", + "p", + "o", + "s", + "t", + "g", + "r", + "e", + "s", + "(", + ")", + "\n" + ] }, - "nbformat": 4, - "nbformat_minor": 5 + { + "cell_type": "code", + "execution_count": null, + "id": "logic-descargas", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "def _load_catalogos():\n", + " if 'df_boms_all' in _state: return\n", + " _state['df_facturas_NA'] = pd.read_sql(\n", + " \"SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO, ESTATUS FROM SFacExp WHERE ESTATUS='NA' ORDER BY FECHAFACTURA_ISO\", scaii_conn)\n", + " _state['df_partidas_all'] = pd.read_sql(\n", + " \"SELECT FACTURAEXPO, LINEA, NUMPARTE AS PT, CANTEXPO, ISNULL(PESONETO,0) AS PESONETO, ISNULL(PESONETOKGS,0) AS PESONETOKGS, MONTOIGIME FROM SPartidasExpo\", scaii_conn)\n", + " _state['df_boms_all'] = pd.read_sql(\"SELECT NUMPARTE AS PT, NUMPARTEBOM AS COMPONENTE_MP, CANTIDAD, UNIMED FROM SMatBOM\", scaii_conn)\n", + " _state['df_sustitutos_all']= pd.read_sql(\"SELECT NUMPARTE, NUMPARTESUSTITUTO AS COMPONENTE_ALTERNO, UNIDADMEDIDA1, UNIDADMEDIDA2 FROM SPartesSustitutos\", scaii_conn)\n", + " _state['df_partepais_all'] = pd.read_sql(\"SELECT FRACCION, PAIS, TIPOFRACCION, TASAIM FROM SPartePais\", scaii_conn)\n", + "\n", + "def _load_saldos_snapshot():\n", + " df = pd.read_sql(\"\"\"\n", + " SELECT NUMPARTE, FACTURAIMPO, PEDIMENTOIMPO, CLASE, PAISORIGEN, FRACCIONIMPO,\n", + " FECHAFACTURA_ISO AS FECHA_ENTRADA, FECHAVENC_ISO AS FECHA_EXPIRACION,\n", + " UMEXITENCIA AS UNIDAD_MEDIDA, CANTEXITENCIA AS CANT_LOTE_ORIG,\n", + " VALORIMPOMN, VALORIMPOME, PESONETO AS PESONETO_LOTE,\n", + " (CANTEXITENCIA - (CANTUSADA + CANTUSADADESP)) AS SALDO_DISPONIBLE\n", + " FROM SSaldoTem\n", + " WHERE (CANTEXITENCIA - (CANTUSADA + CANTUSADADESP)) > 0\n", + " \"\"\", scaii_conn)\n", + " df['UM_KEY'] = df['UNIDAD_MEDIDA'].fillna('').str.strip().str.upper()\n", + " return df\n", + "\n", + "def _consumir(saldos, idx_list, faltante, base_row, tipo, np_usado):\n", + " rows = []\n", + " for idx in idx_list:\n", + " if faltante <= 1e-9: break\n", + " disp = float(saldos.at[idx, 'SALDO_DISPONIBLE'])\n", + " if disp <= 1e-9: continue\n", + " consumo = min(faltante, disp)\n", + " cant_orig = float(saldos.at[idx, 'CANT_LOTE_ORIG'] or 0)\n", + " prop = (consumo / cant_orig) if cant_orig > 0 else 0.0\n", + " rows.append({**base_row,\n", + " 'NUMPARTE_USADO': np_usado, 'TIPO': tipo,\n", + " 'FACTURAIMPO_SALDO': saldos.at[idx, 'FACTURAIMPO'],\n", + " 'PEDIMENTOIMPO': saldos.at[idx, 'PEDIMENTOIMPO'],\n", + " 'CLASE': saldos.at[idx, 'CLASE'],\n", + " 'PAISMERCANCIA': saldos.at[idx, 'PAISORIGEN'],\n", + " 'FRACCION_SALDO': saldos.at[idx, 'FRACCIONIMPO'],\n", + " 'UNIMED_SALDO': saldos.at[idx, 'UNIDAD_MEDIDA'],\n", + " 'FECHA_ENTRADA': saldos.at[idx, 'FECHA_ENTRADA'],\n", + " 'FECHA_EXPIRACION': saldos.at[idx, 'FECHA_EXPIRACION'],\n", + " 'CANT_LOTE_ORIG': cant_orig,\n", + " 'CANT_DESCARGADA': consumo,\n", + " 'VALORMN': float(saldos.at[idx, 'VALORIMPOMN'] or 0) * prop,\n", + " 'VALORME': float(saldos.at[idx, 'VALORIMPOME'] or 0) * prop,\n", + " 'PESONETO_DESC': float(saldos.at[idx, 'PESONETO_LOTE'] or 0) * prop,\n", + " 'STATUS': 'OK'})\n", + " saldos.at[idx, 'SALDO_DISPONIBLE'] = disp - consumo\n", + " faltante -= consumo\n", + " return faltante, rows\n", + "\n", + "def _saldos_idx(saldos, numparte, um_keys, fecha_export):\n", + " um_match = saldos['UM_KEY'] == um_keys if isinstance(um_keys, str) else saldos['UM_KEY'].isin(um_keys)\n", + " mask = ((saldos['NUMPARTE'] == numparte) & um_match\n", + " & (saldos['SALDO_DISPONIBLE'] > 1e-9)\n", + " & (saldos['FECHA_ENTRADA'] <= fecha_export)\n", + " & (saldos['FECHA_EXPIRACION'] >= fecha_export))\n", + " return saldos[mask].sort_values('FECHA_ENTRADA').index.tolist()\n", + "\n", + "def _f(v):\n", + " if pd.isna(v): return None\n", + " if isinstance(v, str):\n", + " v = v.strip()\n", + " if v == '': return None\n", + " try: return float(v)\n", + " except ValueError: return None\n", + " try: return float(v)\n", + " except: return None\n", + "def _adv(v):\n", + " if pd.isna(v): return None\n", + " if isinstance(v, str):\n", + " v = v.strip()\n", + " if v == '': return None\n", + " try: return float(v)\n", + " except ValueError: return v\n", + " return v\n", + "def _s(v): return None if pd.isna(v) else v\n", + "def _f0(v):\n", + " r = _f(v); return 0.0 if r is None else r\n", + "\n", + "INSERT_DESC_SQL = \"\"\"INSERT INTO SDescargaT\n", + " (CONSECUTIVO, FACTEXPO, FACREFERENCIA, FACTIMPO, PEDIMENTOIMPO, PEDIMENTOEXPO, CLASE,\n", + " VALORMN, VALORME, PESONETO, PESOBRUTO, PAISMERCANCIA, FECHADESC, TIPOFRACCION,\n", + " NUMPARTE, CANTDESC, UNIMED, LINEAEXPO, PARTEORIGINAL, PORUTILERIA, TIPOMATEXPO,\n", + " MONTOIGI, ADVALOREMIMPO, TIPODESC)\n", + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\"\"\"\n", + "UPDATE_FACEXP_SQL = \"UPDATE SFacExp SET ESTATUS='AC' WHERE FACTURAEXPO=?\"\n", + "UPDATE_SALDO_SQL = \"\"\"UPDATE SSaldoTem\n", + " SET CANTUSADA=ISNULL(CANTUSADA,0)+?, VALORUSADOMN=ISNULL(VALORUSADOMN,0)+?,\n", + " VALORUSADOME=ISNULL(VALORUSADOME,0)+?, PESOUSADO=ISNULL(PESOUSADO,0)+?,\n", + " PESOBRUTOUSADO=ISNULL(PESOBRUTOUSADO,0)+?\n", + " WHERE FACTURAIMPO=? AND NUMPARTE=? AND UMEXITENCIA=?\"\"\"\n", + "\n", + "def _build_descarga_df(modo_calc='STANDARD', facturas_df=None, partidas_df=None, prev_dict=None, progress=None):\n", + " prog = _Progress(progress)\n", + " _load_catalogos()\n", + " saldos = _load_saldos_snapshot()\n", + " f_use = facturas_df if facturas_df is not None else _state['df_facturas_NA']\n", + " p_use = partidas_df if partidas_df is not None else _state['df_partidas_all']\n", + " b_all, s_all, pp_all = _state['df_boms_all'], _state['df_sustitutos_all'], _state['df_partepais_all']\n", + " partidas_por_factura = p_use.groupby('FACTURAEXPO')\n", + " boms_por_pt = b_all.groupby('PT')\n", + " sustitutos_por_comp = s_all.groupby('NUMPARTE')\n", + " partida_montoigi = p_use.set_index(['FACTURAEXPO','LINEA'])['MONTOIGIME'].to_dict()\n", + " partepais_dict = pp_all.drop_duplicates(subset=['FRACCION','PAIS'], keep='last').set_index(['FRACCION','PAIS'])[['TIPOFRACCION','TASAIM']].to_dict('index')\n", + " prog.setup(len(f_use), 'Procesando facturas')\n", + " all_rows = []\n", + " for i, (_, f) in enumerate(f_use.iterrows()):\n", + " factura, fecha_export, pedimento_ex = f['FACTURAEXPO'], f['FECHAFACTURA_ISO'], f.get('PEDIMENTOEXPO')\n", + " if factura not in partidas_por_factura.groups:\n", + " prog.step(); continue\n", + " for _, p in partidas_por_factura.get_group(factura).iterrows():\n", + " if p['PT'] not in boms_por_pt.groups: continue\n", + " linea = int(p['LINEA']) if pd.notna(p['LINEA']) else 0\n", + " montoigi = partida_montoigi.get((factura, p['LINEA']))\n", + " peso_neto = float(p.get('PESONETO', 0) or 0)\n", + " for _, c in boms_por_pt.get_group(p['PT']).iterrows():\n", + " comp = c['COMPONENTE_MP']\n", + " unimed_bom = (c['UNIMED'] or '').strip().upper()\n", + " if modo_calc == 'KG_PCT':\n", + " cant_req_total = (float(c['CANTIDAD']) / 100.0) * peso_neto\n", + " else:\n", + " cant_req_total = float(p['CANTEXPO']) * float(c['CANTIDAD'])\n", + " cant_prev = 0.0\n", + " if prev_dict is not None:\n", + " cant_prev = float(prev_dict.get((factura, linea, comp), 0.0))\n", + " cant_pend = cant_req_total - cant_prev\n", + " if prev_dict is not None and cant_pend <= 1e-9: continue\n", + " cant_req = cant_pend if prev_dict is not None else cant_req_total\n", + " base_row = {'FACTURAEXPO': factura, 'FECHA_FACTURAEXPO': fecha_export, 'PEDIMENTOEXPO': pedimento_ex,\n", + " 'LINEA': linea, 'PT': p['PT'], 'CANT_PT': float(p['CANTEXPO']),\n", + " 'PESONETO_PARTIDA': peso_neto, 'PCT_BOM': float(c['CANTIDAD']),\n", + " 'MONTOIGI': montoigi, 'COMPONENTE_MP': comp, 'UNIMED_BOM': unimed_bom,\n", + " 'CANT_REQUERIDA': cant_req, 'CANT_PREV': cant_prev}\n", + " faltante = cant_req\n", + " faltante, r = _consumir(saldos, _saldos_idx(saldos, comp, unimed_bom, fecha_export), faltante, base_row, 'ORIGINAL', comp)\n", + " all_rows.extend(r)\n", + " if faltante > 1e-9 and comp in sustitutos_por_comp.groups:\n", + " for _, sub in sustitutos_por_comp.get_group(comp).iterrows():\n", + " if faltante <= 1e-9: break\n", + " um_keys = [str(u).strip().upper() for u in [sub.get('UNIDADMEDIDA1'), sub.get('UNIDADMEDIDA2')] if u and str(u).strip()]\n", + " if not um_keys: continue\n", + " faltante, r = _consumir(saldos, _saldos_idx(saldos, sub['COMPONENTE_ALTERNO'], um_keys, fecha_export),\n", + " faltante, base_row, 'SUSTITUTO', sub['COMPONENTE_ALTERNO'])\n", + " all_rows.extend(r)\n", + " if faltante > 1e-9:\n", + " all_rows.append({**base_row, 'NUMPARTE_USADO': None, 'TIPO': 'FALTANTE',\n", + " 'FACTURAIMPO_SALDO': None, 'PEDIMENTOIMPO': None, 'CLASE': None,\n", + " 'PAISMERCANCIA': None, 'FRACCION_SALDO': None, 'UNIMED_SALDO': None,\n", + " 'FECHA_ENTRADA': None, 'FECHA_EXPIRACION': None, 'CANT_LOTE_ORIG': None,\n", + " 'CANT_DESCARGADA': faltante, 'VALORMN': None, 'VALORME': None, 'PESONETO_DESC': None,\n", + " 'STATUS': 'FALTANTE'})\n", + " prog.step()\n", + " df = pd.DataFrame(all_rows)\n", + " if not df.empty:\n", + " def _pp(row):\n", + " info = partepais_dict.get((row['FRACCION_SALDO'], row['PAISMERCANCIA']))\n", + " if info is None: return pd.Series([None, None])\n", + " return pd.Series([info.get('TIPOFRACCION'), info.get('TASAIM')])\n", + " df[['TIPOFRACCION','ADVALOREMIMPO']] = df.apply(_pp, axis=1)\n", + " df['FECHADESC_CLARION'] = df['FECHA_FACTURAEXPO'].apply(to_clarion)\n", + " prog.done('Pronostico listo')\n", + " return df\n", + "\n", + "def _build_cobertura(df):\n", + " df_d = df.copy()\n", + " df_d['cant_cubierta'] = df_d['CANT_DESCARGADA'].where(df_d['TIPO'].isin(['ORIGINAL','SUSTITUTO']), 0)\n", + " cob_comp = df_d.groupby(['FACTURAEXPO','FECHA_FACTURAEXPO','LINEA','PT','COMPONENTE_MP','UNIMED_BOM','CANT_REQUERIDA'],\n", + " as_index=False, dropna=False).agg(cant_cubierta=('cant_cubierta','sum'))\n", + " cob_comp['pct'] = ((cob_comp['cant_cubierta'] / cob_comp['CANT_REQUERIDA']).fillna(0) * 100).round(2).clip(upper=100)\n", + " cob_fact = cob_comp.groupby(['FACTURAEXPO','FECHA_FACTURAEXPO'], as_index=False, dropna=False).agg(\n", + " componentes_total=('COMPONENTE_MP','count'),\n", + " componentes_100pct=('pct', lambda s: (s >= 99.99).sum()),\n", + " pct_promedio=('pct','mean'))\n", + " cob_fact['pct_promedio'] = cob_fact['pct_promedio'].round(2)\n", + " return cob_fact\n", + "\n", + "def calcular_pronostico_paso8(progress=None):\n", + " df = _build_descarga_df(modo_calc='STANDARD', progress=progress)\n", + " cob = _build_cobertura(df)\n", + " _state['df_descarga_all'] = df\n", + " _state['cobertura_factura'] = cob\n", + " return df, cob\n", + "\n", + "def calcular_pronostico_paso12_kg(progress=None):\n", + " df = _build_descarga_df(modo_calc='KG_PCT', progress=progress)\n", + " cob = _build_cobertura(df)\n", + " _state['df_descarga_kg'] = df\n", + " _state['cobertura_factura_kg']= cob\n", + " return df, cob\n", + "\n", + "def _do_inserts(df_ins, dry_run, log, do_update_status=True, progress=None):\n", + " prog = _Progress(progress)\n", + " if df_ins.empty:\n", + " log('Nada que insertar.'); prog.done('Nada que insertar'); return 0, 0, 0, []\n", + " with scaii_conn.cursor() as cur:\n", + " cur.execute(\"SELECT ISNULL(MAX(CONSECUTIVO),0) FROM SDescargaT\")\n", + " next_consec = int(cur.fetchone()[0]) + 1\n", + " log(f'Proximo CONSECUTIVO: {next_consec}')\n", + " grupos = list(df_ins.groupby('FACTURAEXPO'))\n", + " prog.setup(len(grupos), 'Insertando facturas')\n", + " inserted, updated, saldos_upd, errores = 0, 0, 0, []\n", + " for factura, df_f in grupos:\n", + " cb, ib, sb = next_consec, inserted, saldos_upd\n", + " ok = True\n", + " try:\n", + " with scaii_conn.cursor() as cur:\n", + " for _, r in df_f.iterrows():\n", + " if not dry_run:\n", + " cur.execute(INSERT_DESC_SQL, (\n", + " next_consec, r['FACTURAEXPO'], r['FACTURAEXPO'],\n", + " r['FACTURAIMPO_SALDO'], _s(r['PEDIMENTOIMPO']),\n", + " _s(r['PEDIMENTOEXPO']), _s(r['CLASE']),\n", + " _f(r['VALORMN']), _f(r['VALORME']),\n", + " _f(r['PESONETO_DESC']), _f(r['PESONETO_DESC']),\n", + " _s(r['PAISMERCANCIA']),\n", + " int(r['FECHADESC_CLARION']) if pd.notna(r['FECHADESC_CLARION']) else None,\n", + " _s(r['TIPOFRACCION']), r['NUMPARTE_USADO'],\n", + " float(r['CANT_DESCARGADA']),\n", + " r['UNIMED_SALDO'] if pd.notna(r['UNIMED_SALDO']) else r['UNIMED_BOM'],\n", + " int(r['LINEA']) if pd.notna(r['LINEA']) else 0,\n", + " r['COMPONENTE_MP'], 1, '',\n", + " _f(r['MONTOIGI']), _adv(r['ADVALOREMIMPO']), '0 PARTE'))\n", + " cur.execute(UPDATE_SALDO_SQL, (\n", + " float(r['CANT_DESCARGADA']),\n", + " _f0(r['VALORMN']), _f0(r['VALORME']),\n", + " _f0(r['PESONETO_DESC']), _f0(r['PESONETO_DESC']),\n", + " r['FACTURAIMPO_SALDO'], r['NUMPARTE_USADO'],\n", + " r['UNIMED_SALDO'] if pd.notna(r['UNIMED_SALDO']) else r['UNIMED_BOM']))\n", + " saldos_upd += 1\n", + " next_consec += 1\n", + " inserted += 1\n", + " if not dry_run and do_update_status:\n", + " cur.execute(UPDATE_FACEXP_SQL, (factura,))\n", + " if not dry_run: scaii_conn.commit()\n", + " except Exception as e:\n", + " ok = False\n", + " if not dry_run: scaii_conn.rollback()\n", + " next_consec, inserted, saldos_upd = cb, ib, sb\n", + " errores.append((factura, str(e)))\n", + " if ok and do_update_status: updated += 1\n", + " prog.step()\n", + " prog.done(f'{inserted} insertados')\n", + " return inserted, updated, saldos_upd, errores\n", + "\n", + "def _ejecutar_descarga_NA(modo, dry_run, fecha_desde, fecha_hasta, log, key_df, key_cob, paso_label, progress=None):\n", + " if key_df not in _state:\n", + " log(f'ERROR: corre primero el pronostico del {paso_label}.'); return\n", + " df_da, cob = _state[key_df], _state[key_cob]\n", + " if modo == 'NATURAL':\n", + " elig = cob[cob['componentes_100pct'] == cob['componentes_total']]['FACTURAEXPO'].tolist()\n", + " else:\n", + " elig = cob['FACTURAEXPO'].tolist()\n", + " if fecha_desde or fecha_hasta:\n", + " df_cf = cob.copy()\n", + " df_cf['_f'] = pd.to_datetime(df_cf['FECHA_FACTURAEXPO'], errors='coerce')\n", + " if fecha_desde: df_cf = df_cf[df_cf['_f'] >= pd.to_datetime(fecha_desde)]\n", + " if fecha_hasta: df_cf = df_cf[df_cf['_f'] <= pd.to_datetime(fecha_hasta)]\n", + " rango = set(df_cf['FACTURAEXPO']); elig = [f for f in elig if f in rango]\n", + " df_na = pd.read_sql(\"SELECT FACTURAEXPO FROM SFacExp WHERE ESTATUS='NA'\", scaii_conn)\n", + " set_na = set(df_na['FACTURAEXPO'].astype(str).str.strip())\n", + " omitidas = [f for f in elig if str(f).strip() not in set_na]\n", + " elig = [f for f in elig if str(f).strip() in set_na]\n", + " if omitidas: log(f' Omitidas (ya AC): {len(omitidas)}')\n", + " log(f'Modo: {modo} | DRY_RUN: {dry_run} | Facturas elegibles: {len(elig)}')\n", + " mask = df_da['FACTURAEXPO'].isin(elig) & df_da['TIPO'].isin(['ORIGINAL','SUSTITUTO'])\n", + " df_ins = df_da[mask].copy()\n", + " log(f'Filas a insertar: {len(df_ins)}')\n", + " inserted, updated, saldos_upd, errores = _do_inserts(df_ins, dry_run, log, do_update_status=True, progress=progress)\n", + " log(f'\\n=== RESUMEN {paso_label} ({modo}, DRY_RUN={dry_run}) ===')\n", + " log(f' Insertados : {inserted}')\n", + " log(f' Updates SSaldoTem : {saldos_upd}')\n", + " log(f' Facturas a AC : {updated}')\n", + " log(f' Errores : {len(errores)}')\n", + " for f, e in errores[:5]: log(f' {f}: {e}')\n", + "\n", + "def ejecutar_paso9(modo, dry_run, fecha_desde=None, fecha_hasta=None, log=print, progress=None):\n", + " _ejecutar_descarga_NA(modo, dry_run, fecha_desde, fecha_hasta, log,\n", + " 'df_descarga_all', 'cobertura_factura', 'paso 9', progress=progress)\n", + "\n", + "def ejecutar_paso12_kg(modo, dry_run, fecha_desde=None, fecha_hasta=None, log=print, progress=None):\n", + " _ejecutar_descarga_NA(modo, dry_run, fecha_desde, fecha_hasta, log,\n", + " 'df_descarga_kg', 'cobertura_factura_kg', 'paso 12 (% KGS)', progress=progress)\n", + "\n", + "def _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, modo_calc, paso_label, progress=None):\n", + " prog = _Progress(progress)\n", + " prog.setup(1, 'Cargando facturas AC...')\n", + " sql_fact = \"SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO FROM SFacExp WHERE ESTATUS='AC'\"\n", + " params = []\n", + " if facturas_objetivo:\n", + " sql_fact += f\" AND FACTURAEXPO IN ({','.join(['?']*len(facturas_objetivo))})\"\n", + " params.extend(list(facturas_objetivo))\n", + " if fecha_desde:\n", + " sql_fact += \" AND FECHAFACTURA_ISO >= ?\"; params.append(fecha_desde)\n", + " if fecha_hasta:\n", + " sql_fact += \" AND FECHAFACTURA_ISO <= ?\"; params.append(fecha_hasta)\n", + " sql_fact += \" ORDER BY FECHAFACTURA_ISO\"\n", + " df_fact_ac = pd.read_sql(sql_fact, scaii_conn, params=params)\n", + " log(f'Facturas AC: {len(df_fact_ac)}')\n", + " if df_fact_ac.empty: prog.done('Sin facturas'); return\n", + " factura_list = df_fact_ac['FACTURAEXPO'].tolist()\n", + " prev_parts = []\n", + " for i in range(0, len(factura_list), 500):\n", + " chunk = factura_list[i:i+500]\n", + " ph = ','.join(['?']*len(chunk))\n", + " prev_parts.append(pd.read_sql(f\"\"\"\n", + " SELECT FACTEXPO AS FACTURAEXPO, LINEAEXPO AS LINEA, PARTEORIGINAL AS COMPONENTE_MP,\n", + " SUM(CANTDESC) AS CANT_PREV\n", + " FROM SDescargaT WHERE FACTEXPO IN ({ph})\n", + " GROUP BY FACTEXPO, LINEAEXPO, PARTEORIGINAL\n", + " \"\"\", scaii_conn, params=chunk))\n", + " df_desc_prev = pd.concat(prev_parts) if prev_parts else pd.DataFrame(columns=['FACTURAEXPO','LINEA','COMPONENTE_MP','CANT_PREV'])\n", + " df_desc_prev['LINEA'] = df_desc_prev['LINEA'].astype(int)\n", + " prev_dict = df_desc_prev.set_index(['FACTURAEXPO','LINEA','COMPONENTE_MP'])['CANT_PREV'].to_dict()\n", + " _load_catalogos()\n", + " df_comp = _build_descarga_df(modo_calc=modo_calc, facturas_df=df_fact_ac, prev_dict=prev_dict, progress=progress)\n", + " log(f'Filas calculadas: {len(df_comp)}')\n", + " if df_comp.empty: return\n", + " if modo_comp == 'NATURAL':\n", + " bad = set(df_comp[df_comp['TIPO']=='FALTANTE']['FACTURAEXPO'])\n", + " df_to_ins = df_comp[~df_comp['FACTURAEXPO'].isin(bad) & df_comp['TIPO'].isin(['ORIGINAL','SUSTITUTO'])]\n", + " log(f'Excluidas en NATURAL (con faltante): {len(bad)}')\n", + " else:\n", + " df_to_ins = df_comp[df_comp['TIPO'].isin(['ORIGINAL','SUSTITUTO'])]\n", + " log(f'Filas a insertar: {len(df_to_ins)}')\n", + " inserted, _, saldos_upd, errores = _do_inserts(df_to_ins, dry_run, log, do_update_status=False, progress=progress)\n", + " log(f'\\n=== RESUMEN {paso_label} ({modo_comp}, DRY_RUN={dry_run}) ===')\n", + " log(f' Insertados : {inserted}')\n", + " log(f' Updates SSaldoTem : {saldos_upd}')\n", + " log(f' Errores : {len(errores)}')\n", + " for f, e in errores[:5]: log(f' {f}: {e}')\n", + "\n", + "def ejecutar_paso10(modo_comp, dry_run, fecha_desde=None, fecha_hasta=None, facturas_objetivo=None, log=print, progress=None):\n", + " _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, 'STANDARD', 'paso 10', progress=progress)\n", + "\n", + "def ejecutar_paso12_complementaria_kg(modo_comp, dry_run, fecha_desde=None, fecha_hasta=None, facturas_objetivo=None, log=print, progress=None):\n", + " _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, 'KG_PCT', 'paso 12 complementaria (% KGS)', progress=progress)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "logic-analisis-y-nlp", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "def cargar_analisis_saldos(progress=None):\n", + " prog = _Progress(progress)\n", + " prog.setup(4, 'Cargando SSaldoTem...')\n", + " df = pd.read_sql(\"\"\"\n", + " SELECT NUMPARTE, FACTURAIMPO, PEDIMENTOIMPO, FRACCIONIMPO, PAISORIGEN, CLASE, SECTOR, DESCRIPCIONE,\n", + " UMEXITENCIA AS UNIDAD_MEDIDA, FECHAFACTURA_ISO AS FECHA_ENTRADA, FECHAVENC_ISO AS FECHA_EXPIRACION,\n", + " CANTEXITENCIA AS CANT_LOTE, ISNULL(CANTUSADA,0) AS CANT_USADA, ISNULL(CANTUSADADESP,0) AS CANT_USADA_DESP,\n", + " (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_CANT,\n", + " ISNULL(VALORIMPOMN,0) AS VALOR_LOTE_MN, ISNULL(VALORIMPOME,0) AS VALOR_LOTE_ME,\n", + " ISNULL(VALORUSADOMN,0) AS VALOR_USADO_MN, ISNULL(VALORUSADOME,0) AS VALOR_USADO_ME,\n", + " (ISNULL(VALORIMPOMN,0)-ISNULL(VALORUSADOMN,0)) AS SALDO_VMN,\n", + " (ISNULL(VALORIMPOME,0)-ISNULL(VALORUSADOME,0)) AS SALDO_VME,\n", + " ISNULL(PESONETO,0) AS PESO_NETO_LOTE, ISNULL(PESOBRUTO,0) AS PESO_BRUTO_LOTE,\n", + " ISNULL(PESOUSADO,0) AS PESO_USADO, (ISNULL(PESONETO,0)-ISNULL(PESOUSADO,0)) AS SALDO_PESO_NETO\n", + " FROM SSaldoTem\n", + " \"\"\", scaii_conn)\n", + " prog.step(desc='Procesando fechas...')\n", + " df['FECHA_ENTRADA'] = pd.to_datetime(df['FECHA_ENTRADA'], errors='coerce')\n", + " df['ANIO_ENTRADA'] = df['FECHA_ENTRADA'].dt.year\n", + " _state['df_saldos_full'] = df\n", + " prog.step(desc='SSaldoTem listo')\n", + " return df\n", + "\n", + "def calcular_por_anio_saldos(df):\n", + " return (df[df['SALDO_CANT']>0].groupby('ANIO_ENTRADA', as_index=False, dropna=False)\n", + " .agg(lotes=('NUMPARTE','count'), partes_unicas=('NUMPARTE','nunique'),\n", + " saldo_cant=('SALDO_CANT','sum'), saldo_vmn=('SALDO_VMN','sum'),\n", + " saldo_vme=('SALDO_VME','sum'), saldo_peso_neto=('SALDO_PESO_NETO','sum'))\n", + " .sort_values('ANIO_ENTRADA'))\n", + "\n", + "def calcular_impo_expo_anio(progress=None):\n", + " prog = _Progress(progress)\n", + " prog.setup(3, 'Cargando IMPO...')\n", + " df_imp = pd.read_sql(\"\"\"\n", + " SELECT YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')) AS ANIO,\n", + " COUNT(*) AS partidas_impo, COUNT(DISTINCT NUMPARTE) AS partes_impo,\n", + " SUM(PESONETO) AS peso_neto_impo, SUM(VALORIMPOME) AS valor_me_impo\n", + " FROM SPartidasImpo WHERE FECHAFACTURA IS NOT NULL\n", + " GROUP BY YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01'))\n", + " \"\"\", scaii_conn)\n", + " prog.step(desc='Cargando EXPO...')\n", + " df_exp = pd.read_sql(\"\"\"\n", + " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", + " COUNT(*) AS partidas_expo, COUNT(DISTINCT p.NUMPARTE) AS partes_expo,\n", + " SUM(p.PESONETO) AS peso_neto_expo, SUM(p.VALORTOTALME) AS valor_me_expo\n", + " FROM SPartidasExpo p INNER JOIN SFacExp f ON f.FACTURAEXPO = p.FACTURAEXPO\n", + " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", + " GROUP BY YEAR(f.FECHAFACTURA_ISO)\n", + " \"\"\", scaii_conn)\n", + " prog.step(desc='Comparando...')\n", + " cmp = (df_imp.merge(df_exp, on='ANIO', how='outer').fillna(0).sort_values('ANIO').reset_index(drop=True))\n", + " cmp['ANIO'] = cmp['ANIO'].astype(int)\n", + " for c in ['partidas_impo','partes_impo','partidas_expo','partes_expo']: cmp[c] = cmp[c].astype(int)\n", + " cmp['dif_peso'] = (cmp['peso_neto_expo'] - cmp['peso_neto_impo']).round(2)\n", + " cmp['dif_valor_me'] = (cmp['valor_me_expo'] - cmp['valor_me_impo']).round(2)\n", + " cmp['ratio_peso_expo_impo'] = (cmp['peso_neto_expo'] / cmp['peso_neto_impo'].replace(0, np.nan)).round(4)\n", + " cmp['ratio_valor_expo_impo'] = (cmp['valor_me_expo'] / cmp['valor_me_impo'].replace(0, np.nan)).round(4)\n", + " prog.done('Comparativo listo')\n", + " return df_imp, df_exp, cmp\n", + "\n", + "\n", + "\n", + "def graficar_impo_expo(cmp):\n", + " plt.close('all')\n", + " fig, axes = plt.subplots(2, 1, figsize=(11, 8))\n", + " fig.suptitle('IMPO vs EXPO por aanio‚±o', fontsize=14, fontweight='bold', y=1.0)\n", + " x = cmp['ANIO'].astype(int).values\n", + " xpos = np.arange(len(x)); ancho = 0.4\n", + " cI, cE = '#1976D2', '#F57C00'\n", + " def lab(ax, bars, color, fmt='{:,.0f}'):\n", + " for b in bars:\n", + " h = b.get_height()\n", + " if h > 0:\n", + " ax.text(b.get_x()+b.get_width()/2, h, fmt.format(h),\n", + " ha='center', va='bottom', fontsize=7, color=color, rotation=90)\n", + " ax = axes[0]\n", + " b1 = ax.bar(xpos-ancho/2, cmp['peso_neto_impo'], ancho, label='IMPO', color=cI)\n", + " b2 = ax.bar(xpos+ancho/2, cmp['peso_neto_expo'], ancho, label='EXPO', color=cE)\n", + " ax.set_title('Peso neto'); ax.set_xticks(xpos); ax.set_xticklabels(x)\n", + " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", + " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", + " ax.set_ylim(top=ax.get_ylim()[1]*1.18); lab(ax, b1, cI); lab(ax, b2, cE)\n", + " ax = axes[1]\n", + " b1 = ax.bar(xpos-ancho/2, cmp['valor_me_impo'], ancho, label='IMPO', color=cI)\n", + " b2 = ax.bar(xpos+ancho/2, cmp['valor_me_expo'], ancho, label='EXPO', color=cE)\n", + " ax.set_title('Valor ME (USD)'); ax.set_xticks(xpos); ax.set_xticklabels(x); ax.set_xlabel('Aanio‚±o')\n", + " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'${v:,.0f}'))\n", + " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", + " ax.set_ylim(top=ax.get_ylim()[1]*1.18); lab(ax, b1, cI, '${:,.0f}'); lab(ax, b2, cE, '${:,.0f}')\n", + " plt.tight_layout()\n", + " return fig\n", + "\n", + "def graficar_saldos_anio(por_anio):\n", + " plt.close('all')\n", + " fig, axes = plt.subplots(1, 2, figsize=(13, 5))\n", + " fig.suptitle('Saldo disponible por aanio‚±o de entrada', fontsize=13, fontweight='bold')\n", + " x = por_anio['ANIO_ENTRADA'].astype(int).astype(str).values\n", + " axes[0].bar(x, por_anio['saldo_cant'], color='#42A5F5')\n", + " axes[0].set_title('Cantidad'); axes[0].grid(axis='y', linestyle=':', alpha=0.5)\n", + " axes[0].yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", + " for i, v in enumerate(por_anio['saldo_cant'].values):\n", + " if v > 0: axes[0].text(i, v, f'{v:,.0f}', ha='center', va='bottom', fontsize=8, rotation=90)\n", + " axes[1].bar(x, por_anio['saldo_vmn'], color='#FFA726', label='MN', alpha=0.85)\n", + " axes[1].bar(x, por_anio['saldo_vme'], color='#7E57C2', label='ME', alpha=0.55)\n", + " axes[1].set_title('Valor (MN + ME)'); axes[1].legend(); axes[1].grid(axis='y', linestyle=':', alpha=0.5)\n", + " axes[1].yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'${v:,.0f}'))\n", + " plt.tight_layout()\n", + " return fig\n", + "\n", + "def calcular_pesos_por_anio(progress=None):\n", + " prog = _Progress(progress)\n", + " prog.setup(4, 'Cargando IMPO...')\n", + " df_imp = pd.read_sql(\"\"\"\n", + " SELECT YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')) AS ANIO,\n", + " SUM(PESONETO) AS peso_impo\n", + " FROM SPartidasImpo WHERE FECHAFACTURA IS NOT NULL\n", + " GROUP BY YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01'))\n", + " \"\"\", scaii_conn)\n", + " prog.step(desc='Cargando EXPO...')\n", + " df_exp = pd.read_sql(\"\"\"\n", + " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", + " SUM(p.PESONETO) AS peso_expo\n", + " FROM SPartidasExpo p INNER JOIN SFacExp f ON f.FACTURAEXPO = p.FACTURAEXPO\n", + " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", + " GROUP BY YEAR(f.FECHAFACTURA_ISO)\n", + " \"\"\", scaii_conn)\n", + " prog.step(desc='Cargando CONSUMIDO...')\n", + " df_cons = pd.read_sql(\"\"\"\n", + " SELECT YEAR(FECHAFACTURA_ISO) AS ANIO,\n", + " SUM(ISNULL(PESOUSADO,0)) AS peso_consumido\n", + " FROM SSaldoTem WHERE FECHAFACTURA_ISO IS NOT NULL\n", + " GROUP BY YEAR(FECHAFACTURA_ISO)\n", + " \"\"\", scaii_conn)\n", + " prog.step(desc='Cargando DESCARGAS...')\n", + " df_desc = pd.read_sql(\"\"\"\n", + " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", + " SUM(ISNULL(d.PESONETO,0)) AS peso_descargas\n", + " FROM SDescargaT d INNER JOIN SFacExp f ON f.FACTURAEXPO = d.FACTEXPO\n", + " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", + " GROUP BY YEAR(f.FECHAFACTURA_ISO)\n", + " \"\"\", scaii_conn)\n", + " cmp = (df_imp.merge(df_exp, on='ANIO', how='outer')\n", + " .merge(df_cons, on='ANIO', how='outer')\n", + " .merge(df_desc, on='ANIO', how='outer')\n", + " .fillna(0).sort_values('ANIO').reset_index(drop=True))\n", + " cmp['ANIO'] = cmp['ANIO'].astype(int)\n", + " for c in ['peso_impo','peso_expo','peso_consumido','peso_descargas']:\n", + " cmp[c] = cmp[c].round(2)\n", + " prog.done('Pesos por anio listos')\n", + " return cmp\n", + "\n", + "def graficar_pesos_anio(cmp):\n", + " plt.close('all')\n", + " fig, ax = plt.subplots(figsize=(13, 5.5))\n", + " fig.suptitle('Peso por aanio‚±o anio‚ IMPO / EXPO / CONSUMIDO / DESCARGAS', fontsize=13, fontweight='bold')\n", + " x = cmp['ANIO'].astype(int).values\n", + " xpos = np.arange(len(x)); ancho = 0.2\n", + " cI, cE, cC, cD = '#1976D2', '#F57C00', '#43A047', '#8E24AA'\n", + " b1 = ax.bar(xpos-1.5*ancho, cmp['peso_impo'], ancho, label='IMPO', color=cI)\n", + " b2 = ax.bar(xpos-0.5*ancho, cmp['peso_expo'], ancho, label='EXPO', color=cE)\n", + " b3 = ax.bar(xpos+0.5*ancho, cmp['peso_consumido'], ancho, label='CONSUMIDO', color=cC)\n", + " b4 = ax.bar(xpos+1.5*ancho, cmp['peso_descargas'], ancho, label='DESCARGAS', color=cD)\n", + " ax.set_xticks(xpos); ax.set_xticklabels(x); ax.set_xlabel('Aanio‚±o')\n", + " ax.set_ylabel('Peso neto')\n", + " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", + " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", + " ax.set_ylim(top=ax.get_ylim()[1]*1.18)\n", + " def lab(bars, color):\n", + " for b in bars:\n", + " h = b.get_height()\n", + " if h > 0:\n", + " ax.text(b.get_x()+b.get_width()/2, h, f'{h:,.0f}',\n", + " ha='center', va='bottom', fontsize=6, color=color, rotation=90)\n", + " lab(b1, cI); lab(b2, cE); lab(b3, cC); lab(b4, cD)\n", + " plt.tight_layout()\n", + " return fig\n", + "\n", + "def calcular_cantidades_por_anio(progress=None):\n", + " prog = _Progress(progress)\n", + " prog.setup(2, 'Cargando cantidades IMPO...')\n", + " df_imp = pd.read_sql(\"\"\"\n", + " SELECT YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')) AS ANIO,\n", + " COUNT(*) AS partidas_impo,\n", + " SUM(CANTIMPO) AS cantidad_impo\n", + " FROM SPartidasImpo WHERE FECHAFACTURA IS NOT NULL\n", + " GROUP BY YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01'))\n", + " \"\"\", scaii_conn)\n", + " prog.step(desc='Cargando cantidades EXPO...')\n", + " df_exp = pd.read_sql(\"\"\"\n", + " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", + " COUNT(*) AS partidas_expo,\n", + " SUM(p.CANTEXPO) AS cantidad_expo\n", + " FROM SPartidasExpo p INNER JOIN SFacExp f ON f.FACTURAEXPO = p.FACTURAEXPO\n", + " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", + " GROUP BY YEAR(f.FECHAFACTURA_ISO)\n", + " \"\"\", scaii_conn)\n", + " cmp = (df_imp.merge(df_exp, on='ANIO', how='outer').fillna(0).sort_values('ANIO').reset_index(drop=True))\n", + " cmp['ANIO'] = cmp['ANIO'].astype(int)\n", + " for c in ['partidas_impo','partidas_expo']: cmp[c] = cmp[c].astype(int)\n", + " cmp['cantidad_impo'] = cmp['cantidad_impo'].round(2)\n", + " cmp['cantidad_expo'] = cmp['cantidad_expo'].round(2)\n", + " cmp['diferencia'] = (cmp['cantidad_expo'] - cmp['cantidad_impo']).round(2)\n", + " prog.done('Cantidades por aanio‚±o listas')\n", + " return cmp\n", + "\n", + "def graficar_cantidades_anio(cmp):\n", + " plt.close('all')\n", + " fig, ax = plt.subplots(figsize=(12, 5.5))\n", + " fig.suptitle('Cantidades por aanio‚±o anio‚ IMPO vs EXPO', fontsize=13, fontweight='bold')\n", + " x = cmp['ANIO'].astype(int).values\n", + " xpos = np.arange(len(x)); ancho = 0.4\n", + " cI, cE = '#1976D2', '#F57C00'\n", + " b1 = ax.bar(xpos-ancho/2, cmp['cantidad_impo'], ancho, label='IMPO', color=cI)\n", + " b2 = ax.bar(xpos+ancho/2, cmp['cantidad_expo'], ancho, label='EXPO', color=cE)\n", + " ax.set_xticks(xpos); ax.set_xticklabels(x); ax.set_xlabel('Aanio‚±o')\n", + " ax.set_ylabel('Cantidad (suma de unidades, varias UM)')\n", + " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", + " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", + " ax.set_ylim(top=ax.get_ylim()[1]*1.18)\n", + " def lab(bars, color):\n", + " for b in bars:\n", + " h = b.get_height()\n", + " if h > 0:\n", + " ax.text(b.get_x()+b.get_width()/2, h, f'{h:,.0f}',\n", + " ha='center', va='bottom', fontsize=7, color=color, rotation=90)\n", + " lab(b1, cI); lab(b2, cE)\n", + " plt.tight_layout()\n", + " return fig\n", + "\n", + "def calcular_cantidades_por_anio_um(progress=None):\n", + " prog = _Progress(progress)\n", + " prog.setup(2, 'Cargando cantidades IMPO por UM...')\n", + " df_imp = pd.read_sql(\"\"\"\n", + " SELECT YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')) AS ANIO,\n", + " UPPER(LTRIM(RTRIM(UNIMED))) AS UM,\n", + " COUNT(*) AS partidas_impo,\n", + " SUM(CANTIMPO) AS cantidad_impo\n", + " FROM SPartidasImpo WHERE FECHAFACTURA IS NOT NULL\n", + " GROUP BY YEAR(DATEADD(DAY, FECHAFACTURA-4, '1801-01-01')),\n", + " UPPER(LTRIM(RTRIM(UNIMED)))\n", + " \"\"\", scaii_conn)\n", + " prog.step(desc='Cargando cantidades EXPO por UM...')\n", + " df_exp = pd.read_sql(\"\"\"\n", + " SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n", + " UPPER(LTRIM(RTRIM(p.UNIMED))) AS UM,\n", + " COUNT(*) AS partidas_expo,\n", + " SUM(p.CANTEXPO) AS cantidad_expo\n", + " FROM SPartidasExpo p INNER JOIN SFacExp f ON f.FACTURAEXPO = p.FACTURAEXPO\n", + " WHERE f.FECHAFACTURA_ISO IS NOT NULL\n", + " GROUP BY YEAR(f.FECHAFACTURA_ISO),\n", + " UPPER(LTRIM(RTRIM(p.UNIMED)))\n", + " \"\"\", scaii_conn)\n", + " cmp = (df_imp.merge(df_exp, on=['ANIO','UM'], how='outer').fillna(0)\n", + " .sort_values(['ANIO','UM']).reset_index(drop=True))\n", + " cmp['ANIO'] = cmp['ANIO'].astype(int)\n", + " cmp['UM'] = cmp['UM'].fillna('').astype(str)\n", + " for c in ['partidas_impo','partidas_expo']: cmp[c] = cmp[c].astype(int)\n", + " cmp['cantidad_impo'] = cmp['cantidad_impo'].round(2)\n", + " cmp['cantidad_expo'] = cmp['cantidad_expo'].round(2)\n", + " cmp['diferencia'] = (cmp['cantidad_expo'] - cmp['cantidad_impo']).round(2)\n", + " prog.done('Cantidades por aanio‚±o + UM listas')\n", + " return cmp\n", + "\n", + "def graficar_cantidades_anio_um(cmp, top_ums=None):\n", + " plt.close('all')\n", + " if top_ums is None:\n", + " # Top 4 UMs por volumen total\n", + " totales = cmp.groupby('UM')[['cantidad_impo','cantidad_expo']].sum().sum(axis=1).sort_values(ascending=False)\n", + " top_ums = totales.head(4).index.tolist()\n", + " filt = cmp[cmp['UM'].isin(top_ums)].copy()\n", + " if filt.empty:\n", + " fig, ax = plt.subplots(figsize=(10, 3))\n", + " ax.text(0.5, 0.5, 'Sin datos', ha='center', va='center')\n", + " ax.axis('off')\n", + " return fig\n", + " n = len(top_ums)\n", + " fig, axes = plt.subplots(n, 1, figsize=(12, 3.2*n), squeeze=False)\n", + " fig.suptitle('Cantidades por aanio‚±o (separado por UM, top {} UMs)'.format(n), fontsize=13, fontweight='bold', y=1.0)\n", + " for idx, um in enumerate(top_ums):\n", + " sub = filt[filt['UM'] == um].sort_values('ANIO')\n", + " ax = axes[idx][0]\n", + " x = sub['ANIO'].astype(int).astype(str).values\n", + " xpos = np.arange(len(x)); ancho = 0.4\n", + " b1 = ax.bar(xpos-ancho/2, sub['cantidad_impo'], ancho, label='IMPO', color='#1976D2')\n", + " b2 = ax.bar(xpos+ancho/2, sub['cantidad_expo'], ancho, label='EXPO', color='#F57C00')\n", + " ax.set_title(f'UM = {um or \"(sin UM)\"}', fontsize=11, fontweight='bold')\n", + " ax.set_xticks(xpos); ax.set_xticklabels(x)\n", + " ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n", + " ax.legend(loc='upper left'); ax.grid(axis='y', linestyle=':', alpha=0.5)\n", + " ax.set_ylim(top=ax.get_ylim()[1]*1.18 if ax.get_ylim()[1] > 0 else 1)\n", + " def lab(bars, color):\n", + " for b in bars:\n", + " h = b.get_height()\n", + " if h > 0:\n", + " ax.text(b.get_x()+b.get_width()/2, h, f'{h:,.0f}',\n", + " ha='center', va='bottom', fontsize=7, color=color, rotation=90)\n", + " lab(b1, '#1976D2'); lab(b2, '#F57C00')\n", + " plt.tight_layout()\n", + " return fig\n", + "\n", + "def exportar_excel_analisis(df, por_anio, df_imp, df_exp, cmp, cmp_pesos=None, cmp_cantidades=None, cmp_cantidades_um=None):\n", + " out = f'analisis_ssaldotem_{_dt.datetime.now().strftime(\"%Y%m%d_%H%M%S\")}.xlsx'\n", + " with pd.ExcelWriter(out, engine='openpyxl') as w:\n", + " df.to_excel(w, sheet_name='SSaldoTem_Detalle', index=False)\n", + " por_anio.to_excel(w, sheet_name='Por_Anio', index=False)\n", + " df_imp.to_excel(w, sheet_name='IMPO_x_Anio', index=False)\n", + " df_exp.to_excel(w, sheet_name='EXPO_x_Anio', index=False)\n", + " cmp.to_excel(w, sheet_name='IMPO_vs_EXPO', index=False)\n", + " if cmp_pesos is not None and not cmp_pesos.empty:\n", + " cmp_pesos.to_excel(w, sheet_name='Pesos_x_Anio', index=False)\n", + " if cmp_cantidades is not None and not cmp_cantidades.empty:\n", + " cmp_cantidades.to_excel(w, sheet_name='Cantidades_x_Anio', index=False)\n", + " if cmp_cantidades_um is not None and not cmp_cantidades_um.empty:\n", + " cmp_cantidades_um.to_excel(w, sheet_name='Cantidades_Anio_UM', index=False)\n", + " return os.path.abspath(out)\n", + "\n", + "def generar_sustitutos_nlp(min_sim=0.80, top_n=3, dry_run=True, log=print, progress=None):\n", + " from sklearn.feature_extraction.text import TfidfVectorizer\n", + " from sklearn.metrics.pairwise import cosine_similarity\n", + " prog = _Progress(progress)\n", + " prog.setup(5, 'Cargando catalogo SPartes...')\n", + " df_spartes = pd.read_sql(\"\"\"\n", + " SELECT NUMPARTE, DESCRIPCIONE, FRACCION, UNIMED FROM SPartes\n", + " WHERE DESCRIPCIONE IS NOT NULL AND LTRIM(RTRIM(DESCRIPCIONE)) <> ''\n", + " \"\"\", scaii_conn).drop_duplicates(subset='NUMPARTE').reset_index(drop=True)\n", + " log(f'Catalogo SPartes: {len(df_spartes):,}')\n", + " prog.step(desc='Cargando componentes BOM...')\n", + " df_comp = pd.read_sql(\"SELECT DISTINCT NUMPARTEBOM AS NUMPARTE FROM SMatBOM WHERE NUMPARTEBOM IS NOT NULL\", scaii_conn)\n", + " df_comp = df_comp.merge(df_spartes, on='NUMPARTE', how='left')\n", + " df_comp = df_comp[df_comp['DESCRIPCIONE'].notna()].reset_index(drop=True)\n", + " log(f'Componentes con descripcion: {len(df_comp):,}')\n", + " if df_comp.empty:\n", + " prog.done('Sin componentes'); log('Sin componentes para procesar.'); return\n", + " prog.step(desc='Vectorizando TF-IDF...')\n", + " def limpiar(t):\n", + " if pd.isna(t) or str(t).strip() == '': return ''\n", + " t = re.sub(r'[^\\w\\s]', ' ', str(t).upper().strip())\n", + " return re.sub(r'\\s+', ' ', t).strip()\n", + " def texto(row): return f\"{limpiar(row['DESCRIPCIONE'])} {str(row['UNIMED'] or '').upper().strip()}\".strip()\n", + " df_spartes['texto'] = df_spartes.apply(texto, axis=1)\n", + " df_comp['texto'] = df_comp.apply(texto, axis=1)\n", + " vec = TfidfVectorizer(ngram_range=(1,2), sublinear_tf=True, min_df=1, max_features=80000)\n", + " vec.fit(pd.concat([df_spartes['texto'], df_comp['texto']], ignore_index=True))\n", + " cat_mat = vec.transform(df_spartes['texto'])\n", + " upper = df_spartes['NUMPARTE'].astype(str).str.upper().values\n", + " # Reset progreso para el calculo de similitud por lote\n", + " total_batches = max(1, (len(df_comp) + 199) // 200)\n", + " prog.setup(total_batches, 'Calculando similitud...')\n", + " rows = []\n", + " BATCH = 200\n", + " for s in range(0, len(df_comp), BATCH):\n", + " e = min(s+BATCH, len(df_comp))\n", + " bm = vec.transform(df_comp.iloc[s:e]['texto'])\n", + " sims = cosine_similarity(bm, cat_mat)\n", + " for j, (_, comp) in enumerate(df_comp.iloc[s:e].iterrows()):\n", + " sr = sims[j].copy()\n", + " sr[upper == str(comp['NUMPARTE']).upper()] = 0.0\n", + " order = sr.argsort()[::-1]\n", + " rank = 1\n", + " for idx in order:\n", + " if sr[idx] < min_sim or rank > top_n: break\n", + " sust = df_spartes.iloc[idx]\n", + " rows.append({\n", + " 'NUMPARTE': str(comp['NUMPARTE']).strip(),\n", + " 'NUMPARTESUSTITUTO': str(sust['NUMPARTE']).strip(),\n", + " 'UNIMED_COMP': str(comp['UNIMED'] or '').strip(),\n", + " 'UNIMED_SUST': str(sust['UNIMED'] or '').strip(),\n", + " 'SIMILITUD': round(float(sr[idx]),4), 'RANK': rank,\n", + " })\n", + " rank += 1\n", + " prog.step(desc=f'Similitud {e}/{len(df_comp)}')\n", + " df_sust = pd.DataFrame(rows)\n", + " log(f'Sustitutos calculados: {len(df_sust):,}')\n", + " df_exist = pd.read_sql(\"SELECT NUMPARTE, NUMPARTESUSTITUTO FROM SPartesSustitutos\", scaii_conn)\n", + " pares = set(zip(df_exist['NUMPARTE'].astype(str).str.strip(),\n", + " df_exist['NUMPARTESUSTITUTO'].astype(str).str.strip()))\n", + " df_sust['_dup'] = df_sust.apply(lambda r: (r['NUMPARTE'], r['NUMPARTESUSTITUTO']) in pares, axis=1)\n", + " df_nuevos = df_sust[~df_sust['_dup']].drop(columns='_dup').reset_index(drop=True)\n", + " log(f' Duplicados (omitir): {df_sust[\"_dup\"].sum():,}')\n", + " log(f' NUEVOS a insertar: {len(df_nuevos):,}')\n", + " _state['df_nuevos_sust'] = df_nuevos\n", + " if df_nuevos.empty or dry_run:\n", + " if dry_run: log('DRY_RUN=True. Cambia para insertar.')\n", + " prog.done(f'{len(df_nuevos)} nuevos'); return\n", + " INS = \"\"\"INSERT INTO SPartesSustitutos\n", + " (NUMPARTE, NUMPARTESUSTITUTO, FACTORCONVERSION, UNIDADMEDIDA1, UNIDADMEDIDA2)\n", + " VALUES (?, ?, 0, ?, ?)\"\"\"\n", + " cur = scaii_conn.cursor()\n", + " total_lotes = max(1, (len(df_nuevos) + 499) // 500)\n", + " prog.setup(total_lotes, 'Insertando lotes...')\n", + " inserted = 0\n", + " for s in range(0, len(df_nuevos), 500):\n", + " e = min(s+500, len(df_nuevos))\n", + " params = [(r['NUMPARTE'], r['NUMPARTESUSTITUTO'],\n", + " r['UNIMED_COMP'] or r['UNIMED_SUST'],\n", + " r['UNIMED_SUST'] or r['UNIMED_COMP'])\n", + " for _, r in df_nuevos.iloc[s:e].iterrows()]\n", + " try:\n", + " cur.executemany(INS, params)\n", + " scaii_conn.commit()\n", + " inserted += len(params)\n", + " except Exception as e2:\n", + " scaii_conn.rollback()\n", + " log(f' ERROR lote {s}-{e}: {e2}')\n", + " prog.step()\n", + " log(f'Insertados: {inserted:,}')\n", + " prog.done(f'{inserted} insertados')\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "logic-ctm", + "metadata": { + "tags": [ + "hide-input" + ] + }, + "outputs": [], + "source": [ + "# ============================================================\n", + "# CTM - Reasignacion de descargas de Cambio de Regimen a CTM\n", + "# ============================================================\n", + "\n", + "def cargar_excel_mapping_ctm(file_bytes_or_path):\n", + " \"\"\"Carga el Excel del cliente y normaliza.\n", + " Espera columnas: 'Facturas CTM', 'PEDIMENTO COMPLETO', 'PATENTE', 'ADUANA',\n", + " 'PEDIMENTO', 'Operacion', 'Clave de pedimento'.\n", + " Una sola celda 'Facturas CTM' puede traer varias facturas separadas por coma.\n", + " \"\"\"\n", + " import io\n", + " if isinstance(file_bytes_or_path, (bytes, bytearray)):\n", + " df = pd.read_excel(io.BytesIO(file_bytes_or_path))\n", + " else:\n", + " df = pd.read_excel(file_bytes_or_path)\n", + " # Normalizar nombres de columnas (acentos / mayusc)\n", + " norm = {c: c.strip() for c in df.columns}\n", + " df.rename(columns=norm, inplace=True)\n", + " col_ctm = next((c for c in df.columns if 'CTM' in c.upper() and 'FACTURA' in c.upper()), None)\n", + " col_ped = next((c for c in df.columns if 'PEDIMENTO' in c.upper() and 'COMPLETO' in c.upper()), None)\n", + " if col_ctm is None or col_ped is None:\n", + " raise ValueError('El Excel debe tener columnas Facturas CTM y PEDIMENTO COMPLETO')\n", + " # Expandir CTM separadas por coma\n", + " rows = []\n", + " for _, r in df.iterrows():\n", + " ctm_cell = str(r[col_ctm]) if pd.notna(r[col_ctm]) else ''\n", + " ped_cell = str(r[col_ped]) if pd.notna(r[col_ped]) else ''\n", + " if not ctm_cell.strip() or not ped_cell.strip():\n", + " continue\n", + " for ctm in [c.strip() for c in ctm_cell.split(',') if c.strip()]:\n", + " rows.append({'FACTURA_CTM': ctm, 'PEDIMENTO': ped_cell.strip()})\n", + " mapping = pd.DataFrame(rows).drop_duplicates().reset_index(drop=True)\n", + " return mapping, df\n", + "\n", + "def listar_facturas_ctm():\n", + " \"\"\"Devuelve facturas TIPOFACTURA='CTM' con su estatus.\"\"\"\n", + " return pd.read_sql(\"\"\"\n", + " SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO, ESTATUS, TIPOFACTURA\n", + " FROM SFacExp WHERE TIPOFACTURA='CTM'\n", + " ORDER BY FECHAFACTURA_ISO\n", + " \"\"\", scaii_conn)\n", + "\n", + "def cargar_partidas_ctm_explotadas():\n", + " \"\"\"Partidas CTM con explosion BOM cuando TIPOMAT IN ('PT','SE').\n", + " Una fila por (FACTURA_CTM, LINEA, componente_final).\"\"\"\n", + " return pd.read_sql(\"\"\"\n", + " SELECT PE.FACTURAEXPO AS FACTURA_CTM, PE.LINEA, PE.NUMPARTE AS PT_O_MP,\n", + " PE.CANTEXPO, PE.UNIMED AS UNIMED_PE, PA.TIPOMAT,\n", + " CASE\n", + " WHEN (SELECT COUNT(*) FROM SMatBOM X WHERE X.NUMPARTE = PE.NUMPARTE) = 0 THEN NULL\n", + " WHEN PA.TIPOMAT IN ('PT','SE') THEN B.NUMPARTEBOM\n", + " ELSE PE.NUMPARTE\n", + " END AS COMPONENTE,\n", + " CASE\n", + " WHEN (SELECT COUNT(*) FROM SMatBOM X WHERE X.NUMPARTE = PE.NUMPARTE) = 0 THEN 0\n", + " WHEN PA.TIPOMAT IN ('PT','SE') THEN B.CANTIDAD\n", + " ELSE 1\n", + " END AS CANT_BOM,\n", + " B.UNIMED AS UNIMED_BOM,\n", + " CASE\n", + " WHEN (SELECT COUNT(*) FROM SMatBOM X WHERE X.NUMPARTE = PE.NUMPARTE) = 0 THEN 0\n", + " WHEN PA.TIPOMAT IN ('PT','SE') THEN (B.CANTIDAD * PE.CANTEXPO)\n", + " ELSE PE.CANTEXPO\n", + " END AS CANT_REQUERIDA\n", + " FROM SPartidasExpo PE\n", + " LEFT JOIN SFacExp FE ON FE.FACTURAEXPO = PE.FACTURAEXPO\n", + " LEFT JOIN SPartes PA ON PA.NUMPARTE = PE.NUMPARTE\n", + " LEFT JOIN SMatBOM B ON B.NUMPARTE = PE.NUMPARTE\n", + " WHERE FE.TIPOFACTURA = 'CTM'\n", + " ORDER BY PE.FACTURAEXPO, PE.LINEA, B.NUMPARTEBOM\n", + " \"\"\", scaii_conn)\n", + "\n", + "def cargar_pool_descargas_cr():\n", + " \"\"\"Pool de descargas existentes en facturas de Cambio de Regimen.\n", + " Estas son las candidatas a reasignarse a las CTM.\"\"\"\n", + " return pd.read_sql(\"\"\"\n", + " SELECT D.CONSECUTIVO, D.FACTEXPO AS FACTURA_CR, D.NUMPARTE, D.PARTEORIGINAL,\n", + " D.CANTDESC, D.UNIMED, D.PEDIMENTOIMPO, D.PEDIMENTOEXPO,\n", + " D.FECHADESC, D.LINEAEXPO, D.FACTIMPO, PA.TIPOMAT,\n", + " DATEADD(DAY, D.FECHADESC - 4, '1801-01-01') AS FECHA_DESC_ISO\n", + " FROM SDescargaT D\n", + " INNER JOIN SFacExp FE ON FE.FACTURAEXPO = D.FACTEXPO\n", + " LEFT JOIN SPartes PA ON PA.NUMPARTE = D.NUMPARTE\n", + " WHERE FE.ESCAMBIOREGIMEN = 'S'\n", + " ORDER BY D.FECHADESC, D.CONSECUTIVO\n", + " \"\"\", scaii_conn)\n", + "\n", + "def cargar_sustitutos_dict():\n", + " \"\"\"Devuelve dict {numparte_original: [lista_sustitutos]}.\"\"\"\n", + " df = pd.read_sql('SELECT NUMPARTE, NUMPARTESUSTITUTO FROM SPartesSustitutos', scaii_conn)\n", + " d = {}\n", + " for _, r in df.iterrows():\n", + " d.setdefault(str(r['NUMPARTE']).strip(), []).append(str(r['NUMPARTESUSTITUTO']).strip())\n", + " return d\n", + "\n", + "def analizar_ctm(df_mapping=None, progress=None):\n", + " \"\"\"Analisis Paso A: para cada partida CTM, busca matches en el pool CR.\n", + " Aplica PEPS (FECHA_DESC ascendente). Si df_mapping no es None, prioriza\n", + " descargas cuyo PEDIMENTOIMPO contenga el numero de pedimento mapeado.\n", + "\n", + " Retorna dos DataFrames:\n", + " plan : una fila por descarga CR que se tomaria (o por faltante)\n", + " resumen : agregado por (FACTURA_CTM, LINEA, COMPONENTE)\n", + " \"\"\"\n", + " prog = _Progress(progress)\n", + " prog.setup(4, 'Cargando facturas CTM...')\n", + " df_part = cargar_partidas_ctm_explotadas()\n", + " prog.step(desc='Cargando pool CR...')\n", + " df_pool = cargar_pool_descargas_cr()\n", + " prog.step(desc='Cargando sustitutos...')\n", + " sust = cargar_sustitutos_dict()\n", + "\n", + " # Dict de pedimentos mapeados por factura CTM (si hay mapping)\n", + " map_dict = {}\n", + " if df_mapping is not None and not df_mapping.empty:\n", + " for _, r in df_mapping.iterrows():\n", + " map_dict.setdefault(str(r['FACTURA_CTM']).strip(),\n", + " set()).add(str(r['PEDIMENTO']).strip())\n", + "\n", + " # Pool mutable: usar saldo virtual por consecutivo\n", + " pool = df_pool.copy()\n", + " pool['SALDO_DISPONIBLE'] = pool['CANTDESC']\n", + "\n", + " # Construir indice rapido: pool por (NUMPARTE) y por (PARTEORIGINAL)\n", + " pool_by_np = pool.groupby('NUMPARTE').groups\n", + " pool_by_po = pool.groupby('PARTEORIGINAL').groups\n", + "\n", + " prog.step(desc='Matching CTM vs CR...')\n", + " plan_rows = []\n", + " total = len(df_part)\n", + " prog.setup(max(total, 1), 'Matching')\n", + " for i, p in df_part.iterrows():\n", + " factura_ctm = p['FACTURA_CTM']\n", + " linea = p['LINEA']\n", + " comp = p['COMPONENTE']\n", + " cant_req = float(p['CANT_REQUERIDA'] or 0)\n", + " unimed = p['UNIMED_BOM'] or p['UNIMED_PE'] or ''\n", + " if cant_req <= 0 or not comp:\n", + " plan_rows.append({\n", + " 'FACTURA_CTM': factura_ctm, 'LINEA': linea, 'PT_O_MP': p['PT_O_MP'],\n", + " 'TIPOMAT': p['TIPOMAT'], 'COMPONENTE': comp, 'CANT_REQUERIDA': cant_req,\n", + " 'UNIMED': unimed, 'STATUS': 'SIN_REQUERIMIENTO', 'CONSECUTIVO_CR': None,\n", + " 'FACTURA_CR': None, 'NUMPARTE_CR': None, 'CANT_DISPONIBLE_CR': 0,\n", + " 'CANT_A_TOMAR': 0, 'FECHA_DESC_CR': None, 'PEDIMENTOIMPO_CR': None,\n", + " 'PRIORIDAD_MAPPING': False,\n", + " })\n", + " prog.step()\n", + " continue\n", + "\n", + " # Candidatos: COMPONENTE directo + sustitutos del COMPONENTE\n", + " candidatos_np = [comp] + sust.get(comp, [])\n", + " # Buscar en pool donde NUMPARTE o PARTEORIGINAL coincida con algun candidato\n", + " idx_match = set()\n", + " for cand in candidatos_np:\n", + " if cand in pool_by_np: idx_match.update(pool_by_np[cand])\n", + " if cand in pool_by_po: idx_match.update(pool_by_po[cand])\n", + " sub = pool.loc[list(idx_match)].copy() if idx_match else pool.iloc[0:0].copy()\n", + " if not sub.empty:\n", + " # Marcar prioridad si su pedimento esta mapeado para esta CTM\n", + " peds_target = map_dict.get(str(factura_ctm).strip(), set())\n", + " def _matches_ped(p_imp):\n", + " if not peds_target: return False\n", + " s = str(p_imp or '')\n", + " # Buscar coincidencia parcial del numero de pedimento mapeado\n", + " for pt in peds_target:\n", + " if pt in s or s in pt: return True\n", + " return False\n", + " sub['PRIORIDAD'] = sub['PEDIMENTOIMPO'].apply(_matches_ped)\n", + " # Ordenar: prioridad descendente, despues fecha ascendente PEPS\n", + " sub = sub.sort_values(['PRIORIDAD','FECHADESC','CONSECUTIVO'], ascending=[False, True, True])\n", + " sub = sub[sub['SALDO_DISPONIBLE'] > 1e-9]\n", + "\n", + " faltante = cant_req\n", + " for idx in sub.index:\n", + " if faltante <= 1e-9: break\n", + " disp = float(pool.at[idx, 'SALDO_DISPONIBLE'])\n", + " if disp <= 1e-9: continue\n", + " toma = min(faltante, disp)\n", + " plan_rows.append({\n", + " 'FACTURA_CTM': factura_ctm, 'LINEA': linea, 'PT_O_MP': p['PT_O_MP'],\n", + " 'TIPOMAT': p['TIPOMAT'], 'COMPONENTE': comp, 'CANT_REQUERIDA': cant_req,\n", + " 'UNIMED': unimed, 'STATUS': 'ASIGNADO',\n", + " 'CONSECUTIVO_CR': int(pool.at[idx, 'CONSECUTIVO']),\n", + " 'FACTURA_CR': pool.at[idx, 'FACTURA_CR'],\n", + " 'NUMPARTE_CR': pool.at[idx, 'NUMPARTE'],\n", + " 'PARTEORIGINAL_CR': pool.at[idx, 'PARTEORIGINAL'],\n", + " 'CANT_DISPONIBLE_CR': disp,\n", + " 'CANT_A_TOMAR': toma,\n", + " 'FECHA_DESC_CR': pool.at[idx, 'FECHA_DESC_ISO'],\n", + " 'PEDIMENTOIMPO_CR': pool.at[idx, 'PEDIMENTOIMPO'],\n", + " 'PRIORIDAD_MAPPING': bool(sub.at[idx, 'PRIORIDAD']) if 'PRIORIDAD' in sub.columns else False,\n", + " })\n", + " pool.at[idx, 'SALDO_DISPONIBLE'] = disp - toma\n", + " faltante -= toma\n", + " if faltante > 1e-9:\n", + " plan_rows.append({\n", + " 'FACTURA_CTM': factura_ctm, 'LINEA': linea, 'PT_O_MP': p['PT_O_MP'],\n", + " 'TIPOMAT': p['TIPOMAT'], 'COMPONENTE': comp, 'CANT_REQUERIDA': cant_req,\n", + " 'UNIMED': unimed, 'STATUS': 'FALTANTE', 'CONSECUTIVO_CR': None,\n", + " 'FACTURA_CR': None, 'NUMPARTE_CR': None, 'PARTEORIGINAL_CR': None,\n", + " 'CANT_DISPONIBLE_CR': 0, 'CANT_A_TOMAR': faltante,\n", + " 'FECHA_DESC_CR': None, 'PEDIMENTOIMPO_CR': None,\n", + " 'PRIORIDAD_MAPPING': False,\n", + " })\n", + " prog.step()\n", + "\n", + " plan = pd.DataFrame(plan_rows)\n", + " if plan.empty:\n", + " prog.done('Sin partidas')\n", + " return plan, plan\n", + " # Resumen por (FACTURA_CTM, LINEA, COMPONENTE)\n", + " resumen = (plan.groupby(['FACTURA_CTM','LINEA','COMPONENTE','UNIMED','CANT_REQUERIDA'], as_index=False, dropna=False)\n", + " .agg(cubierto=('CANT_A_TOMAR', lambda s: s[plan.loc[s.index, 'STATUS']=='ASIGNADO'].sum()),\n", + " faltante=('CANT_A_TOMAR', lambda s: s[plan.loc[s.index, 'STATUS']=='FALTANTE'].sum()),\n", + " filas_cr_usadas=('CONSECUTIVO_CR', lambda s: s.notna().sum())))\n", + " resumen['pct_cobertura'] = ((resumen['cubierto'] / resumen['CANT_REQUERIDA']).fillna(0)*100).round(2).clip(upper=100)\n", + " prog.done('Analisis CTM listo')\n", + " _state['ctm_plan'] = plan\n", + " _state['ctm_resumen'] = resumen\n", + " return plan, resumen\n", + "\n", + "def exportar_excel_ctm(plan, resumen):\n", + " out = f'analisis_ctm_{_dt.datetime.now().strftime(\"%Y%m%d_%H%M%S\")}.xlsx'\n", + " with pd.ExcelWriter(out, engine='openpyxl') as w:\n", + " resumen.to_excel(w, sheet_name='Resumen', index=False)\n", + " plan.to_excel(w, sheet_name='Plan_Detalle', index=False)\n", + " plan[plan['STATUS']=='FALTANTE'].to_excel(w, sheet_name='Faltantes', index=False)\n", + " return os.path.abspath(out)\n", + "def generar_plantilla_excel_ctm():\n", + " \"\"\"Crea un Excel de ejemplo con el formato esperado.\"\"\"\n", + " out = f'plantilla_ctm_{_dt.datetime.now().strftime(\"%Y%m%d_%H%M%S\")}.xlsx'\n", + " df = pd.DataFrame([\n", + " {'Facturas CTM': 'AAU112023RFR0481, NIS112023RFR0035',\n", + " 'PEDIMENTO COMPLETO': '75-3076-4021492',\n", + " 'PATENTE': 3076, 'ADUANA': 75, 'PEDIMENTO': 4021492,\n", + " 'Operacion': 'Importacion', 'Clave de pedimento': 'F4'},\n", + " {'Facturas CTM': 'AAU122023RFR0482',\n", + " 'PEDIMENTO COMPLETO': '75-3076-4033174',\n", + " 'PATENTE': 3076, 'ADUANA': 75, 'PEDIMENTO': 4033174,\n", + " 'Operacion': 'Importacion', 'Clave de pedimento': 'F4'},\n", + " {'Facturas CTM': 'AAU062024RFR0488,NIS062024RFR0030',\n", + " 'PEDIMENTO COMPLETO': '75-3076-4133436',\n", + " 'PATENTE': 3076, 'ADUANA': 75, 'PEDIMENTO': 4133436,\n", + " 'Operacion': 'Importacion', 'Clave de pedimento': 'F4'},\n", + " ])\n", + " df.to_excel(out, index=False)\n", + " return os.path.abspath(out)\n", + "\n", + "def ejecutar_reasignacion_ctm(modo='NATURAL', dry_run=True, log=print, progress=None):\n", + " \"\"\"Paso B - Ejecuta el plan generado por analizar_ctm.\n", + " - Cambia FACTEXPO en SDescargaT (caso total) o divide la fila (caso parcial).\n", + " - Inserta fila espejo en SDescargaM cada vez que algo se asigna a la CTM.\n", + " - Actualiza SFacExp: ESTATUS='AC', APLICADESCMANUAL='S', CANT_PARTIDAS=count.\n", + " Modos: NATURAL (solo CTMs 100% cubiertas) | DIRIGIDA (todas con asignaciones).\n", + " Todo en transaccion atomica por factura CTM.\n", + " \"\"\"\n", + " prog = _Progress(progress)\n", + " if 'ctm_plan' not in _state or _state['ctm_plan'].empty:\n", + " log('ERROR: corre primero \"Analizar CTM\" para generar el plan.')\n", + " return\n", + " plan = _state['ctm_plan']; resumen = _state.get('ctm_resumen')\n", + " plan_asignado = plan[plan['STATUS'] == 'ASIGNADO'].copy()\n", + " if plan_asignado.empty:\n", + " log('No hay filas ASIGNADAS en el plan.'); return\n", + "\n", + " if modo == 'NATURAL':\n", + " if resumen is None or resumen.empty:\n", + " log('Sin resumen para evaluar NATURAL. Aborto.'); return\n", + " elig = [f for f, g in resumen.groupby('FACTURA_CTM') if (g['pct_cobertura'] >= 99.99).all()]\n", + " plan_asignado = plan_asignado[plan_asignado['FACTURA_CTM'].isin(elig)]\n", + " log(f'Modo NATURAL: {len(elig)} facturas CTM elegibles (cobertura 100%)')\n", + " else:\n", + " log('Modo DIRIGIDA: procesa todas las facturas con asignaciones')\n", + "\n", + " log(f'Filas a procesar: {len(plan_asignado):,}')\n", + " if plan_asignado.empty: prog.done('Nada que procesar'); return\n", + "\n", + " with scaii_conn.cursor() as cur:\n", + " cur.execute('SELECT ISNULL(MAX(CONSECUTIVO),0) FROM SDescargaM')\n", + " next_m = int(cur.fetchone()[0]) + 1\n", + " log(f'Proximo CONSECUTIVO SDescargaM: {next_m}')\n", + "\n", + " INSERT_M = (\"INSERT INTO SDescargaM (CONSECUTIVO, CONSECUTIVOEXPO, FACTURAEXPO, LINEA, \"\n", + " \"NUMPARTE, CLASE, CANTIDAD, UNIMED, FACTURAIMPO, NUMPARTEMP, \"\n", + " \"VALORIMPOMN, VALORIMPOME, PAIS, PESONETO, PESOBRUTO, \"\n", + " \"TIPOFRACCION, SECTOR, FACTURADEF, PROCEDENCIA, FACTURA, \"\n", + " \"TIPODESPERDICIO, ORDENVENTA, TOMARSALDOBASEALPT) \"\n", + " \"VALUES (\" + ','.join(['?']*23) + \")\")\n", + "\n", + " UPDATE_SFACEXP = (\"UPDATE SFacExp SET ESTATUS='AC', APLICADESCMANUAL='S', \"\n", + " \"CANT_PARTIDAS = (SELECT COUNT(*) FROM SPartidasExpo \"\n", + " \"WHERE FACTURAEXPO = SFacExp.FACTURAEXPO) WHERE FACTURAEXPO=?\")\n", + "\n", + " facturas = plan_asignado['FACTURA_CTM'].unique()\n", + " prog.setup(len(facturas), 'Procesando CTMs')\n", + " ins_m = stat = 0\n", + " errores = []\n", + "\n", + " for factura_ctm in facturas:\n", + " filas_factura = plan_asignado[plan_asignado['FACTURA_CTM'] == factura_ctm]\n", + " try:\n", + " with scaii_conn.cursor() as cur:\n", + " cur.execute(\"SELECT CONSECUTIVO FROM SFacExp WHERE FACTURAEXPO=?\", factura_ctm)\n", + " _rf = cur.fetchone()\n", + " consec_factura_ctm = int(_rf[0]) if _rf and _rf[0] is not None else 0\n", + " for _, fila in filas_factura.iterrows():\n", + " consec_cr = int(fila['CONSECUTIVO_CR'])\n", + " cant_tomar = float(fila['CANT_A_TOMAR'])\n", + " linea_ctm = int(fila['LINEA']) if pd.notna(fila['LINEA']) else 0\n", + " numparte_pt_ctm = str(fila['PT_O_MP']).strip() if pd.notna(fila['PT_O_MP']) else None\n", + " cur.execute(\"SELECT FACTIMPO, CLASE, CANTDESC, UNIMED, VALORMN, VALORME, \"\n", + " \"PESONETO, PESOBRUTO, PAISMERCANCIA, TIPOFRACCION, SECTOR, \"\n", + " \"PARTEORIGINAL, ORDENVENTA \"\n", + " \"FROM SDescargaT WHERE CONSECUTIVO=?\", consec_cr)\n", + " r = cur.fetchone()\n", + " if r is None:\n", + " log(f' WARN consec {consec_cr} ya no existe; se salta'); continue\n", + " cantdesc_actual = float(r.CANTDESC or 0)\n", + " if cantdesc_actual <= 1e-9:\n", + " log(f' WARN consec {consec_cr} tiene CANTDESC=0; se salta'); continue\n", + " p = 1.0 if cant_tomar >= cantdesc_actual else (cant_tomar / cantdesc_actual)\n", + "\n", + " def esc(val):\n", + " v = float(val or 0); return v * p\n", + "\n", + " m_cantidad = esc(r.CANTDESC); m_vmn = esc(r.VALORMN); m_vme = esc(r.VALORME)\n", + " m_pneto = esc(r.PESONETO); m_pbruto = esc(r.PESOBRUTO)\n", + "\n", + " # SOLO INSERT espejo en SDescargaM. SDescargaT no se toca.\n", + " if not dry_run:\n", + " cur.execute(INSERT_M,\n", + " next_m, consec_factura_ctm, factura_ctm, linea_ctm,\n", + " numparte_pt_ctm, r.CLASE, m_cantidad, r.UNIMED,\n", + " r.FACTIMPO, r.PARTEORIGINAL,\n", + " m_vmn, m_vme, r.PAISMERCANCIA, m_pneto, m_pbruto,\n", + " r.TIPOFRACCION, r.SECTOR,\n", + " '', 'TEM', r.FACTIMPO, 'N', r.ORDENVENTA, '')\n", + " next_m += 1; ins_m += 1\n", + "\n", + " if not dry_run:\n", + " cur.execute(UPDATE_SFACEXP, factura_ctm)\n", + " stat += 1\n", + " if not dry_run:\n", + " scaii_conn.commit()\n", + " except Exception as e:\n", + " if not dry_run: scaii_conn.rollback()\n", + " errores.append((factura_ctm, str(e)))\n", + " log(f' ERROR {factura_ctm}: {e}')\n", + " prog.step()\n", + "\n", + " prog.done(f'{ins_m} mirror ops')\n", + " log(f'\\n=== RESUMEN Paso B ({modo}, DRY_RUN={dry_run}) ===')\n", + " log(f' SDescargaM insertadas (espejo) : {ins_m:,}')\n", + " log(f' Facturas CTM -> ESTATUS=AC + APLICADESCMANUAL=S + CANT_PARTIDAS: {stat:,}')\n", + " log(f' SDescargaT NO se modifica (solo se usa como referencia)')\n", + " log(f' Errores : {len(errores):,}')\n", + " for f, e in errores[:5]:\n", + " log(f' {f}: {e}')\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "logic-saldos-vencidos", + "metadata": {}, + "outputs": [], + "source": [ + "# Fix completo para SaldosVencidos: usa PK de 6 campos en SSaldoTem\n", + "# y match por PAISMERCANCIA en SDescargaT.\n", + "\n", + "# Las funciones aqui REEMPLAZAN las de la celda logic-saldos-vencidos.\n", + "\n", + "import io as _io_sv\n", + "import datetime as _dt_sv\n", + "\n", + "def cargar_excel_saldos_vencidos(path):\n", + " \"\"\"Lee Excel con 3 columnas: FACTURAIMPO, CANTIDAD_SALDO, FRACCION_IMPO.\"\"\"\n", + " df = pd.read_excel(path, dtype=str)\n", + " norm = {c: c.strip().upper().replace(' ', '_') for c in df.columns}\n", + " df = df.rename(columns=norm)\n", + " aliases = {\n", + " 'FACTURAIMPO': ['FACTURAIMPO', 'FACTURA_IMPO', 'FACTURA'],\n", + " 'CANTIDAD_SALDO': ['CANTIDAD_SALDO', 'CANT_SALDO', 'CANTIDAD', 'SALDO'],\n", + " 'FRACCION_IMPO': ['FRACCION_IMPO', 'FRACCIONIMPO', 'FRACCION'],\n", + " }\n", + " out = {}\n", + " for std, opts in aliases.items():\n", + " for o in opts:\n", + " if o in df.columns:\n", + " out[std] = df[o]; break\n", + " if std not in out:\n", + " raise ValueError(f'Falta la columna {std} (acepta: {opts})')\n", + " df2 = pd.DataFrame(out)\n", + " df2['FACTURAIMPO'] = df2['FACTURAIMPO'].astype(str).str.strip()\n", + " df2['FRACCION_IMPO'] = df2['FRACCION_IMPO'].astype(str).str.strip()\n", + " df2['CANTIDAD_SALDO'] = pd.to_numeric(df2['CANTIDAD_SALDO'], errors='coerce').fillna(0)\n", + " df2 = df2[df2['CANTIDAD_SALDO'] > 0]\n", + " return df2.reset_index(drop=True)\n", + "\n", + "\n", + "def generar_plantilla_excel_saldos_vencidos():\n", + " df = pd.DataFrame([\n", + " {'FACTURAIMPO': 'F1234567', 'CANTIDAD_SALDO': 100.0, 'FRACCION_IMPO': '85044010'},\n", + " {'FACTURAIMPO': 'F1234568', 'CANTIDAD_SALDO': 50.5, 'FRACCION_IMPO': '85044010'},\n", + " {'FACTURAIMPO': 'F1234569', 'CANTIDAD_SALDO': 25.0, 'FRACCION_IMPO': '73181500'},\n", + " ])\n", + " buf = _io_sv.BytesIO()\n", + " with pd.ExcelWriter(buf, engine='openpyxl') as w:\n", + " df.to_excel(w, sheet_name='SaldosVencidos', index=False)\n", + " buf.seek(0)\n", + " return buf.read()\n", + "\n", + "\n", + "# PK de 6 campos para identificar univocamente una fila de SSaldoTem\n", + "_PK_SALDO = ['FACTURAIMPO', 'PEDIMENTOIMPO', 'FRACCIONIMPO', 'NUMPARTE', 'UMEXITENCIA', 'PAISORIGEN']\n", + "\n", + "# Llave de match con SDescargaT (las descargas no traen PEDIMENTOIMPO/FRACCIONIMPO\n", + "# necesariamente alineados con SSaldoTem; matchamos por los 4 campos que SI son comparables).\n", + "_MATCH_DESC = ['FACTIMPO', 'NUMPARTE', 'UNIMED', 'PAISMERCANCIA']\n", + "\n", + "\n", + "def _enriquecer_saldos_con_tasas(df):\n", + " if df.empty: return df\n", + " if 'CANTIDAD_SALDO' in df.columns:\n", + " df['SALDO_APLICABLE'] = df[['CANTIDAD_SALDO', 'SALDO_DISPONIBLE']].min(axis=1)\n", + " else:\n", + " df['SALDO_APLICABLE'] = df['SALDO_DISPONIBLE']\n", + " denom = df['CANTEXITENCIA'].replace(0, np.nan)\n", + " df['TASA_VMN'] = (df['VALORIMPOMN'] / denom).fillna(0)\n", + " df['TASA_VME'] = (df['VALORIMPOME'] / denom).fillna(0)\n", + " df['TASA_PNETO'] = (df['PESONETO'] / denom).fillna(0)\n", + " df['TASA_PBRUTO'] = (df['PESOBRUTO'] / denom).fillna(0)\n", + " # Normalizar claves a string strip\n", + " for k in _PK_SALDO:\n", + " if k in df.columns:\n", + " df[k] = df[k].astype(str).str.strip()\n", + " return df.reset_index(drop=True)\n", + "\n", + "\n", + "def cargar_saldos_vencidos_ssaldotem(df_excel, fecha_ini, fecha_fin):\n", + " \"\"\"Modo Excel: filtra SSaldoTem por rango FECHAFACTURA_ISO y matchea con Excel\n", + " por (FACTURAIMPO, FRACCIONIMPO). Trae los 6 campos de PK.\"\"\"\n", + " sql = \"\"\"\n", + " SELECT FACTURAIMPO, PEDIMENTOIMPO, FRACCIONIMPO, NUMPARTE, UMEXITENCIA, PAISORIGEN,\n", + " FECHAFACTURA_ISO, FECHAVENC_ISO,\n", + " CANTEXITENCIA,\n", + " ISNULL(CANTUSADA,0) AS CANTUSADA,\n", + " ISNULL(CANTUSADADESP,0) AS CANTUSADADESP,\n", + " (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_DISPONIBLE,\n", + " ISNULL(VALORIMPOMN,0) AS VALORIMPOMN,\n", + " ISNULL(VALORIMPOME,0) AS VALORIMPOME,\n", + " ISNULL(PESONETO,0) AS PESONETO,\n", + " ISNULL(PESOBRUTO,0) AS PESOBRUTO\n", + " FROM SSaldoTem\n", + " WHERE FECHAFACTURA_ISO BETWEEN ? AND ?\n", + " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", + " \"\"\"\n", + " df = pd.read_sql(sql, scaii_conn, params=(fecha_ini, fecha_fin))\n", + " if df.empty: return df\n", + " pares = df_excel[['FACTURAIMPO', 'FRACCION_IMPO', 'CANTIDAD_SALDO']].copy()\n", + " pares = pares.rename(columns={'FRACCION_IMPO': 'FRACCIONIMPO'})\n", + " pares['FACTURAIMPO'] = pares['FACTURAIMPO'].astype(str).str.strip()\n", + " pares['FRACCIONIMPO'] = pares['FRACCIONIMPO'].astype(str).str.strip()\n", + " df['FACTURAIMPO'] = df['FACTURAIMPO'].astype(str).str.strip()\n", + " df['FRACCIONIMPO'] = df['FRACCIONIMPO'].astype(str).str.strip()\n", + " df = df.merge(pares, on=['FACTURAIMPO', 'FRACCIONIMPO'], how='inner')\n", + " return _enriquecer_saldos_con_tasas(df)\n", + "\n", + "\n", + "def cargar_saldos_vencidos_auto(fecha_ini, fecha_fin, fecha_corte=None):\n", + " \"\"\"Modo Automatico: SALDO_DISPONIBLE > 0 + FECHAVENC_ISO < fecha_corte (default hoy).\"\"\"\n", + " if fecha_corte is None:\n", + " fecha_corte = _dt_sv.date.today().isoformat()\n", + " sql = \"\"\"\n", + " SELECT FACTURAIMPO, PEDIMENTOIMPO, FRACCIONIMPO, NUMPARTE, UMEXITENCIA, PAISORIGEN,\n", + " FECHAFACTURA_ISO, FECHAVENC_ISO,\n", + " CANTEXITENCIA,\n", + " ISNULL(CANTUSADA,0) AS CANTUSADA,\n", + " ISNULL(CANTUSADADESP,0) AS CANTUSADADESP,\n", + " (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_DISPONIBLE,\n", + " ISNULL(VALORIMPOMN,0) AS VALORIMPOMN,\n", + " ISNULL(VALORIMPOME,0) AS VALORIMPOME,\n", + " ISNULL(PESONETO,0) AS PESONETO,\n", + " ISNULL(PESOBRUTO,0) AS PESOBRUTO\n", + " FROM SSaldoTem\n", + " WHERE FECHAFACTURA_ISO BETWEEN ? AND ?\n", + " AND FECHAVENC_ISO IS NOT NULL\n", + " AND FECHAVENC_ISO < ?\n", + " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", + " \"\"\"\n", + " df = pd.read_sql(sql, scaii_conn, params=(fecha_ini, fecha_fin, fecha_corte))\n", + " return _enriquecer_saldos_con_tasas(df)\n", + "\n", + "\n", + "def cargar_descargas_candidatas_sv(df_saldos):\n", + " \"\"\"SDescargaT por las FACTIMPO involucradas; trae PAISMERCANCIA para match completo.\"\"\"\n", + " if df_saldos.empty: return pd.DataFrame()\n", + " facturas = df_saldos['FACTURAIMPO'].astype(str).str.strip().unique().tolist()\n", + " if not facturas: return pd.DataFrame()\n", + " out = []\n", + " LOTE = 1000\n", + " for i in range(0, len(facturas), LOTE):\n", + " sub = facturas[i:i+LOTE]\n", + " placeholders = ','.join(['?'] * len(sub))\n", + " sql = f\"\"\"\n", + " SELECT CONSECUTIVO, FACTIMPO, NUMPARTE, UNIMED, PAISMERCANCIA, FACTEXPO,\n", + " ISNULL(CANTDESC,0) AS CANTDESC\n", + " FROM SDescargaT\n", + " WHERE FACTIMPO IN ({placeholders})\n", + " AND ISNULL(CANTDESC,0) > 0\n", + " \"\"\"\n", + " out.append(pd.read_sql(sql, scaii_conn, params=sub))\n", + " df = pd.concat(out, ignore_index=True) if out else pd.DataFrame()\n", + " if df.empty: return df\n", + " for k in ['FACTIMPO', 'NUMPARTE', 'UNIMED', 'PAISMERCANCIA']:\n", + " df[k] = df[k].fillna('').astype(str).str.strip()\n", + " return df\n", + "\n", + "\n", + "def _prorratear_saldos(df_saldos, df_desc, prog):\n", + " \"\"\"Motor de prorrateo. Match con descargas por (FACTIMPO, NUMPARTE, UNIMED, PAISMERCANCIA).\n", + " Cada fila del plan propaga los 6 campos PK del saldo.\"\"\"\n", + " plan_rows = []\n", + " resumen_rows = []\n", + " desc_by_key = {}\n", + " if not df_desc.empty:\n", + " for key, g in df_desc.groupby(_MATCH_DESC):\n", + " desc_by_key[key] = g\n", + " for _, s in df_saldos.iterrows():\n", + " fimpo = str(s['FACTURAIMPO']).strip()\n", + " pedimpo = str(s['PEDIMENTOIMPO']).strip()\n", + " fraccion = str(s['FRACCIONIMPO']).strip()\n", + " numparte = str(s['NUMPARTE']).strip()\n", + " um = str(s['UMEXITENCIA']).strip()\n", + " pais = str(s['PAISORIGEN']).strip()\n", + " candidatas = desc_by_key.get((fimpo, numparte, um, pais))\n", + " base_resumen = {\n", + " 'FACTURAIMPO': fimpo, 'PEDIMENTOIMPO': pedimpo, 'FRACCIONIMPO': fraccion,\n", + " 'NUMPARTE': numparte, 'UMEXITENCIA': um, 'PAISORIGEN': pais,\n", + " 'FECHAFACTURA_ISO': s.get('FECHAFACTURA_ISO'),\n", + " 'FECHAVENC_ISO': s.get('FECHAVENC_ISO'),\n", + " 'SALDO_APLICABLE': float(s['SALDO_APLICABLE']),\n", + " }\n", + " if candidatas is None or candidatas.empty:\n", + " resumen_rows.append({**base_resumen, 'CANT_DESCARGAS': 0, 'STATUS': 'SIN_DESCARGAS'})\n", + " continue\n", + " total_cantdesc = float(candidatas['CANTDESC'].sum())\n", + " if total_cantdesc <= 1e-9:\n", + " resumen_rows.append({**base_resumen, 'CANT_DESCARGAS': 0, 'STATUS': 'CANTDESC_CERO'})\n", + " continue\n", + " saldo_apl = float(s['SALDO_APLICABLE'])\n", + " tasa_vmn = float(s['TASA_VMN'])\n", + " tasa_vme = float(s['TASA_VME'])\n", + " tasa_pn = float(s['TASA_PNETO'])\n", + " tasa_pb = float(s['TASA_PBRUTO'])\n", + " for _, d in candidatas.iterrows():\n", + " prop = float(d['CANTDESC']) / total_cantdesc\n", + " pc = saldo_apl * prop\n", + " plan_rows.append({\n", + " 'FACTURAIMPO': fimpo, 'PEDIMENTOIMPO': pedimpo, 'FRACCIONIMPO': fraccion,\n", + " 'NUMPARTE': numparte, 'UMEXITENCIA': um, 'PAISORIGEN': pais,\n", + " 'CONSECUTIVO_DESC': int(d['CONSECUTIVO']),\n", + " 'FACTEXPO': d['FACTEXPO'],\n", + " 'CANTDESC_ACTUAL': float(d['CANTDESC']),\n", + " 'PROPORCION': prop,\n", + " 'PORCION_CANT': pc,\n", + " 'PORCION_VMN': pc * tasa_vmn,\n", + " 'PORCION_VME': pc * tasa_vme,\n", + " 'PORCION_PNETO': pc * tasa_pn,\n", + " 'PORCION_PBRUTO': pc * tasa_pb,\n", + " })\n", + " resumen_rows.append({**base_resumen, 'CANT_DESCARGAS': len(candidatas), 'STATUS': 'PRORRATEADO'})\n", + " if prog is not None: prog.done('Analisis listo')\n", + " return pd.DataFrame(plan_rows), pd.DataFrame(resumen_rows)\n", + "\n", + "\n", + "def analizar_saldos_vencidos(df_excel, fecha_ini, fecha_fin, progress=None):\n", + " prog = _Progress(progress)\n", + " prog.setup(3, 'Cargando saldos SSaldoTem...')\n", + " df_saldos = cargar_saldos_vencidos_ssaldotem(df_excel, fecha_ini, fecha_fin)\n", + " prog.step(desc=f'Saldos matched: {len(df_saldos)}')\n", + " df_desc = cargar_descargas_candidatas_sv(df_saldos)\n", + " prog.step(desc=f'Descargas candidatas: {len(df_desc)}')\n", + " return _prorratear_saldos(df_saldos, df_desc, prog)\n", + "\n", + "\n", + "def analizar_saldos_vencidos_auto(fecha_ini, fecha_fin, fecha_corte=None, progress=None):\n", + " prog = _Progress(progress)\n", + " prog.setup(3, 'Buscando saldos vencidos en SSaldoTem...')\n", + " df_saldos = cargar_saldos_vencidos_auto(fecha_ini, fecha_fin, fecha_corte)\n", + " prog.step(desc=f'Saldos vencidos: {len(df_saldos)}')\n", + " df_desc = cargar_descargas_candidatas_sv(df_saldos)\n", + " prog.step(desc=f'Descargas candidatas: {len(df_desc)}')\n", + " return _prorratear_saldos(df_saldos, df_desc, prog)\n", + "\n", + "\n", + "def exportar_excel_saldos_vencidos(plan, resumen, ruta):\n", + " with pd.ExcelWriter(ruta, engine='openpyxl') as w:\n", + " if not plan.empty: plan.to_excel(w, sheet_name='Plan_Detalle', index=False)\n", + " if not resumen.empty: resumen.to_excel(w, sheet_name='Resumen', index=False)\n", + "\n", + "\n", + "def ejecutar_saldos_vencidos(plan, dry_run=True, progress=None, log=print):\n", + " \"\"\"Paso B: UPDATE SDescargaT por descarga + UPDATE SSaldoTem por saldo.\n", + " Usa los 6 campos PK del saldo para identificar univocamente la fila.\"\"\"\n", + " assert isinstance(dry_run, bool), 'dry_run debe ser bool'\n", + " prog = _Progress(progress)\n", + " if plan is None or plan.empty:\n", + " log('ERROR: plan vacio, corre primero \"Analizar\".')\n", + " return\n", + " UPD_DESC = \"\"\"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 = ?\"\"\"\n", + " SEL_SALDO = \"\"\"SELECT (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0))\n", + " FROM SSaldoTem\n", + " WHERE FACTURAIMPO=? AND PEDIMENTOIMPO=? AND FRACCIONIMPO=?\n", + " AND NUMPARTE=? AND UMEXITENCIA=? AND PAISORIGEN=?\"\"\"\n", + " UPD_SALDO = \"\"\"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 PEDIMENTOIMPO=? AND FRACCIONIMPO=?\n", + " AND NUMPARTE=? AND UMEXITENCIA=? AND PAISORIGEN=?\"\"\"\n", + " saldos = plan[_PK_SALDO].drop_duplicates().reset_index(drop=True)\n", + " prog.setup(len(saldos), 'Procesando saldos')\n", + " upd_desc = upd_saldo = errores = capados = 0\n", + " err_list = []\n", + " for _, s in saldos.iterrows():\n", + " pk = (s['FACTURAIMPO'], s['PEDIMENTOIMPO'], s['FRACCIONIMPO'],\n", + " s['NUMPARTE'], s['UMEXITENCIA'], s['PAISORIGEN'])\n", + " etiqueta = f\"{pk[0]}/{pk[1]}/{pk[2]}/{pk[3]}/{pk[4]}/{pk[5]}\"\n", + " mask = (plan['FACTURAIMPO']==pk[0]) & (plan['PEDIMENTOIMPO']==pk[1]) & \\\n", + " (plan['FRACCIONIMPO']==pk[2]) & (plan['NUMPARTE']==pk[3]) & \\\n", + " (plan['UMEXITENCIA']==pk[4]) & (plan['PAISORIGEN']==pk[5])\n", + " fil = plan[mask].copy()\n", + " if fil.empty: prog.step(); continue\n", + " sum_c = float(fil['PORCION_CANT'].sum())\n", + " try:\n", + " with scaii_conn.cursor() as cur:\n", + " cur.execute(SEL_SALDO, *pk)\n", + " row = cur.fetchone()\n", + " if row is None:\n", + " log(f' WARN saldo {etiqueta} no existe; se salta')\n", + " prog.step(); continue\n", + " disp = float(row[0] or 0)\n", + " if sum_c > disp + 1e-6:\n", + " factor = disp / sum_c if sum_c > 0 else 0\n", + " log(f' CAPEO {etiqueta}: sum={sum_c:.4f} > disp={disp:.4f} (factor={factor:.4f})')\n", + " for col in ['PORCION_CANT','PORCION_VMN','PORCION_VME','PORCION_PNETO','PORCION_PBRUTO']:\n", + " fil[col] = fil[col] * factor\n", + " capados += 1\n", + " sum_c = float(fil['PORCION_CANT'].sum())\n", + " sum_vmn = float(fil['PORCION_VMN'].sum())\n", + " sum_vme = float(fil['PORCION_VME'].sum())\n", + " sum_pn = float(fil['PORCION_PNETO'].sum())\n", + " sum_pb = float(fil['PORCION_PBRUTO'].sum())\n", + " if sum_c <= 1e-9:\n", + " log(f' SKIP {etiqueta}: factor=0, no hay nada que aplicar')\n", + " prog.step(); continue\n", + " for _, p in fil.iterrows():\n", + " if not dry_run:\n", + " cur.execute(UPD_DESC,\n", + " float(p['PORCION_CANT']), float(p['PORCION_VMN']), float(p['PORCION_VME']),\n", + " float(p['PORCION_PNETO']), float(p['PORCION_PBRUTO']),\n", + " int(p['CONSECUTIVO_DESC']))\n", + " upd_desc += 1\n", + " if not dry_run:\n", + " cur.execute(UPD_SALDO, sum_c, sum_vmn, sum_vme, sum_pn, sum_pb, *pk)\n", + " upd_saldo += 1\n", + " if not dry_run: scaii_conn.commit()\n", + " except Exception as e:\n", + " if not dry_run: scaii_conn.rollback()\n", + " errores += 1\n", + " err_list.append((etiqueta, str(e)))\n", + " log(f' ERROR {etiqueta}: {e}')\n", + " prog.step()\n", + " prog.done(f'{upd_desc} desc / {upd_saldo} saldos')\n", + " log(f'\\n=== RESUMEN Saldos Vencidos (DRY_RUN={dry_run}) ===')\n", + " log(f' SDescargaT actualizadas: {upd_desc:,}')\n", + " log(f' SSaldoTem actualizadas: {upd_saldo:,}')\n", + " log(f' Saldos capeados al 100%: {capados:,}')\n", + " log(f' Errores : {errores:,}')\n", + " for f, e in err_list[:5]:\n", + " log(f' {f}: {e}')\n", + "\n", + "\n", + "def cargar_saldos_vencidos_por_anio(fecha_corte=None, eje='VENCIMIENTO'):\n", + " if fecha_corte is None:\n", + " fecha_corte = _dt_sv.date.today().isoformat()\n", + " col_fecha = 'FECHAFACTURA_ISO' if eje == 'FACTURA' else 'FECHAVENC_ISO'\n", + " alias_anio = 'ANIO_FACTURA' if eje == 'FACTURA' else 'ANIO_VENC'\n", + " sql = f\"\"\"\n", + " SELECT YEAR(CAST({col_fecha} AS DATE)) AS {alias_anio},\n", + " COUNT(*) AS LOTES,\n", + " COUNT(DISTINCT FACTURAIMPO) AS FACTURAS,\n", + " SUM(CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_CANT,\n", + " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", + " CASE WHEN CANTEXITENCIA > 0\n", + " THEN (ISNULL(VALORIMPOMN,0) * 1.0 / CANTEXITENCIA)\n", + " ELSE 0 END) AS SALDO_VMN,\n", + " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", + " CASE WHEN CANTEXITENCIA > 0\n", + " THEN (ISNULL(VALORIMPOME,0) * 1.0 / CANTEXITENCIA)\n", + " ELSE 0 END) AS SALDO_VME\n", + " FROM SSaldoTem\n", + " WHERE FECHAVENC_ISO IS NOT NULL\n", + " AND FECHAVENC_ISO < ?\n", + " AND {col_fecha} IS NOT NULL\n", + " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", + " GROUP BY YEAR(CAST({col_fecha} AS DATE))\n", + " ORDER BY {alias_anio}\n", + " \"\"\"\n", + " return pd.read_sql(sql, scaii_conn, params=(fecha_corte,))\n", + "\n", + "\n", + "def graficar_saldos_vencidos_por_anio(df, metric='SALDO_VMN'):\n", + " import matplotlib.ticker as _mtick\n", + " if df is None or df.empty:\n", + " print('Sin datos para graficar.')\n", + " return\n", + " fig, ax = plt.subplots(figsize=(10, 5))\n", + " col_x = 'ANIO_FACTURA' if 'ANIO_FACTURA' in df.columns else 'ANIO_VENC'\n", + " x = df[col_x].astype(int).astype(str).tolist()\n", + " y = df[metric].astype(float).tolist()\n", + " bars = ax.bar(x, y, color='#1565C0', edgecolor='#0D47A1')\n", + " for i, b in enumerate(bars):\n", + " lotes = int(df.iloc[i]['LOTES'])\n", + " ax.text(b.get_x() + b.get_width()/2, b.get_height(),\n", + " f'{lotes:,} lotes', ha='center', va='bottom', fontsize=9, color='#333')\n", + " titulos = {\n", + " 'SALDO_VMN': 'Saldos vencidos por anio - Valor MN (pesos)',\n", + " 'SALDO_VME': 'Saldos vencidos por anio - Valor ME (dolares)',\n", + " 'SALDO_CANT': 'Saldos vencidos por anio - Cantidad disponible',\n", + " 'LOTES': 'Saldos vencidos por anio - Cantidad de lotes',\n", + " }\n", + " ax.set_title(titulos.get(metric, f'Saldos vencidos por anio - {metric}'),\n", + " fontsize=13, fontweight='bold', color='#0D47A1')\n", + " ax.set_xlabel('Anio')\n", + " ax.set_ylabel(metric)\n", + " ax.yaxis.set_major_formatter(_mtick.FuncFormatter(lambda v, _: f'{v:,.0f}'))\n", + " ax.grid(axis='y', linestyle='--', alpha=0.5)\n", + " plt.tight_layout()\n", + " plt.show()\n", + " display(df.assign(\n", + " SALDO_CANT=df['SALDO_CANT'].round(2),\n", + " SALDO_VMN=df['SALDO_VMN'].round(2),\n", + " SALDO_VME=df['SALDO_VME'].round(2),\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "logic-valores", + "metadata": {}, + "outputs": [], + "source": [ + "# =============================================================\n", + "# VALORES - Ajuste de VALORTOTALME / VALORTOTALMN en SPartidasExpo\n", + "# Prorrateo proporcional al valor actual de cada partida.\n", + "# =============================================================\n", + "\n", + "import io as _io_val\n", + "\n", + "def cargar_excel_valores(path):\n", + " \"\"\"Lee Excel con 3 columnas: PEDIMENTO, VALOR_ME, VALOR_MN.\"\"\"\n", + " df = pd.read_excel(path, dtype=str)\n", + " norm = {c: c.strip().upper().replace(' ', '_') for c in df.columns}\n", + " df = df.rename(columns=norm)\n", + " aliases = {\n", + " 'PEDIMENTO': ['PEDIMENTO', 'PEDIMENTOEXPO', 'PEDIMENTO_EXPO'],\n", + " 'VALOR_ME': ['VALOR_ME', 'VALORME', 'VALOR_M_E', 'VALORTOTALME'],\n", + " 'VALOR_MN': ['VALOR_MN', 'VALORMN', 'VALOR_M_N', 'VALORTOTALMN'],\n", + " }\n", + " out = {}\n", + " for std, opts in aliases.items():\n", + " for o in opts:\n", + " if o in df.columns:\n", + " out[std] = df[o]; break\n", + " if std not in out:\n", + " raise ValueError(f'Falta la columna {std} (acepta: {opts})')\n", + " df2 = pd.DataFrame(out)\n", + " df2['PEDIMENTO'] = df2['PEDIMENTO'].astype(str).str.strip()\n", + " df2['VALOR_ME'] = pd.to_numeric(df2['VALOR_ME'], errors='coerce').fillna(0)\n", + " df2['VALOR_MN'] = pd.to_numeric(df2['VALOR_MN'], errors='coerce').fillna(0)\n", + " df2 = df2[df2['PEDIMENTO'] != ''].reset_index(drop=True)\n", + " return df2\n", + "\n", + "\n", + "def generar_plantilla_excel_valores():\n", + " df = pd.DataFrame([\n", + " {'PEDIMENTO': '07-3429-4015540', 'VALOR_ME': 12345.67, 'VALOR_MN': 234567.89},\n", + " {'PEDIMENTO': '07-3429-4015541', 'VALOR_ME': 8000.00, 'VALOR_MN': 152000.00},\n", + " ])\n", + " buf = _io_val.BytesIO()\n", + " with pd.ExcelWriter(buf, engine='openpyxl') as w:\n", + " df.to_excel(w, sheet_name='Valores', index=False)\n", + " buf.seek(0)\n", + " return buf.read()\n", + "\n", + "\n", + "def cargar_partidas_expo_pedimento(pedimento, db=None, esquema=None):\n", + " \"\"\"Trae partidas SPartidasExpo de un pedimento via SFacExp.\n", + " Si se pasan db y esquema, usa 3-part naming [db].[esquema].SPartidasExpo\n", + " para apuntar a otra BD de la misma instancia SQL Server.\"\"\"\n", + " if db and esquema:\n", + " prefix = f'[{db}].[{esquema}]'\n", + " else:\n", + " prefix = ''\n", + " pa = f'{prefix}.SPartidasExpo' if prefix else 'SPartidasExpo'\n", + " fa = f'{prefix}.SFacExp' if prefix else 'SFacExp'\n", + " sql = f\"\"\"\n", + " SELECT se.FACTURAEXPO, se.LINEA, spe.TIPOCAMBIO,\n", + " ISNULL(se.VALORTOTALME,0) AS VALORTOTALME,\n", + " ISNULL(se.VALORTOTALMN,0) AS VALORTOTALMN,\n", + " ISNULL(se.COSTOUNITARIOME,0) AS COSTOUNITARIOME,\n", + " ISNULL(se.CANTEXPO,0) AS CANTEXPO\n", + " FROM {pa} se\n", + " INNER JOIN {fa} spe ON spe.FACTURAEXPO = se.FACTURAEXPO\n", + " WHERE spe.PEDIMENTOEXPO = ?\n", + " ORDER BY se.FACTURAEXPO, se.LINEA\n", + " \"\"\"\n", + " return pd.read_sql(sql, scaii_conn, params=(str(pedimento).strip(),))\n", + "\n", + "\n", + "def analizar_valores(df_excel, modo='APLICAR_SIEMPRE', umbral_pct=50.0,\n", + " usar_shelter=False, progress=None, log=print):\n", + " \"\"\"Paso A: arma el plan de prorrateo. Devuelve (plan, resumen).\n", + " plan: una fila por partida con AJUSTE_ME, AJUSTE_MN, NUEVO_ME, NUEVO_MN.\n", + " resumen: una fila por pedimento con STATUS, factor, diferencia, etc.\n", + "\n", + " modo='APLICAR_SIEMPRE': escala todo sin importar la magnitud.\n", + " modo='USAR_UMBRAL': si |factor - 1| > umbral_pct/100, marca FUERA_DE_UMBRAL y no se aplica.\n", + " \"\"\"\n", + " prog = _Progress(progress)\n", + " if df_excel.empty:\n", + " log('Excel vacio.'); return pd.DataFrame(), pd.DataFrame()\n", + " # Localizar pedimentos en otras BDs si se pidio shelter.\n", + " ubic_map = {} # pedimento -> [(db, esquema), ...]\n", + " if usar_shelter:\n", + " try:\n", + " log('Buscando pedimentos en todas las BDs de la instancia...')\n", + " reporte, _, _ = buscar_pedimentos_en_bds(df_excel, progress=progress, log=log)\n", + " # Reconstruir mapping db/esquema (uno por (pedimento, BD))\n", + " sub = pd.read_sql(\"\"\"\n", + " SELECT name FROM sys.databases\n", + " WHERE database_id > 4 AND state_desc = 'ONLINE' AND HAS_DBACCESS(name) = 1\n", + " \"\"\", scaii_conn)\n", + " sub_set = set(sub['name'].astype(str).tolist())\n", + " for _, rr in reporte.iterrows():\n", + " if not rr['BasesEncontradas']: continue\n", + " ped = str(rr['PEDIMENTO']).strip()\n", + " for tok in str(rr['BasesEncontradas']).split(','):\n", + " tok = tok.strip()\n", + " if '.' in tok:\n", + " db, esq = tok.rsplit('.', 1)\n", + " if db in sub_set:\n", + " ubic_map.setdefault(ped, []).append((db, esq))\n", + " except Exception as e:\n", + " log(f'WARN shelter fallo: {e}. Usando BD actual.')\n", + " ubic_map = {}\n", + " prog.setup(len(df_excel), 'Analizando pedimentos...')\n", + " plan_rows = []\n", + " resumen_rows = []\n", + " umb = float(umbral_pct) / 100.0\n", + " for _, r in df_excel.iterrows():\n", + " pedimento = str(r['PEDIMENTO']).strip()\n", + " v_me_esp = float(r['VALOR_ME'] or 0)\n", + " v_mn_esp = float(r['VALOR_MN'] or 0)\n", + " # Lista de (db, esquema) a procesar para este pedimento\n", + " if usar_shelter:\n", + " destinos = ubic_map.get(pedimento, [])\n", + " if not destinos:\n", + " resumen_rows.append({'PEDIMENTO': pedimento, 'BaseDeDatos': '', 'Esquema': '',\n", + " 'SUM_ME_ACTUAL': 0, 'SUM_MN_ACTUAL': 0,\n", + " 'VALOR_ME_ESPERADO': v_me_esp, 'VALOR_MN_ESPERADO': v_mn_esp,\n", + " 'PARTIDAS': 0, 'FACTOR_ME': 0, 'FACTOR_MN': 0,\n", + " 'STATUS': 'NO_ENCONTRADO_EN_BDS'})\n", + " prog.step(desc=pedimento[:30]); continue\n", + " else:\n", + " destinos = [(None, None)]\n", + " # Procesar cada destino\n", + " for (db_dest, esq_dest) in destinos:\n", + " try:\n", + " df_part = cargar_partidas_expo_pedimento(pedimento, db=db_dest, esquema=esq_dest)\n", + " except Exception as e:\n", + " log(f' ERROR query {pedimento} [{db_dest or DB_ACTUAL}]: {e}')\n", + " resumen_rows.append({'PEDIMENTO': pedimento,\n", + " 'BaseDeDatos': db_dest or DB_ACTUAL, 'Esquema': esq_dest or '',\n", + " 'SUM_ME_ACTUAL': 0, 'SUM_MN_ACTUAL': 0,\n", + " 'VALOR_ME_ESPERADO': v_me_esp, 'VALOR_MN_ESPERADO': v_mn_esp,\n", + " 'PARTIDAS': 0, 'FACTOR_ME': 0, 'FACTOR_MN': 0,\n", + " 'STATUS': f'ERROR: {e}'})\n", + " continue\n", + " if df_part.empty:\n", + " resumen_rows.append({'PEDIMENTO': pedimento,\n", + " 'BaseDeDatos': db_dest or DB_ACTUAL, 'Esquema': esq_dest or '',\n", + " 'SUM_ME_ACTUAL': 0, 'SUM_MN_ACTUAL': 0,\n", + " 'VALOR_ME_ESPERADO': v_me_esp, 'VALOR_MN_ESPERADO': v_mn_esp,\n", + " 'PARTIDAS': 0, 'FACTOR_ME': 0, 'FACTOR_MN': 0,\n", + " 'STATUS': 'SIN_PARTIDAS'})\n", + " continue\n", + " sum_me = float(df_part['VALORTOTALME'].astype(float).sum())\n", + " sum_mn = float(df_part['VALORTOTALMN'].astype(float).sum())\n", + " if abs(sum_me) < 1e-9 and v_me_esp > 0:\n", + " status = 'SIN_BASE_ME'\n", + " elif abs(sum_mn) < 1e-9 and v_mn_esp > 0:\n", + " status = 'SIN_BASE_MN'\n", + " else:\n", + " f_me_chk = (v_me_esp / sum_me) if sum_me > 1e-9 else 0.0\n", + " f_mn_chk = (v_mn_esp / sum_mn) if sum_mn > 1e-9 else 0.0\n", + " if modo == 'USAR_UMBRAL':\n", + " if abs(f_me_chk - 1.0) > umb or abs(f_mn_chk - 1.0) > umb:\n", + " status = 'FUERA_DE_UMBRAL'\n", + " else:\n", + " status = 'AJUSTAR'\n", + " else:\n", + " status = 'AJUSTAR'\n", + "\n", + " f_me = (v_me_esp / sum_me) if sum_me > 1e-9 else 0.0\n", + " f_mn = (v_mn_esp / sum_mn) if sum_mn > 1e-9 else 0.0\n", + "\n", + " if status == 'AJUSTAR':\n", + " df_part = df_part.copy()\n", + " df_part['NUEVO_ME'] = (df_part['VALORTOTALME'].astype(float) * f_me).round(6)\n", + " df_part['NUEVO_MN'] = (df_part['VALORTOTALMN'].astype(float) * f_mn).round(6)\n", + " diff_me = round(v_me_esp - df_part['NUEVO_ME'].sum(), 6)\n", + " diff_mn = round(v_mn_esp - df_part['NUEVO_MN'].sum(), 6)\n", + " if abs(diff_me) > 1e-9:\n", + " idx_last = df_part.index[-1]\n", + " df_part.at[idx_last, 'NUEVO_ME'] = round(df_part.at[idx_last, 'NUEVO_ME'] + diff_me, 6)\n", + " if abs(diff_mn) > 1e-9:\n", + " idx_last = df_part.index[-1]\n", + " df_part.at[idx_last, 'NUEVO_MN'] = round(df_part.at[idx_last, 'NUEVO_MN'] + diff_mn, 6)\n", + " df_part['AJUSTE_ME'] = (df_part['NUEVO_ME'] - df_part['VALORTOTALME'].astype(float)).round(6)\n", + " df_part['AJUSTE_MN'] = (df_part['NUEVO_MN'] - df_part['VALORTOTALMN'].astype(float)).round(6)\n", + " df_part['PEDIMENTO'] = pedimento\n", + " for _, p in df_part.iterrows():\n", + " plan_rows.append({\n", + " 'PEDIMENTO': pedimento,\n", + " 'BaseDeDatos': db_dest or DB_ACTUAL,\n", + " 'Esquema': esq_dest or '',\n", + " 'FACTURAEXPO': p['FACTURAEXPO'],\n", + " 'LINEA': int(p['LINEA']),\n", + " 'CANTEXPO': float(p['CANTEXPO']),\n", + " 'TIPOCAMBIO': float(p['TIPOCAMBIO'] or 0),\n", + " 'VALORTOTALME_ACTUAL': float(p['VALORTOTALME']),\n", + " 'VALORTOTALMN_ACTUAL': float(p['VALORTOTALMN']),\n", + " 'AJUSTE_ME': float(p['AJUSTE_ME']),\n", + " 'AJUSTE_MN': float(p['AJUSTE_MN']),\n", + " 'NUEVO_ME': float(p['NUEVO_ME']),\n", + " 'NUEVO_MN': float(p['NUEVO_MN']),\n", + " })\n", + "\n", + " resumen_rows.append({\n", + " 'PEDIMENTO': pedimento,\n", + " 'BaseDeDatos': db_dest or DB_ACTUAL,\n", + " 'Esquema': esq_dest or '',\n", + " 'SUM_ME_ACTUAL': round(sum_me, 6),\n", + " 'SUM_MN_ACTUAL': round(sum_mn, 6),\n", + " 'VALOR_ME_ESPERADO': v_me_esp,\n", + " 'VALOR_MN_ESPERADO': v_mn_esp,\n", + " 'PARTIDAS': len(df_part),\n", + " 'FACTOR_ME': round(f_me, 6),\n", + " 'FACTOR_MN': round(f_mn, 6),\n", + " 'STATUS': status,\n", + " })\n", + " prog.step(desc=pedimento[:30])\n", + " prog.done('Analisis listo')\n", + " return pd.DataFrame(plan_rows), pd.DataFrame(resumen_rows)\n", + "\n", + "\n", + "def exportar_excel_valores(plan, resumen, ruta):\n", + " with pd.ExcelWriter(ruta, engine='openpyxl') as w:\n", + " if not plan.empty: plan.to_excel(w, sheet_name='Plan_Detalle', index=False)\n", + " if not resumen.empty: resumen.to_excel(w, sheet_name='Resumen', index=False)\n", + "\n", + "\n", + "def ejecutar_valores(plan, dry_run=True, aplicar_vtmn=True, aplicar_mptemp=False, progress=None, log=print):\n", + " \"\"\"Paso B: UPDATE SPartidasExpo por cada partida del plan, usando 3-part\n", + " naming si la fila trae BaseDeDatos/Esquema (cuando se uso shelter).\n", + " Transaccion por (pedimento, BD) rollback si alguna partida falla.\"\"\"\n", + " assert isinstance(dry_run, bool), 'dry_run debe ser bool'\n", + " prog = _Progress(progress)\n", + " if plan is None or plan.empty:\n", + " log('ERROR: plan vacio, corre primero \"Analizar\".')\n", + " return\n", + " # Si el plan no trae las columnas (compat), las agregamos vacias\n", + " if 'BaseDeDatos' not in plan.columns:\n", + " plan = plan.copy(); plan['BaseDeDatos'] = ''\n", + " if 'Esquema' not in plan.columns:\n", + " plan = plan.copy(); plan['Esquema'] = ''\n", + " # Agrupar por (pedimento, BD, esquema)\n", + " grupos = plan.groupby(['PEDIMENTO', 'BaseDeDatos', 'Esquema'], dropna=False)\n", + " prog.setup(len(grupos), 'Procesando pedimentos')\n", + " upd = errores = 0\n", + " err_list = []\n", + " for (ped, db, esq), fil in grupos:\n", + " if db and esq:\n", + " target = f'[{db}].[{esq}].SPartidasExpo'\n", + " else:\n", + " target = 'SPartidasExpo'\n", + " sets, params_tmpl = ['VALORTOTALME = ?'], ['ME']\n", + " if aplicar_vtmn:\n", + " sets.append('VALORTOTALMN = ?'); params_tmpl.append('MN')\n", + " if aplicar_mptemp:\n", + " sets.append('ValorMPTempMN = ?'); params_tmpl.append('MN')\n", + " upd_sql = f\"UPDATE {target} SET {', '.join(sets)} WHERE FACTURAEXPO = ? AND LINEA = ?\"\n", + " try:\n", + " with scaii_conn.cursor() as cur:\n", + " for _, p in fil.iterrows():\n", + " if not dry_run:\n", + " vals = []\n", + " for t in params_tmpl:\n", + " vals.append(float(p['NUEVO_ME']) if t == 'ME' else float(p['NUEVO_MN']))\n", + " vals.extend([p['FACTURAEXPO'], int(p['LINEA'])])\n", + " cur.execute(upd_sql, *vals)\n", + " upd += 1\n", + " if not dry_run: scaii_conn.commit()\n", + " except Exception as e:\n", + " if not dry_run: scaii_conn.rollback()\n", + " errores += 1\n", + " etq = f'{ped} [{db or DB_ACTUAL}]' if db else str(ped)\n", + " err_list.append((etq, str(e)))\n", + " log(f' ERROR {etq}: {e}')\n", + " prog.step(desc=str(ped)[:30])\n", + " prog.done(f'{upd} partidas')\n", + " bds = plan['BaseDeDatos'].replace('', pd.NA).dropna().unique().tolist()\n", + " log(f'\\n=== RESUMEN Valores (DRY_RUN={dry_run}) ===')\n", + " log(f' Partidas actualizadas: {upd:,}')\n", + " log(f' Pedimentos procesados: {plan[\"PEDIMENTO\"].nunique():,}')\n", + " log(f' BDs tocadas : {bds if bds else \"(solo la actual)\"}')\n", + " log(f' Errores : {errores:,}')\n", + " for f, e in err_list[:5]:\n", + " log(f' {f}: {e}')\n", + "\n", + "\n", + "# =============================================================\n", + "# SHELTER - Busca pedimentos en todas las BDs de la instancia\n", + "# (todas las que tengan SPedimentos.PEDIMENTO)\n", + "# =============================================================\n", + "\n", + "def buscar_pedimentos_en_bds(df_excel, progress=None, log=print):\n", + " \"\"\"Recorre sys.databases, detecta BDs con SPedimentos.PEDIMENTO y busca\n", + " los pedimentos del Excel en cada una. Devuelve (reporte, resumen_bd, faltantes).\"\"\"\n", + " prog = _Progress(progress)\n", + " if df_excel is None or df_excel.empty:\n", + " log('Excel vacio.')\n", + " return pd.DataFrame(), pd.DataFrame(), []\n", + " pedimentos = [str(p).strip() for p in df_excel['PEDIMENTO'].astype(str).tolist() if str(p).strip()]\n", + " if not pedimentos:\n", + " log('Sin pedimentos validos.')\n", + " return pd.DataFrame(), pd.DataFrame(), []\n", + "\n", + " # 1) BDs online accesibles\n", + " try:\n", + " df_dbs = pd.read_sql(\"\"\"\n", + " SELECT name FROM sys.databases\n", + " WHERE database_id > 4 AND state_desc = 'ONLINE' AND HAS_DBACCESS(name) = 1\n", + " ORDER BY name\n", + " \"\"\", scaii_conn)\n", + " except Exception as e:\n", + " log(f'ERROR listando BDs: {e}')\n", + " return pd.DataFrame(), pd.DataFrame(), []\n", + " log(f'BDs online accesibles: {len(df_dbs)}')\n", + " if df_dbs.empty:\n", + " return pd.DataFrame(), pd.DataFrame(), pedimentos\n", + "\n", + " # 2) Por cada BD, detectar SPedimentos.PEDIMENTO + buscar\n", + " prog.setup(len(df_dbs), 'Recorriendo BDs...')\n", + " rows = []\n", + " LOTE = 1000\n", + " for _, r in df_dbs.iterrows():\n", + " db = str(r['name'])\n", + " try:\n", + " sql_check = f\"\"\"\n", + " SELECT s.name AS Esquema\n", + " FROM [{db}].sys.tables t\n", + " JOIN [{db}].sys.schemas s ON s.schema_id = t.schema_id\n", + " JOIN [{db}].sys.columns c ON c.object_id = t.object_id\n", + " WHERE t.name = 'SPedimentos' AND c.name = 'PEDIMENTO'\n", + " \"\"\"\n", + " df_check = pd.read_sql(sql_check, scaii_conn)\n", + " except Exception as e:\n", + " log(f' [{db}] saltada (metadata): {e}')\n", + " prog.step(desc=db[:30]); continue\n", + " if df_check.empty:\n", + " prog.step(desc=db[:30]); continue\n", + " for _, ec in df_check.iterrows():\n", + " esquema = str(ec['Esquema'])\n", + " for i in range(0, len(pedimentos), LOTE):\n", + " sub = pedimentos[i:i+LOTE]\n", + " placeholders = ','.join(['?'] * len(sub))\n", + " sql_search = (f\"SELECT DISTINCT PEDIMENTO \"\n", + " f\"FROM [{db}].[{esquema}].SPedimentos \"\n", + " f\"WHERE PEDIMENTO IN ({placeholders})\")\n", + " try:\n", + " df_found = pd.read_sql(sql_search, scaii_conn, params=tuple(sub))\n", + " for _, f in df_found.iterrows():\n", + " rows.append({\n", + " 'PEDIMENTO': str(f['PEDIMENTO']).strip(),\n", + " 'BaseDeDatos': db,\n", + " 'Esquema': esquema,\n", + " })\n", + " except Exception as e:\n", + " log(f' [{db}].[{esquema}] saltada (busqueda): {e}')\n", + " break\n", + " prog.step(desc=db[:30])\n", + " df_result = pd.DataFrame(rows)\n", + "\n", + " # 3) Reporte por pedimento\n", + " df_in = pd.DataFrame({'PEDIMENTO': pedimentos}).drop_duplicates().reset_index(drop=True)\n", + " if df_result.empty:\n", + " reporte = df_in.assign(BasesEncontradas='', NumDBs=0)\n", + " else:\n", + " df_result['Ubicacion'] = df_result['BaseDeDatos'] + '.' + df_result['Esquema']\n", + " agg = (df_result.groupby('PEDIMENTO', as_index=False)\n", + " .agg(BasesEncontradas=('Ubicacion', lambda s: ', '.join(sorted(set(s)))),\n", + " NumDBs=('Ubicacion', 'nunique')))\n", + " reporte = df_in.merge(agg, on='PEDIMENTO', how='left')\n", + " reporte['BasesEncontradas'] = reporte['BasesEncontradas'].fillna('')\n", + " reporte['NumDBs'] = reporte['NumDBs'].fillna(0).astype(int)\n", + "\n", + " # 4) Resumen por BD\n", + " if df_result.empty:\n", + " resumen_bd = pd.DataFrame(columns=['BaseDeDatos', 'Esquema', 'PedimentosEncontrados'])\n", + " else:\n", + " resumen_bd = (df_result.groupby(['BaseDeDatos', 'Esquema'])\n", + " .size().reset_index(name='PedimentosEncontrados')\n", + " .sort_values('PedimentosEncontrados', ascending=False))\n", + "\n", + " # 5) Faltantes\n", + " faltantes = reporte.loc[reporte['NumDBs'] == 0, 'PEDIMENTO'].tolist()\n", + "\n", + " prog.done(f'{len(df_result)} hits en {df_result[\"BaseDeDatos\"].nunique() if not df_result.empty else 0} BDs')\n", + " return reporte, resumen_bd, faltantes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "logic-datastage", + "metadata": {}, + "outputs": [], + "source": [ + "# =============================================================\n", + "# DATASTAGE - Carga de archivos .asc en SQLite (tablas Registro)\n", + "# Migrado de PHP/Postgres a SQLite local.\n", + "# =============================================================\n", + "\n", + "import re as _re_ds\n", + "import csv as _csv_ds\n", + "from pathlib import Path as _Path_ds\n", + "\n", + "# En SQLite los tipos relevantes para limpiar valores numericos\n", + "_TIPOS_NUMERICOS_SQ = {'numeric', 'integer', 'real', 'float'}\n", + "\n", + "_BATCH_DS = 200\n", + "\n", + "\n", + "def _ds_conn():\n", + " \"\"\"Devuelve una conexion sqlite3 fresca a la base local de DataStage.\"\"\"\n", + " if not DATASTAGE_OK:\n", + " raise RuntimeError(DATASTAGE_MSG or 'SQLite no disponible')\n", + " return _sqlite3.connect(DATASTAGE_DB)\n", + "\n", + "\n", + "def extraer_tipo_registro(nombre_archivo):\n", + " \"\"\"'725199_501.asc' -> '501' | '725199_Inci.asc' -> 'Inci'.\"\"\"\n", + " m = _re_ds.search(r'_(\\d{3})\\.asc$', nombre_archivo, flags=_re_ds.IGNORECASE)\n", + " if m: return m.group(1)\n", + " m = _re_ds.search(r'_(\\w+)\\.asc$', nombre_archivo, flags=_re_ds.IGNORECASE)\n", + " if m: return m.group(1)\n", + " return None\n", + "\n", + "\n", + "def obtener_esquema_tabla(conn, tabla):\n", + " \"\"\"Columnas y tipos de una tabla SQLite via PRAGMA table_info.\"\"\"\n", + " cur = conn.execute(f'PRAGMA table_info(\"{tabla}\")')\n", + " rows = cur.fetchall()\n", + " # PRAGMA table_info: (cid, name, type, notnull, dflt_value, pk)\n", + " return [{'column_name': r[1], 'data_type': (r[2] or '').lower()} for r in rows]\n", + "\n", + "\n", + "def listar_tablas_registro(conn):\n", + " \"\"\"Tablas que empiezan con 'Registro' (case-insensitive).\"\"\"\n", + " cur = conn.execute(\n", + " \"SELECT name FROM sqlite_master \"\n", + " \"WHERE type='table' AND lower(name) LIKE 'registro%' \"\n", + " \"ORDER BY name\"\n", + " )\n", + " return [r[0] for r in cur.fetchall()]\n", + "\n", + "\n", + "def listar_archivos_asc(ruta_raiz):\n", + " \"\"\"Devuelve lista de Path con todos los .asc encontrados.\n", + " Soporta dos layouts: ruta/YYYY/*.asc (HONDA) o ruta/*.asc (plano).\"\"\"\n", + " raiz = _Path_ds(ruta_raiz)\n", + " if not raiz.exists() or not raiz.is_dir():\n", + " return []\n", + " archivos = []\n", + " subdirs_anio = [d for d in raiz.iterdir() if d.is_dir() and d.name.isdigit()]\n", + " if subdirs_anio:\n", + " for d in sorted(subdirs_anio):\n", + " for f in sorted(d.glob('*.asc')):\n", + " if f.is_file():\n", + " archivos.append(f)\n", + " for f in sorted(raiz.glob('*.asc')):\n", + " if f.is_file():\n", + " archivos.append(f)\n", + " return archivos\n", + "\n", + "\n", + "def _limpiar_valor(valor, tipo_dato):\n", + " v = (valor or '').strip()\n", + " # Filtrar NUL y caracteres de control\n", + " v = v.replace('\\x00', '').replace('\\r', '')\n", + " if v == '': return None\n", + " tipo = (tipo_dato or '').lower()\n", + " if tipo in _TIPOS_NUMERICOS_SQ:\n", + " v = v.replace(',', '.')\n", + " v = _re_ds.sub(r'[^0-9.\\-]', '', v)\n", + " if v in ('', '-'): return None\n", + " return v\n", + "\n", + "\n", + "def cargar_archivo_asc(conn, ruta_archivo):\n", + " \"\"\"Carga un .asc en su tabla Registro. Usa SAVEPOINT por archivo:\n", + " un error no rompe los archivos anteriores. Devuelve dict con resultado.\"\"\"\n", + " p = _Path_ds(ruta_archivo)\n", + " nombre = p.name\n", + " registro = extraer_tipo_registro(nombre)\n", + " if registro is None:\n", + " return {'archivo': nombre, 'tabla': None, 'filas': 0,\n", + " 'estatus': 'SKIP', 'mensaje': 'No se reconoce la estructura'}\n", + " tabla = f'Registro{registro}'\n", + " columnas = obtener_esquema_tabla(conn, tabla)\n", + " if not columnas:\n", + " return {'archivo': nombre, 'tabla': tabla, 'filas': 0,\n", + " 'estatus': 'SKIP', 'mensaje': f\"Tabla '{tabla}' no existe\"}\n", + " nombres_cols = [c['column_name'] for c in columnas]\n", + " tipos_cols = {c['column_name']: c['data_type'] for c in columnas}\n", + " n = len(nombres_cols)\n", + " cols_sql = ', '.join(f'\"{c}\"' for c in nombres_cols)\n", + " placeholders = ', '.join(['?'] * n)\n", + " insert_sql = f'INSERT INTO \"{tabla}\" ({cols_sql}) VALUES ({placeholders})'\n", + " filas_ins = 0\n", + " conn.execute('SAVEPOINT archivo_sp')\n", + " try:\n", + " with open(p, 'r', encoding='latin-1') as f:\n", + " reader = _csv_ds.reader(f, delimiter='|')\n", + " next(reader, None) # header\n", + " batch = []\n", + " for fila in reader:\n", + " fila = fila[:n]\n", + " while len(fila) < n:\n", + " fila.append('')\n", + " valores = [_limpiar_valor(v, tipos_cols[nombres_cols[i]])\n", + " for i, v in enumerate(fila)]\n", + " batch.append(tuple(valores))\n", + " if len(batch) >= _BATCH_DS:\n", + " conn.executemany(insert_sql, batch)\n", + " filas_ins += len(batch)\n", + " batch = []\n", + " if batch:\n", + " conn.executemany(insert_sql, batch)\n", + " filas_ins += len(batch)\n", + " conn.execute('RELEASE SAVEPOINT archivo_sp')\n", + " return {'archivo': nombre, 'tabla': tabla, 'filas': filas_ins,\n", + " 'estatus': 'OK', 'mensaje': f'{filas_ins:,} filas insertadas'}\n", + " except Exception as e:\n", + " conn.execute('ROLLBACK TO SAVEPOINT archivo_sp')\n", + " return {'archivo': nombre, 'tabla': tabla, 'filas': filas_ins,\n", + " 'estatus': 'ERROR', 'mensaje': str(e)}\n", + "\n", + "\n", + "def cargar_directorio_datastage(ruta_raiz, progress=None, log=print):\n", + " \"\"\"Itera todos los .asc encontrados y carga en su tabla Registro.\"\"\"\n", + " prog = _Progress(progress)\n", + " archivos = listar_archivos_asc(ruta_raiz)\n", + " if not archivos:\n", + " log(f'No se encontraron .asc en: {ruta_raiz}')\n", + " return pd.DataFrame()\n", + " log(f'Encontrados {len(archivos):,} archivos .asc')\n", + " prog.setup(len(archivos), 'Cargando .asc')\n", + " conn = _ds_conn()\n", + " # Control manual de transaccion para usar SAVEPOINT por archivo\n", + " conn.isolation_level = None\n", + " conn.execute('BEGIN')\n", + " resultados = []\n", + " ok = sk = er = 0\n", + " total_filas = 0\n", + " try:\n", + " for p in archivos:\n", + " r = cargar_archivo_asc(conn, p)\n", + " resultados.append(r)\n", + " if r['estatus'] == 'OK':\n", + " ok += 1; total_filas += r['filas']\n", + " elif r['estatus'] == 'SKIP':\n", + " sk += 1\n", + " else:\n", + " er += 1\n", + " log(f\" ERROR {r['archivo']}: {r['mensaje']}\")\n", + " prog.step(desc=f\"{r['estatus']} {r['archivo'][:25]}\")\n", + " conn.execute('COMMIT')\n", + " except Exception as e:\n", + " try: conn.execute('ROLLBACK')\n", + " except Exception: pass\n", + " log(f'EXCEPCION GLOBAL: {e}')\n", + " finally:\n", + " conn.close()\n", + " prog.done(f'{ok} OK / {sk} SKIP / {er} ERR')\n", + " log(f'\\n=== RESUMEN DataStage ===')\n", + " log(f' Archivos OK : {ok:,}')\n", + " log(f' Archivos SKIP : {sk:,}')\n", + " log(f' Archivos ERROR : {er:,}')\n", + " log(f' Filas insertadas: {total_filas:,}')\n", + " return pd.DataFrame(resultados)\n", + "\n", + "\n", + "def previsualizar_archivos_ds(ruta_raiz):\n", + " \"\"\"Devuelve DataFrame con archivos detectados + tabla destino + tamanio.\"\"\"\n", + " archivos = listar_archivos_asc(ruta_raiz)\n", + " rows = []\n", + " for p in archivos:\n", + " reg = extraer_tipo_registro(p.name)\n", + " rows.append({\n", + " 'archivo': p.name,\n", + " 'tabla_destino': f'Registro{reg}' if reg else '(no detectado)',\n", + " 'tamanio_kb': round(p.stat().st_size / 1024, 1),\n", + " 'ruta': str(p),\n", + " })\n", + " return pd.DataFrame(rows)\n", + "\n", + "\n", + "def truncar_tablas_registro(progress=None, log=print):\n", + " \"\"\"DELETE FROM en todas las tablas Registro* (SQLite no tiene TRUNCATE).\n", + " Devuelve dict {tabla: filas_antes}.\"\"\"\n", + " prog = _Progress(progress)\n", + " conn = _ds_conn()\n", + " resultado = {}\n", + " try:\n", + " tablas = listar_tablas_registro(conn)\n", + " if not tablas:\n", + " log('No hay tablas Registro* en la base.')\n", + " return resultado\n", + " prog.setup(len(tablas), 'Truncando tablas')\n", + " for t in tablas:\n", + " antes = conn.execute(f'SELECT COUNT(*) FROM \"{t}\"').fetchone()[0]\n", + " conn.execute(f'DELETE FROM \"{t}\"')\n", + " resultado[t] = antes\n", + " log(f' DELETE FROM {t}: {antes:,} filas eliminadas')\n", + " prog.step(desc=t[:30])\n", + " conn.commit()\n", + " prog.done(f'{len(tablas)} tablas truncadas')\n", + " log(f'\\nTotal: {sum(resultado.values()):,} filas eliminadas en {len(tablas)} tablas.')\n", + " except Exception as e:\n", + " try: conn.rollback()\n", + " except Exception: pass\n", + " log(f'ERROR: {e}')\n", + " prog.error('Error')\n", + " finally:\n", + " conn.close()\n", + " return resultado\n", + "\n", + "\n", + "def estadisticas_tablas_registro(progress=None):\n", + " \"\"\"Cuenta filas por tabla Registro*. Devuelve DataFrame columnas: tabla, filas.\"\"\"\n", + " prog = _Progress(progress)\n", + " conn = _ds_conn()\n", + " try:\n", + " tablas = listar_tablas_registro(conn)\n", + " if not tablas:\n", + " return pd.DataFrame(columns=['tabla', 'filas'])\n", + " prog.setup(len(tablas), 'Contando filas')\n", + " rows = []\n", + " for t in tablas:\n", + " n = conn.execute(f'SELECT COUNT(*) FROM \"{t}\"').fetchone()[0]\n", + " rows.append({'tabla': t, 'filas': n})\n", + " prog.step(desc=t[:30])\n", + " prog.done('Listo')\n", + " return pd.DataFrame(rows)\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "def obtener_muestra_tabla(tabla, limit=100, offset=0):\n", + " \"\"\"Devuelve hasta `limit` filas de la tabla (con OFFSET) como DataFrame.\"\"\"\n", + " conn = _ds_conn()\n", + " try:\n", + " sql = f'SELECT * FROM \"{tabla}\" LIMIT ? OFFSET ?'\n", + " return pd.read_sql(sql, conn, params=(int(limit), int(offset)))\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "# ============================================================\n", + "# REPORTES DataStage (migrados de Postgres a SQLite)\n", + "# Cambios respecto al SQL original:\n", + "# - LEFT(x, n) -> substr(x, 1, n)\n", + "# - RIGHT(LEFT(x::text,4),2) -> substr(x, 3, 2) (extrae YY)\n", + "# - col::text -> CAST(col AS TEXT) o se omite\n", + "# - EXTRACT(YEAR FROM x) -> CAST(strftime('%Y', x) AS INTEGER)\n", + "# - LATERAL JOIN -> subconsultas correlacionadas\n", + "# - %s -> ?\n", + "# ============================================================\n", + "\n", + "def cat_pedimentos_ds(fecha_ini, fecha_fin):\n", + " \"\"\"Estructura CAT Pedimentos. Toma Registro501 en el rango y marca si fue\n", + " rectificado (existe en Registro701 como pedimento anterior).\n", + " Migrado de generar_estructuracat.php.\"\"\"\n", + " sql = \"\"\"\n", + " WITH RECURSIVE historial_rect AS (\n", + " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", + " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", + " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", + " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", + " FROM \"Registro701\" R7\n", + " UNION ALL\n", + " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", + " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", + " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", + " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", + " FROM \"Registro701\" R7\n", + " INNER JOIN historial_rect HR ON\n", + " (substr(R7.\"FechaOperacionAnterior\", 3, 2) || '-' ||\n", + " substr(R7.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n", + " R7.\"PatenteAnterior\" || '-' || R7.\"PedimentoAnterior\")\n", + " =\n", + " (substr(HR.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(HR.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " HR.\"Patente\" || '-' || HR.\"Pedimento\")\n", + " )\n", + " SELECT\n", + " (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\") AS \"PEDIMENTO\",\n", + " CASE WHEN CAST(Q1.\"TipoOperacion\" AS TEXT) = '1' THEN 'I'\n", + " WHEN CAST(Q1.\"TipoOperacion\" AS TEXT) = '2' THEN 'E'\n", + " ELSE 'Otro' END AS \"TIPO PEDIMENTO\",\n", + " CASE WHEN EXISTS (\n", + " SELECT 1 FROM historial_rect H\n", + " WHERE (substr(H.\"FechaOperacionAnterior\", 3, 2) || '-' ||\n", + " substr(H.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n", + " H.\"PatenteAnterior\" || '-' || H.\"PedimentoAnterior\")\n", + " = (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", + " ) THEN 'R1' ELSE Q1.\"ClaveDocumento\" END AS \"CLAVE PEDIMENTO\",\n", + " Q1.\"FechaPagoReal\" AS \"FECHA PAGO\",\n", + " Q1.\"SeccionAduaneraEntrada\" AS \"SECCION ADUANERA\",\n", + " Q1.\"MedioTransporteEntrada_Salida\" AS \"MEDIO TRANSPORTE ENTRADA\",\n", + " Q1.\"MedioTransporteArribo\" AS \"MEDIO TRANSPORTE ARRIBO\",\n", + " Q1.\"MedioTransporteSalida\" AS \"MEDIO TRANSPORTE SALIDA\",\n", + " CASE WHEN EXISTS (\n", + " SELECT 1 FROM historial_rect H\n", + " WHERE (substr(H.\"FechaOperacionAnterior\", 3, 2) || '-' ||\n", + " substr(H.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n", + " H.\"PatenteAnterior\" || '-' || H.\"PedimentoAnterior\")\n", + " = (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", + " ) THEN 'Si' ELSE 'No' END AS \"SE RECTIFICO\",\n", + " (SELECT (substr(H.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(H.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " H.\"Patente\" || '-' || H.\"Pedimento\")\n", + " FROM historial_rect H\n", + " WHERE (substr(H.\"FechaOperacionAnterior\", 3, 2) || '-' ||\n", + " substr(H.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n", + " H.\"PatenteAnterior\" || '-' || H.\"PedimentoAnterior\")\n", + " = (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", + " ORDER BY H.\"FechaPagoReal\" DESC LIMIT 1) AS \"PEDIMENTO RECTIFICADO\",\n", + " Q1.\"TotalSeguros\" AS \"SEGUROS\",\n", + " Q1.\"TotalEmbalajes\" AS \"EMBALAJES\",\n", + " Q1.\"TotalIncrementables\" AS \"OTROS INCREMENTALES\"\n", + " FROM \"Registro501\" Q1\n", + " WHERE Q1.\"FechaPagoReal\" BETWEEN ? AND ?\n", + " ORDER BY Q1.\"FechaPagoReal\"\n", + " \"\"\"\n", + " conn = _ds_conn()\n", + " try:\n", + " return pd.read_sql(sql, conn, params=(fecha_ini, fecha_fin))\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "def cat_pedimentos_rect_ds(fecha_ini, fecha_fin):\n", + " \"\"\"Estructura CAT Pedimentos Rectificados. Desde Registro701 con tipo de\n", + " operacion tomado de Registro501 que lo origino.\n", + " Migrado de generar_estructuracatScaf.php.\"\"\"\n", + " sql = \"\"\"\n", + " SELECT\n", + " (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\") AS \"PEDIMENTO\",\n", + " COALESCE(CAST(Q2.\"TipoOperacion\" AS TEXT), 'Desconocido') AS \"TIPO PEDIMENTO\",\n", + " Q1.\"ClaveDocumento\" AS \"CLAVE PEDIMENTO\",\n", + " Q1.\"FechaPagoReal\" AS \"FECHA PAGO\",\n", + " Q1.\"SeccionAduanera\" AS \"SECCION ADUANERA\",\n", + " CASE WHEN Q3.\"Pedimento\" IS NOT NULL THEN 'SI' ELSE 'NO' END AS \"SE RECTIFICO\",\n", + " COALESCE(\n", + " (substr(Q3.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q3.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q3.\"Patente\" || '-' || Q3.\"Pedimento\"), '') AS \"PEDIMENTO RECTIFICADO\",\n", + " Q2.\"MedioTransporteEntrada_Salida\" AS \"MEDIO TRANSPORTE ENTRADA\",\n", + " Q2.\"MedioTransporteArribo\" AS \"MEDIO TRANSPORTE ARRIBO\",\n", + " Q2.\"MedioTransporteSalida\" AS \"MEDIO TRANSPORTE SALIDA\",\n", + " Q2.\"TotalSeguros\" AS \"SEGUROS\",\n", + " Q2.\"TotalEmbalajes\" AS \"EMBALAJES\",\n", + " Q2.\"TotalIncrementables\" AS \"OTROS INCREMENTALES\"\n", + " FROM \"Registro701\" Q1\n", + " LEFT JOIN \"Registro501\" Q2 ON\n", + " (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", + " =\n", + " (substr(Q2.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q2.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q2.\"Patente\" || '-' || Q2.\"Pedimento\")\n", + " LEFT JOIN \"Registro701\" Q3 ON\n", + " (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", + " =\n", + " (substr(Q3.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q3.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q3.\"Patente\" || '-' || Q3.\"PedimentoAnterior\")\n", + " WHERE Q1.\"FechaPagoReal\" BETWEEN ? AND ?\n", + " ORDER BY Q1.\"FechaPagoReal\"\n", + " \"\"\"\n", + " conn = _ds_conn()\n", + " try:\n", + " return pd.read_sql(sql, conn, params=(fecha_ini, fecha_fin))\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "def rectificados_ds(fecha_ini=None, fecha_fin=None, search=''):\n", + " \"\"\"Lista pedimentos de Registro501 que tienen rectificacion en Registro701.\n", + " Filtros opcionales por rango de fechas y por texto libre.\n", + " Migrado de rectificados.php.\"\"\"\n", + " sql = \"\"\"\n", + " SELECT R501.\"Patente\", R501.\"Pedimento\", R501.\"SeccionAduanera\",\n", + " R501.\"ClaveDocumento\", R501.\"FechaPagoReal\"\n", + " FROM \"Registro501\" R501\n", + " WHERE EXISTS (\n", + " SELECT 1 FROM \"Registro701\" R701\n", + " WHERE R701.\"PedimentoAnterior\" = R501.\"Pedimento\"\n", + " AND R701.\"PatenteAnterior\" = R501.\"Patente\"\n", + " AND R701.\"SeccionAduaneraAnterior\" = R501.\"SeccionAduanera\"\n", + " AND CAST(strftime('%Y', R501.\"FechaPagoReal\") AS INTEGER)\n", + " = CAST(strftime('%Y', R701.\"FechaOperacionAnterior\") AS INTEGER)\n", + " )\n", + " \"\"\"\n", + " params = []\n", + " if fecha_ini and fecha_fin:\n", + " sql += ' AND R501.\"FechaPagoReal\" BETWEEN ? AND ?'\n", + " params += [fecha_ini, fecha_fin]\n", + " if search:\n", + " sql += (' AND (R501.\"Pedimento\" LIKE ? OR R501.\"Patente\" LIKE ? '\n", + " 'OR R501.\"ClaveDocumento\" LIKE ?)')\n", + " like = f'%{search}%'\n", + " params += [like, like, like]\n", + " sql += ' ORDER BY R501.\"FechaPagoReal\" DESC'\n", + " conn = _ds_conn()\n", + " try:\n", + " return pd.read_sql(sql, conn, params=tuple(params))\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "def historial_rectificaciones_ds(patente, pedimento, seccion_aduanera, anio_operacion):\n", + " \"\"\"Cadena recursiva de rectificaciones para un pedimento dado.\n", + " Migrado de obtener_historial.php.\"\"\"\n", + " sql = \"\"\"\n", + " WITH RECURSIVE historial AS (\n", + " SELECT R7.\"Patente\", R7.\"Pedimento\", R7.\"SeccionAduanera\",\n", + " R7.\"ClaveDocumento\", R7.\"FechaPago\", R7.\"PedimentoAnterior\",\n", + " R7.\"PatenteAnterior\", R7.\"SeccionAduaneraAnterior\",\n", + " R7.\"DocumentoAnterior\", R7.\"FechaOperacionAnterior\",\n", + " R7.\"FechaPagoReal\"\n", + " FROM \"Registro701\" R7\n", + " WHERE R7.\"PedimentoAnterior\" = ?\n", + " AND R7.\"PatenteAnterior\" = ?\n", + " AND R7.\"SeccionAduaneraAnterior\" = ?\n", + " AND CAST(strftime('%Y', R7.\"FechaOperacionAnterior\") AS INTEGER) = ?\n", + " UNION ALL\n", + " SELECT R7.\"Patente\", R7.\"Pedimento\", R7.\"SeccionAduanera\",\n", + " R7.\"ClaveDocumento\", R7.\"FechaPago\", R7.\"PedimentoAnterior\",\n", + " R7.\"PatenteAnterior\", R7.\"SeccionAduaneraAnterior\",\n", + " R7.\"DocumentoAnterior\", R7.\"FechaOperacionAnterior\",\n", + " R7.\"FechaPagoReal\"\n", + " FROM \"Registro701\" R7\n", + " INNER JOIN historial HR ON\n", + " R7.\"PedimentoAnterior\" = HR.\"Pedimento\"\n", + " AND R7.\"PatenteAnterior\" = HR.\"Patente\"\n", + " AND R7.\"SeccionAduaneraAnterior\" = HR.\"SeccionAduanera\"\n", + " AND CAST(strftime('%Y', R7.\"FechaOperacionAnterior\") AS INTEGER)\n", + " = CAST(strftime('%Y', HR.\"FechaOperacionAnterior\") AS INTEGER)\n", + " )\n", + " SELECT * FROM historial ORDER BY \"FechaPagoReal\" ASC\n", + " \"\"\"\n", + " conn = _ds_conn()\n", + " try:\n", + " return pd.read_sql(sql, conn, params=(\n", + " str(pedimento), str(patente), str(seccion_aduanera), int(anio_operacion)))\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "def exportar_df_a_excel(df, nombre_prefijo):\n", + " \"\"\"Guarda DataFrame en xlsx con timestamp y devuelve la ruta.\"\"\"\n", + " import datetime as _dt\n", + " ts = _dt.datetime.now().strftime('%Y%m%d_%H%M%S')\n", + " ruta = os.path.join(os.getcwd(), f'{nombre_prefijo}_{ts}.xlsx')\n", + " df.to_excel(ruta, index=False)\n", + " return ruta\n", + "\n", + "\n", + "def encabezado_facturas_ds(fecha_ini, fecha_fin, tipo_op):\n", + " \"\"\"Encabezado de facturas (Impo o Expo). Migrado de\n", + " generar_estructura_factImpo.php y generar_estructura_factExpo.php.\n", + " tipo_op: 1 = Impo, 2 = Expo.\"\"\"\n", + " if int(tipo_op) not in (1, 2):\n", + " raise ValueError(\"tipo_op debe ser 1 (Impo) o 2 (Expo)\")\n", + " sql = \"\"\"\n", + " WITH RECURSIVE historial_rect AS (\n", + " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", + " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", + " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", + " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", + " FROM \"Registro701\" R7\n", + " UNION ALL\n", + " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", + " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", + " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", + " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", + " FROM \"Registro701\" R7\n", + " INNER JOIN historial_rect HR ON\n", + " (substr(R7.\"FechaOperacionAnterior\", 3, 2) || '-' ||\n", + " substr(R7.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n", + " R7.\"PatenteAnterior\" || '-' || R7.\"PedimentoAnterior\")\n", + " =\n", + " (substr(HR.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(HR.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " HR.\"Patente\" || '-' || HR.\"Pedimento\")\n", + " )\n", + " SELECT\n", + " (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\") AS \"PEDIMENTO\",\n", + " '1' AS \"REMESA\",\n", + " CASE WHEN EXISTS (\n", + " SELECT 1 FROM historial_rect H\n", + " WHERE (substr(H.\"FechaOperacionAnterior\", 3, 2) || '-' ||\n", + " substr(H.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n", + " H.\"PatenteAnterior\" || '-' || H.\"PedimentoAnterior\")\n", + " = (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", + " ) THEN Q1.\"Pedimento\" || '-' || 'R1'\n", + " ELSE Q1.\"Pedimento\" || '-' || Q1.\"ClaveDocumento\"\n", + " END AS \"NUMERO FACTURA\",\n", + " Q1.\"FechaPagoReal\" AS \"FECHA FACTURA\",\n", + " Q1.\"TipoCambio\" AS \"TIPO DE CAMBIO\",\n", + " '1' AS \"CLAVE PROVEEDOR\",\n", + " '8' AS \"CLAVE VENDIDO A\",\n", + " '8' AS \"CLAVE ENVIADO A\",\n", + " Q1.\"Patente\" AS \"AGENTE ADUANAL\",\n", + " '' AS \"CLAVE TRANSPORTISTA\",\n", + " '' AS \"NOMBRE CONDUCTOR\",\n", + " '' AS \"TIPO TRANSPORTE\",\n", + " '' AS \"NUMERO TRANSPORTE\",\n", + " 'ME' AS \"TIPO MONEDA\",\n", + " 'USD' AS \"CLAVE MONEDA\",\n", + " '' AS \"FLETES\",\n", + " '' AS \"VALORE SEGUROS\",\n", + " '' AS \"SEGUROS\",\n", + " '' AS \"EMBALAJES\",\n", + " '' AS \"OTROS INCREMENTALES\",\n", + " '' AS \"CLAVE INTERCOM\",\n", + " '' AS \"PRECINTO\",\n", + " Q1.\"FechaPagoReal\" AS \"FECHA EMISION\",\n", + " 'KILOS' AS \"TIPO PESO\",\n", + " '' AS \"E-DOCUMENT\",\n", + " '' AS \"NUM.OPERACION\",\n", + " Q1.\"SeccionAduanera\" AS \"ADUANA DE CRUCE\",\n", + " '' AS \"OBSERVACIONES E\",\n", + " '' AS \"LOCALIZACION\"\n", + " FROM \"Registro501\" Q1\n", + " WHERE CAST(Q1.\"TipoOperacion\" AS TEXT) = ?\n", + " AND Q1.\"FechaPagoReal\" BETWEEN ? AND ?\n", + " AND NOT EXISTS (\n", + " SELECT 1 FROM \"Registro701\" R7\n", + " WHERE (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", + " = (substr(R7.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(R7.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n", + " R7.\"PatenteAnterior\" || '-' || R7.\"Pedimento\")\n", + " )\n", + " ORDER BY Q1.\"FechaPagoReal\" ASC\n", + " \"\"\"\n", + " conn = _ds_conn()\n", + " try:\n", + " return pd.read_sql(sql, conn, params=(str(int(tipo_op)), fecha_ini, fecha_fin))\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "def tipo_cambio_ds(fecha_ini, fecha_fin):\n", + " \"\"\"Estructura Tipo de Cambio 501. Migrado de generar_estructura_tipo_cambio.php.\n", + " Devuelve PEDIMENTO, ClaveDocumento, TipoCambio, FechaPagoReal y una columna\n", + " INCONSISTENTE = True cuando existen distintos TipoCambio para la misma fecha.\"\"\"\n", + " sql = \"\"\"\n", + " SELECT\n", + " (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\") AS \"PEDIMENTO\",\n", + " Q1.\"ClaveDocumento\" AS \"CLAVE DOCUMENTO\",\n", + " Q1.\"TipoCambio\" AS \"TIPO CAMBIO\",\n", + " Q1.\"FechaPagoReal\" AS \"FECHA PAGO REAL\"\n", + " FROM \"Registro501\" Q1\n", + " WHERE Q1.\"FechaPagoReal\" BETWEEN ? AND ?\n", + " ORDER BY Q1.\"FechaPagoReal\" ASC\n", + " \"\"\"\n", + " conn = _ds_conn()\n", + " try:\n", + " df = pd.read_sql(sql, conn, params=(fecha_ini, fecha_fin))\n", + " finally:\n", + " conn.close()\n", + " if df.empty:\n", + " df['INCONSISTENTE'] = []\n", + " return df\n", + " # Inconsistencia: fechas (formato d/m/Y) donde hay > 1 valor distinto de TIPO CAMBIO\n", + " fechas_norm = pd.to_datetime(df['FECHA PAGO REAL']).dt.strftime('%d/%m/%Y')\n", + " distintos = df.assign(_fnorm=fechas_norm).groupby('_fnorm')['TIPO CAMBIO'].nunique()\n", + " fechas_incon = set(distintos[distintos > 1].index)\n", + " df['INCONSISTENTE'] = fechas_norm.isin(fechas_incon)\n", + " return df\n", + "\n", + "\n", + "def exportar_tipo_cambio_excel(df, ruta):\n", + " \"\"\"Exporta tipo_cambio_ds() a xlsx con celdas TIPO CAMBIO en rojo cuando\n", + " INCONSISTENTE=True. Usa openpyxl.\"\"\"\n", + " from openpyxl import Workbook\n", + " from openpyxl.styles import PatternFill, Alignment, Font\n", + " wb = Workbook()\n", + " ws = wb.active\n", + " ws.title = 'TipoCambio'\n", + " columnas_out = ['PEDIMENTO', 'CLAVE DOCUMENTO', 'TIPO CAMBIO', 'FECHA PAGO REAL']\n", + " for j, h in enumerate(columnas_out, start=1):\n", + " c = ws.cell(row=1, column=j, value=h)\n", + " c.alignment = Alignment(horizontal='center')\n", + " c.font = Font(bold=True)\n", + " rojo = PatternFill(start_color='FFFF0000', end_color='FFFF0000', fill_type='solid')\n", + " fblanca = Font(color='FFFFFFFF')\n", + " for i, fila in enumerate(df.itertuples(index=False), start=2):\n", + " d = fila._asdict() if hasattr(fila, '_asdict') else dict(zip(df.columns, fila))\n", + " ws.cell(row=i, column=1, value=d.get('PEDIMENTO')).alignment = Alignment(horizontal='center')\n", + " ws.cell(row=i, column=2, value=d.get('CLAVE DOCUMENTO')).alignment = Alignment(horizontal='center')\n", + " c_tc = ws.cell(row=i, column=3, value=d.get('TIPO CAMBIO'))\n", + " c_tc.alignment = Alignment(horizontal='center')\n", + " if d.get('INCONSISTENTE'):\n", + " c_tc.fill = rojo\n", + " c_tc.font = fblanca\n", + " fpr = d.get('FECHA PAGO REAL')\n", + " try:\n", + " fpr = pd.to_datetime(fpr).strftime('%d/%m/%Y')\n", + " except Exception:\n", + " fpr = str(fpr)\n", + " ws.cell(row=i, column=4, value=fpr).alignment = Alignment(horizontal='center')\n", + " wb.save(ruta)\n", + " return ruta\n", + "\n", + "\n", + "# ============================================================\n", + "# PARTIDAS - Catalogo de NUMPARTES y asignacion por similitud\n", + "# ============================================================\n", + "\n", + "_UM_MAP_551 = {\n", + " 1: 'KGS', 2: 'GR', 3: 'CM', 4: 'CM2', 5: 'BD FT', 6: 'PZA',\n", + " 7: 'CBZA',8: 'LT', 9: 'PAR', 12:'JGO',14:'TON', 17:'DEC',\n", + " 18:'CIEN',19:'DOCE',20:'CAJA',21:'BTL',22:'CARAT'\n", + "}\n", + "\n", + "\n", + "def _ds_limpiar_texto(t):\n", + " \"\"\"Mismo limpiador que usa el NLP de sustitutos.\"\"\"\n", + " if pd.isna(t) or str(t).strip() == '': return ''\n", + " s = _re_ds.sub(r'[^\\w\\s]', ' ', str(t).upper().strip())\n", + " return _re_ds.sub(r'\\s+', ' ', s).strip()\n", + "\n", + "\n", + "def crear_tabla_base_numpartes():\n", + " \"\"\"DDL idempotente para la tabla del catalogo del cliente.\"\"\"\n", + " conn = _ds_conn()\n", + " try:\n", + " conn.execute(\"\"\"\n", + " CREATE TABLE IF NOT EXISTS base_numpartes (\n", + " numparte TEXT PRIMARY KEY,\n", + " descripcion TEXT,\n", + " unimed TEXT,\n", + " fraccion TEXT\n", + " )\n", + " \"\"\")\n", + " conn.commit()\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "def cargar_excel_base_numpartes(path, log=print):\n", + " \"\"\"Lee Excel con columnas NUMPARTE, DESCRIPCION, UNIDAD DE MEDIDA, FRACCION.\n", + " Acepta variantes y hace UPSERT (acumular).\"\"\"\n", + " crear_tabla_base_numpartes()\n", + " df = pd.read_excel(path, dtype=str)\n", + " norm = {c: c.strip().upper().replace(' ', '_') for c in df.columns}\n", + " df = df.rename(columns=norm)\n", + " aliases = {\n", + " 'NUMPARTE': ['NUMPARTE', 'NUM_PARTE', 'NUMERO_DE_PARTE', 'NUMERO_PARTE', 'PARTE'],\n", + " 'DESCRIPCION': ['DESCRIPCION', 'DESCRIPCION_PARTE', 'DESC', 'DESCRIPCIONE'],\n", + " 'UNIMED': ['UNIDAD_DE_MEDIDA', 'UNIMED', 'UM', 'UNIDAD'],\n", + " 'FRACCION': ['FRACCION', 'FRACCION_ARANCELARIA', 'FRACC'],\n", + " }\n", + " out = {}\n", + " for std, opts in aliases.items():\n", + " for o in opts:\n", + " if o in df.columns:\n", + " out[std] = df[o]; break\n", + " if std not in out and std != 'FRACCION':\n", + " raise ValueError(f'Falta la columna {std} (acepta: {opts})')\n", + " if std not in out:\n", + " out[std] = ''\n", + " df2 = pd.DataFrame(out)\n", + " for c in df2.columns:\n", + " df2[c] = df2[c].fillna('').astype(str).str.strip()\n", + " df2 = df2[df2['NUMPARTE'] != ''].drop_duplicates(subset='NUMPARTE').reset_index(drop=True)\n", + " log(f'Filas validas en el Excel: {len(df2):,}')\n", + " conn = _ds_conn()\n", + " upserted = 0\n", + " try:\n", + " sql = \"\"\"\n", + " INSERT INTO base_numpartes (numparte, descripcion, unimed, fraccion)\n", + " VALUES (?, ?, ?, ?)\n", + " ON CONFLICT(numparte) DO UPDATE SET\n", + " descripcion = excluded.descripcion,\n", + " unimed = excluded.unimed,\n", + " fraccion = excluded.fraccion\n", + " \"\"\"\n", + " rows = [(r['NUMPARTE'], r['DESCRIPCION'], r['UNIMED'], r['FRACCION'])\n", + " for _, r in df2.iterrows()]\n", + " conn.executemany(sql, rows)\n", + " upserted = len(rows)\n", + " conn.commit()\n", + " except Exception as e:\n", + " conn.rollback()\n", + " log(f'ERROR: {e}')\n", + " finally:\n", + " conn.close()\n", + " log(f'Upsert completado: {upserted:,} filas')\n", + " return upserted\n", + "\n", + "\n", + "def listar_base_numpartes(limit=500):\n", + " \"\"\"Devuelve DataFrame con el catalogo actual.\"\"\"\n", + " crear_tabla_base_numpartes()\n", + " conn = _ds_conn()\n", + " try:\n", + " return pd.read_sql(\n", + " 'SELECT numparte, descripcion, unimed, fraccion FROM base_numpartes '\n", + " 'ORDER BY numparte LIMIT ?', conn, params=(int(limit),))\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "def truncar_base_numpartes(log=print):\n", + " \"\"\"DELETE FROM base_numpartes. Devuelve filas eliminadas.\"\"\"\n", + " crear_tabla_base_numpartes()\n", + " conn = _ds_conn()\n", + " try:\n", + " n = conn.execute('SELECT COUNT(*) FROM base_numpartes').fetchone()[0]\n", + " conn.execute('DELETE FROM base_numpartes')\n", + " conn.commit()\n", + " log(f'base_numpartes truncada: {n:,} filas eliminadas')\n", + " return n\n", + " except Exception as e:\n", + " conn.rollback()\n", + " log(f'ERROR: {e}')\n", + " return 0\n", + " finally:\n", + " conn.close()\n", + "\n", + "\n", + "def _ds_um_sigla(num_um):\n", + " \"\"\"Mapea codigo numerico de UM del 551 a sigla. Si ya es string, devuelve uppercase.\"\"\"\n", + " if pd.isna(num_um): return ''\n", + " try:\n", + " n = int(num_um)\n", + " return _UM_MAP_551.get(n, str(num_um).strip().upper())\n", + " except (ValueError, TypeError):\n", + " return str(num_um).strip().upper()\n", + "\n", + "\n", + "def _cargar_partidas_551(fecha_ini, fecha_fin, tipo_op):\n", + " \"\"\"Carga partidas del Registro551 en el rango. En SQLite usamos subconsultas\n", + " correlacionadas para obtener ClaveDocumento/TipoCambio del Registro501\n", + " asociado, y EXISTS Registro701 para flag rectificado.\"\"\"\n", + " sql = \"\"\"\n", + " SELECT\n", + " Q1.\"Patente\" AS patente,\n", + " Q1.\"Pedimento\" AS pedimento,\n", + " Q1.\"SeccionAduanera\" AS seccion_aduanera,\n", + " Q1.\"Fraccion\" AS fraccion,\n", + " Q1.\"SecuenciaFraccion\" AS secuencia_fraccion,\n", + " Q1.\"DescripcionMercancia\" AS descripcion_mercancia,\n", + " Q1.\"PrecioUnitario\" AS precio_unitario,\n", + " Q1.\"ValorAduana\" AS valor_aduana,\n", + " Q1.\"ValorComercial\" AS valor_comercial,\n", + " Q1.\"ValorDolares\" AS valor_dolares,\n", + " Q1.\"ValorAgregado\" AS valor_agregado,\n", + " Q1.\"CantidadUMComercial\" AS cantidad_um_comercial,\n", + " Q1.\"UnidadMedidaComercial\" AS unidad_medida_comercial,\n", + " Q1.\"CantidadUMTarifa\" AS cantidad_um_tarifa,\n", + " Q1.\"UnidadMedidaTarifa\" AS unidad_medida_tarifa,\n", + " Q1.\"MetodoValorizacion\" AS metodo_valorizacion,\n", + " Q1.\"PaisOrigenDestino\" AS pais_origen_destino,\n", + " Q1.\"ClaveDocumento\" AS clave_documento_551,\n", + " Q1.\"FechaPagoReal\" AS fecha_pago_real,\n", + " Q1.\"TipoOperacion\" AS tipo_operacion,\n", + " (SELECT R501i.\"ClaveDocumento\" FROM \"Registro501\" R501i\n", + " WHERE R501i.\"Patente\" = Q1.\"Patente\"\n", + " AND R501i.\"Pedimento\" = Q1.\"Pedimento\"\n", + " AND R501i.\"SeccionAduanera\" = Q1.\"SeccionAduanera\"\n", + " LIMIT 1) AS clave_documento_501,\n", + " (SELECT R501i.\"TipoCambio\" FROM \"Registro501\" R501i\n", + " WHERE R501i.\"Patente\" = Q1.\"Patente\"\n", + " AND R501i.\"Pedimento\" = Q1.\"Pedimento\"\n", + " AND R501i.\"SeccionAduanera\" = Q1.\"SeccionAduanera\"\n", + " LIMIT 1) AS tipo_cambio_501,\n", + " CASE WHEN EXISTS (\n", + " SELECT 1 FROM \"Registro701\" R7\n", + " WHERE (substr(Q1.\"FechaPagoReal\", 3, 2) || '-' ||\n", + " substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n", + " Q1.\"Patente\" || '-' || Q1.\"Pedimento\")\n", + " = (substr(R7.\"FechaOperacionAnterior\", 3, 2) || '-' ||\n", + " substr(R7.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n", + " R7.\"PatenteAnterior\" || '-' || R7.\"PedimentoAnterior\")\n", + " ) THEN 1 ELSE 0 END AS rectificado\n", + " FROM \"Registro551\" Q1\n", + " WHERE CAST(Q1.\"TipoOperacion\" AS TEXT) = ?\n", + " AND Q1.\"FechaPagoReal\" BETWEEN ? AND ?\n", + " ORDER BY Q1.\"FechaPagoReal\", Q1.\"Patente\", Q1.\"Pedimento\", Q1.\"SecuenciaFraccion\"\n", + " \"\"\"\n", + " conn = _ds_conn()\n", + " try:\n", + " df = pd.read_sql(sql, conn, params=(str(int(tipo_op)), fecha_ini, fecha_fin))\n", + " finally:\n", + " conn.close()\n", + " # Dedupe defensivo: si el Registro551 se cargo N veces, no multiplicar partidas\n", + " antes = len(df)\n", + " df = df.drop_duplicates(\n", + " subset=['patente', 'pedimento', 'seccion_aduanera', 'fraccion', 'secuencia_fraccion'],\n", + " keep='first').reset_index(drop=True)\n", + " dups = antes - len(df)\n", + " if dups > 0:\n", + " print(f' [INFO] Se omitieron {dups:,} filas duplicadas del Registro551 '\n", + " f'(misma Patente+Pedimento+SeccionAduanera+Fraccion+SecuenciaFraccion).')\n", + " return df\n", + "\n", + "\n", + "def asignar_numpartes_551(fecha_ini, fecha_fin, tipo_op, umbral_sim=0.80,\n", + " progress=None, log=print):\n", + " \"\"\"Genera la Estructura de Partidas a partir de Registro551, asignando NUMPARTE\n", + " por similitud contra base_numpartes y agrupando huerfanas con 'MP-R'.\"\"\"\n", + " assert int(tipo_op) in (1, 2), 'tipo_op debe ser 1 (Impo) o 2 (Expo)'\n", + " from sklearn.feature_extraction.text import TfidfVectorizer\n", + " from sklearn.metrics.pairwise import cosine_similarity\n", + " prog = _Progress(progress)\n", + " prog.setup(5, 'Cargando Registro551...')\n", + " df = _cargar_partidas_551(fecha_ini, fecha_fin, tipo_op)\n", + " log(f'Partidas Registro551 ({\"IMPO\" if int(tipo_op)==1 else \"EXPO\"}): {len(df):,}')\n", + " if df.empty:\n", + " prog.done('Sin datos'); return df\n", + " prog.step(desc='Preparando textos...')\n", + "\n", + " df['um_sigla'] = df['unidad_medida_comercial'].apply(_ds_um_sigla)\n", + " df['fraccion4'] = df['fraccion'].fillna('').astype(str).str[:4]\n", + " df['desc_norm'] = df['descripcion_mercancia'].apply(_ds_limpiar_texto)\n", + " df['NUMERO_PARTE'] = ''\n", + " df['MATCH_TIPO'] = ''\n", + "\n", + " try:\n", + " df_base = listar_base_numpartes(limit=10_000_000)\n", + " except Exception as e:\n", + " log(f'WARN cargando base_numpartes: {e}')\n", + " df_base = pd.DataFrame(columns=['numparte','descripcion','unimed','fraccion'])\n", + " if not df_base.empty:\n", + " df_base['fraccion4'] = df_base['fraccion'].fillna('').astype(str).str[:4]\n", + " df_base['um_sigla'] = df_base['unimed'].fillna('').astype(str).str.upper().str.strip()\n", + " df_base['desc_norm'] = df_base['descripcion'].apply(_ds_limpiar_texto)\n", + " log(f'Catalogo base_numpartes: {len(df_base):,}')\n", + "\n", + " secuenciales_por_f4 = {}\n", + " grupos = list(df.groupby(['fraccion4', 'um_sigla'], dropna=False))\n", + " total_grupos = len(grupos)\n", + " prog.setup(total_grupos, 'Procesando grupos...')\n", + "\n", + " for procesados, ((f4, um), g) in enumerate(grupos, start=1):\n", + " idxs_g = list(g.index)\n", + " textos_g = g['desc_norm'].tolist()\n", + " base_sub = df_base[(df_base['fraccion4'] == f4) & (df_base['um_sigla'] == um)] \\\n", + " if not df_base.empty else df_base\n", + " sin_base_idx = []\n", + " if not base_sub.empty and any(t for t in base_sub['desc_norm'].tolist()):\n", + " try:\n", + " vec = TfidfVectorizer(ngram_range=(1,2), sublinear_tf=True,\n", + " min_df=1, max_features=20000)\n", + " vec.fit(pd.concat([base_sub['desc_norm'],\n", + " pd.Series(textos_g)], ignore_index=True))\n", + " base_mat = vec.transform(base_sub['desc_norm'].tolist())\n", + " grp_mat = vec.transform(textos_g)\n", + " sims = cosine_similarity(grp_mat, base_mat)\n", + " for j, idx in enumerate(idxs_g):\n", + " best_j = int(sims[j].argmax())\n", + " best_s = float(sims[j][best_j])\n", + " if best_s >= umbral_sim and textos_g[j]:\n", + " df.at[idx, 'NUMERO_PARTE'] = str(base_sub.iloc[best_j]['numparte'])\n", + " df.at[idx, 'MATCH_TIPO'] = f'BASE ({best_s:.2f})'\n", + " else:\n", + " sin_base_idx.append(idx)\n", + " except Exception as e:\n", + " log(f' WARN TF-IDF base en grupo ({f4},{um}): {e}')\n", + " sin_base_idx.extend(idxs_g)\n", + " else:\n", + " sin_base_idx.extend(idxs_g)\n", + "\n", + " if sin_base_idx:\n", + " textos_h = [df.at[i, 'desc_norm'] for i in sin_base_idx]\n", + " no_vacios = [(i, t) for i, t in zip(sin_base_idx, textos_h) if t]\n", + " if no_vacios:\n", + " idxs_h = [x[0] for x in no_vacios]\n", + " txts_h = [x[1] for x in no_vacios]\n", + " try:\n", + " vec = TfidfVectorizer(ngram_range=(1,2), sublinear_tf=True,\n", + " min_df=1, max_features=20000)\n", + " mat = vec.fit_transform(txts_h)\n", + " sims = cosine_similarity(mat, mat)\n", + " parent = list(range(len(idxs_h)))\n", + " def _find(x):\n", + " while parent[x] != x:\n", + " parent[x] = parent[parent[x]]; x = parent[x]\n", + " return x\n", + " for a in range(len(idxs_h)):\n", + " for b in range(a+1, len(idxs_h)):\n", + " if sims[a][b] >= umbral_sim:\n", + " ra, rb = _find(a), _find(b)\n", + " if ra != rb: parent[rb] = ra\n", + " grupos_loc = {}\n", + " for a in range(len(idxs_h)):\n", + " grupos_loc.setdefault(_find(a), []).append(idxs_h[a])\n", + " for miembros in grupos_loc.values():\n", + " secuenciales_por_f4[f4] = secuenciales_por_f4.get(f4, 0) + 1\n", + " nuevo_np = f\"MP{f4 or 'XXXX'}-R{secuenciales_por_f4[f4]:03d}\"\n", + " for idx in miembros:\n", + " df.at[idx, 'NUMERO_PARTE'] = nuevo_np\n", + " df.at[idx, 'MATCH_TIPO'] = 'AUTO'\n", + " except Exception as e:\n", + " log(f' WARN union-find grupo ({f4},{um}): {e}')\n", + " for idx in idxs_h:\n", + " secuenciales_por_f4[f4] = secuenciales_por_f4.get(f4, 0) + 1\n", + " df.at[idx, 'NUMERO_PARTE'] = f\"MP{f4 or 'XXXX'}-R{secuenciales_por_f4[f4]:03d}\"\n", + " df.at[idx, 'MATCH_TIPO'] = 'AUTO'\n", + " for idx in sin_base_idx:\n", + " if df.at[idx, 'NUMERO_PARTE'] == '':\n", + " secuenciales_por_f4[f4] = secuenciales_por_f4.get(f4, 0) + 1\n", + " df.at[idx, 'NUMERO_PARTE'] = f\"MP{f4 or 'XXXX'}-R{secuenciales_por_f4[f4]:03d}\"\n", + " df.at[idx, 'MATCH_TIPO'] = 'AUTO_SINDESC'\n", + " prog.step(desc=f'{procesados}/{total_grupos}')\n", + "\n", + " df['anio_corto'] = pd.to_datetime(df['fecha_pago_real'], errors='coerce') \\\n", + " .dt.year.astype(str).str[-2:]\n", + " df['ped_full'] = (df['anio_corto'] + '-' + df['seccion_aduanera'].astype(str).str[:2]\n", + " + '-' + df['patente'].astype(str).str.zfill(4)\n", + " + '-' + df['pedimento'].astype(str).str.zfill(7))\n", + " df['clave_eff'] = df.apply(\n", + " lambda r: 'R1' if r['rectificado'] == 1\n", + " else (r['clave_documento_501'] or r['clave_documento_551'] or ''),\n", + " axis=1)\n", + " df['factura'] = df['pedimento'].astype(str).str.zfill(7).str[-7:] + '-' + df['clave_eff'].fillna('')\n", + " df['linea_seq'] = df.groupby('factura').cumcount() + 1\n", + "\n", + " prog.done('Asignacion completa')\n", + " cnt = df['MATCH_TIPO'].apply(lambda s: 'BASE' if str(s).startswith('BASE') else s).value_counts().to_dict()\n", + " log(f'Distribucion de match: {cnt}')\n", + "\n", + " # Costo unitario:\n", + " # Impo: ValorDolares / CantidadUMComercial\n", + " # Expo: (ValorDolares - ValorAgregado * TipoCambio) / CantidadUMComercial\n", + " _vd = pd.to_numeric(df['valor_dolares'], errors='coerce').fillna(0.0)\n", + " _va = pd.to_numeric(df['valor_agregado'], errors='coerce').fillna(0.0)\n", + " _tc = pd.to_numeric(df['tipo_cambio_501'], errors='coerce').fillna(0.0)\n", + " _qty = pd.to_numeric(df['cantidad_um_comercial'], errors='coerce').replace(0, np.nan)\n", + " if int(tipo_op) == 1:\n", + " df['costo_unitario_calc'] = (_vd / _qty).round(6)\n", + " else:\n", + " df['costo_unitario_calc'] = ((_vd - (_va * _tc)) / _qty).round(6)\n", + " df['costo_unitario_calc'] = df['costo_unitario_calc'].fillna(0.0)\n", + "\n", + " out = pd.DataFrame({\n", + " 'NUMERO FACTURA': df['factura'],\n", + " 'FECHA PAGO REAL': df['fecha_pago_real'],\n", + " 'LINEA': df['linea_seq'],\n", + " 'NUMERO DE PARTE': df['NUMERO_PARTE'],\n", + " 'CANTIDAD IMPORTADA': df['cantidad_um_comercial'],\n", + " 'UNIDAD DE MEDIDA': df['um_sigla'],\n", + " 'COSTO UNITARIO': df['costo_unitario_calc'],\n", + " 'PESO NETO': '',\n", + " 'PESO BRUTO': '',\n", + " 'CANTIDAD BULTOS': df['cantidad_um_tarifa'],\n", + " 'CLAVE BULTOS': df['unidad_medida_tarifa'],\n", + " 'PAIS ORIGEN': df['pais_origen_destino'],\n", + " 'FRACCION ARANCELARIA': df['fraccion'],\n", + " 'PREFERENCIA ARANCELARIA':'',\n", + " 'SECTOR': '',\n", + " 'FRACCION AMERICANA': '',\n", + " 'ORDEN DE COMPRA': '',\n", + " 'METODO DE VALORACION': df['metodo_valorizacion'],\n", + " 'NUMERO DE GUIA': '',\n", + " 'NUMERO DE ENTRADA': '',\n", + " 'CLIENTE': '',\n", + " 'FORMA DE PAGO': '',\n", + " 'MONTO IGI': df['valor_aduana'],\n", + " 'LOCALIZACION': '',\n", + " 'PERMISO RO': '',\n", + " 'LINEA RO': '',\n", + " 'VALOR TOTAL': df['valor_comercial'],\n", + " 'LOTE': df['linea_seq'],\n", + " 'INFORMACION ADICIONAL': df['secuencia_fraccion'],\n", + " 'CANTIDAD AUXILIAR': '',\n", + " 'U.M. AUXILIAR': '',\n", + " 'NUMERO DE ENTRADA 2': '',\n", + " 'MATCH_TIPO': df['MATCH_TIPO'],\n", + " })\n", + " return out\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ui", + "metadata": {}, + "outputs": [], + "source": [ + "header = W.HTML()\n", + "def _refresh_header():\n", + " color = '#1565C0' if CONEXION_OK else '#C62828'\n", + " header.value = (\n", + " f'
'\n", + " f'

Sistema de Utilerias 2.0 SCAII

'\n", + " f'
{CONEXION_MSG}
'\n", + " )\n", + "_refresh_header()\n", + "OUT_STYLE = {'border':'1px solid #ddd','padding':'8px','min_height':'120px'}\n", + "BAR_LAYOUT = {'width':'600px', 'height':'25px'}\n", + "BAR_STYLE = {'description_width':'170px'}\n", + "\n", + "def _mkbar(desc='Listo'):\n", + " return W.IntProgress(value=0, min=0, max=1, description=desc, layout=BAR_LAYOUT, style=BAR_STYLE, bar_style='')\n", + "\n", + "# ----- Tab 0: Conexion -----\n", + "out_conn = W.Output(layout=OUT_STYLE)\n", + "db_dropdown = W.Dropdown(options=[], description='Base de datos:', layout={'width':'500px'}, style={'description_width':'120px'})\n", + "btn_refresh = W.Button(description='Refrescar lista', icon='refresh', layout={'width':'180px'})\n", + "btn_conectar = W.Button(description='Conectar a esta DB', button_style='primary', icon='plug', layout={'width':'220px'})\n", + "lbl_actual = W.HTML()\n", + "\n", + "def _refresh_lista():\n", + " with out_conn:\n", + " clear_output()\n", + " print('Cargando lista de bases de datos...')\n", + " try: dbs = listar_databases()\n", + " except Exception as e: print(f'ERROR listando DBs: {e}'); return\n", + " if not dbs: print('No se pudieron obtener bases de datos. Revisa credenciales y permisos.')\n", + " else:\n", + " print(f'Encontradas {len(dbs)} bases:')\n", + " for db in dbs: print(f' - {db}')\n", + " db_dropdown.options = dbs\n", + " if DB_ACTUAL and DB_ACTUAL in dbs: db_dropdown.value = DB_ACTUAL\n", + " lbl_actual.value = f'DB actual: {DB_ACTUAL or \"sin conexion\"}'\n", + "btn_refresh.on_click(lambda _: _refresh_lista())\n", + "\n", + "def _on_conectar(_):\n", + " with out_conn:\n", + " clear_output()\n", + " if not db_dropdown.value: print('Selecciona una base de datos.'); return\n", + " target = db_dropdown.value\n", + " print(f'Conectando a [{target}]...')\n", + " ok = conectar_a_db(target)\n", + " if ok: print(f'OK. Cache reseteado. Ahora todas las pestanas usan [{DB_ACTUAL}].')\n", + " else: print(f'FALLO: {CONEXION_MSG}')\n", + " _refresh_header()\n", + " lbl_actual.value = f'DB actual: {DB_ACTUAL or \"sin conexion\"}'\n", + "btn_conectar.on_click(_on_conectar)\n", + "\n", + "tab_conn = W.VBox([\n", + " W.HTML('

Conexion a SQL Server

'\n", + " f'

Server: {SCAII_SERVER} | Usuario: {SCAII_USER}

'),\n", + " lbl_actual,\n", + " W.HTML('

Selecciona la base de datos. El cambio aplica a todas las pestanas; '\n", + " 'el cache de pronostico/analisis se resetea al cambiar.

'),\n", + " W.HBox([db_dropdown, btn_refresh]),\n", + " btn_conectar, out_conn,\n", + "])\n", + "_refresh_lista()\n", + "\n", + "# ----- Tab 1: Descargas -----\n", + "out_pron = W.Output(layout=OUT_STYLE); bar_pron = _mkbar('Pronostico')\n", + "out_p9 = W.Output(layout=OUT_STYLE); bar_p9 = _mkbar('Paso 9')\n", + "out_p10 = W.Output(layout=OUT_STYLE); bar_p10 = _mkbar('Paso 10')\n", + "btn_pron = W.Button(description='Calcular pronostico (paso 8)', button_style='primary', icon='play', layout={'width':'260px'})\n", + "modo9 = W.Dropdown(options=['NATURAL','DIRIGIDA'], value='NATURAL', description='Modo:', layout={'width':'250px'})\n", + "dry9 = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", + "fd9 = W.Text(placeholder='YYYY-MM-DD', description='Desde:', layout={'width':'250px'})\n", + "fh9 = W.Text(placeholder='YYYY-MM-DD', description='Hasta:', layout={'width':'250px'})\n", + "btn_p9 = W.Button(description='Ejecutar paso 9 (NA->AC)', button_style='warning', icon='check', layout={'width':'260px'})\n", + "modo10 = W.Dropdown(options=['DIRIGIDA','NATURAL'], value='DIRIGIDA', description='Modo:', layout={'width':'250px'})\n", + "dry10 = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", + "fd10 = W.Text(placeholder='YYYY-MM-DD', description='Desde:', layout={'width':'250px'})\n", + "fh10 = W.Text(placeholder='YYYY-MM-DD', description='Hasta:', layout={'width':'250px'})\n", + "facts_obj= W.Text(placeholder=\"factura1,factura2 (opcional)\", description='Facturas:', layout={'width':'480px'})\n", + "btn_p10 = W.Button(description='Ejecutar paso 10 (complementaria)', button_style='warning', icon='plus-square', layout={'width':'320px'})\n", + "\n", + "def _on_pron(_):\n", + " with out_pron:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})'); print('Cargando catalogos y calculando...'); t0 = time.time()\n", + " calcular_pronostico_paso8(progress=bar_pron)\n", + " cob = _state['cobertura_factura']; df = _state['df_descarga_all']\n", + " print(f'Tiempo: {time.time()-t0:.1f}s | Filas: {len(df):,} | Facturas: {len(cob):,}')\n", + " n100 = (cob['componentes_100pct'] == cob['componentes_total']).sum()\n", + " print(f' 100% cobertura: {n100:,} | parcial: {len(cob)-n100:,}')\n", + "btn_pron.on_click(_on_pron)\n", + "\n", + "def _on_p9(_):\n", + " with out_p9:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " ejecutar_paso9(modo9.value, dry9.value, fd9.value or None, fh9.value or None, log=print, progress=bar_p9)\n", + "btn_p9.on_click(_on_p9)\n", + "\n", + "def _on_p10(_):\n", + " with out_p10:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " fobj = [f.strip() for f in (facts_obj.value or '').split(',') if f.strip()] or None\n", + " ejecutar_paso10(modo10.value, dry10.value, fd10.value or None, fh10.value or None, fobj, log=print, progress=bar_p10)\n", + "btn_p10.on_click(_on_p10)\n", + "\n", + "tab_desc = W.VBox([\n", + " W.HTML('

Paso 8 anio‚ Pronostico de cobertura

'),\n", + " btn_pron, bar_pron, out_pron,\n", + " W.HTML('

Paso 9 anio‚ Descargas pendientes (NA anio‚¢ AC)

'),\n", + " W.HBox([modo9, dry9]), W.HBox([fd9, fh9]), btn_p9, bar_p9, out_p9,\n", + " W.HTML('

Paso 10 anio‚ Complementaria (sobre facturas AC)

'),\n", + " W.HBox([modo10, dry10]), W.HBox([fd10, fh10]), facts_obj, btn_p10, bar_p10, out_p10,\n", + "])\n", + "\n", + "# ----- Tab 2: Analisis Saldos -----\n", + "out_an_log = W.Output(layout=OUT_STYLE)\n", + "out_an_anio = W.Output()\n", + "out_an_imp_exp= W.Output()\n", + "out_an_pesos = W.Output()\n", + "out_an_cant = W.Output()\n", + "out_an_cant_um = W.Output()\n", + "bar_an = _mkbar('Analisis')\n", + "btn_an = W.Button(description='Cargar y analizar SSaldoTem', button_style='primary', icon='database', layout={'width':'280px'})\n", + "btn_an_xlsx = W.Button(description='Exportar Excel', button_style='success', icon='file-excel-o', layout={'width':'200px'})\n", + "\n", + "def _on_an(_):\n", + " with out_an_log:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " print('Cargando SSaldoTem...'); t0 = time.time()\n", + " df = cargar_analisis_saldos(progress=bar_an)\n", + " print(f' {len(df):,} lotes | tiempo: {time.time()-t0:.1f}s')\n", + " por_anio = calcular_por_anio_saldos(df); _state['por_anio_saldos'] = por_anio\n", + " print(f' Saldo cant: {df[\"SALDO_CANT\"].sum():,.2f}')\n", + " print(f' Saldo MN: ${df[\"SALDO_VMN\"].sum():,.2f}')\n", + " print(f' Saldo ME: ${df[\"SALDO_VME\"].sum():,.2f}')\n", + " print('Calculando IMPO vs EXPO...')\n", + " df_imp, df_exp, cmp = calcular_impo_expo_anio(progress=bar_an)\n", + " _state['df_imp'], _state['df_exp'], _state['cmp_impo_expo'] = df_imp, df_exp, cmp\n", + " print(f' IMPO aanio‚±os: {len(df_imp)} | EXPO aanio‚±os: {len(df_exp)} | Comparativo: {len(cmp)}')\n", + " print('Calculando pesos IMPO/EXPO/SALDO por aanio‚±o...')\n", + " cmp_pesos = calcular_pesos_por_anio(progress=bar_an)\n", + " _state['cmp_pesos_anio'] = cmp_pesos\n", + " print(f' Comparativo pesos: {len(cmp_pesos)} aanio‚±os')\n", + " print('Calculando cantidades IMPO/EXPO por aanio‚±o...')\n", + " cmp_cant = calcular_cantidades_por_anio(progress=bar_an)\n", + " _state['cmp_cantidades_anio'] = cmp_cant\n", + " print(f' Comparativo cantidades: {len(cmp_cant)} aanio‚±os')\n", + " print('Calculando cantidades IMPO/EXPO por aanio‚±o + UM...')\n", + " cmp_cant_um = calcular_cantidades_por_anio_um(progress=bar_an)\n", + " _state['cmp_cantidades_anio_um'] = cmp_cant_um\n", + " print(f' Comparativo cantidades por UM: {len(cmp_cant_um)} filas')\n", + " with out_an_anio:\n", + " clear_output()\n", + " display(HTML('

Saldo disponible por aanio‚±o (de entrada del lote)

'))\n", + " display(_state['por_anio_saldos'])\n", + " if not _state['por_anio_saldos'].empty:\n", + " fig = graficar_saldos_anio(_state['por_anio_saldos']); display(fig); plt.close(fig)\n", + " with out_an_imp_exp:\n", + " clear_output()\n", + " display(HTML('

IMPO vs EXPO por aanio‚±o

'))\n", + " display(_state['cmp_impo_expo'])\n", + " if not _state['cmp_impo_expo'].empty:\n", + " fig = graficar_impo_expo(_state['cmp_impo_expo']); display(fig); plt.close(fig)\n", + " with out_an_pesos:\n", + " clear_output()\n", + " display(HTML('

Peso por aanio‚±o anio‚ IMPO / EXPO / CONSUMIDO / DESCARGAS

'))\n", + " display(_state['cmp_pesos_anio'])\n", + " if not _state['cmp_pesos_anio'].empty:\n", + " fig = graficar_pesos_anio(_state['cmp_pesos_anio']); display(fig); plt.close(fig)\n", + " with out_an_cant:\n", + " clear_output()\n", + " display(HTML('

Cantidades por aanio‚±o anio‚ IMPO vs EXPO

'))\n", + " display(_state['cmp_cantidades_anio'])\n", + " if not _state['cmp_cantidades_anio'].empty:\n", + " fig = graficar_cantidades_anio(_state['cmp_cantidades_anio']); display(fig); plt.close(fig)\n", + " with out_an_cant_um:\n", + " clear_output()\n", + " display(HTML('

Cantidades por aanio‚±o + UM (separado por unidad de medida)

'))\n", + " display(_state['cmp_cantidades_anio_um'])\n", + " if not _state['cmp_cantidades_anio_um'].empty:\n", + " fig = graficar_cantidades_anio_um(_state['cmp_cantidades_anio_um']); display(fig); plt.close(fig)\n", + "btn_an.on_click(_on_an)\n", + "\n", + "def _on_an_xlsx(_):\n", + " with out_an_log:\n", + " if 'df_saldos_full' not in _state: print('Corre primero \"Cargar y analizar\".'); return\n", + " path = exportar_excel_analisis(_state['df_saldos_full'], _state['por_anio_saldos'],\n", + " _state['df_imp'], _state['df_exp'], _state['cmp_impo_expo'],\n", + " _state.get('cmp_pesos_anio'),\n", + " _state.get('cmp_cantidades_anio'),\n", + " _state.get('cmp_cantidades_anio_um'))\n", + " print(f'Excel: {path}')\n", + "btn_an_xlsx.on_click(_on_an_xlsx)\n", + "\n", + "tab_an = W.VBox([\n", + " W.HTML('

Ananio‚¡lisis SSaldoTem + IMPO vs EXPO

'),\n", + " W.HBox([btn_an, btn_an_xlsx]), bar_an, out_an_log, out_an_anio, out_an_imp_exp, out_an_pesos, out_an_cant, out_an_cant_um,\n", + "])\n", + "\n", + "# ----- Tab 3: Sustitutos NLP -----\n", + "out_nlp = W.Output(layout=OUT_STYLE); bar_nlp = _mkbar('Sustitutos NLP')\n", + "min_sim = W.FloatSlider(value=0.80, min=0.5, max=1.0, step=0.05, description='Min similitud:', readout_format='.0%', layout={'width':'380px'})\n", + "top_n = W.IntSlider(value=3, min=1, max=10, step=1, description='Top-N:', layout={'width':'380px'})\n", + "dry_nlp = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", + "btn_nlp = W.Button(description='Generar sustitutos NLP', button_style='primary', icon='magic', layout={'width':'260px'})\n", + "\n", + "def _on_nlp(_):\n", + " with out_nlp:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " try:\n", + " generar_sustitutos_nlp(min_sim.value, top_n.value, dry_nlp.value, log=print, progress=bar_nlp)\n", + " if 'df_nuevos_sust' in _state and not _state['df_nuevos_sust'].empty:\n", + " print('\\nMuestra de los primeros 10:')\n", + " display(_state['df_nuevos_sust'].head(10))\n", + " except Exception as e: print(f'ERROR: {e}')\n", + "btn_nlp.on_click(_on_nlp)\n", + "\n", + "tab_nlp = W.VBox([\n", + " W.HTML('

Sustitutos NLP anio‚ TF-IDF + coseno

'),\n", + " min_sim, top_n, dry_nlp, btn_nlp, bar_nlp, out_nlp,\n", + "])\n", + "\n", + "# ----- Tab 4: Descarga % KGS (paso 12) -----\n", + "out_pron12 = W.Output(layout=OUT_STYLE); bar_pron12 = _mkbar('Pronostico KG')\n", + "out_p12 = W.Output(layout=OUT_STYLE); bar_p12 = _mkbar('Paso 12')\n", + "out_p12c = W.Output(layout=OUT_STYLE); bar_p12c = _mkbar('Paso 12 comp')\n", + "btn_pron12 = W.Button(description='Calcular pronostico KG', button_style='primary', icon='play', layout={'width':'260px'})\n", + "modo12 = W.Dropdown(options=['NATURAL','DIRIGIDA'], value='NATURAL', description='Modo:', layout={'width':'250px'})\n", + "dry12 = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", + "fd12 = W.Text(placeholder='YYYY-MM-DD', description='Desde:', layout={'width':'250px'})\n", + "fh12 = W.Text(placeholder='YYYY-MM-DD', description='Hasta:', layout={'width':'250px'})\n", + "btn_p12 = W.Button(description='Ejecutar paso 12 (NA->AC)', button_style='warning', icon='check', layout={'width':'260px'})\n", + "modo12c = W.Dropdown(options=['DIRIGIDA','NATURAL'], value='DIRIGIDA', description='Modo:', layout={'width':'250px'})\n", + "dry12c = W.Checkbox(value=True, description='DRY_RUN (simular)')\n", + "fd12c = W.Text(placeholder='YYYY-MM-DD', description='Desde:', layout={'width':'250px'})\n", + "fh12c = W.Text(placeholder='YYYY-MM-DD', description='Hasta:', layout={'width':'250px'})\n", + "facts_obj12= W.Text(placeholder=\"factura1,factura2 (opcional)\", description='Facturas:', layout={'width':'480px'})\n", + "btn_p12c = W.Button(description='Ejecutar complementaria KG', button_style='warning', icon='plus-square', layout={'width':'320px'})\n", + "\n", + "def _on_pron12(_):\n", + " with out_pron12:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})'); print('Calculando pronostico KG...'); t0 = time.time()\n", + " calcular_pronostico_paso12_kg(progress=bar_pron12)\n", + " cob = _state['cobertura_factura_kg']; df = _state['df_descarga_kg']\n", + " print(f'Tiempo: {time.time()-t0:.1f}s | Filas: {len(df):,} | Facturas: {len(cob):,}')\n", + " n100 = (cob['componentes_100pct'] == cob['componentes_total']).sum()\n", + " print(f' 100% cobertura: {n100:,} | parcial: {len(cob)-n100:,}')\n", + "btn_pron12.on_click(_on_pron12)\n", + "\n", + "def _on_p12(_):\n", + " with out_p12:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " ejecutar_paso12_kg(modo12.value, dry12.value, fd12.value or None, fh12.value or None, log=print, progress=bar_p12)\n", + "btn_p12.on_click(_on_p12)\n", + "\n", + "def _on_p12c(_):\n", + " with out_p12c:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " fobj = [f.strip() for f in (facts_obj12.value or '').split(',') if f.strip()] or None\n", + " ejecutar_paso12_complementaria_kg(modo12c.value, dry12c.value, fd12c.value or None, fh12c.value or None, fobj, log=print, progress=bar_p12c)\n", + "btn_p12c.on_click(_on_p12c)\n", + "\n", + "tab_kg = W.VBox([\n", + " W.HTML('

Paso 12 anio‚ Descarga por % de KGS

'\n", + " '

'\n", + " 'cant_req = (BOM.CANTIDAD / 100) anio‚ PESONETO de la partida. Mismo PEPS, UM y sustitutos.

'),\n", + " btn_pron12, bar_pron12, out_pron12,\n", + " W.HTML('

Paso 12 anio‚ Descargas pendientes KG (NA anio‚¢ AC)

'),\n", + " W.HBox([modo12, dry12]), W.HBox([fd12, fh12]), btn_p12, bar_p12, out_p12,\n", + " W.HTML('

Paso 12 anio‚ Complementaria KG (sobre facturas AC)

'),\n", + " W.HBox([modo12c, dry12c]), W.HBox([fd12c, fh12c]), facts_obj12, btn_p12c, bar_p12c, out_p12c,\n", + "])\n", + "\n", + "\n", + "# ----- Tab 5: CTM (Reasignacion de descargas) -----\n", + "out_ctm_log = W.Output(layout=OUT_STYLE)\n", + "out_ctm_tabla = W.Output()\n", + "bar_ctm = _mkbar('Analisis CTM')\n", + "upload_ctm = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n", + "chk_use_mapping= W.Checkbox(value=True, description='Usar mapping para priorizar')\n", + "btn_plantilla = W.Button(description='Descargar plantilla Excel', button_style='info', icon='download', layout={'width':'260px'})\n", + "btn_ctm_analizar = W.Button(description='Analizar CTM (sin escribir)', button_style='primary', icon='search', layout={'width':'280px'})\n", + "btn_ctm_xlsx = W.Button(description='Exportar Excel completo', button_style='success', icon='file-excel-o', layout={'width':'240px'})\n", + "\n", + "_html_formato = '''\n", + "
\n", + "Formato esperado del Excel del cliente\n", + "

La primera hoja del archivo debe contener al menos estas columnas (solo las dos primeras son obligatorias):

\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
ColumnaEjemploObligatoria
Facturas CTMAAU112023RFR0481, NIS112023RFR0035SI
PEDIMENTO COMPLETO75-3076-4021492SI
PATENTE3076no
ADUANA75no
PEDIMENTO4021492no
OperacionImportacionno
Clave de pedimentoF4no
\n", + "

La celda Facturas CTM puede traer varias facturas separadas por coma; la herramienta las separa automaticamente.

\n", + "
\n", + "'''\n", + "\n", + "def _on_plantilla(_):\n", + " with out_ctm_log:\n", + " clear_output()\n", + " path = generar_plantilla_excel_ctm()\n", + " display(HTML(f'
'\n", + " f'Plantilla generada
'\n", + " f'{path}
'\n", + " f'Abre el archivo, agrega tus datos, guarda y luego subelo con el boton \"Subir Excel\".
'))\n", + "btn_plantilla.on_click(_on_plantilla)\n", + "\n", + "def _on_ctm_analizar(_):\n", + " with out_ctm_log:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " mapping_df = None\n", + " if chk_use_mapping.value and len(upload_ctm.value) > 0:\n", + " try:\n", + " if isinstance(upload_ctm.value, dict):\n", + " fname = list(upload_ctm.value.keys())[0]\n", + " file_bytes = upload_ctm.value[fname]['content']\n", + " else:\n", + " file_bytes = upload_ctm.value[0]['content']\n", + " mapping_df, raw = cargar_excel_mapping_ctm(file_bytes)\n", + " _state['ctm_mapping'] = mapping_df\n", + " print(f'Excel cargado: {len(mapping_df):,} relaciones CTM<->Pedimento')\n", + " except Exception as e:\n", + " print(f'WARN cargando Excel: {e} (se procesa sin mapping)')\n", + " mapping_df = None\n", + " elif chk_use_mapping.value:\n", + " print('Sin Excel cargado; se procesa sin prioridad de mapping.')\n", + " print('Analizando facturas CTM...')\n", + " plan, resumen = analizar_ctm(df_mapping=mapping_df, progress=bar_ctm)\n", + " print(f'Plan: {len(plan):,} filas | Resumen: {len(resumen):,} combinaciones')\n", + " if not resumen.empty:\n", + " asignados = (plan['STATUS']=='ASIGNADO').sum()\n", + " faltantes = (plan['STATUS']=='FALTANTE').sum()\n", + " print(f' Filas ASIGNADAS: {asignados:,}')\n", + " print(f' Filas FALTANTE : {faltantes:,}')\n", + " display(HTML('

En pantalla se muestran solo las primeras 50 filas del plan. '\n", + " 'Para ver el detalle completo presiona Exportar Excel completo.

'))\n", + " with out_ctm_tabla:\n", + " clear_output()\n", + " if 'ctm_resumen' in _state and not _state['ctm_resumen'].empty:\n", + " display(HTML('

Resumen CTM (por factura + linea + componente)

'))\n", + " display(_state['ctm_resumen'])\n", + " display(HTML('

Plan detalle (primeras 50 filas)

'))\n", + " display(_state['ctm_plan'].head(50))\n", + "btn_ctm_analizar.on_click(_on_ctm_analizar)\n", + "\n", + "def _on_ctm_xlsx(_):\n", + " with out_ctm_log:\n", + " if 'ctm_plan' not in _state:\n", + " display(HTML('
Corre primero Analizar CTM.
'))\n", + " return\n", + " path = exportar_excel_ctm(_state['ctm_plan'], _state['ctm_resumen'])\n", + " display(HTML(f'
'\n", + " f'Excel generado con el detalle completo
'\n", + " f'{path}
'\n", + " f'Contiene tres hojas: Resumen, Plan_Detalle (todas las filas, no solo 50) y Faltantes.
'))\n", + "btn_ctm_xlsx.on_click(_on_ctm_xlsx)\n", + "\n", + "# ---- Paso B - Ejecucion (CTM) ----\n", + "modo_ctm = W.Dropdown(options=['NATURAL','DIRIGIDA'], value='NATURAL', description='Modo:', layout={'width':'250px'})\n", + "dry_ctm = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n", + "btn_ctm_ejecutar = W.Button(description='Ejecutar reasignacion (Paso B)', button_style='warning', icon='play', layout={'width':'300px'})\n", + "bar_ctm_b = _mkbar('Ejecucion CTM')\n", + "out_ctm_ejec = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ctm_ejecutar(_):\n", + " with out_ctm_ejec:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " ejecutar_reasignacion_ctm(modo=modo_ctm.value, dry_run=dry_ctm.value, log=print, progress=bar_ctm_b)\n", + "btn_ctm_ejecutar.on_click(_on_ctm_ejecutar)\n", + "tab_ctm = W.VBox([\n", + " W.HTML('

CTM - Reasignacion de descargas desde Cambio de Regimen

'\n", + " '

'\n", + " 'Analiza las facturas CTM pendientes y propone una asignacion de descargas '\n", + " 'tomadas del pool existente del modulo de Cambio de Regimen. Esta pestana '\n", + " 'es solo lectura (Paso A): no escribe en la base de datos.

'),\n", + " W.HTML(_html_formato),\n", + " btn_plantilla,\n", + " W.HTML('

Sube tu Excel con el mapeo Facturas CTM <-> Pedimento F4:

'),\n", + " W.HBox([upload_ctm, chk_use_mapping]),\n", + " W.HBox([btn_ctm_analizar, btn_ctm_xlsx]), bar_ctm,\n", + " out_ctm_log, out_ctm_tabla,\n", + " W.HTML('

Paso B - Ejecutar reasignacion en la base de datos

'\n", + " '

Toma el plan calculado arriba y aplica los cambios en SDescargaT y SFacExp. Corre primero con DRY_RUN activado para revisar.

'),\n", + " W.HBox([modo_ctm, dry_ctm]),\n", + " btn_ctm_ejecutar, bar_ctm_b,\n", + " out_ctm_ejec,\n", + "])\n", + "\n", + "\n", + "# ===== Tab 7: Saldos Vencidos (Utileria Forma 5) =====\n", + "upload_sv = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n", + "btn_plantilla_sv = W.Button(description='Descargar plantilla Excel', button_style='info', icon='download', layout={'width':'250px'})\n", + "w_fecha_ini_sv = W.DatePicker(description='Fecha inicio:', value=None, layout={'width':'260px'})\n", + "w_fecha_fin_sv = W.DatePicker(description='Fecha fin:', value=None, layout={'width':'260px'})\n", + "btn_sv_analizar = W.Button(description='Analizar (sin escribir)', button_style='primary', icon='search', layout={'width':'280px'})\n", + "btn_sv_xlsx = W.Button(description='Exportar Excel completo', button_style='info', icon='download', layout={'width':'280px'})\n", + "bar_sv = _mkbar('Analisis Saldos Vencidos')\n", + "out_sv_log = W.Output(layout=OUT_STYLE)\n", + "out_sv_tabla = W.Output(layout=OUT_STYLE)\n", + "\n", + "_html_formato_sv = '''\n", + "
\n", + "Formato esperado del Excel (3 columnas)\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
ColumnaEjemplo
FACTURAIMPOF1234567
CANTIDAD_SALDO100.0
FRACCION_IMPO85044010
\n", + "

Las fechas inicio/fin filtran SSaldoTem por FECHAFACTURA_ISO.

\n", + "
\n", + "'''\n", + "\n", + "def _on_plantilla_sv(_):\n", + " with out_sv_log:\n", + " clear_output()\n", + " bts = generar_plantilla_excel_saldos_vencidos()\n", + " path = os.path.join(os.getcwd(), 'plantilla_saldos_vencidos.xlsx')\n", + " with open(path, 'wb') as f: f.write(bts)\n", + " display(HTML(f'
'\n", + " f'Plantilla generada
'\n", + " f'{path}
'))\n", + "btn_plantilla_sv.on_click(_on_plantilla_sv)\n", + "\n", + "def _on_sv_analizar(_):\n", + " with out_sv_log:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " if len(upload_sv.value) == 0:\n", + " print('ERROR: sube un Excel primero.'); return\n", + " if w_fecha_ini_sv.value is None or w_fecha_fin_sv.value is None:\n", + " print('ERROR: define fecha inicio y fecha fin.'); return\n", + " try:\n", + " if isinstance(upload_sv.value, dict):\n", + " fname = list(upload_sv.value.keys())[0]\n", + " file_bytes = upload_sv.value[fname]['content']\n", + " else:\n", + " file_bytes = upload_sv.value[0]['content']\n", + " tmp_path = os.path.join(os.getcwd(), '_upload_sv.xlsx')\n", + " with open(tmp_path, 'wb') as f: f.write(file_bytes)\n", + " df_excel = cargar_excel_saldos_vencidos(tmp_path)\n", + " print(f'Excel cargado: {len(df_excel):,} filas')\n", + " except Exception as e:\n", + " print(f'ERROR cargando Excel: {e}'); return\n", + " print('Analizando saldos vencidos...')\n", + " plan, resumen = analizar_saldos_vencidos(df_excel, str(w_fecha_ini_sv.value), str(w_fecha_fin_sv.value), progress=bar_sv)\n", + " _state['sv_plan'] = plan\n", + " _state['sv_resumen'] = resumen\n", + " print(f'Plan: {len(plan):,} filas | Resumen: {len(resumen):,} saldos')\n", + " if not resumen.empty:\n", + " ok = (resumen['STATUS']=='PRORRATEADO').sum()\n", + " sin = (resumen['STATUS']=='SIN_DESCARGAS').sum()\n", + " cz = (resumen['STATUS']=='CANTDESC_CERO').sum()\n", + " print(f' Saldos PRORRATEADOS : {ok:,}')\n", + " print(f' Saldos SIN_DESCARGAS: {sin:,}')\n", + " print(f' Saldos CANTDESC_CERO: {cz:,}')\n", + " with out_sv_tabla:\n", + " clear_output()\n", + " if 'sv_resumen' in _state and not _state['sv_resumen'].empty:\n", + " display(HTML('

Resumen Saldos Vencidos

'))\n", + " display(_state['sv_resumen'])\n", + " display(HTML('

Plan detalle (primeras 50 filas)

'))\n", + " display(_state['sv_plan'].head(50))\n", + "btn_sv_analizar.on_click(_on_sv_analizar)\n", + "\n", + "def _on_sv_xlsx(_):\n", + " with out_sv_log:\n", + " if 'sv_plan' not in _state:\n", + " display(HTML('
Corre primero Analizar.
'))\n", + " return\n", + " path = os.path.join(os.getcwd(), 'plan_saldos_vencidos.xlsx')\n", + " exportar_excel_saldos_vencidos(_state['sv_plan'], _state['sv_resumen'], path)\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{path}
'))\n", + "btn_sv_xlsx.on_click(_on_sv_xlsx)\n", + "\n", + "dry_sv = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n", + "btn_sv_ejecutar = W.Button(description='Ejecutar prorrateo (Paso B)', button_style='warning', icon='play', layout={'width':'300px'})\n", + "bar_sv_b = _mkbar('Ejecucion Saldos Vencidos')\n", + "out_sv_ejec = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_sv_ejecutar(_):\n", + " with out_sv_ejec:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " if 'sv_plan' not in _state:\n", + " print('ERROR: corre primero Analizar.'); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " ejecutar_saldos_vencidos(_state['sv_plan'], dry_run=dry_sv.value, log=print, progress=bar_sv_b)\n", + "btn_sv_ejecutar.on_click(_on_sv_ejecutar)\n", + "\n", + "tab_sv = W.VBox([\n", + " W.HTML('

Saldos Vencidos - Utileria Forma 5

'\n", + " '

'\n", + " 'Prorratea masivamente saldos IMPO vencidos entre las descargas EXPO que comparten '\n", + " '(FACTURAIMPO, NUMPARTE, UMEXITENCIA). El Paso A es solo lectura; el Paso B aplica '\n", + " 'UPDATE en SDescargaT y SSaldoTem (no toca SFacExp).

'),\n", + " W.HTML(_html_formato_sv),\n", + " btn_plantilla_sv,\n", + " W.HTML('

Sube tu Excel y define el rango de fechas (FECHAFACTURA_ISO):

'),\n", + " W.HBox([upload_sv]),\n", + " W.HBox([w_fecha_ini_sv, w_fecha_fin_sv]),\n", + " W.HBox([btn_sv_analizar, btn_sv_xlsx]), bar_sv,\n", + " out_sv_log, out_sv_tabla,\n", + " W.HTML('

Paso B - Ejecutar prorrateo en la base de datos

'\n", + " '

UPDATE en cada SDescargaT matched + UPDATE en SSaldoTem (CANTUSADA, VALORUSADOMN/ME, PESOUSADO, PESOBRUTOUSADO). Corre primero con DRY_RUN activado.

'),\n", + " W.HBox([dry_sv]),\n", + " btn_sv_ejecutar, bar_sv_b,\n", + " out_sv_ejec,\n", + "])\n", + "\n", + "\n", + "# ===== Saldos Vencidos - Modo Automatico (sin Excel) =====\n", + "w_fecha_ini_sv2 = W.DatePicker(description='Fecha inicio:', value=None, layout={'width':'260px'})\n", + "w_fecha_fin_sv2 = W.DatePicker(description='Fecha fin:', value=None, layout={'width':'260px'})\n", + "btn_sv2_analizar = W.Button(description='Buscar saldos vencidos', button_style='primary', icon='search', layout={'width':'280px'})\n", + "btn_sv2_xlsx = W.Button(description='Exportar Excel completo', button_style='info', icon='download', layout={'width':'280px'})\n", + "bar_sv2 = _mkbar('Busqueda automatica')\n", + "out_sv2_log = W.Output(layout=OUT_STYLE)\n", + "out_sv2_tabla = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_sv2_analizar(_):\n", + " with out_sv2_log:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " if w_fecha_ini_sv2.value is None or w_fecha_fin_sv2.value is None:\n", + " print('ERROR: define fecha inicio y fecha fin.'); return\n", + " import datetime as _dt\n", + " hoy = _dt.date.today().isoformat()\n", + " print(f'Buscando saldos en SSaldoTem con FECHAFACTURA_ISO entre {w_fecha_ini_sv2.value} y {w_fecha_fin_sv2.value}')\n", + " print(f' y FECHAVENC_ISO < {hoy} (vencidos a hoy)')\n", + " plan, resumen = analizar_saldos_vencidos_auto(\n", + " str(w_fecha_ini_sv2.value), str(w_fecha_fin_sv2.value), fecha_corte=hoy, progress=bar_sv2)\n", + " _state['sv2_plan'] = plan\n", + " _state['sv2_resumen'] = resumen\n", + " print(f'Plan: {len(plan):,} filas | Resumen: {len(resumen):,} saldos vencidos')\n", + " if not resumen.empty:\n", + " ok = (resumen['STATUS']=='PRORRATEADO').sum()\n", + " sin = (resumen['STATUS']=='SIN_DESCARGAS').sum()\n", + " cz = (resumen['STATUS']=='CANTDESC_CERO').sum()\n", + " print(f' Saldos PRORRATEADOS : {ok:,}')\n", + " print(f' Saldos SIN_DESCARGAS: {sin:,}')\n", + " print(f' Saldos CANTDESC_CERO: {cz:,}')\n", + " with out_sv2_tabla:\n", + " clear_output()\n", + " if 'sv2_resumen' in _state and not _state['sv2_resumen'].empty:\n", + " display(HTML('

Resumen Saldos Vencidos (busqueda automatica)

'))\n", + " display(_state['sv2_resumen'])\n", + " display(HTML('

Plan detalle (primeras 50 filas)

'))\n", + " display(_state['sv2_plan'].head(50))\n", + "btn_sv2_analizar.on_click(_on_sv2_analizar)\n", + "\n", + "def _on_sv2_xlsx(_):\n", + " with out_sv2_log:\n", + " if 'sv2_plan' not in _state:\n", + " display(HTML('
Corre primero Buscar saldos vencidos.
'))\n", + " return\n", + " path = os.path.join(os.getcwd(), 'plan_saldos_vencidos_auto.xlsx')\n", + " exportar_excel_saldos_vencidos(_state['sv2_plan'], _state['sv2_resumen'], path)\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{path}
'))\n", + "btn_sv2_xlsx.on_click(_on_sv2_xlsx)\n", + "\n", + "dry_sv2 = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n", + "btn_sv2_ejecutar = W.Button(description='Ejecutar prorrateo (Paso B)', button_style='warning', icon='play', layout={'width':'300px'})\n", + "bar_sv2_b = _mkbar('Ejecucion Saldos Vencidos (auto)')\n", + "out_sv2_ejec = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_sv2_ejecutar(_):\n", + " with out_sv2_ejec:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " if 'sv2_plan' not in _state:\n", + " print('ERROR: corre primero Buscar saldos vencidos.'); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " ejecutar_saldos_vencidos(_state['sv2_plan'], dry_run=dry_sv2.value, log=print, progress=bar_sv2_b)\n", + "btn_sv2_ejecutar.on_click(_on_sv2_ejecutar)\n", + "\n", + "tab_sv.children = tuple(list(tab_sv.children) + [\n", + " W.HTML('

Modo Automatico - sin Excel

'\n", + " '

'\n", + " 'Define el rango de fechas (filtra FECHAFACTURA_ISO) y el sistema busca automaticamente '\n", + " 'los saldos con FECHAVENC_ISO < hoy (vencidos) y SALDO_DISPONIBLE > 0. '\n", + " 'Aplica el mismo prorrateo y los mismos UPDATEs que el modo Excel.

'),\n", + " W.HBox([w_fecha_ini_sv2, w_fecha_fin_sv2]),\n", + " W.HBox([btn_sv2_analizar, btn_sv2_xlsx]), bar_sv2,\n", + " out_sv2_log, out_sv2_tabla,\n", + " W.HTML('

Paso B - Ejecutar prorrateo automatico

'\n", + " '

Mismos UPDATEs: SDescargaT por descarga + SSaldoTem por saldo. Corre primero con DRY_RUN activado.

'),\n", + " W.HBox([dry_sv2]),\n", + " btn_sv2_ejecutar, bar_sv2_b,\n", + " out_sv2_ejec,\n", + "])\n", + "\n", + "\n", + "# ===== Saldos Vencidos - Grafica por anio =====\n", + "w_sv_metric = W.Dropdown(\n", + " options=[('Valor MN','SALDO_VMN'),('Valor ME','SALDO_VME'),\n", + " ('Cantidad','SALDO_CANT'),('Lotes','LOTES')],\n", + " value='SALDO_VMN', description='Metrica:', layout={'width':'260px'})\n", + "w_sv_eje = W.Dropdown(\n", + " options=[('Anio Vencimiento','VENCIMIENTO'),('Anio Factura','FACTURA')],\n", + " value='VENCIMIENTO', description='Eje X:', layout={'width':'260px'})\n", + "btn_sv_grafica = W.Button(description='Ver grafica saldos vencidos por anio',\n", + " button_style='primary', icon='bar-chart', layout={'width':'320px'})\n", + "out_sv_grafica = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_sv_grafica(_):\n", + " with out_sv_grafica:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " import datetime as _dt\n", + " hoy = _dt.date.today().isoformat()\n", + " print(f'Vencidos a {hoy} (FECHAVENC_ISO < hoy y SALDO_DISPONIBLE > 0)')\n", + " df = cargar_saldos_vencidos_por_anio(fecha_corte=hoy, eje=w_sv_eje.value)\n", + " if df.empty:\n", + " print('No hay saldos vencidos.'); return\n", + " print(f'Anios con saldos vencidos: {len(df)}')\n", + " graficar_saldos_vencidos_por_anio(df, metric=w_sv_metric.value)\n", + "btn_sv_grafica.on_click(_on_sv_grafica)\n", + "\n", + "tab_sv.children = tuple(list(tab_sv.children) + [\n", + " W.HTML('

Saldos vencidos por anio (vista general)

'\n", + " '

'\n", + " 'Resumen global de saldos en SSaldoTem cuyo FECHAVENC_ISO < hoy y '\n", + " 'SALDO_DISPONIBLE > 0, agrupados por anio de vencimiento. '\n", + " 'No depende del rango de fechas de arriba.

'),\n", + " W.HBox([w_sv_metric, w_sv_eje, btn_sv_grafica]),\n", + " out_sv_grafica,\n", + "])\n", + "\n", + "\n", + "# ===== Tab 8: DataStage (Subir .asc a SQLite local) =====\n", + "w_ds_ruta = W.Text(\n", + " value=DATASTAGE_ROOT or '', placeholder=r'C:\\ruta\\DATASTAGE_HONDA',\n", + " description='Carpeta:', layout={'width':'650px'},\n", + " style={'description_width':'80px'})\n", + "btn_ds_listar = W.Button(description='Listar archivos', button_style='info', icon='search', layout={'width':'200px'})\n", + "btn_ds_cargar = W.Button(description='Cargar todos a SQLite', button_style='warning', icon='upload', layout={'width':'260px'})\n", + "btn_ds_tablas = W.Button(description='Ver tablas Registro', button_style='', icon='database', layout={'width':'280px'})\n", + "bar_ds = _mkbar('DataStage')\n", + "out_ds_lista = W.Output(layout=OUT_STYLE)\n", + "out_ds_log = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _ds_msg_conexion():\n", + " if not DATASTAGE_OK:\n", + " display(HTML(f'
'\n", + " f'SQLite no disponible.
'\n", + " f'{DATASTAGE_MSG}
'\n", + " f'Revisa permisos de escritura en la carpeta del .exe (donde se crea datastage.db).
'))\n", + " return False\n", + " display(HTML(f'
{DATASTAGE_MSG}
'))\n", + " return True\n", + "\n", + "def _on_ds_listar(_):\n", + " with out_ds_lista:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " ruta = (w_ds_ruta.value or '').strip()\n", + " if not ruta:\n", + " print('ERROR: define la ruta de la carpeta con los .asc')\n", + " return\n", + " df = previsualizar_archivos_ds(ruta)\n", + " if df.empty:\n", + " print(f'No se encontraron .asc en: {ruta}')\n", + " return\n", + " print(f'Archivos detectados: {len(df):,}')\n", + " display(df.head(200))\n", + " if len(df) > 200:\n", + " print(f'(mostrando 200 de {len(df)})')\n", + " # Resumen por tabla destino\n", + " resumen = (df.groupby('tabla_destino', as_index=False)\n", + " .agg(archivos=('archivo','count'),\n", + " tamanio_kb=('tamanio_kb','sum')))\n", + " display(HTML('

Resumen por tabla destino

'))\n", + " display(resumen)\n", + "btn_ds_listar.on_click(_on_ds_listar)\n", + "\n", + "def _on_ds_cargar(_):\n", + " with out_ds_log:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " ruta = (w_ds_ruta.value or '').strip()\n", + " if not ruta:\n", + " print('ERROR: define la ruta de la carpeta con los .asc')\n", + " return\n", + " print(f'Iniciando carga desde: {ruta}')\n", + " df_res = cargar_directorio_datastage(ruta, progress=bar_ds, log=print)\n", + " if not df_res.empty:\n", + " _state['ds_resultados'] = df_res\n", + " display(HTML('

Resultado por archivo

'))\n", + " display(df_res)\n", + "btn_ds_cargar.on_click(_on_ds_cargar)\n", + "\n", + "def _on_ds_tablas(_):\n", + " with out_ds_lista:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " try:\n", + " with _ds_conn() as c:\n", + " tablas = listar_tablas_registro(c)\n", + " if not tablas:\n", + " print('No hay tablas Registro* en la base SQLite.')\n", + " return\n", + " print(f'Tablas Registro en la base: {len(tablas)}')\n", + " df = pd.DataFrame({'tabla': tablas})\n", + " display(df)\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_tablas.on_click(_on_ds_tablas)\n", + "\n", + "tab_datastage = W.VBox([\n", + " W.HTML('

DataStage - Subir archivos .asc a SQLite

'\n", + " '

'\n", + " 'Carga masiva de archivos .asc del DataStage en sus tablas Registro<NNN>. '\n", + " 'Migrado del modulo PHP HOME/DATASTAGE. La estructura de los archivos '\n", + " 'se infiere del nombre (_NNN.ascRegistroNNN); '\n", + " 'el separador es |, el encoding es latin-1, y se omite el header.

'),\n", + " W.HTML('
'\n", + " 'Estructura esperada de la carpeta'\n", + " '
    '\n", + " '
  • Layout HONDA: RAIZ/2020/*.asc, RAIZ/2021/*.asc, ... (solo .asc directos, sin entrar a subcarpetas de meses)
  • '\n", + " '
  • Layout plano: RAIZ/*.asc
  • '\n", + " '
'\n", + " '

Tablas destino se crean automaticamente al primer arranque (base datastage.db).

'\n", + " '
'),\n", + " W.HBox([w_ds_ruta]),\n", + " W.HBox([btn_ds_listar, btn_ds_tablas, btn_ds_cargar]), bar_ds,\n", + " out_ds_lista,\n", + " W.HTML('

Log de carga

'),\n", + " out_ds_log,\n", + "])\n", + "\n", + "\n", + "\n", + "\n", + "# ===== DataStage: Limpiar y Estadisticas =====\n", + "chk_ds_confirmar_truncate = W.Checkbox(\n", + " value=False, description='Confirmo limpiar TODAS las tablas Registro*',\n", + " indent=False, layout={'width':'420px'})\n", + "btn_ds_truncate = W.Button(description='Limpiar todas las tablas',\n", + " button_style='danger', icon='trash', layout={'width':'250px'})\n", + "btn_ds_stats = W.Button(description='Ver estadisticas',\n", + " button_style='primary', icon='chart-bar', layout={'width':'200px'})\n", + "w_ds_tabla_sel = W.Dropdown(options=[], description='Tabla:',\n", + " layout={'width':'320px'})\n", + "w_ds_limit = W.IntText(value=100, description='Filas:',\n", + " layout={'width':'180px'}, style={'description_width':'60px'})\n", + "btn_ds_ver_datos = W.Button(description='Ver datos',\n", + " button_style='info', icon='eye', layout={'width':'180px'})\n", + "btn_ds_export = W.Button(description='Exportar a Excel',\n", + " button_style='', icon='file-excel', layout={'width':'200px'})\n", + "bar_ds_stats = _mkbar('Estadisticas')\n", + "out_ds_stats = W.Output(layout=OUT_STYLE)\n", + "out_ds_datos = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ds_truncate(_):\n", + " with out_ds_log:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not chk_ds_confirmar_truncate.value:\n", + " display(HTML('
Marca el checkbox de confirmacion antes de ejecutar.
'))\n", + " return\n", + " print('Truncando todas las tablas Registro*...')\n", + " res = truncar_tablas_registro(progress=bar_ds, log=print)\n", + " chk_ds_confirmar_truncate.value = False\n", + " if res:\n", + " df = pd.DataFrame([{'tabla': k, 'filas_eliminadas': v} for k, v in res.items()])\n", + " display(HTML('

Resultado del TRUNCATE

'))\n", + " display(df)\n", + "btn_ds_truncate.on_click(_on_ds_truncate)\n", + "\n", + "def _on_ds_stats(_):\n", + " with out_ds_stats:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " df = estadisticas_tablas_registro(progress=bar_ds_stats)\n", + " if df.empty:\n", + " print('No hay tablas Registro* en la base SQLite.')\n", + " return\n", + " total_filas = df['filas'].sum()\n", + " total_tablas = len(df)\n", + " total_con_datos = (df['filas'] > 0).sum()\n", + " display(HTML(\n", + " f'
'\n", + " f'Total: '\n", + " f'{total_tablas} tablas | {total_con_datos} con datos | '\n", + " f'{total_filas:,} filas en total
'))\n", + " df_show = df.copy()\n", + " df_show['filas'] = df_show['filas'].apply(lambda n: f'{n:,}')\n", + " display(df_show)\n", + " _state['ds_stats'] = df\n", + " # Llenar dropdown para visor\n", + " tablas_con_datos = df[df['filas'] > 0]['tabla'].tolist()\n", + " w_ds_tabla_sel.options = tablas_con_datos\n", + "btn_ds_stats.on_click(_on_ds_stats)\n", + "\n", + "def _on_ds_ver_datos(_):\n", + " with out_ds_datos:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " tabla = w_ds_tabla_sel.value\n", + " if not tabla:\n", + " print('Selecciona una tabla primero (corre \"Ver estadisticas\" antes).')\n", + " return\n", + " limit = max(1, int(w_ds_limit.value or 100))\n", + " try:\n", + " df = obtener_muestra_tabla(tabla, limit=limit, offset=0)\n", + " display(HTML(f'

{tabla} (primeras {limit})

'))\n", + " display(df)\n", + " _state['ds_muestra'] = df\n", + " _state['ds_muestra_tabla'] = tabla\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_ver_datos.on_click(_on_ds_ver_datos)\n", + "\n", + "def _on_ds_export(_):\n", + " with out_ds_datos:\n", + " if 'ds_muestra' not in _state or _state['ds_muestra'].empty:\n", + " display(HTML('
No hay datos cargados. Corre \"Ver datos\" primero.
'))\n", + " return\n", + " tabla = _state.get('ds_muestra_tabla', 'tabla')\n", + " ruta = os.path.join(os.getcwd(), f'{tabla}.xlsx')\n", + " _state['ds_muestra'].to_excel(ruta, index=False)\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{ruta}
'))\n", + "btn_ds_export.on_click(_on_ds_export)\n", + "\n", + "tab_datastage.children = tuple(list(tab_datastage.children) + [\n", + " W.HTML('

Estadisticas de tablas Registro*

'\n", + " '

Cuenta de filas por tabla. Equivalente al modulo PHP datastage.php: '\n", + " 'permite ver totales y explorar el contenido cargado.

'),\n", + " W.HBox([btn_ds_stats]), bar_ds_stats,\n", + " out_ds_stats,\n", + " W.HTML('
Explorar contenido
'),\n", + " W.HBox([w_ds_tabla_sel, w_ds_limit, btn_ds_ver_datos, btn_ds_export]),\n", + " out_ds_datos,\n", + " W.HTML('

Zona peligrosa

'\n", + " '

TRUNCATE TABLE en todas las tablas Registro*. '\n", + " 'Elimina TODAS las filas (no se puede deshacer). Util para reiniciar la carga desde cero.

'),\n", + " W.HBox([chk_ds_confirmar_truncate]),\n", + " W.HBox([btn_ds_truncate]),\n", + "])\n", + "\n", + "\n", + "\n", + "\n", + "# ===== DataStage: Reportes de Pedimentos =====\n", + "def _ds_report_block(titulo_html, fi_widget, ff_widget, btn_widget,\n", + " btn_export_widget, out_widget):\n", + " return W.VBox([\n", + " W.HTML(titulo_html),\n", + " W.HBox([fi_widget, ff_widget, btn_widget, btn_export_widget]),\n", + " out_widget,\n", + " ])\n", + "\n", + "# --- 1) Estructura CAT Pedimentos ---\n", + "w_ds_cat_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", + "w_ds_cat_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", + "btn_ds_cat_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", + "btn_ds_cat_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", + "out_ds_cat = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ds_cat(_):\n", + " with out_ds_cat:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not w_ds_cat_fi.value or not w_ds_cat_ff.value:\n", + " print('Define fecha inicio y fecha fin.'); return\n", + " print(f'Consultando CAT Pedimentos {w_ds_cat_fi.value} a {w_ds_cat_ff.value}...')\n", + " try:\n", + " df = cat_pedimentos_ds(str(w_ds_cat_fi.value), str(w_ds_cat_ff.value))\n", + " print(f'Filas: {len(df):,}')\n", + " _state['ds_cat_df'] = df\n", + " display(df.head(200))\n", + " if len(df) > 200:\n", + " print(f'(mostrando 200 de {len(df)})')\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_cat_run.on_click(_on_ds_cat)\n", + "\n", + "def _on_ds_cat_export(_):\n", + " with out_ds_cat:\n", + " if 'ds_cat_df' not in _state or _state['ds_cat_df'].empty:\n", + " display(HTML('
Corre Generar primero.
'))\n", + " return\n", + " ruta = exportar_df_a_excel(_state['ds_cat_df'], 'Estructura_CAT_Pedimentos')\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{ruta}
'))\n", + "btn_ds_cat_export.on_click(_on_ds_cat_export)\n", + "\n", + "# --- 2) Estructura CAT Pedimentos Rectificados ---\n", + "w_ds_catr_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", + "w_ds_catr_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", + "btn_ds_catr_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", + "btn_ds_catr_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", + "out_ds_catr = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ds_catr(_):\n", + " with out_ds_catr:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not w_ds_catr_fi.value or not w_ds_catr_ff.value:\n", + " print('Define fecha inicio y fecha fin.'); return\n", + " print(f'Consultando CAT Pedimentos Rectificados {w_ds_catr_fi.value} a {w_ds_catr_ff.value}...')\n", + " try:\n", + " df = cat_pedimentos_rect_ds(str(w_ds_catr_fi.value), str(w_ds_catr_ff.value))\n", + " print(f'Filas: {len(df):,}')\n", + " _state['ds_catr_df'] = df\n", + " display(df.head(200))\n", + " if len(df) > 200:\n", + " print(f'(mostrando 200 de {len(df)})')\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_catr_run.on_click(_on_ds_catr)\n", + "\n", + "def _on_ds_catr_export(_):\n", + " with out_ds_catr:\n", + " if 'ds_catr_df' not in _state or _state['ds_catr_df'].empty:\n", + " display(HTML('
Corre Generar primero.
'))\n", + " return\n", + " ruta = exportar_df_a_excel(_state['ds_catr_df'], 'Estructura_CAT_Pedimentos_Rectificados')\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{ruta}
'))\n", + "btn_ds_catr_export.on_click(_on_ds_catr_export)\n", + "\n", + "# --- 3) Rastreo Rectificaciones (con historial recursivo) ---\n", + "w_ds_rect_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", + "w_ds_rect_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", + "w_ds_rect_search = W.Text(description='Buscar:', placeholder='Pedimento, patente o clave',\n", + " layout={'width':'320px'}, style={'description_width':'70px'})\n", + "btn_ds_rect_run = W.Button(description='Listar', button_style='primary', icon='search', layout={'width':'120px'})\n", + "btn_ds_rect_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", + "out_ds_rect = W.Output(layout=OUT_STYLE)\n", + "\n", + "w_ds_hist_pat = W.Text(description='Patente:', layout={'width':'220px'}, style={'description_width':'80px'})\n", + "w_ds_hist_ped = W.Text(description='Pedimento:', layout={'width':'260px'}, style={'description_width':'80px'})\n", + "w_ds_hist_sec = W.Text(description='Seccion Ad.:', layout={'width':'220px'}, style={'description_width':'80px'})\n", + "w_ds_hist_anio = W.IntText(value=2024, description='Anio:', layout={'width':'150px'}, style={'description_width':'60px'})\n", + "btn_ds_hist_run = W.Button(description='Ver historial', button_style='info', icon='clock-o', layout={'width':'180px'})\n", + "out_ds_hist = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ds_rect(_):\n", + " with out_ds_rect:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " try:\n", + " fi = str(w_ds_rect_fi.value) if w_ds_rect_fi.value else None\n", + " ff = str(w_ds_rect_ff.value) if w_ds_rect_ff.value else None\n", + " df = rectificados_ds(fi, ff, (w_ds_rect_search.value or '').strip())\n", + " print(f'Pedimentos rectificados encontrados: {len(df):,}')\n", + " _state['ds_rect_df'] = df\n", + " display(df.head(200))\n", + " if len(df) > 200:\n", + " print(f'(mostrando 200 de {len(df)})')\n", + " display(HTML('Tip: copia Patente / Pedimento / SeccionAduanera / Anio a la seccion de abajo para ver el historial recursivo.'))\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_rect_run.on_click(_on_ds_rect)\n", + "\n", + "def _on_ds_rect_export(_):\n", + " with out_ds_rect:\n", + " if 'ds_rect_df' not in _state or _state['ds_rect_df'].empty:\n", + " display(HTML('
Corre Listar primero.
'))\n", + " return\n", + " ruta = exportar_df_a_excel(_state['ds_rect_df'], 'Pedimentos_Rectificados')\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{ruta}
'))\n", + "btn_ds_rect_export.on_click(_on_ds_rect_export)\n", + "\n", + "def _on_ds_hist(_):\n", + " with out_ds_hist:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not (w_ds_hist_pat.value and w_ds_hist_ped.value and w_ds_hist_sec.value):\n", + " print('Completa Patente, Pedimento, Seccion Aduanera y Anio.'); return\n", + " try:\n", + " df = historial_rectificaciones_ds(\n", + " w_ds_hist_pat.value.strip(),\n", + " w_ds_hist_ped.value.strip(),\n", + " w_ds_hist_sec.value.strip(),\n", + " int(w_ds_hist_anio.value))\n", + " print(f'Filas en cadena: {len(df):,}')\n", + " display(df)\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_hist_run.on_click(_on_ds_hist)\n", + "\n", + "tab_estructuras = W.VBox([\n", + " W.HTML('

Estructuras SCAII - Reportes de Pedimentos

'\n", + " '

Migracion de las opciones del menu PHP Pedimentos: '\n", + " 'CAT Pedimentos, CAT Pedimentos Rectificados y Rastreo de Rectificaciones (con historial recursivo). '\n", + " 'Las consultas se ejecutan sobre SQLite local (tablas Registro501 y Registro701).

'),\n", + " _ds_report_block(\n", + " '
Estructura CAT Pedimentos
'\n", + " '

Pedimentos de Registro501 en el rango, indicando si fueron rectificados.

',\n", + " w_ds_cat_fi, w_ds_cat_ff, btn_ds_cat_run, btn_ds_cat_export, out_ds_cat),\n", + " _ds_report_block(\n", + " '
Estructura CAT Pedimentos Rectificados
'\n", + " '

Pedimentos rectificados de Registro701 en el rango, '\n", + " 'enlazados con el tipo de operacion de Registro501.

',\n", + " w_ds_catr_fi, w_ds_catr_ff, btn_ds_catr_run, btn_ds_catr_export, out_ds_catr),\n", + " W.HTML('
Rastreo de Rectificaciones
'\n", + " '

Pedimentos de Registro501 que fueron rectificados, con filtros y opcion de ver la cadena historica.

'),\n", + " W.HBox([w_ds_rect_fi, w_ds_rect_ff, w_ds_rect_search]),\n", + " W.HBox([btn_ds_rect_run, btn_ds_rect_export]),\n", + " out_ds_rect,\n", + " W.HTML('
Historial recursivo de un pedimento
'\n", + " '

Equivalente al modal obtener_historial.php.

'),\n", + " W.HBox([w_ds_hist_pat, w_ds_hist_ped, w_ds_hist_sec, w_ds_hist_anio]),\n", + " W.HBox([btn_ds_hist_run]),\n", + " out_ds_hist,\n", + "])\n", + "\n", + "\n", + "\n", + "\n", + "# --- 4) Encabezado Facturas Importacion ---\n", + "w_ds_fimpo_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", + "w_ds_fimpo_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", + "btn_ds_fimpo_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", + "btn_ds_fimpo_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", + "out_ds_fimpo = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ds_fimpo(_):\n", + " with out_ds_fimpo:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not w_ds_fimpo_fi.value or not w_ds_fimpo_ff.value:\n", + " print('Define fecha inicio y fecha fin.'); return\n", + " print(f'Consultando facturas IMPO {w_ds_fimpo_fi.value} a {w_ds_fimpo_ff.value}...')\n", + " try:\n", + " df = encabezado_facturas_ds(str(w_ds_fimpo_fi.value), str(w_ds_fimpo_ff.value), 1)\n", + " print(f'Filas: {len(df):,}')\n", + " _state['ds_fimpo_df'] = df\n", + " display(df.head(200))\n", + " if len(df) > 200:\n", + " print(f'(mostrando 200 de {len(df)})')\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_fimpo_run.on_click(_on_ds_fimpo)\n", + "\n", + "def _on_ds_fimpo_export(_):\n", + " with out_ds_fimpo:\n", + " if 'ds_fimpo_df' not in _state or _state['ds_fimpo_df'].empty:\n", + " display(HTML('
Corre Generar primero.
')); return\n", + " ruta = exportar_df_a_excel(_state['ds_fimpo_df'], 'Estructura_facturasImpo_501')\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{ruta}
'))\n", + "btn_ds_fimpo_export.on_click(_on_ds_fimpo_export)\n", + "\n", + "# --- 5) Encabezado Facturas Exportacion ---\n", + "w_ds_fexpo_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", + "w_ds_fexpo_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", + "btn_ds_fexpo_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", + "btn_ds_fexpo_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", + "out_ds_fexpo = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ds_fexpo(_):\n", + " with out_ds_fexpo:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not w_ds_fexpo_fi.value or not w_ds_fexpo_ff.value:\n", + " print('Define fecha inicio y fecha fin.'); return\n", + " print(f'Consultando facturas EXPO {w_ds_fexpo_fi.value} a {w_ds_fexpo_ff.value}...')\n", + " try:\n", + " df = encabezado_facturas_ds(str(w_ds_fexpo_fi.value), str(w_ds_fexpo_ff.value), 2)\n", + " print(f'Filas: {len(df):,}')\n", + " _state['ds_fexpo_df'] = df\n", + " display(df.head(200))\n", + " if len(df) > 200:\n", + " print(f'(mostrando 200 de {len(df)})')\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_fexpo_run.on_click(_on_ds_fexpo)\n", + "\n", + "def _on_ds_fexpo_export(_):\n", + " with out_ds_fexpo:\n", + " if 'ds_fexpo_df' not in _state or _state['ds_fexpo_df'].empty:\n", + " display(HTML('
Corre Generar primero.
')); return\n", + " ruta = exportar_df_a_excel(_state['ds_fexpo_df'], 'Estructura_facturasExpo_501')\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{ruta}
'))\n", + "btn_ds_fexpo_export.on_click(_on_ds_fexpo_export)\n", + "\n", + "tab_estructuras.children = tuple(list(tab_estructuras.children) + [\n", + " W.HTML('

Encabezado de Facturas

'\n", + " '

Genera el encabezado de facturas de importacion (TipoOperacion=1) o exportacion '\n", + " '(TipoOperacion=2) excluyendo los pedimentos que ya fueron rectificados.

'),\n", + " _ds_report_block(\n", + " '
Encabezado Facturas Importacion
'\n", + " '

Pedimentos IMPO de Registro501 en el rango (excluye rectificados).

',\n", + " w_ds_fimpo_fi, w_ds_fimpo_ff, btn_ds_fimpo_run, btn_ds_fimpo_export, out_ds_fimpo),\n", + " _ds_report_block(\n", + " '
Encabezado Facturas Exportacion
'\n", + " '

Pedimentos EXPO de Registro501 en el rango (excluye rectificados).

',\n", + " w_ds_fexpo_fi, w_ds_fexpo_ff, btn_ds_fexpo_run, btn_ds_fexpo_export, out_ds_fexpo),\n", + "])\n", + "\n", + "\n", + "\n", + "\n", + "# --- 6) Estructura Tipo de Cambio 501 ---\n", + "w_ds_tc_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", + "w_ds_tc_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", + "btn_ds_tc_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", + "btn_ds_tc_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'200px'})\n", + "out_ds_tc = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ds_tc(_):\n", + " with out_ds_tc:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not w_ds_tc_fi.value or not w_ds_tc_ff.value:\n", + " print('Define fecha inicio y fecha fin.'); return\n", + " print(f'Consultando Tipo de Cambio {w_ds_tc_fi.value} a {w_ds_tc_ff.value}...')\n", + " try:\n", + " df = tipo_cambio_ds(str(w_ds_tc_fi.value), str(w_ds_tc_ff.value))\n", + " n_incon = int(df['INCONSISTENTE'].sum()) if not df.empty else 0\n", + " n_fechas_incon = df.loc[df['INCONSISTENTE'], 'FECHA PAGO REAL'].astype(str).str[:10].nunique() if n_incon else 0\n", + " print(f'Filas: {len(df):,} | Filas con TipoCambio inconsistente: {n_incon:,} ({n_fechas_incon} fechas distintas)')\n", + " _state['ds_tc_df'] = df\n", + " # Mostrar con celdas resaltadas (Styler)\n", + " styler = (df.head(200).style\n", + " .apply(lambda r: ['background-color:#FF0000;color:white;font-weight:bold' if r['INCONSISTENTE'] and c == 'TIPO CAMBIO' else ''\n", + " for c in df.columns], axis=1))\n", + " display(styler)\n", + " if len(df) > 200:\n", + " print(f'(mostrando 200 de {len(df)})')\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_tc_run.on_click(_on_ds_tc)\n", + "\n", + "def _on_ds_tc_export(_):\n", + " with out_ds_tc:\n", + " if 'ds_tc_df' not in _state or _state['ds_tc_df'].empty:\n", + " display(HTML('
Corre Generar primero.
')); return\n", + " import datetime as _dt_xc\n", + " ts = _dt_xc.datetime.now().strftime('%Y%m%d_%H%M%S')\n", + " ruta = os.path.join(os.getcwd(), f'Estructura_Tipo_Cambio_501_{ts}.xlsx')\n", + " exportar_tipo_cambio_excel(_state['ds_tc_df'], ruta)\n", + " display(HTML(f'
'\n", + " f'Excel generado (celdas con inconsistencia en rojo)
'\n", + " f'{ruta}
'))\n", + "btn_ds_tc_export.on_click(_on_ds_tc_export)\n", + "\n", + "tab_estructuras.children = tuple(list(tab_estructuras.children) + [\n", + " W.HTML('

Tipo de Cambio

'\n", + " '

Estructura de Tipo de Cambio del Registro501. '\n", + " 'Detecta automaticamente inconsistencias: fechas que tienen mas de un valor distinto '\n", + " 'de TIPO CAMBIO en sus pedimentos (resaltadas en rojo).

'),\n", + " _ds_report_block(\n", + " '
Estructura Tipo de Cambio 501
'\n", + " '

Pedimentos de Registro501 en el rango con su Tipo de Cambio.

',\n", + " w_ds_tc_fi, w_ds_tc_ff, btn_ds_tc_run, btn_ds_tc_export, out_ds_tc),\n", + "])\n", + "\n", + "\n", + "\n", + "\n", + "# ===== Catalogo base de NUMPARTES =====\n", + "upload_basenp = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n", + "btn_basenp_cargar = W.Button(description='Cargar a base (upsert)', button_style='primary', icon='upload', layout={'width':'260px'})\n", + "btn_basenp_ver = W.Button(description='Ver base actual', button_style='info', icon='database', layout={'width':'200px'})\n", + "chk_basenp_confirmar = W.Checkbox(value=False, description='Confirmo TRUNCATE de base_numpartes',\n", + " indent=False, layout={'width':'380px'})\n", + "btn_basenp_truncar = W.Button(description='Limpiar base', button_style='danger', icon='trash', layout={'width':'180px'})\n", + "out_basenp = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_basenp_cargar(_):\n", + " with out_basenp:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if len(upload_basenp.value) == 0:\n", + " print('Sube un Excel primero.'); return\n", + " try:\n", + " if isinstance(upload_basenp.value, dict):\n", + " fname = list(upload_basenp.value.keys())[0]\n", + " fb = upload_basenp.value[fname]['content']\n", + " else:\n", + " fb = upload_basenp.value[0]['content']\n", + " tmp = os.path.join(os.getcwd(), '_upload_basenp.xlsx')\n", + " with open(tmp, 'wb') as f: f.write(fb)\n", + " n = cargar_excel_base_numpartes(tmp, log=print)\n", + " print(f'OK. {n} filas cargadas/actualizadas.')\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_basenp_cargar.on_click(_on_basenp_cargar)\n", + "\n", + "def _on_basenp_ver(_):\n", + " with out_basenp:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " try:\n", + " df = listar_base_numpartes(limit=500)\n", + " print(f'base_numpartes: {len(df):,} filas (max 500 mostradas)')\n", + " display(df)\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_basenp_ver.on_click(_on_basenp_ver)\n", + "\n", + "def _on_basenp_truncar(_):\n", + " with out_basenp:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not chk_basenp_confirmar.value:\n", + " display(HTML('
Marca el checkbox de confirmacion antes de ejecutar.
')); return\n", + " try:\n", + " n = truncar_base_numpartes(log=print)\n", + " chk_basenp_confirmar.value = False\n", + " print(f'OK. {n} filas eliminadas.')\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_basenp_truncar.on_click(_on_basenp_truncar)\n", + "\n", + "\n", + "# ===== Estructura de Partidas Impo =====\n", + "w_ds_pimpo_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", + "w_ds_pimpo_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", + "w_ds_pimpo_umb = W.FloatSlider(value=0.80, min=0.50, max=1.00, step=0.05,\n", + " description='Umbral sim:', readout_format='.2f', layout={'width':'380px'})\n", + "btn_ds_pimpo_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", + "btn_ds_pimpo_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", + "bar_ds_pimpo = _mkbar('Partidas IMPO')\n", + "out_ds_pimpo = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ds_pimpo(_):\n", + " with out_ds_pimpo:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not w_ds_pimpo_fi.value or not w_ds_pimpo_ff.value:\n", + " print('Define fecha inicio y fecha fin.'); return\n", + " try:\n", + " df = asignar_numpartes_551(str(w_ds_pimpo_fi.value), str(w_ds_pimpo_ff.value),\n", + " 1, float(w_ds_pimpo_umb.value),\n", + " progress=bar_ds_pimpo, log=print)\n", + " print(f'Filas: {len(df):,}')\n", + " _state['ds_pimpo_df'] = df\n", + " display(df.head(200))\n", + " if len(df) > 200:\n", + " print(f'(mostrando 200 de {len(df)})')\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_pimpo_run.on_click(_on_ds_pimpo)\n", + "\n", + "def _on_ds_pimpo_export(_):\n", + " with out_ds_pimpo:\n", + " if 'ds_pimpo_df' not in _state or _state['ds_pimpo_df'].empty:\n", + " display(HTML('
Corre Generar primero.
')); return\n", + " ruta = exportar_df_a_excel(_state['ds_pimpo_df'], 'Estructura_Partidas_Impo')\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{ruta}
'))\n", + "btn_ds_pimpo_export.on_click(_on_ds_pimpo_export)\n", + "\n", + "\n", + "# ===== Estructura de Partidas Expo =====\n", + "w_ds_pexpo_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n", + "w_ds_pexpo_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n", + "w_ds_pexpo_umb = W.FloatSlider(value=0.80, min=0.50, max=1.00, step=0.05,\n", + " description='Umbral sim:', readout_format='.2f', layout={'width':'380px'})\n", + "btn_ds_pexpo_run = W.Button(description='Generar', button_style='primary', icon='play', layout={'width':'120px'})\n", + "btn_ds_pexpo_export = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'180px'})\n", + "bar_ds_pexpo = _mkbar('Partidas EXPO')\n", + "out_ds_pexpo = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_ds_pexpo(_):\n", + " with out_ds_pexpo:\n", + " clear_output()\n", + " if not _ds_msg_conexion(): return\n", + " if not w_ds_pexpo_fi.value or not w_ds_pexpo_ff.value:\n", + " print('Define fecha inicio y fecha fin.'); return\n", + " try:\n", + " df = asignar_numpartes_551(str(w_ds_pexpo_fi.value), str(w_ds_pexpo_ff.value),\n", + " 2, float(w_ds_pexpo_umb.value),\n", + " progress=bar_ds_pexpo, log=print)\n", + " print(f'Filas: {len(df):,}')\n", + " _state['ds_pexpo_df'] = df\n", + " display(df.head(200))\n", + " if len(df) > 200:\n", + " print(f'(mostrando 200 de {len(df)})')\n", + " except Exception as e:\n", + " print(f'ERROR: {e}')\n", + "btn_ds_pexpo_run.on_click(_on_ds_pexpo)\n", + "\n", + "def _on_ds_pexpo_export(_):\n", + " with out_ds_pexpo:\n", + " if 'ds_pexpo_df' not in _state or _state['ds_pexpo_df'].empty:\n", + " display(HTML('
Corre Generar primero.
')); return\n", + " ruta = exportar_df_a_excel(_state['ds_pexpo_df'], 'Estructura_Partidas_Expo')\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{ruta}
'))\n", + "btn_ds_pexpo_export.on_click(_on_ds_pexpo_export)\n", + "\n", + "\n", + "tab_estructuras.children = tuple(list(tab_estructuras.children) + [\n", + " W.HTML('

Catalogo base de NUMPARTES

'\n", + " '

Catalogo del cliente con columnas NUMPARTE, DESCRIPCION, UNIDAD DE MEDIDA, FRACCION. '\n", + " 'Se usa para asignar NUMPARTE a las partidas del Registro551 por similitud. La subida hace upsert (acumula).

'),\n", + " W.HBox([upload_basenp, btn_basenp_cargar, btn_basenp_ver]),\n", + " W.HBox([chk_basenp_confirmar, btn_basenp_truncar]),\n", + " out_basenp,\n", + " W.HTML('

Estructura de Partidas

'\n", + " '

Genera la estructura de partidas a partir de Registro551. '\n", + " 'Asigna NUMPARTE buscando primero en el catalogo (TF-IDF + cosine sobre descripcion, con fraccion 4d y UM exactas); '\n", + " 'las que no encuentran match se agrupan entre si por similitud y reciben NUMPARTE auto MP<F4>-R<NNN>.

'),\n", + " W.HTML('
Partidas Importacion
'),\n", + " W.HBox([w_ds_pimpo_fi, w_ds_pimpo_ff, w_ds_pimpo_umb]),\n", + " W.HBox([btn_ds_pimpo_run, btn_ds_pimpo_export]), bar_ds_pimpo,\n", + " out_ds_pimpo,\n", + " W.HTML('
Partidas Exportacion
'),\n", + " W.HBox([w_ds_pexpo_fi, w_ds_pexpo_ff, w_ds_pexpo_umb]),\n", + " W.HBox([btn_ds_pexpo_run, btn_ds_pexpo_export]), bar_ds_pexpo,\n", + " out_ds_pexpo,\n", + "])\n", + "\n", + "\n", + "\n", + "\n", + "# ===== Tab Valores: Ajuste de VALORTOTALME / VALORTOTALMN =====\n", + "upload_val = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n", + "btn_plantilla_val = W.Button(description='Descargar plantilla', button_style='info', icon='download', layout={'width':'220px'})\n", + "w_val_modo = W.RadioButtons(\n", + " options=[('Aplicar siempre', 'APLICAR_SIEMPRE'), ('Usar umbral %', 'USAR_UMBRAL')],\n", + " value='APLICAR_SIEMPRE', description='Modo:', layout={'width':'320px'})\n", + "chk_val_shelter = W.Checkbox(value=False,\n", + " description='Buscar en todas las BDs (shelter)', indent=False,\n", + " layout={'width':'380px'})\n", + "w_val_umbral = W.FloatSlider(value=50.0, min=1.0, max=500.0, step=1.0,\n", + " description='Umbral %:', readout_format='.0f', layout={'width':'380px'})\n", + "btn_val_analizar = W.Button(description='Analizar', button_style='primary', icon='search', layout={'width':'180px'})\n", + "btn_val_xlsx = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'200px'})\n", + "bar_val = _mkbar('Valores')\n", + "out_val_log = W.Output(layout=OUT_STYLE)\n", + "out_val_tabla = W.Output(layout=OUT_STYLE)\n", + "\n", + "_html_formato_val = ('
'\n", + " 'Formato esperado del Excel (3 columnas)'\n", + " ''\n", + " ''\n", + " ''\n", + " ''\n", + " ''\n", + " '
ColumnaEjemplo
PEDIMENTO07-3429-4015540
VALOR_ME12345.67
VALOR_MN234567.89
'\n", + " '

Por cada pedimento se buscan sus partidas de exportacion y se prorratean los valores hasta cuadrar al 100%.

'\n", + " '
')\n", + "\n", + "def _on_plantilla_val(_):\n", + " with out_val_log:\n", + " clear_output()\n", + " bts = generar_plantilla_excel_valores()\n", + " path = os.path.join(os.getcwd(), 'plantilla_valores.xlsx')\n", + " with open(path, 'wb') as f: f.write(bts)\n", + " display(HTML(f'
'\n", + " f'Plantilla generada
'\n", + " f'{path}
'))\n", + "btn_plantilla_val.on_click(_on_plantilla_val)\n", + "\n", + "def _on_val_analizar(_):\n", + " with out_val_log:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " if len(upload_val.value) == 0:\n", + " print('ERROR: sube un Excel primero.'); return\n", + " try:\n", + " if isinstance(upload_val.value, dict):\n", + " fname = list(upload_val.value.keys())[0]\n", + " fb = upload_val.value[fname]['content']\n", + " else:\n", + " fb = upload_val.value[0]['content']\n", + " tmp = os.path.join(os.getcwd(), '_upload_valores.xlsx')\n", + " with open(tmp, 'wb') as f: f.write(fb)\n", + " df_excel = cargar_excel_valores(tmp)\n", + " print(f'Excel cargado: {len(df_excel):,} pedimentos')\n", + " except Exception as e:\n", + " print(f'ERROR cargando Excel: {e}'); return\n", + " plan, resumen = analizar_valores(df_excel,\n", + " modo=w_val_modo.value, umbral_pct=float(w_val_umbral.value),\n", + " usar_shelter=bool(chk_val_shelter.value),\n", + " progress=bar_val, log=print)\n", + " _state['val_plan'] = plan\n", + " _state['val_resumen'] = resumen\n", + " if not resumen.empty:\n", + " cnt = resumen['STATUS'].value_counts().to_dict()\n", + " print(f'Resumen: {cnt}')\n", + " with out_val_tabla:\n", + " clear_output()\n", + " if 'val_resumen' in _state and not _state['val_resumen'].empty:\n", + " display(HTML('

Resumen por pedimento

'))\n", + " display(_state['val_resumen'])\n", + " if 'val_plan' in _state and not _state['val_plan'].empty:\n", + " display(HTML('

Plan detalle (primeras 50 filas)

'))\n", + " display(_state['val_plan'].head(50))\n", + "btn_val_analizar.on_click(_on_val_analizar)\n", + "\n", + "def _on_val_xlsx(_):\n", + " with out_val_log:\n", + " if 'val_plan' not in _state:\n", + " display(HTML('
Corre Analizar primero.
')); return\n", + " path = os.path.join(os.getcwd(), 'plan_valores.xlsx')\n", + " exportar_excel_valores(_state['val_plan'], _state['val_resumen'], path)\n", + " display(HTML(f'
'\n", + " f'Excel generado
'\n", + " f'{path}
'))\n", + "btn_val_xlsx.on_click(_on_val_xlsx)\n", + "\n", + "dry_val = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n", + "btn_val_ejecutar = W.Button(description='Ejecutar ajuste (Paso B)', button_style='warning', icon='play', layout={'width':'280px'})\n", + "bar_val_b = _mkbar('Ejecucion Valores')\n", + "out_val_ejec = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_val_ejecutar(_):\n", + " with out_val_ejec:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " if 'val_plan' not in _state:\n", + " print('ERROR: corre primero Analizar.'); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " ejecutar_valores(_state['val_plan'], dry_run=dry_val.value, log=print, progress=bar_val_b)\n", + "btn_val_ejecutar.on_click(_on_val_ejecutar)\n", + "\n", + "tab_valores = W.VBox([\n", + " W.HTML('

Valores - Ajuste de VALORTOTALME / VALORTOTALMN

'\n", + " '

'\n", + " 'Sube un Excel con los valores ME y MN esperados por pedimento. La herramienta '\n", + " 'busca las partidas de exportacion de cada pedimento y prorratea los valores '\n", + " 'proporcionalmente al actual de cada una hasta cuadrar 100%.

'),\n", + " W.HTML(_html_formato_val),\n", + " btn_plantilla_val,\n", + " W.HTML('

Sube tu Excel:

'),\n", + " W.HBox([upload_val]),\n", + " W.HBox([w_val_modo]),\n", + " W.HBox([chk_val_shelter]),\n", + " W.HBox([w_val_umbral]),\n", + " W.HBox([btn_val_analizar, btn_val_xlsx]), bar_val,\n", + " out_val_log, out_val_tabla,\n", + " W.HTML('

Paso B - Ejecutar UPDATE en SPartidasExpo

'\n", + " '

UPDATE VALORTOTALME y VALORTOTALMN por (FACTURAEXPO, LINEA). '\n", + " 'Corre primero con DRY_RUN activado.

'),\n", + " W.HBox([dry_val]),\n", + " btn_val_ejecutar, bar_val_b,\n", + " out_val_ejec,\n", + "])\n", + "\n", + "\n", + "\n", + "\n", + "# ===== Shelter: buscar pedimentos en todas las BDs =====\n", + "btn_val_shelter = W.Button(description='Buscar pedimentos en todas las BDs',\n", + " button_style='info', icon='search-plus', layout={'width':'320px'})\n", + "bar_val_shelter = _mkbar('Shelter')\n", + "out_val_shelter = W.Output(layout=OUT_STYLE)\n", + "\n", + "def _on_val_shelter(_):\n", + " with out_val_shelter:\n", + " clear_output()\n", + " if not CONEXION_OK: print(CONEXION_MSG); return\n", + " print(f'(DB actual: {DB_ACTUAL})')\n", + " if len(upload_val.value) == 0:\n", + " print('ERROR: sube un Excel primero.'); return\n", + " try:\n", + " if isinstance(upload_val.value, dict):\n", + " fname = list(upload_val.value.keys())[0]\n", + " fb = upload_val.value[fname]['content']\n", + " else:\n", + " fb = upload_val.value[0]['content']\n", + " tmp = os.path.join(os.getcwd(), '_upload_valores.xlsx')\n", + " with open(tmp, 'wb') as f: f.write(fb)\n", + " df_excel = cargar_excel_valores(tmp)\n", + " print(f'Excel: {len(df_excel):,} pedimentos')\n", + " except Exception as e:\n", + " print(f'ERROR Excel: {e}'); return\n", + " reporte, resumen_bd, faltantes = buscar_pedimentos_en_bds(\n", + " df_excel, progress=bar_val_shelter, log=print)\n", + " _state['val_shelter_reporte'] = reporte\n", + " _state['val_shelter_resumen_bd'] = resumen_bd\n", + " _state['val_shelter_faltantes'] = faltantes\n", + " encontrados = len(reporte) - len(faltantes) if not reporte.empty else 0\n", + " print(f'Encontrados: {encontrados} | Faltantes: {len(faltantes)}')\n", + " if not reporte.empty:\n", + " display(HTML('

Donde esta cada pedimento

'))\n", + " display(reporte)\n", + " if not resumen_bd.empty:\n", + " display(HTML('

Resumen por base de datos

'))\n", + " display(resumen_bd)\n", + " if faltantes:\n", + " display(HTML('

No encontrados en ninguna BD

'))\n", + " display(pd.DataFrame({'PEDIMENTO': faltantes}))\n", + "btn_val_shelter.on_click(_on_val_shelter)\n", + "\n", + "tab_valores.children = tuple(list(tab_valores.children) + [\n", + " W.HTML('

Shelter - Buscar pedimentos en otras BDs

'\n", + " '

'\n", + " 'Recorre todas las bases de datos de la instancia SQL Server (las que el usuario '\n", + " 'puede acceder y tienen la tabla SPedimentos.PEDIMENTO) y reporta en '\n", + " 'cual(es) base(s) vive cada pedimento del Excel. Solo es diagnostico — no '\n", + " 'modifica nada. Util cuando el cliente tiene varias BDs y no sabes a cual '\n", + " 'apuntar el ajuste.

'),\n", + " W.HBox([btn_val_shelter]), bar_val_shelter,\n", + " out_val_shelter,\n", + "])\n", + "\n", + "\n", + "tabs = W.Tab(children=[tab_conn, tab_desc, tab_an, tab_nlp, tab_kg, tab_ctm, tab_sv, tab_datastage, tab_estructuras, tab_valores])\n", + "tabs.set_title(0, 'Conexion')\n", + "tabs.set_title(1, 'Descargas')\n", + "tabs.set_title(2, 'Analisis Saldos')\n", + "tabs.set_title(3, 'Sustitutos NLP')\n", + "tabs.set_title(4, 'Descarga % KGS')\n", + "tabs.set_title(5, 'CTM')\n", + "tabs.set_title(6, 'Saldos Vencidos')\n", + "tabs.set_title(7, 'DataStage')\n", + "tabs.set_title(8, 'Estructuras SCAII')\n", + "tabs.set_title(9, 'Valores')\n", + "display(header, tabs)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } \ No newline at end of file diff --git a/app/build_exe.bat b/app/build_exe.bat new file mode 100755 index 0000000..7388daa --- /dev/null +++ b/app/build_exe.bat @@ -0,0 +1,131 @@ +@echo off +REM ============================================================ +REM Build .exe de Utilerias RECON 2 con PyInstaller (modo onedir) +REM Usa un conda env limpio para empaquetado reproducible. +REM ============================================================ + +setlocal +cd /d "%~dp0" + +set ENV_NAME=utilerias-recon-build +set PY_VERSION=3.11 + +REM ------------------------------------------------------------ +REM 1) Verificar conda +REM ------------------------------------------------------------ +where conda >nul 2>nul +if errorlevel 1 ( + echo [ERROR] Conda no esta en PATH. Abre "Anaconda Prompt" y vuelve a ejecutar. + goto :end +) + +REM ------------------------------------------------------------ +REM 2) Crear env si no existe +REM ------------------------------------------------------------ +call conda env list | findstr /B "%ENV_NAME% " >nul +if errorlevel 1 ( + echo [INFO] Creando env conda "%ENV_NAME%" con Python %PY_VERSION%... + call conda create -y -n %ENV_NAME% python=%PY_VERSION% + if errorlevel 1 ( + echo [ERROR] No se pudo crear el env. + goto :end + ) +) + +REM ------------------------------------------------------------ +REM 3) Activar env e instalar dependencias +REM ------------------------------------------------------------ +call conda activate %ENV_NAME% +if errorlevel 1 ( + echo [ERROR] No se pudo activar el env. + goto :end +) + +echo [INFO] Instalando dependencias... +call pip install --upgrade pip +call pip install -r requirements.txt +if errorlevel 1 ( + echo [ERROR] Fallo la instalacion de dependencias. + goto :end +) + +REM ------------------------------------------------------------ +REM 4) Limpiar builds previos +REM ------------------------------------------------------------ +if exist build rmdir /s /q build +if exist dist rmdir /s /q dist +if exist UtileriasReconV2.spec del /q UtileriasReconV2.spec + +REM ------------------------------------------------------------ +REM 5) Ejecutar PyInstaller (onedir, noconsole) +REM ------------------------------------------------------------ +echo [INFO] Construyendo .exe (esto tarda 3-8 minutos)... + +pyinstaller launcher.py ^ + --name UtileriasReconV2 ^ + --onedir ^ + --noconsole ^ + --noconfirm ^ + --collect-all voila ^ + --collect-all jupyter_server ^ + --collect-all jupyter_client ^ + --collect-all jupyter_core ^ + --collect-all nbformat ^ + --collect-all nbconvert ^ + --collect-all ipykernel ^ + --collect-all ipywidgets ^ + --collect-all jupyterlab_pygments ^ + --collect-all notebook ^ + --collect-all rfc3987_syntax ^ + --collect-all jsonschema_specifications ^ + --collect-all jsonschema ^ + --collect-all jupyter_events ^ + --collect-all nbclient ^ + --collect-all terminado ^ + --collect-all debugpy ^ + --collect-all matplotlib_inline ^ + --collect-all matplotlib ^ + --collect-all dotenv ^ + --collect-all sklearn ^ + --collect-all scipy ^ + --collect-all pandas ^ + --collect-all numpy ^ + --collect-all sqlalchemy ^ + --collect-all openpyxl ^ + --hidden-import pyodbc ^ + --hidden-import sqlalchemy.dialects.sqlite ^ + --hidden-import sqlalchemy.dialects.mssql ^ + --hidden-import sklearn.utils._typedefs ^ + --hidden-import sklearn.neighbors._partition_nodes + +if errorlevel 1 ( + echo. + echo [ERROR] PyInstaller fallo. Revisa el log arriba. + goto :end +) + +REM ------------------------------------------------------------ +REM 6) Copiar app.ipynb y .env.example al lado del .exe +REM ------------------------------------------------------------ +copy /Y app.ipynb dist\UtileriasReconV2\app.ipynb >nul +echo [INFO] app.ipynb copiado a dist\UtileriasReconV2\ + +if exist ..\.env.example ( + copy /Y ..\.env.example dist\UtileriasReconV2\.env.example >nul + echo [INFO] .env.example copiado a dist\UtileriasReconV2\ +) + +echo. +echo ============================================================ +echo BUILD EXITOSO (--onedir) +echo Carpeta lista: dist\UtileriasReconV2\ +echo Ejecutable: dist\UtileriasReconV2\UtileriasReconV2.exe +echo. +echo Siguiente paso: empaquetar como instalador con Inno Setup +echo .\build_installer.bat +echo ============================================================ + +:end +echo. +pause +endlocal diff --git a/app/build_installer.bat b/app/build_installer.bat new file mode 100755 index 0000000..5d06775 --- /dev/null +++ b/app/build_installer.bat @@ -0,0 +1,72 @@ +@echo off +REM ============================================================ +REM Build completo: PyInstaller (onedir) + Inno Setup +REM Genera installer_output\UtileriasReconV2_Setup_X.X.X.exe +REM ============================================================ + +setlocal +cd /d "%~dp0" + +REM ------------------------------------------------------------ +REM 1) Localizar el compilador de Inno Setup (ISCC.exe) +REM ------------------------------------------------------------ +set "ISCC=" +if exist "%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe" set "ISCC=%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe" +if exist "%ProgramFiles%\Inno Setup 6\ISCC.exe" set "ISCC=%ProgramFiles%\Inno Setup 6\ISCC.exe" + +if "%ISCC%"=="" ( + echo [ERROR] No se encontro Inno Setup 6. + echo Descargalo de: https://jrsoftware.org/isdl.php + echo Instala la version "Stable Release", luego vuelve a ejecutar este script. + goto :end +) +echo [INFO] Inno Setup encontrado en: %ISCC% + +REM ------------------------------------------------------------ +REM 2) Construir el .exe con PyInstaller (onedir) +REM ------------------------------------------------------------ +echo. +echo [INFO] Paso 1/2: PyInstaller... +call build_exe.bat +if errorlevel 1 ( + echo [ERROR] PyInstaller fallo. Revisa el log arriba. + goto :end +) + +if not exist "dist\UtileriasReconV2\UtileriasReconV2.exe" ( + echo [ERROR] No se genero dist\UtileriasReconV2\UtileriasReconV2.exe + goto :end +) + +REM ------------------------------------------------------------ +REM 3) Limpiar build previo del instalador +REM ------------------------------------------------------------ +if exist installer_output rmdir /s /q installer_output + +REM ------------------------------------------------------------ +REM 4) Compilar el instalador +REM ------------------------------------------------------------ +echo. +echo [INFO] Paso 2/2: Inno Setup... +"%ISCC%" installer.iss +if errorlevel 1 ( + echo [ERROR] Inno Setup fallo. + goto :end +) + +echo. +echo ============================================================ +echo INSTALADOR LISTO +echo Archivo: installer_output\UtileriasReconV2_Setup_1.0.0.exe +echo. +echo Distribuye SOLO ese .exe. El usuario: +echo 1. Ejecuta el instalador (1 vez, ~30 seg) +echo 2. Edita .env desde el menu Inicio (acceso directo) +echo 3. Doble clic en el icono del escritorio +echo 4. La app arranca en ~3 seg (sin re-extraer) +echo ============================================================ + +:end +echo. +pause +endlocal diff --git a/app/installer.iss b/app/installer.iss new file mode 100755 index 0000000..58399c8 --- /dev/null +++ b/app/installer.iss @@ -0,0 +1,67 @@ +; ============================================================ +; Instalador de Utilerias RECON 2 +; Empaqueta dist\UtileriasReconV2\ en un instalador .exe unico. +; Requiere Inno Setup 6+: https://jrsoftware.org/isdl.php +; ============================================================ + +#define MyAppName "Utilerias RECON 2" +#define MyAppVersion "1.0.0" +#define MyAppPublisher "Tecma / CurtManufacturing" +#define MyAppExeName "UtileriasReconV2.exe" + +[Setup] +AppId={{B3F1A7A8-4D2E-4A6B-9C5A-2C0D1F8E9A11} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +DefaultDirName={autopf}\UtileriasReconV2 +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +OutputDir=installer_output +OutputBaseFilename=UtileriasReconV2_Setup_{#MyAppVersion} +Compression=lzma2/ultra64 +SolidCompression=yes +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +WizardStyle=modern +UninstallDisplayIcon={app}\{#MyAppExeName} + +[Languages] +Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" + +[Tasks] +Name: "desktopicon"; Description: "Crear acceso directo en el escritorio"; GroupDescription: "Iconos adicionales:" + +[Files] +; Empaqueta TODA la carpeta dist\UtileriasReconV2\ generada por PyInstaller. +; Excluimos *.map (source maps de JS, solo para debug del browser, no se usan) +; porque tienen nombres absurdamente largos y rompen la compresion. +Source: "dist\UtileriasReconV2\*"; DestDir: "{app}"; Excludes: "*.map"; Flags: ignoreversion recursesubdirs createallsubdirs +; Plantilla .env (el usuario la copia como .env y edita) +Source: "..\.env.example"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\Editar configuracion (.env)"; Filename: "notepad.exe"; Parameters: """{app}\.env""" +Name: "{group}\Desinstalar {#MyAppName}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "Ejecutar {#MyAppName} ahora"; Flags: nowait postinstall skipifsilent + +[Code] +// Si no existe .env, copia .env.example como .env tras instalar +procedure CurStepChanged(CurStep: TSetupStep); +var + EnvFile, EnvExample: string; +begin + if CurStep = ssPostInstall then + begin + EnvFile := ExpandConstant('{app}\.env'); + EnvExample := ExpandConstant('{app}\.env.example'); + if (not FileExists(EnvFile)) and FileExists(EnvExample) then + CopyFile(EnvExample, EnvFile, False); + end; +end; diff --git a/app/launcher.py b/app/launcher.py index b9c8f61..d8016f4 100644 --- a/app/launcher.py +++ b/app/launcher.py @@ -1,10 +1,9 @@ """ -Launcher de la app GENPACT V2. +Launcher de la app GENPACT V2 - Utilerias RECON 2. -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. +Arranca Voila in-process (sin subprocess) para que funcione empaquetado con +PyInstaller. Si el .exe es invocado como kernel de IPython, enruta al +ipykernel_launcher en vez de relanzar Voila (evita el bucle infinito). """ import os import sys @@ -12,36 +11,113 @@ 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. - """ +# ============================================================ +# Diagnostico: escribir log al disco apenas arranque el launcher. +# Si este archivo no aparece, el problema es PyInstaller / antivirus +# bloqueando la extraccion antes de que Python siquiera empiece. +# ============================================================ +def _diag_log(msg): + try: + log_path = os.path.join(os.path.expanduser('~'), 'utilerias_recon_boot.log') + with open(log_path, 'a', encoding='utf-8') as f: + f.write(f'{time.strftime("%Y-%m-%d %H:%M:%S")} | {msg}\n') + except Exception: + pass + +_diag_log(f'=== LAUNCHER START ===') +_diag_log(f'sys.executable={sys.executable}') +_diag_log(f'sys.argv={sys.argv}') +_diag_log(f'cwd={os.getcwd()}') +_diag_log(f'frozen={getattr(sys, "frozen", False)}') + +try: + if sys.stdout is not None: + print(f'[boot] launcher iniciando, sys.executable={sys.executable}', flush=True) + print(f'[boot] argv={sys.argv}', flush=True) + print(f'[boot] cwd={os.getcwd()}', flush=True) +except Exception: + pass + + +# ============================================================ +# Cargar .env ANTES de cualquier cosa para que las variables +# queden en os.environ y se hereden a Voila + kernels subprocess. +# (En --onefile el cwd del kernel apunta al temp de PyInstaller, +# no a la carpeta del .exe, asi que load_dotenv relativo falla.) +# ============================================================ +def _exe_dir() -> Path: + if getattr(sys, 'frozen', False): + return Path(sys.executable).parent + return Path(__file__).resolve().parent + +_env_file = _exe_dir() / '.env' +if _env_file.exists(): + try: + from dotenv import load_dotenv + load_dotenv(_env_file, override=True) + except ImportError: + pass + + +# ============================================================ +# Router de modo: detectar invocacion como kernel de Jupyter +# jupyter_client spawnea: sys.executable -m ipykernel_launcher -f conn.json +# En el .exe eso seria: UtileriasReconV2.exe -m ipykernel_launcher -f conn.json +# ============================================================ +if 'ipykernel_launcher' in sys.argv: + # En modo empaquetado, sys.stdout/stderr heredados del padre (Voila) + # pueden ser invalidos para flush() y romper ipykernel.init_io. + # ipykernel los reemplaza por canales ZMQ despues, asi que devnull es seguro. + if getattr(sys, 'frozen', False): + try: + _devnull = open(os.devnull, 'w') + sys.stdout = _devnull + sys.stderr = _devnull + except Exception: + pass + sys.argv = [a for a in sys.argv if a not in ('-m', 'ipykernel_launcher')] + from ipykernel import kernelapp + kernelapp.launch_new_instance() + sys.exit(0) + + +# ============================================================ +# Localizar app.ipynb y .env +# ============================================================ +def resources_dir() -> Path: + """Recursos embebidos (app.ipynb bundleado). En PyInstaller usa _MEIPASS.""" + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): + return Path(sys._MEIPASS) + return Path(sys.executable).parent if getattr(sys, 'frozen', False) \ + else Path(__file__).resolve().parent + + +def user_dir() -> Path: + """Archivos del usuario (.env). Siempre al lado del .exe.""" 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' +RES = resources_dir() +USR = user_dir() -# 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) +# Buscar app.ipynb: primero al lado del .exe (override), luego en recursos +APP_NB = USR / 'app.ipynb' +if not APP_NB.exists(): + APP_NB = RES / 'app.ipynb' + +# cwd donde este el .env (para que python-dotenv lo encuentre) +if (USR / '.env').exists(): + os.chdir(USR) +elif (USR.parent / '.env').exists(): + os.chdir(USR.parent) else: - os.chdir(BASE) + os.chdir(USR) -# ------------------------------------------------------------------ -# Buscar puerto libre -# ------------------------------------------------------------------ def find_free_port(preferido: int = 8866) -> int: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: @@ -50,7 +126,6 @@ def find_free_port(preferido: int = 8866) -> int: 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] @@ -58,63 +133,66 @@ def find_free_port(preferido: int = 8866) -> int: return p -# ------------------------------------------------------------------ -# Arrancar Voila -# ------------------------------------------------------------------ +def fatal(msg: str): + """Muestra error y sale. Si no hay consola, usa MessageBox de Windows.""" + try: + if sys.stdout is not None: + print(f'ERROR: {msg}') + except Exception: + pass + try: + import tkinter + from tkinter import messagebox + root = tkinter.Tk() + root.withdraw() + messagebox.showerror('Utilerias RECON 2', msg) + except Exception: + pass + sys.exit(1) + + def main(): if not APP_NB.exists(): - print(f'ERROR: no se encuentra {APP_NB}') - sys.exit(1) + fatal(f'No se encuentra app.ipynb en {APP_NB}') 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) + if sys.stdout is not None: + print('=' * 60) + print(' GENPACT V2 - Sistema de Descargas SCAII') + print('=' * 60) + 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 + # Abrir browser cuando Voila acepte conexiones def _open(): - for _ in range(60): + for _ in range(120): 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() + # Lanzar Voila in-process (sin subprocess) + sys.argv = [ + 'voila', + str(APP_NB), + f'--port={port}', + '--no-browser', + '--Voila.ip=127.0.0.1', + '--strip_sources=True', + ] try: - proc.wait() - except KeyboardInterrupt: - print('\nDeteniendo Voila...') - proc.terminate() + from voila.app import Voila + Voila.launch_instance() + except SystemExit: + pass + except Exception as e: + fatal(f'Voila fallo al iniciar: {e}') if __name__ == '__main__': diff --git a/app/requirements.txt b/app/requirements.txt index 60beffb..4bff176 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -11,5 +11,4 @@ jupyter_server>=2.0 notebook>=7.0 scikit-learn>=1.3 pyinstaller>=6.0 -psycopg2-binary>=2.9 sqlalchemy>=2.0 diff --git a/app/schema_registro_sqlite.sql b/app/schema_registro_sqlite.sql new file mode 100755 index 0000000..645386f --- /dev/null +++ b/app/schema_registro_sqlite.sql @@ -0,0 +1,384 @@ +CREATE TABLE IF NOT EXISTS "Registro501" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "TipoOperacion" TEXT, + "ClaveDocumento" TEXT, + "SeccionAduaneraEntrada" TEXT, + "CurpContribuyente" TEXT, + "Rfc" TEXT, + "CurpAgenteA" TEXT, + "TipoCambio" NUMERIC, + "TotalFletes" NUMERIC, + "TotalSeguros" NUMERIC, + "TotalEmbalajes" NUMERIC, + "TotalIncrementables" NUMERIC, + "TotalDeducibles" NUMERIC, + "PesoBrutoMercancia" NUMERIC, + "MedioTransporteSalida" TEXT, + "MedioTransporteArribo" TEXT, + "MedioTransporteEntrada_Salida" TEXT, + "DestinoMercancia" TEXT, + "NombreContribuyente" TEXT, + "CalleContribuyente" TEXT, + "NumInteriorContribuyente" TEXT, + "NumExteriorContribuyente" TEXT, + "CPContribuyente" TEXT, + "MunicipioContribuyente" TEXT, + "EntidadFedContribuyente" TEXT, + "PaisContribuyente" TEXT, + "TipoPedimento" TEXT, + "FechaRecepcionPedimento" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro502" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "RfcTransportista" TEXT, + "CurpTransportista" TEXT, + "NombreTransportista" TEXT, + "PaisTransporte" TEXT, + "IdentificadorTransporte" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro503" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "NumeroGuia" TEXT, + "TipoGuia" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro504" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "NumContenedor" TEXT, + "TipoContenedor" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro505" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "FechaFacturacion" TEXT, + "NumeroFactura" TEXT, + "TerminoFacturacion" TEXT, + "MonedaFacturacion" TEXT, + "ValorDolares" NUMERIC, + "ValorMonedaExtranjera" NUMERIC, + "PaisFacturacion" TEXT, + "EntidadFedFacturacion" TEXT, + "IndentFiscalProveedor" TEXT, + "ProveedorMercancia" TEXT, + "CalleProveedor" TEXT, + "NumInteriorProveedor" TEXT, + "NumExteriorProveedor" TEXT, + "CpProveedor" TEXT, + "MunicipioProveedor" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro506" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "TipoFecha" TEXT, + "FechaOperacion" TEXT, + "FechaValidacionPagoR" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro507" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "ClaveCaso" TEXT, + "IdentificadorCaso" TEXT, + "TipoPedimento" TEXT, + "ComplementoCaso" TEXT, + "FechaValidacionPagoR" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro508" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "InstitucionEmisora" TEXT, + "NumeroCuenta" TEXT, + "FolioConstancia" TEXT, + "FechaConstancia" TEXT, + "TipoCuenta" TEXT, + "ClaveGarantia" TEXT, + "ValorUnitarioTitulo" NUMERIC, + "TotalGarantia" NUMERIC, + "CantidadUnidades" NUMERIC, + "TitulosAsignados" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro509" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "ClaveContribucion" TEXT, + "TasaContribucion" NUMERIC, + "TipoTasa" TEXT, + "TipoPedimento" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro510" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "ClaveContribucion" TEXT, + "FormaPago" TEXT, + "ImportePago" NUMERIC, + "TipoPedimento" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro511" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "SecuenciaObservacion" TEXT, + "Observaciones" TEXT, + "TipoPedimento" TEXT, + "FechaValidacionPagoR" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro512" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "PatenteAduanalOrig" TEXT, + "PedimentoOriginal" TEXT, + "SeccionAduaneraDespOrig" TEXT, + "DocumentoOriginal" TEXT, + "FechaOperacionOrig" TEXT, + "FraccionOriginal" TEXT, + "UnidadMedida" TEXT, + "MercanciaDescargada" NUMERIC, + "TipoPedimento" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro520" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "IndentFiscalDestinatario" TEXT, + "NombreDestinatarioMercancia" TEXT, + "CalleDestinatario" TEXT, + "NumInteriorDestinatario" TEXT, + "NumExteriorDestinatario" TEXT, + "CpDestinatario" TEXT, + "MunicpioDestinatario" TEXT, + "PaisDestinatario" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro551" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "Fraccion" TEXT, + "SecuenciaFraccion" TEXT, + "SubdivisionFraccion" TEXT, + "DescripcionMercancia" TEXT, + "PrecioUnitario" NUMERIC, + "ValorAduana" NUMERIC, + "ValorComercial" NUMERIC, + "ValorDolares" NUMERIC, + "CantidadUMComercial" NUMERIC, + "UnidadMedidaComercial" TEXT, + "CantidadUMTarifa" NUMERIC, + "UnidadMedidaTarifa" TEXT, + "ValorAgregado" NUMERIC, + "ClaveVinculacion" TEXT, + "MetodoValorizacion" TEXT, + "CodigoMercanciaProducto" TEXT, + "MarcaMercanciaProducto" TEXT, + "ModeloMercanciaProducto" TEXT, + "PaisOrigenDestino" TEXT, + "PaisCompradorVendedor" TEXT, + "EntidadFedOrigen" TEXT, + "EntidadFedDestino" TEXT, + "EntidadFedComprador" TEXT, + "EntidadFedVendedor" TEXT, + "TipoOperacion" TEXT, + "ClaveDocumento" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro552" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "Fraccion" TEXT, + "SecuenciaFraccion" TEXT, + "VinNumeroSerie" TEXT, + "KilometrajeVehiculo" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro553" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "Fraccion" TEXT, + "SecuenciaFraccion" TEXT, + "ClavePermiso" TEXT, + "FirmaDescargo" TEXT, + "NumeroPermiso" TEXT, + "ValorComercialDolares" NUMERIC, + "CantidadMUMTarifa" NUMERIC, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro554" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "Fraccion" TEXT, + "SecuenciaFraccion" TEXT, + "ClaveCaso" TEXT, + "IdentificadorCaso" TEXT, + "ComplementoCaso" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro555" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "Fraccion" TEXT, + "SecuenciaFraccion" TEXT, + "InstitucionEmisora" TEXT, + "NumeroCuenta" TEXT, + "FolioConstancia" TEXT, + "FechaConstancia" TEXT, + "ClaveGarantia" TEXT, + "ValorUnitarioTitulo" NUMERIC, + "TotalGarantia" NUMERIC, + "CantidadUnidadesMedida" NUMERIC, + "TitulosAsignados" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro556" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "Fraccion" TEXT, + "SecuenciaFraccion" TEXT, + "ClaveContribucion" TEXT, + "TasaContribucion" NUMERIC, + "TipoTasa" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro557" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "Fraccion" TEXT, + "SecuenciaFraccion" TEXT, + "ClaveContribucion" TEXT, + "FormaPago" TEXT, + "ImportePago" NUMERIC, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro558" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "Fraccion" TEXT, + "SecuenciaFraccion" TEXT, + "SecuenciaObservacion" TEXT, + "Observaciones" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro701" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "ClaveDocumento" TEXT, + "FechaPago" TEXT, + "PedimentoAnterior" TEXT, + "PatenteAnterior" TEXT, + "SeccionAduaneraAnterior" TEXT, + "DocumentoAnterior" TEXT, + "FechaOperacionAnterior" TEXT, + "PedimentoOriginal" TEXT, + "PatenteAduanalOrig" TEXT, + "SeccionAduaneraDespOrig" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "Registro702" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "ClaveContribucion" TEXT, + "FormaPago" TEXT, + "ImportePago" NUMERIC, + "TipoPedimento" TEXT, + "FechaPagoReal" TEXT +); + +CREATE TABLE IF NOT EXISTS "RegistroInci" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "ConsecutivoRemesa" TEXT, + "NumeroSeleccion" TEXT, + "FechaInicioReconocimiento" TEXT, + "HoraInicioReconocimiento" TEXT, + "FechaFinReconocimiento" TEXT, + "HoraFinReconocimiento" TEXT, + "Fraccion" TEXT, + "SecuenciaFraccion" TEXT, + "ClaveDocumento" TEXT, + "TipoOperacion" TEXT, + "GradoIncidencia" TEXT, + "FechaSeleccion" TEXT +); + +CREATE TABLE IF NOT EXISTS "RegistroResumen" ( + "Folio" TEXT, + "RFCoPatenteAduanal" TEXT, + "Fecha_Inicial" TEXT, + "Fecha_Final" TEXT, + "Fecha_Ejecucion" TEXT, + "Total_Fracciones" INTEGER, + "Total_Contribuciones" INTEGER +); + +CREATE TABLE IF NOT EXISTS "RegistroSel" ( + "Patente" TEXT, + "Pedimento" TEXT, + "SeccionAduanera" TEXT, + "ConsecutivoRemesa" TEXT, + "NumeroSeleccion" TEXT, + "FechaSeleccion" TEXT, + "HoraSeleccion" TEXT, + "SemaforoFiscal" TEXT, + "ClaveDocumento" TEXT, + "TipoOperacion" TEXT +); + +CREATE TABLE IF NOT EXISTS base_numpartes ( + numparte TEXT PRIMARY KEY, + descripcion TEXT, + unimed TEXT, + fraccion TEXT +); \ No newline at end of file