feature/Descargas por % de incremento de peso o cantidad

This commit is contained in:
Ernesto Herrera
2026-05-25 08:20:43 -06:00
parent a65f34d852
commit c450cfa150

View File

@@ -521,7 +521,447 @@
" _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, 'STANDARD', 'paso 10', progress=progress)\n", " _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, 'STANDARD', 'paso 10', progress=progress)\n",
"\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", "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)" " _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, 'KG_PCT', 'paso 12 complementaria (% KGS)', progress=progress)\n",
"\n",
"\n",
"# =============================================================\n",
"# REASIGNACION DE LINEAS EXPO\n",
"# Mueve filas de SDescargaT (FACTEXPO, FACREFERENCIA, LINEAEXPO) a otra\n",
"# factura/linea EXPO indicada en un Excel.\n",
"# =============================================================\n",
"\n",
"import io as _io_reasig\n",
"\n",
"def generar_plantilla_reasignacion():\n",
" \"\"\"Devuelve bytes de un xlsx con la estructura esperada.\"\"\"\n",
" df = pd.DataFrame([\n",
" {'FACTURA_EXPO': 'EXP240001', 'LINEA_EXPO': 1, 'NUMPARTE': 'ABC-12345',\n",
" 'UNIMED': 'PZA', 'LINEA_EXPO_NUEVA': 2, 'FACTURA_NUEVA': 'EXP240002'},\n",
" {'FACTURA_EXPO': 'EXP240003', 'LINEA_EXPO': 5, 'NUMPARTE': 'DEF-67890',\n",
" 'UNIMED': 'KGS', 'LINEA_EXPO_NUEVA': 5, 'FACTURA_NUEVA': 'EXP240003'},\n",
" ])\n",
" buf = _io_reasig.BytesIO()\n",
" with pd.ExcelWriter(buf, engine='openpyxl') as w:\n",
" df.to_excel(w, sheet_name='Reasignacion', index=False)\n",
" buf.seek(0)\n",
" return buf.read()\n",
"\n",
"\n",
"def cargar_excel_reasignacion(path):\n",
" \"\"\"Lee Excel con 6 columnas. Acepta variantes razonables de nombres.\"\"\"\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",
" 'FACTURA_EXPO': ['FACTURA_EXPO', 'FACTURAEXPO', 'FACTURA_ORIGEN', 'FACTEXPO'],\n",
" 'LINEA_EXPO': ['LINEA_EXPO', 'LINEAEXPO', 'LINEA_ORIGEN', 'LINEA'],\n",
" 'NUMPARTE': ['NUMPARTE', 'NUM_PARTE', 'NUMERO_PARTE', 'PARTE'],\n",
" 'UNIMED': ['UNIMED', 'UNIDAD_DE_MEDIDA', 'UM', 'UNIDAD'],\n",
" 'LINEA_EXPO_NUEVA': ['LINEA_EXPO_NUEVA', 'LINEANUEVA', 'LINEA_NUEVA', 'LINEA_DESTINO'],\n",
" 'FACTURA_NUEVA': ['FACTURA_NUEVA', 'FACTURANUEVA', 'FACTURA_DESTINO'],\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",
" for c in ['FACTURA_EXPO','NUMPARTE','UNIMED','FACTURA_NUEVA']:\n",
" df2[c] = df2[c].fillna('').astype(str).str.strip()\n",
" for c in ['LINEA_EXPO','LINEA_EXPO_NUEVA']:\n",
" df2[c] = pd.to_numeric(df2[c], errors='coerce').fillna(0).astype(int)\n",
" df2 = df2[df2['FACTURA_EXPO'] != ''].reset_index(drop=True)\n",
" return df2\n",
"\n",
"\n",
"def analizar_reasignacion(df_excel, progress=None, log=print):\n",
" \"\"\"Paso A: para cada fila del Excel verifica que (a) la descarga origen\n",
" sea unica en SDescargaT, (b) el destino exista en SFacExp+SPartidasExpo.\n",
" Devuelve (plan_ok, inconsistencias).\"\"\"\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",
" prog.setup(len(df_excel), 'Validando filas...')\n",
" ok_rows = []\n",
" inc_rows = []\n",
" for _, r in df_excel.iterrows():\n",
" f_ori = r['FACTURA_EXPO']\n",
" l_ori = int(r['LINEA_EXPO'])\n",
" npart = r['NUMPARTE']\n",
" umed = r['UNIMED']\n",
" l_new = int(r['LINEA_EXPO_NUEVA'])\n",
" f_new = r['FACTURA_NUEVA']\n",
" base = {'FACTURA_EXPO': f_ori, 'LINEA_EXPO': l_ori, 'NUMPARTE': npart,\n",
" 'UNIMED': umed, 'LINEA_EXPO_NUEVA': l_new, 'FACTURA_NUEVA': f_new}\n",
" # 1) Buscar descarga origen (debe ser UNICA)\n",
" try:\n",
" df_d = pd.read_sql(\"\"\"\n",
" SELECT CONSECUTIVO FROM SDescargaT\n",
" WHERE FACTEXPO=? AND LINEAEXPO=? AND NUMPARTE=? AND UNIMED=?\n",
" \"\"\", scaii_conn, params=(f_ori, l_ori, npart, umed))\n",
" except Exception as e:\n",
" inc_rows.append({**base, 'STATUS': 'ERROR_QUERY_ORIGEN', 'DETALLE': str(e),\n",
" 'CONSECUTIVO_DESC': None})\n",
" prog.step(); continue\n",
" if df_d.empty:\n",
" inc_rows.append({**base, 'STATUS': 'ORIGEN_NO_ENCONTRADO', 'DETALLE': 'Sin filas en SDescargaT',\n",
" 'CONSECUTIVO_DESC': None})\n",
" prog.step(); continue\n",
" if len(df_d) > 1:\n",
" inc_rows.append({**base, 'STATUS': 'ORIGEN_DUPLICADO',\n",
" 'DETALLE': f'{len(df_d)} filas en SDescargaT con esa combinacion',\n",
" 'CONSECUTIVO_DESC': ', '.join(str(c) for c in df_d['CONSECUTIVO'].tolist())})\n",
" prog.step(); continue\n",
" consec = int(df_d.iloc[0]['CONSECUTIVO'])\n",
" # 2) Validar destino existe en SFacExp + SPartidasExpo (factura + linea)\n",
" try:\n",
" df_dest = pd.read_sql(\"\"\"\n",
" SELECT pe.LINEA\n",
" FROM SPartidasExpo pe\n",
" INNER JOIN SFacExp fe ON fe.FACTURAEXPO = pe.FACTURAEXPO\n",
" WHERE pe.FACTURAEXPO = ? AND pe.LINEA = ?\n",
" \"\"\", scaii_conn, params=(f_new, l_new))\n",
" except Exception as e:\n",
" inc_rows.append({**base, 'STATUS': 'ERROR_QUERY_DESTINO', 'DETALLE': str(e),\n",
" 'CONSECUTIVO_DESC': consec})\n",
" prog.step(); continue\n",
" if df_dest.empty:\n",
" inc_rows.append({**base, 'STATUS': 'DESTINO_NO_EXISTE',\n",
" 'DETALLE': f'No existe FACTURA_NUEVA={f_new} con LINEA_EXPO_NUEVA={l_new}',\n",
" 'CONSECUTIVO_DESC': consec})\n",
" prog.step(); continue\n",
" # Todo OK\n",
" ok_rows.append({**base, 'CONSECUTIVO_DESC': consec, 'STATUS': 'OK'})\n",
" prog.step()\n",
" prog.done(f'{len(ok_rows)} OK / {len(inc_rows)} inconsistencias')\n",
" log(f'Validacion: {len(ok_rows)} OK, {len(inc_rows)} inconsistencias.')\n",
" return pd.DataFrame(ok_rows), pd.DataFrame(inc_rows)\n",
"\n",
"\n",
"def exportar_excel_reasignacion(plan, inconsistencias, ruta):\n",
" \"\"\"Guarda Plan_OK + Inconsistencias en hojas separadas.\"\"\"\n",
" with pd.ExcelWriter(ruta, engine='openpyxl') as w:\n",
" if not plan.empty:\n",
" plan.to_excel(w, sheet_name='Plan_OK', index=False)\n",
" if not inconsistencias.empty:\n",
" inconsistencias.to_excel(w, sheet_name='Inconsistencias', index=False)\n",
"\n",
"\n",
"def ejecutar_reasignacion(plan_ok, dry_run=True, progress=None, log=print):\n",
" \"\"\"Paso B: UPDATE SDescargaT SET FACTEXPO, FACREFERENCIA, LINEAEXPO\n",
" para cada fila del plan OK. Transaccion por fila.\"\"\"\n",
" assert isinstance(dry_run, bool), 'dry_run debe ser bool'\n",
" prog = _Progress(progress)\n",
" if plan_ok is None or plan_ok.empty:\n",
" log('ERROR: plan vacio. Corre primero Analizar.')\n",
" return\n",
" prog.setup(len(plan_ok), 'Reasignando descargas')\n",
" UPD = \"\"\"UPDATE SDescargaT\n",
" SET FACTEXPO = ?, FACREFERENCIA = ?, LINEAEXPO = ?\n",
" WHERE CONSECUTIVO = ?\"\"\"\n",
" upd = errores = 0\n",
" err_list = []\n",
" for _, r in plan_ok.iterrows():\n",
" consec = int(r['CONSECUTIVO_DESC'])\n",
" f_new = str(r['FACTURA_NUEVA']).strip()\n",
" l_new = int(r['LINEA_EXPO_NUEVA'])\n",
" try:\n",
" with scaii_conn.cursor() as cur:\n",
" if not dry_run:\n",
" cur.execute(UPD, f_new, f_new, l_new, consec)\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",
" err_list.append((consec, str(e)))\n",
" log(f' ERROR consec {consec}: {e}')\n",
" prog.step()\n",
" prog.done(f'{upd} reasignaciones')\n",
" log(f'\\n=== RESUMEN Reasignacion (DRY_RUN={dry_run}) ===')\n",
" log(f' Descargas reasignadas: {upd:,}')\n",
" log(f' Errores : {errores:,}')\n",
" for c, e in err_list[:5]:\n",
" log(f' {c}: {e}')\n",
"\n",
"\n",
"# =============================================================\n",
"# CONSUMO DE SALDOS VENCIDOS POR RANGO (complementaria con BOM del Excel)\n",
"# =============================================================\n",
"\n",
"import io as _io_csv\n",
"import datetime as _dt_csv\n",
"\n",
"_UMS_PESO_CSV = {'KGS', 'KG', 'TON'}\n",
"\n",
"def generar_plantilla_consumo_vencidos():\n",
" df = pd.DataFrame([\n",
" {'NUMPARTE_COMPONENTE': 'COMP-001', 'PCT_MATERIAL': 8.5, 'NUMPARTE_PT': 'PT-ABC-123', 'UNIMED': 'KGS'},\n",
" {'NUMPARTE_COMPONENTE': 'COMP-002', 'PCT_MATERIAL': 2.0, 'NUMPARTE_PT': 'PT-ABC-123', 'UNIMED': 'PZA'},\n",
" {'NUMPARTE_COMPONENTE': 'COMP-003', 'PCT_MATERIAL': 12.0, 'NUMPARTE_PT': 'PT-XYZ-789', 'UNIMED': 'TON'},\n",
" ])\n",
" buf = _io_csv.BytesIO()\n",
" with pd.ExcelWriter(buf, engine='openpyxl') as w:\n",
" df.to_excel(w, sheet_name='ConsumoVencidos', index=False)\n",
" buf.seek(0)\n",
" return buf.read()\n",
"\n",
"\n",
"def cargar_excel_consumo_vencidos(path):\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_COMPONENTE': ['NUMPARTE_COMPONENTE', 'COMPONENTE', 'NUMPARTE_COMP', 'COMP'],\n",
" 'PCT_MATERIAL': ['PCT_MATERIAL', 'PCT', '%_MATERIAL', '%MATERIAL', 'PORCENTAJE'],\n",
" 'NUMPARTE_PT': ['NUMPARTE_PT', 'PT', 'NUMPARTE_PRODUCTO_TERMINADO', 'PRODUCTO_TERMINADO'],\n",
" 'UNIMED': ['UNIMED', 'UNIDAD_DE_MEDIDA', 'UM', 'UNIDAD'],\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['NUMPARTE_COMPONENTE'] = df2['NUMPARTE_COMPONENTE'].astype(str).str.strip()\n",
" df2['NUMPARTE_PT'] = df2['NUMPARTE_PT'].astype(str).str.strip()\n",
" df2['UNIMED'] = df2['UNIMED'].astype(str).str.strip().str.upper()\n",
" df2['PCT_MATERIAL'] = pd.to_numeric(df2['PCT_MATERIAL'], errors='coerce').fillna(0)\n",
" df2 = df2[(df2['NUMPARTE_COMPONENTE'] != '') & (df2['NUMPARTE_PT'] != '')].reset_index(drop=True)\n",
" return df2\n",
"\n",
"\n",
"def listar_partidas_expo_por_pt(pts):\n",
" \"\"\"Lista las partidas EXPO en facturas AC cuyo NUMPARTE esta en pts.\n",
" Devuelve un resumen por PT y la lista detallada.\"\"\"\n",
" pts = list({str(p).strip() for p in pts if str(p).strip()})\n",
" if not pts:\n",
" return pd.DataFrame(), pd.DataFrame()\n",
" LOTE = 1000\n",
" detalle_parts = []\n",
" for i in range(0, len(pts), LOTE):\n",
" sub = pts[i:i+LOTE]\n",
" ph = ','.join(['?'] * len(sub))\n",
" detalle_parts.append(pd.read_sql(f\"\"\"\n",
" SELECT pe.FACTURAEXPO, pe.LINEA, pe.NUMPARTE AS PT, pe.CANTEXPO,\n",
" ISNULL(pe.PESONETO,0) AS PESONETO, fe.FECHAFACTURA_ISO,\n",
" fe.PEDIMENTOEXPO, fe.ESTATUS\n",
" FROM SPartidasExpo pe\n",
" INNER JOIN SFacExp fe ON fe.FACTURAEXPO = pe.FACTURAEXPO\n",
" WHERE fe.ESTATUS = 'AC' AND pe.NUMPARTE IN ({ph})\n",
" ORDER BY fe.FECHAFACTURA_ISO, pe.FACTURAEXPO, pe.LINEA\n",
" \"\"\", scaii_conn, params=sub))\n",
" detalle = pd.concat(detalle_parts, ignore_index=True) if detalle_parts else pd.DataFrame()\n",
" if detalle.empty:\n",
" resumen = pd.DataFrame(columns=['PT','partidas','facturas','peso_neto_total','cantexpo_total'])\n",
" else:\n",
" resumen = (detalle.groupby('PT', as_index=False)\n",
" .agg(partidas=('LINEA', 'count'),\n",
" facturas=('FACTURAEXPO', 'nunique'),\n",
" peso_neto_total=('PESONETO', 'sum'),\n",
" cantexpo_total=('CANTEXPO', 'sum')))\n",
" return detalle, resumen\n",
"\n",
"\n",
"def _cargar_saldos_vencidos_rango(fecha_ini, fecha_fin, fecha_corte=None):\n",
" \"\"\"SSaldoTem con FECHAVENC_ISO entre rango Y < fecha_corte (hoy).\"\"\"\n",
" if fecha_corte is None:\n",
" fecha_corte = _dt_csv.date.today().isoformat()\n",
" 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",
" ISNULL(VALORIMPOMN,0) AS VALORIMPOMN,\n",
" ISNULL(VALORIMPOME,0) AS VALORIMPOME,\n",
" ISNULL(PESONETO,0) AS PESONETO_LOTE,\n",
" (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_DISPONIBLE\n",
" FROM SSaldoTem\n",
" WHERE FECHAVENC_ISO IS NOT NULL\n",
" AND FECHAVENC_ISO BETWEEN ? AND ?\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",
" df['UM_KEY'] = df['UNIDAD_MEDIDA'].fillna('').str.strip().str.upper()\n",
" return df.reset_index(drop=True)\n",
"\n",
"\n",
"def _consumir_saldos_local(saldos, idx_list, faltante, base_row, tipo, np_usado):\n",
" \"\"\"Variante de _consumir que opera sobre saldos cargados localmente.\n",
" Devuelve (faltante_restante, lista_filas).\"\"\"\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 > 1e-9 else 0.0\n",
" rows.append({**base_row,\n",
" 'NUMPARTE_USADO': np_usado,\n",
" '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",
"\n",
"def analizar_consumo_vencidos(df_excel, fecha_ini, fecha_fin, progress=None, log=print):\n",
" \"\"\"Paso A: arma df_comp + resumen + inconsistencias. NO escribe.\"\"\"\n",
" prog = _Progress(progress)\n",
" if df_excel is None or df_excel.empty:\n",
" log('Excel vacio.')\n",
" return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()\n",
"\n",
" # 1) Partidas EXPO con PT del Excel\n",
" pts = df_excel['NUMPARTE_PT'].unique().tolist()\n",
" detalle_partidas, _ = listar_partidas_expo_por_pt(pts)\n",
" if detalle_partidas.empty:\n",
" log('No hay partidas EXPO con esos PT en facturas AC.')\n",
" return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()\n",
" log(f'Partidas EXPO afectadas: {len(detalle_partidas):,}')\n",
"\n",
" # 2) Descargas previas\n",
" facturas = detalle_partidas['FACTURAEXPO'].unique().tolist()\n",
" prev_parts = []\n",
" for i in range(0, len(facturas), 500):\n",
" chunk = facturas[i:i+500]\n",
" ph = ','.join(['?']*len(chunk))\n",
" prev_parts.append(pd.read_sql(f\"\"\"\n",
" SELECT FACTEXPO AS FACTURAEXPO, LINEAEXPO AS LINEA,\n",
" PARTEORIGINAL AS COMPONENTE_MP, 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_prev = pd.concat(prev_parts, ignore_index=True) if prev_parts else pd.DataFrame(columns=['FACTURAEXPO','LINEA','COMPONENTE_MP','CANT_PREV'])\n",
" if not df_prev.empty:\n",
" df_prev['LINEA'] = df_prev['LINEA'].astype(int)\n",
" prev_dict = (df_prev.set_index(['FACTURAEXPO','LINEA','COMPONENTE_MP'])['CANT_PREV'].to_dict()\n",
" if not df_prev.empty else {})\n",
" log(f'Descargas previas indexadas: {len(prev_dict):,}')\n",
"\n",
" # 3) Saldos vencidos en el rango\n",
" saldos = _cargar_saldos_vencidos_rango(fecha_ini, fecha_fin)\n",
" log(f'Saldos vencidos en rango: {len(saldos):,}')\n",
"\n",
" # 4) Recorrer partidas, aplicar BOM del Excel\n",
" excel_por_pt = df_excel.groupby('NUMPARTE_PT')\n",
" inconsistencias = []\n",
" all_rows = []\n",
" prog.setup(len(detalle_partidas), 'Procesando partidas...')\n",
" for _, p in detalle_partidas.iterrows():\n",
" factura, linea, pt = p['FACTURAEXPO'], int(p['LINEA']), p['PT']\n",
" peso_neto = float(p['PESONETO'] or 0)\n",
" cantexpo = float(p['CANTEXPO'] or 0)\n",
" if pt not in excel_por_pt.groups:\n",
" prog.step(); continue\n",
" for _, comp in excel_por_pt.get_group(pt).iterrows():\n",
" componente = comp['NUMPARTE_COMPONENTE']\n",
" pct = float(comp['PCT_MATERIAL'] or 0) / 100.0\n",
" unimed = (comp['UNIMED'] or '').strip().upper()\n",
" if unimed in _UMS_PESO_CSV:\n",
" cant_req_total = pct * peso_neto\n",
" else:\n",
" cant_req_total = pct * cantexpo\n",
" if cant_req_total <= 1e-9:\n",
" inconsistencias.append({'FACTURAEXPO': factura, 'LINEA': linea, 'PT': pt,\n",
" 'COMPONENTE': componente, 'UNIMED': unimed, 'PCT': comp['PCT_MATERIAL'],\n",
" 'STATUS': 'CANT_REQ_CERO',\n",
" 'DETALLE': 'PESONETO/CANTEXPO en 0 o PCT en 0'})\n",
" continue\n",
" cant_prev = float(prev_dict.get((factura, linea, componente), 0.0))\n",
" cant_pend = cant_req_total - cant_prev\n",
" if cant_pend <= 1e-9:\n",
" continue # ya estaba cubierto\n",
" # Buscar saldos del componente, FIFO por FECHA_ENTRADA\n",
" saldos_idx = saldos.index[saldos['NUMPARTE'].astype(str).str.strip() == componente].tolist()\n",
" saldos_idx = sorted(saldos_idx, key=lambda i: (saldos.at[i, 'FECHA_ENTRADA'] or ''))\n",
" base_row = {'FACTURAEXPO': factura, 'FECHA_FACTURAEXPO': p['FECHAFACTURA_ISO'],\n",
" 'PEDIMENTOEXPO': p['PEDIMENTOEXPO'],\n",
" 'LINEA': linea, 'PT': pt, 'CANT_PT': cantexpo,\n",
" 'PESONETO_PARTIDA': peso_neto, 'PCT_BOM': comp['PCT_MATERIAL'],\n",
" 'MONTOIGI': None, 'COMPONENTE_MP': componente, 'UNIMED_BOM': unimed,\n",
" 'CANT_REQUERIDA': cant_pend, 'CANT_PREV': cant_prev}\n",
" faltante, rows = _consumir_saldos_local(saldos, saldos_idx, cant_pend, base_row,\n",
" 'ORIGINAL', componente)\n",
" all_rows.extend(rows)\n",
" if faltante > 1e-9:\n",
" inconsistencias.append({'FACTURAEXPO': factura, 'LINEA': linea, 'PT': pt,\n",
" 'COMPONENTE': componente, 'UNIMED': unimed, 'PCT': comp['PCT_MATERIAL'],\n",
" 'CANT_REQUERIDA': cant_pend,\n",
" 'CANT_DESCARGADA': cant_pend - faltante, 'CANT_FALTANTE': faltante,\n",
" 'STATUS': 'SIN_SALDO_SUFICIENTE',\n",
" 'DETALLE': f'Faltan {faltante:.4f} {unimed} del componente'})\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,\n",
" 'PESONETO_DESC': None, 'STATUS': 'FALTANTE'})\n",
" prog.step()\n",
" prog.done('Analisis listo')\n",
" df_comp = pd.DataFrame(all_rows)\n",
" # Catalogos PartePais para TIPOFRACCION/ADVALOREMIMPO (igual que _build_descarga_df)\n",
" if not df_comp.empty:\n",
" try:\n",
" _load_catalogos()\n",
" pp = _state.get('df_partepais_all', pd.DataFrame())\n",
" if not pp.empty:\n",
" pp_d = pp.drop_duplicates(subset=['FRACCION','PAIS'], keep='last') .set_index(['FRACCION','PAIS'])[['TIPOFRACCION','TASAIM']].to_dict('index')\n",
" def _pp(row):\n",
" info = pp_d.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_comp[['TIPOFRACCION','ADVALOREMIMPO']] = df_comp.apply(_pp, axis=1)\n",
" except Exception as e:\n",
" log(f' WARN catalogos: {e}')\n",
" df_comp['FECHADESC_CLARION'] = df_comp['FECHA_FACTURAEXPO'].apply(to_clarion)\n",
" resumen = pd.DataFrame()\n",
" if not df_comp.empty:\n",
" agg = (df_comp.groupby('TIPO').size().reset_index(name='filas'))\n",
" resumen = agg\n",
" log(f'Filas calculadas: {len(df_comp):,} | Inconsistencias: {len(inconsistencias):,}')\n",
" return df_comp, resumen, pd.DataFrame(inconsistencias)\n",
"\n",
"\n",
"def ejecutar_consumo_vencidos(df_comp, modo_comp='DIRIGIDA', dry_run=True,\n",
" progress=None, log=print):\n",
" \"\"\"Paso B: reutiliza _do_inserts (mismo INSERT que el Paso 10/12 complementaria).\"\"\"\n",
" assert isinstance(dry_run, bool), 'dry_run debe ser bool'\n",
" if df_comp is None or df_comp.empty:\n",
" log('ERROR: plan vacio.'); 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,\n",
" do_update_status=False, progress=progress)\n",
" log(f'\\n=== RESUMEN consumo vencidos ({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}')"
] ]
}, },
{ {
@@ -4687,6 +5127,279 @@
"])\n", "])\n",
"\n", "\n",
"\n", "\n",
"\n",
"\n",
"# ===== Reasignacion de Lineas EXPO (en pestania Descargas) =====\n",
"upload_reasig = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n",
"btn_reasig_plantilla = W.Button(description='Descargar plantilla', button_style='info', icon='download', layout={'width':'220px'})\n",
"btn_reasig_analizar = W.Button(description='Analizar', button_style='primary', icon='search', layout={'width':'180px'})\n",
"btn_reasig_xlsx = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'200px'})\n",
"dry_reasig = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n",
"btn_reasig_ejecutar = W.Button(description='Ejecutar reasignacion', button_style='warning', icon='play', layout={'width':'260px'})\n",
"bar_reasig = _mkbar('Reasignacion')\n",
"out_reasig_log = W.Output(layout=OUT_STYLE)\n",
"out_reasig_tabla = W.Output(layout=OUT_STYLE)\n",
"\n",
"_html_formato_reasig = ('<div style=\"background:#E3F2FD;border:1px solid #1565C0;padding:12px;border-radius:6px;margin:8px 0;\">'\n",
" '<b style=\"color:#0D47A1;\">Formato esperado del Excel (6 columnas)</b>'\n",
" '<table style=\"border-collapse:collapse;font-size:12px;margin-top:6px;\">'\n",
" '<tr style=\"background:#1565C0;color:white;\"><th style=\"padding:4px 10px;text-align:left;\">Columna</th><th style=\"padding:4px 10px;text-align:left;\">Ejemplo</th></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>FACTURA_EXPO</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">EXP240001</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>LINEA_EXPO</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">1</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>NUMPARTE</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">ABC-12345</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>UNIMED</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">PZA</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>LINEA_EXPO_NUEVA</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">2</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>FACTURA_NUEVA</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">EXP240002</td></tr>'\n",
" '</table>'\n",
" '<p style=\"margin:8px 0 0 0;color:#555;font-size:11px;\">Las 4 primeras identifican la descarga origen (debe ser UNICA en SDescargaT). Las 2 ultimas son el destino (debe existir en SFacExp+SPartidasExpo).</p>'\n",
" '</div>')\n",
"\n",
"def _on_reasig_plantilla(_):\n",
" with out_reasig_log:\n",
" clear_output()\n",
" bts = generar_plantilla_reasignacion()\n",
" path = os.path.join(os.getcwd(), 'plantilla_reasignacion_lineas_expo.xlsx')\n",
" with open(path, 'wb') as f: f.write(bts)\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:10px;border-radius:6px;margin:8px 0;\">'\n",
" f'<b style=\"color:#1B5E20;\">Plantilla generada</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;color:#333;\">{path}</span></div>'))\n",
"btn_reasig_plantilla.on_click(_on_reasig_plantilla)\n",
"\n",
"def _on_reasig_analizar(_):\n",
" with out_reasig_log:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" if len(upload_reasig.value) == 0:\n",
" print('ERROR: sube un Excel primero.'); return\n",
" try:\n",
" if isinstance(upload_reasig.value, dict):\n",
" fname = list(upload_reasig.value.keys())[0]\n",
" fb = upload_reasig.value[fname]['content']\n",
" else:\n",
" fb = upload_reasig.value[0]['content']\n",
" tmp = os.path.join(os.getcwd(), '_upload_reasig.xlsx')\n",
" with open(tmp, 'wb') as f: f.write(fb)\n",
" df_excel = cargar_excel_reasignacion(tmp)\n",
" print(f'Excel cargado: {len(df_excel):,} filas')\n",
" except Exception as e:\n",
" print(f'ERROR Excel: {e}'); return\n",
" plan_ok, incons = analizar_reasignacion(df_excel, progress=bar_reasig, log=print)\n",
" _state['reasig_plan_ok'] = plan_ok\n",
" _state['reasig_incons'] = incons\n",
" with out_reasig_tabla:\n",
" clear_output()\n",
" if 'reasig_plan_ok' in _state and not _state['reasig_plan_ok'].empty:\n",
" display(HTML('<h4>Plan OK (se aplicaran en Paso B)</h4>'))\n",
" display(_state['reasig_plan_ok'])\n",
" if 'reasig_incons' in _state and not _state['reasig_incons'].empty:\n",
" display(HTML('<h4 style=\"color:#C62828;\">Inconsistencias (NO se aplican)</h4>'))\n",
" display(_state['reasig_incons'])\n",
"btn_reasig_analizar.on_click(_on_reasig_analizar)\n",
"\n",
"def _on_reasig_xlsx(_):\n",
" with out_reasig_log:\n",
" if 'reasig_plan_ok' not in _state and 'reasig_incons' not in _state:\n",
" display(HTML('<div style=\"color:#C62828;\">Corre Analizar primero.</div>')); return\n",
" path = os.path.join(os.getcwd(), 'reasignacion_lineas_expo.xlsx')\n",
" exportar_excel_reasignacion(_state.get('reasig_plan_ok', pd.DataFrame()),\n",
" _state.get('reasig_incons', pd.DataFrame()), path)\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:10px;border-radius:6px;margin:8px 0;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;color:#333;\">{path}</span></div>'))\n",
"btn_reasig_xlsx.on_click(_on_reasig_xlsx)\n",
"\n",
"def _on_reasig_ejecutar(_):\n",
" with out_reasig_log:\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if 'reasig_plan_ok' not in _state:\n",
" print('ERROR: corre primero Analizar.'); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" ejecutar_reasignacion(_state['reasig_plan_ok'], dry_run=dry_reasig.value,\n",
" log=print, progress=bar_reasig)\n",
"btn_reasig_ejecutar.on_click(_on_reasig_ejecutar)\n",
"\n",
"tab_desc.children = tuple(list(tab_desc.children) + [\n",
" W.HTML('<h3 style=\"margin-top:24px;border-top:2px solid #00838F;padding-top:14px;\">Reasignacion de Lineas EXPO</h3>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Sube un Excel con las descargas a reasignar. La utileria '\n",
" 'cambia <b>FACTEXPO, FACREFERENCIA y LINEAEXPO</b> en SDescargaT (no crea nuevas filas, no '\n",
" 'toca SDescargaM ni SFacExp). Valida que cada descarga origen sea unica y que el destino exista; '\n",
" 'las que no cumplen quedan en la hoja de inconsistencias.</p>'),\n",
" W.HTML(_html_formato_reasig),\n",
" btn_reasig_plantilla,\n",
" W.HTML('<p style=\"margin-top:14px;\"><b>Sube tu Excel:</b></p>'),\n",
" W.HBox([upload_reasig]),\n",
" W.HBox([btn_reasig_analizar, btn_reasig_xlsx]), bar_reasig,\n",
" out_reasig_log, out_reasig_tabla,\n",
" W.HTML('<h4 style=\"margin-top:20px;border-top:2px solid #FFA726;padding-top:14px;\">Paso B - Ejecutar reasignacion</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Solo aplica las filas del <b>Plan OK</b>. '\n",
" 'Las inconsistencias se ignoran. <b>Corre primero con DRY_RUN activado.</b></p>'),\n",
" W.HBox([dry_reasig]),\n",
" btn_reasig_ejecutar,\n",
"])\n",
"\n",
"\n",
"\n",
"\n",
"# ===== Consumo de Saldos Vencidos por Rango (en pestania Descargas) =====\n",
"upload_csv_ = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel')\n",
"btn_csv_plantilla = W.Button(description='Descargar plantilla', button_style='info', icon='download', layout={'width':'220px'})\n",
"w_csv_fi = W.DatePicker(description='Vence desde:', layout={'width':'260px'})\n",
"w_csv_ff = W.DatePicker(description='Vence hasta:', layout={'width':'260px'})\n",
"w_csv_modo = W.RadioButtons(options=[('NATURAL','NATURAL'),('DIRIGIDA','DIRIGIDA')],\n",
" value='DIRIGIDA', description='Modo:', layout={'width':'320px'})\n",
"btn_csv_listar = W.Button(description='Listar exportaciones', button_style='', icon='list', layout={'width':'220px'})\n",
"btn_csv_analizar = W.Button(description='Analizar', button_style='primary', icon='search', layout={'width':'180px'})\n",
"btn_csv_xlsx = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'200px'})\n",
"dry_csv = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n",
"btn_csv_ejecutar = W.Button(description='Ejecutar (Paso B)', button_style='warning', icon='play', layout={'width':'260px'})\n",
"bar_csv = _mkbar('Consumo vencidos')\n",
"out_csv_log = W.Output(layout=OUT_STYLE)\n",
"out_csv_tabla = W.Output(layout=OUT_STYLE)\n",
"\n",
"_html_csv = ('<div style=\"background:#E3F2FD;border:1px solid #1565C0;padding:12px;border-radius:6px;margin:8px 0;\">'\n",
" '<b style=\"color:#0D47A1;\">Formato esperado del Excel (4 columnas)</b>'\n",
" '<table style=\"border-collapse:collapse;font-size:12px;margin-top:6px;\">'\n",
" '<tr style=\"background:#1565C0;color:white;\"><th style=\"padding:4px 10px;text-align:left;\">Columna</th><th style=\"padding:4px 10px;text-align:left;\">Ejemplo</th></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>NUMPARTE_COMPONENTE</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">COMP-001</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>PCT_MATERIAL</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">8.5</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>NUMPARTE_PT</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">PT-ABC-123</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>UNIMED</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">KGS</td></tr>'\n",
" '</table>'\n",
" '<p style=\"margin:8px 0 0 0;color:#555;font-size:11px;\">'\n",
" 'Si UNIMED es KGS/TON, la cantidad requerida = (PCT/100) x PESONETO de la partida. '\n",
" 'Si es PZA/LT/etc, = (PCT/100) x CANTEXPO. El rango filtra saldos en SSaldoTem por '\n",
" 'FECHAVENC_ISO; solo se consideran los vencidos a hoy.'\n",
" '</p></div>')\n",
"\n",
"def _on_csv_plantilla(_):\n",
" with out_csv_log:\n",
" clear_output()\n",
" bts = generar_plantilla_consumo_vencidos()\n",
" path = os.path.join(os.getcwd(), 'plantilla_consumo_vencidos.xlsx')\n",
" with open(path, 'wb') as f: f.write(bts)\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:10px;border-radius:6px;margin:8px 0;\">'\n",
" f'<b style=\"color:#1B5E20;\">Plantilla generada</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;color:#333;\">{path}</span></div>'))\n",
"btn_csv_plantilla.on_click(_on_csv_plantilla)\n",
"\n",
"def _on_csv_listar(_):\n",
" with out_csv_tabla:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if len(upload_csv_.value) == 0:\n",
" print('Sube primero el Excel.'); return\n",
" try:\n",
" if isinstance(upload_csv_.value, dict):\n",
" fname = list(upload_csv_.value.keys())[0]\n",
" fb = upload_csv_.value[fname]['content']\n",
" else:\n",
" fb = upload_csv_.value[0]['content']\n",
" tmp = os.path.join(os.getcwd(), '_upload_csv.xlsx')\n",
" with open(tmp, 'wb') as f: f.write(fb)\n",
" df_excel = cargar_excel_consumo_vencidos(tmp)\n",
" print(f'Excel: {len(df_excel):,} combinaciones (componente, PT)')\n",
" detalle, resumen = listar_partidas_expo_por_pt(df_excel['NUMPARTE_PT'].unique().tolist())\n",
" print(f'Partidas EXPO totales en facturas AC con esos PT: {len(detalle):,}')\n",
" if not resumen.empty:\n",
" display(HTML('<h4>Resumen por PT</h4>'))\n",
" display(resumen)\n",
" if not detalle.empty:\n",
" display(HTML('<h4>Partidas EXPO (primeras 200)</h4>'))\n",
" display(detalle.head(200))\n",
" except Exception as e:\n",
" print(f'ERROR: {e}')\n",
"btn_csv_listar.on_click(_on_csv_listar)\n",
"\n",
"def _on_csv_analizar(_):\n",
" with out_csv_log:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if len(upload_csv_.value) == 0:\n",
" print('Sube primero el Excel.'); return\n",
" if not w_csv_fi.value or not w_csv_ff.value:\n",
" print('Define el rango (Vence desde / Vence hasta).'); return\n",
" try:\n",
" if isinstance(upload_csv_.value, dict):\n",
" fname = list(upload_csv_.value.keys())[0]\n",
" fb = upload_csv_.value[fname]['content']\n",
" else:\n",
" fb = upload_csv_.value[0]['content']\n",
" tmp = os.path.join(os.getcwd(), '_upload_csv.xlsx')\n",
" with open(tmp, 'wb') as f: f.write(fb)\n",
" df_excel = cargar_excel_consumo_vencidos(tmp)\n",
" print(f'Excel: {len(df_excel):,} combinaciones')\n",
" except Exception as e:\n",
" print(f'ERROR Excel: {e}'); return\n",
" df_comp, resumen, incons = analizar_consumo_vencidos(\n",
" df_excel, str(w_csv_fi.value), str(w_csv_ff.value),\n",
" progress=bar_csv, log=print)\n",
" _state['csv_plan'] = df_comp\n",
" _state['csv_resumen']= resumen\n",
" _state['csv_incons'] = incons\n",
" with out_csv_tabla:\n",
" clear_output()\n",
" if 'csv_resumen' in _state and not _state['csv_resumen'].empty:\n",
" display(HTML('<h4>Resumen por TIPO</h4>'))\n",
" display(_state['csv_resumen'])\n",
" if 'csv_plan' in _state and not _state['csv_plan'].empty:\n",
" display(HTML('<h4>Plan detalle (primeras 100)</h4>'))\n",
" display(_state['csv_plan'].head(100))\n",
" if 'csv_incons' in _state and not _state['csv_incons'].empty:\n",
" display(HTML('<h4 style=\"color:#C62828;\">Inconsistencias</h4>'))\n",
" display(_state['csv_incons'])\n",
"btn_csv_analizar.on_click(_on_csv_analizar)\n",
"\n",
"def _on_csv_xlsx(_):\n",
" with out_csv_log:\n",
" if 'csv_plan' not in _state:\n",
" display(HTML('<div style=\"color:#C62828;\">Corre Analizar primero.</div>')); return\n",
" path = os.path.join(os.getcwd(), 'plan_consumo_vencidos.xlsx')\n",
" with pd.ExcelWriter(path, engine='openpyxl') as w:\n",
" if not _state['csv_plan'].empty:\n",
" _state['csv_plan'].to_excel(w, sheet_name='Plan_Detalle', index=False)\n",
" if 'csv_resumen' in _state and not _state['csv_resumen'].empty:\n",
" _state['csv_resumen'].to_excel(w, sheet_name='Resumen', index=False)\n",
" if 'csv_incons' in _state and not _state['csv_incons'].empty:\n",
" _state['csv_incons'].to_excel(w, sheet_name='Inconsistencias', index=False)\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:10px;border-radius:6px;margin:8px 0;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;color:#333;\">{path}</span></div>'))\n",
"btn_csv_xlsx.on_click(_on_csv_xlsx)\n",
"\n",
"def _on_csv_ejecutar(_):\n",
" with out_csv_log:\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if 'csv_plan' not in _state:\n",
" print('Corre Analizar primero.'); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" ejecutar_consumo_vencidos(_state['csv_plan'], modo_comp=w_csv_modo.value,\n",
" dry_run=dry_csv.value, log=print, progress=bar_csv)\n",
"btn_csv_ejecutar.on_click(_on_csv_ejecutar)\n",
"\n",
"tab_desc.children = tuple(list(tab_desc.children) + [\n",
" W.HTML('<h3 style=\"margin-top:24px;border-top:2px solid #9C27B0;padding-top:14px;\">Consumo de Saldos Vencidos por Rango</h3>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Sube un Excel con el BOM puntual (componente, %, PT, UM) '\n",
" 'y un rango de fechas. La utileria recorre las partidas EXPO en facturas AC con cada PT, calcula la cantidad '\n",
" 'requerida del componente segun la UM, descuenta lo ya descargado, consume saldos vencidos en el rango (FIFO) '\n",
" 'y genera <b>descargas complementarias</b> usando el mismo motor del Paso 10/12.</p>'),\n",
" W.HTML(_html_csv),\n",
" btn_csv_plantilla,\n",
" W.HTML('<p style=\"margin-top:14px;\"><b>Sube tu Excel y define el rango de vencimiento:</b></p>'),\n",
" W.HBox([upload_csv_]),\n",
" W.HBox([w_csv_fi, w_csv_ff]),\n",
" W.HBox([btn_csv_listar, btn_csv_analizar, btn_csv_xlsx]), bar_csv,\n",
" out_csv_log, out_csv_tabla,\n",
" W.HTML('<h4 style=\"margin-top:20px;border-top:2px solid #FFA726;padding-top:14px;\">Paso B - Ejecutar consumo</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Inserta las descargas como complementarias. '\n",
" 'NATURAL excluye facturas con faltantes; DIRIGIDA inserta lo que se pudo cubrir. '\n",
" '<b>Corre primero con DRY_RUN activado.</b></p>'),\n",
" W.HBox([w_csv_modo]),\n",
" W.HBox([dry_csv]),\n",
" btn_csv_ejecutar,\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 = 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(0, 'Conexion')\n",
"tabs.set_title(1, 'Descargas')\n", "tabs.set_title(1, 'Descargas')\n",