feature/analiticas de saldos

This commit is contained in:
Ernesto Herrera
2026-05-27 06:58:50 -06:00
parent c450cfa150
commit e77813a45c
2 changed files with 400 additions and 31 deletions

View File

@@ -1384,7 +1384,143 @@
" prog.done(f'{inserted} insertados')\n",
"\n",
"\n",
"\n"
"def calcular_saldos_descargas_por_anio_fraccion(top_n=30, fraccion_digitos=4, progress=None):\n",
" \"\"\"Calcula peso/cantidad/valor IMPORTADO y DESCARGADO por (anio, fraccion).\n",
" fraccion_digitos: 4, 6 u 8. top_n: limita a las N fracciones con mas actividad.\"\"\"\n",
" prog = _Progress(progress)\n",
" prog.setup(2, 'Cargando IMPO por fraccion...')\n",
" fdig = int(fraccion_digitos)\n",
" sql_impo = f\"\"\"\n",
" SELECT YEAR(FECHAFACTURA_ISO) AS ANIO,\n",
" LEFT(FRACCIONIMPO, {fdig}) AS FRACCION,\n",
" SUM(ISNULL(PESONETO,0)) AS peso_impo,\n",
" SUM(ISNULL(CANTEXITENCIA,0)) AS cant_impo,\n",
" SUM(ISNULL(VALORIMPOMN,0)) AS valor_impo_mn\n",
" FROM SSaldoTem\n",
" WHERE FECHAFACTURA_ISO IS NOT NULL AND FRACCIONIMPO IS NOT NULL\n",
" GROUP BY YEAR(FECHAFACTURA_ISO), LEFT(FRACCIONIMPO, {fdig})\n",
" \"\"\"\n",
" df_i = pd.read_sql(sql_impo, scaii_conn)\n",
" prog.step(desc='Cargando DESCARGAS por fraccion...')\n",
" sql_desc = f\"\"\"\n",
" SELECT YEAR(f.FECHAFACTURA_ISO) AS ANIO,\n",
" LEFT(sp.FRACCION, {fdig}) AS FRACCION,\n",
" SUM(ISNULL(d.PESONETO,0)) AS peso_descargas,\n",
" SUM(ISNULL(d.CANTDESC,0)) AS cant_descargas,\n",
" SUM(ISNULL(d.VALORMN,0)) AS valor_descargas_mn\n",
" FROM SDescargaT d\n",
" INNER JOIN SFacExp f ON f.FACTURAEXPO = d.FACTEXPO\n",
" INNER JOIN SPartes sp ON sp.NUMPARTE = d.NUMPARTE\n",
" WHERE f.FECHAFACTURA_ISO IS NOT NULL AND sp.FRACCION IS NOT NULL\n",
" GROUP BY YEAR(f.FECHAFACTURA_ISO), LEFT(sp.FRACCION, {fdig})\n",
" \"\"\"\n",
" df_d = pd.read_sql(sql_desc, scaii_conn)\n",
" cmp = df_i.merge(df_d, on=['ANIO','FRACCION'], how='outer').fillna(0)\n",
" if cmp.empty:\n",
" prog.done('Sin datos'); return cmp\n",
" cmp['ANIO'] = cmp['ANIO'].astype(int)\n",
" tot = (cmp.groupby('FRACCION')[['peso_impo','peso_descargas']].sum().sum(axis=1)\n",
" .sort_values(ascending=False).head(int(top_n)))\n",
" cmp = cmp[cmp['FRACCION'].isin(tot.index)].copy()\n",
" for c in ['peso_impo','peso_descargas','cant_impo','cant_descargas',\n",
" 'valor_impo_mn','valor_descargas_mn']:\n",
" if c in cmp.columns: cmp[c] = cmp[c].round(2)\n",
" prog.done(f'{len(tot)} fracciones')\n",
" return cmp.sort_values(['ANIO','FRACCION']).reset_index(drop=True)\n",
"\n",
"\n",
"\n",
"def graficar_saldos_descargas_anio_fraccion(cmp, metric='peso'):\n",
" \"\"\"Por cada anio, dos barras separadas (IMPO solido y DESC con hatch),\n",
" apiladas internamente por fraccion con % en cada segmento. Totales rotados 90.\n",
" Las barras estan visualmente agrupadas dentro de un 'bloque' por anio y los\n",
" anios estan bien separados entre si.\"\"\"\n",
" plt.close('all')\n",
" if cmp is None or cmp.empty:\n",
" print('Sin datos para graficar.'); return None\n",
" if metric == 'cantidad':\n",
" col_i, col_d, ylab = 'cant_impo', 'cant_descargas', 'Cantidad'\n",
" elif metric == 'valor_mn':\n",
" col_i, col_d, ylab = 'valor_impo_mn', 'valor_descargas_mn', 'Valor MN'\n",
" else:\n",
" col_i, col_d, ylab = 'peso_impo', 'peso_descargas', 'Peso neto'\n",
" anios = sorted(cmp['ANIO'].astype(int).unique())\n",
" total_por_frac = (cmp.groupby('FRACCION')[[col_i, col_d]].sum().sum(axis=1)\n",
" .sort_values(ascending=False))\n",
" fracs = total_por_frac.index.tolist()\n",
" pv_i = (cmp.pivot_table(index='ANIO', columns='FRACCION', values=col_i, aggfunc='sum')\n",
" .reindex(anios).fillna(0))\n",
" pv_d = (cmp.pivot_table(index='ANIO', columns='FRACCION', values=col_d, aggfunc='sum')\n",
" .reindex(anios).fillna(0))\n",
" tot_i = pv_i.sum(axis=1).values\n",
" tot_d = pv_d.sum(axis=1).values\n",
" # Mas espacio entre anios para que IMPO/DESC no se empalmen entre vecinos\n",
" sep_anios = 1.6 # multiplicador de separacion entre anios\n",
" xpos = np.arange(len(anios), dtype=float) * sep_anios\n",
" width = 0.55\n",
" gap = 0.65 # separacion entre IMPO y DESC dentro del mismo anio\n",
" fig, ax = plt.subplots(figsize=(max(14, len(anios)*1.6), 8))\n",
" fig.suptitle(f'IMPO vs DESCARGAS por anio - apilado por fraccion ({ylab})',\n",
" fontsize=13, fontweight='bold')\n",
" cmap = plt.cm.tab20\n",
" colors = [cmap(i % 20) for i in range(len(fracs))]\n",
" bot_i = np.zeros(len(anios)); bot_d = np.zeros(len(anios))\n",
" for i, frac in enumerate(fracs):\n",
" iv = pv_i[frac].astype(float).values if frac in pv_i.columns else np.zeros(len(anios))\n",
" dv = pv_d[frac].astype(float).values if frac in pv_d.columns else np.zeros(len(anios))\n",
" ax.bar(xpos - gap/2, iv, width, bottom=bot_i, color=colors[i],\n",
" edgecolor='white', linewidth=0.5, label=frac)\n",
" ax.bar(xpos + gap/2, dv, width, bottom=bot_d, color=colors[i],\n",
" edgecolor='white', linewidth=0.5, hatch='///')\n",
" for j in range(len(anios)):\n",
" # % en segmentos significativos (>= 5%)\n",
" if tot_i[j] > 0 and iv[j]/tot_i[j] >= 0.05:\n",
" ax.text(xpos[j] - gap/2, bot_i[j] + iv[j]/2,\n",
" f'{iv[j]/tot_i[j]*100:.0f}%',\n",
" ha='center', va='center', fontsize=7, color='black')\n",
" if tot_d[j] > 0 and dv[j]/tot_d[j] >= 0.05:\n",
" ax.text(xpos[j] + gap/2, bot_d[j] + dv[j]/2,\n",
" f'{dv[j]/tot_d[j]*100:.0f}%',\n",
" ha='center', va='center', fontsize=7, color='black')\n",
" bot_i += iv; bot_d += dv\n",
" ymax = max(bot_i.max() if len(bot_i) else 0, bot_d.max() if len(bot_d) else 0, 1)\n",
" # Totales arriba, rotados 90 grados para que no se traslapen\n",
" for j, x in enumerate(xpos):\n",
" if tot_i[j] > 0:\n",
" ax.text(x - gap/2, bot_i[j] + ymax*0.005,\n",
" f'{tot_i[j]:,.0f}',\n",
" ha='center', va='bottom', fontsize=8, color='#1976D2',\n",
" fontweight='bold', rotation=90)\n",
" if tot_d[j] > 0:\n",
" ax.text(x + gap/2, bot_d[j] + ymax*0.005,\n",
" f'{tot_d[j]:,.0f}',\n",
" ha='center', va='bottom', fontsize=8, color='#8E24AA',\n",
" fontweight='bold', rotation=90)\n",
" # XTicks: solo el anio, centrado entre las dos barras\n",
" ax.set_xticks(xpos)\n",
" ax.set_xticklabels(anios, fontsize=10, fontweight='bold')\n",
" # Sub-etiquetas IMPO/DESC abajo de cada barra (mas separadas del eje X)\n",
" yoff = -ymax * 0.045\n",
" for j, x in enumerate(xpos):\n",
" ax.text(x - gap/2, yoff, 'IMPO', ha='center', va='top',\n",
" fontsize=7, color='#1976D2', fontweight='bold')\n",
" ax.text(x + gap/2, yoff, 'DESC', ha='center', va='top',\n",
" fontsize=7, color='#8E24AA', fontweight='bold')\n",
" ax.set_xlabel('\\nAnio (IMPO = importado solido, DESC = descargado con rayas)', fontsize=10)\n",
" ax.set_ylabel(ylab)\n",
" ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n",
" # Dejar espacio extra arriba para los totales rotados\n",
" ax.set_ylim(top=ymax * 1.25)\n",
" max_leg = 30\n",
" handles, labels = ax.get_legend_handles_labels()\n",
" if len(labels) > max_leg:\n",
" handles = handles[:max_leg]; labels = labels[:max_leg] + [f'... (+{len(fracs)-max_leg})']\n",
" handles.append(plt.Rectangle((0,0),1,1, color='lightgray'))\n",
" ax.legend(handles, labels, title='Fraccion', bbox_to_anchor=(1.02, 1),\n",
" loc='upper left', fontsize=7, ncol=1)\n",
" ax.grid(axis='y', linestyle=':', alpha=0.4)\n",
" plt.tight_layout()\n",
" return fig"
]
},
{
@@ -2121,39 +2257,194 @@
" return pd.read_sql(sql, scaii_conn, params=(fecha_corte,))\n",
"\n",
"\n",
"def graficar_saldos_vencidos_por_anio(df, metric='SALDO_VMN'):\n",
"def cargar_saldos_vencidos_por_fraccion(fecha_corte=None, fraccion_digitos=4, top_n=30):\n",
" \"\"\"Saldos vencidos agrupados por LEFT(FRACCIONIMPO, N). Top N fracciones.\"\"\"\n",
" if fecha_corte is None:\n",
" fecha_corte = _dt_sv.date.today().isoformat()\n",
" fdig = int(fraccion_digitos)\n",
" sql = f\"\"\"\n",
" SELECT LEFT(FRACCIONIMPO, {fdig}) AS FRACCION,\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",
" SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n",
" CASE WHEN CANTEXITENCIA > 0\n",
" THEN (ISNULL(PESONETO,0) * 1.0 / CANTEXITENCIA)\n",
" ELSE 0 END) AS SALDO_PESONETO\n",
" FROM SSaldoTem\n",
" WHERE FECHAVENC_ISO IS NOT NULL\n",
" AND FECHAVENC_ISO < ?\n",
" AND FRACCIONIMPO IS NOT NULL\n",
" AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n",
" GROUP BY LEFT(FRACCIONIMPO, {fdig})\n",
" ORDER BY SALDO_VMN DESC\n",
" \"\"\"\n",
" df = pd.read_sql(sql, scaii_conn, params=(fecha_corte,))\n",
" if int(top_n) and len(df) > int(top_n):\n",
" df = df.head(int(top_n))\n",
" return df\n",
"\n",
"\n",
"def cargar_saldos_vencidos_por_unimed(fecha_corte=None):\n",
" \"\"\"Saldos vencidos agrupados por UMEXITENCIA.\"\"\"\n",
" if fecha_corte is None:\n",
" fecha_corte = _dt_sv.date.today().isoformat()\n",
" sql = \"\"\"\n",
" SELECT ISNULL(NULLIF(LTRIM(RTRIM(UMEXITENCIA)),''),'(sin UM)') AS UMEXITENCIA,\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",
" SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n",
" CASE WHEN CANTEXITENCIA > 0\n",
" THEN (ISNULL(PESONETO,0) * 1.0 / CANTEXITENCIA)\n",
" ELSE 0 END) AS SALDO_PESONETO\n",
" FROM SSaldoTem\n",
" WHERE FECHAVENC_ISO IS NOT NULL\n",
" AND FECHAVENC_ISO < ?\n",
" AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n",
" GROUP BY ISNULL(NULLIF(LTRIM(RTRIM(UMEXITENCIA)),''),'(sin UM)')\n",
" ORDER BY SALDO_VMN DESC\n",
" \"\"\"\n",
" return pd.read_sql(sql, scaii_conn, params=(fecha_corte,))\n",
"\n",
"\n",
"def _fmt_sv_df(df):\n",
" \"\"\"Devuelve un Styler con formato de dinero y cantidades.\"\"\"\n",
" if df is None or df.empty: return df\n",
" fmt = {}\n",
" if 'SALDO_VMN' in df.columns: fmt['SALDO_VMN'] = '${:,.2f}'\n",
" if 'SALDO_VME' in df.columns: fmt['SALDO_VME'] = 'US${:,.2f}'\n",
" if 'SALDO_CANT' in df.columns: fmt['SALDO_CANT'] = '{:,.2f}'\n",
" if 'SALDO_PESONETO' in df.columns: fmt['SALDO_PESONETO'] = '{:,.2f}'\n",
" if 'LOTES' in df.columns: fmt['LOTES'] = '{:,}'\n",
" if 'FACTURAS' in df.columns: fmt['FACTURAS'] = '{:,}'\n",
" try:\n",
" return df.style.format(fmt).set_properties(**{'text-align':'right'})\n",
" except Exception:\n",
" return df\n",
"\n",
"\n",
"def _fmt_eje_y(metric, ax):\n",
" \"\"\"Aplica formato $ o numerico al eje Y segun la metrica.\"\"\"\n",
" import matplotlib.ticker as _mtick\n",
" if metric in ('SALDO_VMN',):\n",
" ax.yaxis.set_major_formatter(_mtick.FuncFormatter(lambda v,_: f'${v:,.0f}'))\n",
" elif metric in ('SALDO_VME',):\n",
" ax.yaxis.set_major_formatter(_mtick.FuncFormatter(lambda v,_: f'US${v:,.0f}'))\n",
" else:\n",
" ax.yaxis.set_major_formatter(_mtick.FuncFormatter(lambda v,_: f'{v:,.0f}'))\n",
"\n",
"\n",
"def _fmt_etq_metric(metric, value):\n",
" if metric == 'SALDO_VMN': return f'${value:,.0f}'\n",
" if metric == 'SALDO_VME': return f'US${value:,.0f}'\n",
" return f'{value:,.0f}'\n",
"\n",
"\n",
"_TITULOS_METRIC_SV = {\n",
" 'SALDO_VMN': 'Valor MN (pesos mexicanos)',\n",
" 'SALDO_VME': 'Valor ME (dolares)',\n",
" 'SALDO_CANT': 'Cantidad disponible',\n",
" 'SALDO_PESONETO': 'Peso neto',\n",
" 'LOTES': 'Cantidad de lotes',\n",
" 'FACTURAS': 'Cantidad de facturas',\n",
"}\n",
"\n",
"\n",
"def graficar_saldos_vencidos_por_anio(df, metric='SALDO_VMN'):\n",
" plt.close('all')\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",
" print('Sin datos para graficar.'); return None\n",
" col_x = 'ANIO_FACTURA' if 'ANIO_FACTURA' in df.columns else 'ANIO_VENC'\n",
" fig, ax = plt.subplots(figsize=(11, 5.5))\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",
" f'{lotes:,} lotes\\n{_fmt_etq_metric(metric, b.get_height())}',\n",
" ha='center', va='bottom', fontsize=8, color='#333')\n",
" titulo_metric = _TITULOS_METRIC_SV.get(metric, metric)\n",
" ax.set_title(f'Saldos vencidos por anio - {titulo_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.set_xlabel('Anio'); ax.set_ylabel(titulo_metric)\n",
" _fmt_eje_y(metric, ax)\n",
" ax.grid(axis='y', linestyle='--', alpha=0.5)\n",
" ax.set_ylim(top=max(y) * 1.18 if y else 1)\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",
" ))"
" display(_fmt_sv_df(df))\n",
" return fig\n",
"\n",
"\n",
"def graficar_saldos_vencidos_por_fraccion(df, metric='SALDO_VMN'):\n",
" plt.close('all')\n",
" if df is None or df.empty:\n",
" print('Sin datos para graficar.'); return None\n",
" fig, ax = plt.subplots(figsize=(max(11, len(df)*0.6), 6.5))\n",
" x = df['FRACCION'].astype(str).tolist()\n",
" y = df[metric].astype(float).tolist()\n",
" bars = ax.bar(x, y, color='#6A1B9A', edgecolor='#4A148C')\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:,}\\n{_fmt_etq_metric(metric, b.get_height())}',\n",
" ha='center', va='bottom', fontsize=7, color='#333', rotation=90)\n",
" titulo_metric = _TITULOS_METRIC_SV.get(metric, metric)\n",
" ax.set_title(f'Saldos vencidos por fraccion - {titulo_metric}',\n",
" fontsize=13, fontweight='bold', color='#4A148C')\n",
" ax.set_xlabel('Fraccion arancelaria'); ax.set_ylabel(titulo_metric)\n",
" _fmt_eje_y(metric, ax)\n",
" ax.grid(axis='y', linestyle='--', alpha=0.5)\n",
" ax.set_ylim(top=max(y) * 1.25 if y else 1)\n",
" plt.xticks(rotation=45, ha='right', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.show()\n",
" display(_fmt_sv_df(df))\n",
" return fig\n",
"\n",
"\n",
"def graficar_saldos_vencidos_por_unimed(df, metric='SALDO_VMN'):\n",
" plt.close('all')\n",
" if df is None or df.empty:\n",
" print('Sin datos para graficar.'); return None\n",
" fig, ax = plt.subplots(figsize=(max(11, len(df)*0.7), 5.5))\n",
" x = df['UMEXITENCIA'].astype(str).tolist()\n",
" y = df[metric].astype(float).tolist()\n",
" bars = ax.bar(x, y, color='#00838F', edgecolor='#006064')\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\\n{_fmt_etq_metric(metric, b.get_height())}',\n",
" ha='center', va='bottom', fontsize=8, color='#333')\n",
" titulo_metric = _TITULOS_METRIC_SV.get(metric, metric)\n",
" ax.set_title(f'Saldos vencidos por unidad de medida - {titulo_metric}',\n",
" fontsize=13, fontweight='bold', color='#006064')\n",
" ax.set_xlabel('Unidad de medida'); ax.set_ylabel(titulo_metric)\n",
" _fmt_eje_y(metric, ax)\n",
" ax.grid(axis='y', linestyle='--', alpha=0.5)\n",
" ax.set_ylim(top=max(y) * 1.18 if y else 1)\n",
" plt.tight_layout()\n",
" plt.show()\n",
" display(_fmt_sv_df(df))\n",
" return fig"
]
},
{
@@ -4205,13 +4496,22 @@
"\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",
" options=[('Valor MN ($)','SALDO_VMN'),('Valor ME (US$)','SALDO_VME'),\n",
" ('Cantidad disponible','SALDO_CANT'),('Peso neto','SALDO_PESONETO'),\n",
" ('Lotes','LOTES')],\n",
" value='SALDO_VMN', description='Metrica:', layout={'width':'280px'})\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",
"w_sv_agrup = W.Dropdown(\n",
" options=[('Por Anio','ANIO'),('Por Fraccion','FRACCION'),('Por Unidad de medida','UNIMED')],\n",
" value='ANIO', description='Agrupar:', layout={'width':'260px'})\n",
"w_sv_fdig = W.RadioButtons(\n",
" options=[('4 digitos', 4), ('6 digitos', 6), ('8 digitos', 8)],\n",
" value=4, description='Frac. digitos:', layout={'width':'260px'})\n",
"w_sv_topn = W.IntSlider(value=30, min=5, max=100, step=1,\n",
" description='Top N:', layout={'width':'360px'})\n",
"btn_sv_grafica = W.Button(description='Ver grafica de saldos vencidos',\n",
" button_style='primary', icon='bar-chart', layout={'width':'320px'})\n",
"out_sv_grafica = W.Output(layout=OUT_STYLE)\n",
"\n",
@@ -4223,11 +4523,26 @@
" 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",
" try:\n",
" if w_sv_agrup.value == 'FRACCION':\n",
" df = cargar_saldos_vencidos_por_fraccion(\n",
" fecha_corte=hoy, fraccion_digitos=int(w_sv_fdig.value),\n",
" top_n=int(w_sv_topn.value))\n",
" if df.empty: print('No hay saldos vencidos.'); return\n",
" print(f'Fracciones con saldos vencidos: {len(df)} (top {int(w_sv_topn.value)})')\n",
" graficar_saldos_vencidos_por_fraccion(df, metric=w_sv_metric.value)\n",
" elif w_sv_agrup.value == 'UNIMED':\n",
" df = cargar_saldos_vencidos_por_unimed(fecha_corte=hoy)\n",
" if df.empty: print('No hay saldos vencidos.'); return\n",
" print(f'Unidades de medida con saldos vencidos: {len(df)}')\n",
" graficar_saldos_vencidos_por_unimed(df, metric=w_sv_metric.value)\n",
" else:\n",
" df = cargar_saldos_vencidos_por_anio(fecha_corte=hoy, eje=w_sv_eje.value)\n",
" if df.empty: 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",
" except Exception as e:\n",
" print(f'ERROR: {e}')\n",
"btn_sv_grafica.on_click(_on_sv_grafica)\n",
"\n",
"tab_sv.children = tuple(list(tab_sv.children) + [\n",
@@ -4236,7 +4551,10 @@
" 'Resumen global de saldos en SSaldoTem cuyo <b>FECHAVENC_ISO &lt; hoy</b> y '\n",
" 'SALDO_DISPONIBLE &gt; 0, agrupados por anio de vencimiento. '\n",
" 'No depende del rango de fechas de arriba.</p>'),\n",
" W.HBox([w_sv_metric, w_sv_eje, btn_sv_grafica]),\n",
" W.HBox([w_sv_agrup, w_sv_metric]),\n",
" W.HBox([w_sv_eje, w_sv_fdig, w_sv_topn]),\n",
" W.HTML('<small style=\"color:#777;\">Eje X aplica solo a \"Por Anio\". Fraccion digitos y Top N aplican solo a \"Por Fraccion\".</small>'),\n",
" W.HBox([btn_sv_grafica]),\n",
" out_sv_grafica,\n",
"])\n",
"\n",
@@ -5400,6 +5718,57 @@
"])\n",
"\n",
"\n",
"\n",
"\n",
"# ===== Saldo IMPO vs Descargas por fraccion =====\n",
"w_frac_topn = W.IntSlider(value=30, min=3, max=100, step=1, description='Top fracciones:',\n",
" layout={'width':'380px'})\n",
"w_frac_dig = W.RadioButtons(options=[('4 digitos (Capitulo+Partida)', 4),\n",
" ('6 digitos (Subpartida)', 6),\n",
" ('8 digitos (Completa)', 8)],\n",
" value=4, description='Granularidad:', layout={'width':'380px'})\n",
"w_frac_met = W.Dropdown(options=[('Peso neto','peso'),('Cantidad','cantidad'),('Valor MN','valor_mn')],\n",
" value='peso', description='Metrica:', layout={'width':'260px'})\n",
"btn_frac = W.Button(description='Calcular saldos vs descargas por fraccion',\n",
" button_style='primary', icon='bar-chart', layout={'width':'380px'})\n",
"bar_frac = _mkbar('Por fraccion')\n",
"out_frac_log = W.Output(layout=OUT_STYLE)\n",
"out_frac_tabla = W.Output(layout=OUT_STYLE)\n",
"\n",
"def _on_frac(_):\n",
" with out_frac_log:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" df = calcular_saldos_descargas_por_anio_fraccion(\n",
" top_n=int(w_frac_topn.value),\n",
" fraccion_digitos=int(w_frac_dig.value),\n",
" progress=bar_frac)\n",
" _state['frac_df'] = df\n",
" if df.empty:\n",
" print('Sin datos.'); return\n",
" print(f'Filas: {len(df):,} | Anios: {df[\"ANIO\"].nunique()} | Fracciones: {df[\"FRACCION\"].nunique()}')\n",
" fig = graficar_saldos_descargas_anio_fraccion(df, metric=w_frac_met.value)\n",
" if fig is not None: display(fig); plt.close(fig)\n",
" with out_frac_tabla:\n",
" clear_output()\n",
" if 'frac_df' in _state and not _state['frac_df'].empty:\n",
" display(HTML('<h4>Tabla detalle por anio y fraccion</h4>'))\n",
" display(_state['frac_df'])\n",
"btn_frac.on_click(_on_frac)\n",
"\n",
"tab_an.children = tuple(list(tab_an.children) + [\n",
" W.HTML('<h3 style=\"margin-top:24px;border-top:2px solid #6A1B9A;padding-top:14px;\">Saldo IMPO disponible vs Descargas por anio y fraccion</h3>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Cruza SSaldoTem (saldo IMPO disponible) y '\n",
" 'SDescargaT (descargas) agrupando por <b>anio</b> y <b>fraccion arancelaria</b>. '\n",
" 'Muestra solo las fracciones con mas actividad (Top N).</p>'),\n",
" W.HBox([w_frac_topn]),\n",
" W.HBox([w_frac_dig]),\n",
" W.HBox([w_frac_met, btn_frac]),\n",
" bar_frac, out_frac_log, out_frac_tabla,\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",