Files
utilerias-recon-2-exe/app/app.ipynb
Kevin_Ramirez 040af3a802 merge: integrar feature BOM, reintegro y diagnostico paso 12 (upstream)
Merge de upstream/main (commit c80f6c7):
- Funciones BOM: UNIMED_BOM, VALORIVAMN en queries
- generar_plantilla_reintegro + widgets UI reintegro (WIP)
- Diagnostico Paso 12 (% KGS)
- Manual_Reintegro.docx
- tab_bom nuevo

Fix conflictos de nombres:
- btn_frac_um / bar_frac_um para evitar colision con analytics frac

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 15:42:10 -05:00

23078 lines
574 KiB
Plaintext

{
"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"
]
},
{
"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, VALORIVAMN, VALORIVAME, PESONETO, PESOBRUTO, PAISMERCANCIA, FECHADESC, TIPOFRACCION,\n",
" SECTOR, NUMPARTEEXPO, NUMPARTE, CANTDESC, UNIMED, LINEAEXPO, PARTEORIGINAL, PORUTILERIA, TIPOMATEXPO,\n",
" MONTOIGI, ADVALOREMIMPO, TIPODESC,\n",
" PAGOIMPUESTO, TIENECERT, ORDENVENTA, ACTUALREPARACION,\n",
" CONSECPARCIAL, LINEAEXPOREF, APARTADOCTM, DESCARGASM, CANTIDADRETORNADASM)\n",
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\"\"\"\n",
"def _to_clarion_time(t=None):\n",
" \"\"\"HORAACTUAL en formato Clarion (centisegundos desde medianoche, base 1).\"\"\"\n",
" if t is None: t = _dt.datetime.now().time()\n",
" return (t.hour*3600 + t.minute*60 + t.second) * 100 + 1\n",
"\n",
"def _build_update_facexp_sql(cambia_estatus, manual):\n",
" \"\"\"Devuelve (sql, lista_params_extra_a_anteceder). El orden de placeholders es:\n",
" [APLICADESCMANUAL,] FECHAACTUAL, HORAACTUAL, [FECHACAPTURA,]\n",
" COMENTARIOSESTATUS, COMOFUEPROCESADA, FACTURAEXPO\n",
" y el SQL contiene la subquery de CANT_PARTIDAS.\"\"\"\n",
" sets = []\n",
" if cambia_estatus:\n",
" sets.append(\"ESTATUS = 'AC'\")\n",
" sets.append(\"APLICADESCMANUAL = ?\")\n",
" sets.append(\"CANT_PARTIDAS = (SELECT COUNT(*) FROM SPartidasExpo WHERE FACTURAEXPO = SFacExp.FACTURAEXPO)\")\n",
" sets.append(\"GENDESCPARTIDAS = 'S'\")\n",
" sets.append(\"FECHAACTUAL = ?\")\n",
" sets.append(\"HORAACTUAL = ?\")\n",
" if cambia_estatus:\n",
" sets.append(\"FECHACAPTURA = ?\")\n",
" sets.append(\"USUARIOACT = 'UTILERIA'\")\n",
" if cambia_estatus:\n",
" sets.append(\"USUARIOCAP = 'UTILERIA'\")\n",
" sets.append(\"COMENTARIOSESTATUS = ?\")\n",
" sets.append(\"COMOFUEPROCESADA = ?\")\n",
" sql = \"UPDATE SFacExp SET \" + \", \".join(sets) + \" WHERE FACTURAEXPO = ?\"\n",
" return sql\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, log=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",
" # Si no se paso prev_dict, calcularlo ahora: descargas existentes para\n",
" # las facturas que estamos procesando. Asi siempre descontamos lo previo\n",
" # y NUNCA hay sobredescarga, sin importar si la factura es NA o AC.\n",
" if prev_dict is None:\n",
" try:\n",
" factura_list = f_use['FACTURAEXPO'].astype(str).str.strip().tolist()\n",
" except Exception:\n",
" factura_list = []\n",
" if factura_list:\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",
" try:\n",
" prev_parts.append(pd.read_sql(f\"\"\"\n",
" SELECT FACTEXPO AS FACTURAEXPO, LINEAEXPO AS LINEA,\n",
" PARTEORIGINAL AS COMPONENTE_MP,\n",
" SUM(ISNULL(CANTDESC,0)) AS CANT_PREV\n",
" FROM SDescargaT WHERE FACTEXPO IN ({ph})\n",
" GROUP BY FACTEXPO, LINEAEXPO, PARTEORIGINAL\n",
" \"\"\", scaii_conn, params=chunk))\n",
" except Exception:\n",
" pass\n",
" df_prev = pd.concat(prev_parts, ignore_index=True) if prev_parts else pd.DataFrame()\n",
" if not df_prev.empty:\n",
" df_prev['LINEA'] = df_prev['LINEA'].astype(int)\n",
" df_prev['COMPONENTE_MP'] = df_prev['COMPONENTE_MP'].fillna('').astype(str).str.strip()\n",
" prev_dict = df_prev.set_index(['FACTURAEXPO','LINEA','COMPONENTE_MP'])['CANT_PREV'].to_dict()\n",
" fact_con_prev = df_prev['FACTURAEXPO'].nunique()\n",
" if log is not None and fact_con_prev > 0:\n",
" log(f' [INFO] Descargas previas detectadas en {fact_con_prev} factura(s); se descontaran del calculo (incremental).')\n",
" else:\n",
" prev_dict = {}\n",
" else:\n",
" prev_dict = {}\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 = float(prev_dict.get((factura, linea, comp), 0.0))\n",
" cant_pend = cant_req_total - cant_prev\n",
" if cant_pend <= 1e-9: continue\n",
" cant_req = cant_pend\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, log=print):\n",
" df = _build_descarga_df(modo_calc='STANDARD', progress=progress, log=log)\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, log=print):\n",
" df = _build_descarga_df(modo_calc='KG_PCT', progress=progress, log=log)\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, insert_sdm=False, aplica_desc_manual=False, paso_label=\"Descarga\", modo=\"NATURAL\"):\n",
" \"\"\"Inserta en SDescargaT y actualiza SSaldoTem por cada fila de df_ins.\n",
" Si insert_sdm=True, ademas inserta una fila espejo en SDescargaM\n",
" (formato como CTM): CONSECUTIVO secuencial, CONSECUTIVOEXPO = SFacExp.CONSECUTIVO,\n",
" PROCEDENCIA='TEM', TIPODESPERDICIO='N', FACTURA = FACTURAIMPO.\"\"\"\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 SDescargaT: {next_consec}')\n",
" next_consec_m = 0\n",
" if insert_sdm and not dry_run:\n",
" with scaii_conn.cursor() as cur:\n",
" cur.execute(\"SELECT ISNULL(MAX(CONSECUTIVO),0) FROM SDescargaM\")\n",
" next_consec_m = int(cur.fetchone()[0]) + 1\n",
" log(f'Proximo CONSECUTIVO SDescargaM: {next_consec_m}')\n",
" INSERT_M_SQL = (\"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",
" grupos = list(df_ins.groupby('FACTURAEXPO'))\n",
" prog.setup(len(grupos), 'Insertando facturas')\n",
" inserted, updated, saldos_upd, ins_m, errores = 0, 0, 0, 0, []\n",
" for factura, df_f in grupos:\n",
" cb, ib, sb, mb = next_consec, inserted, saldos_upd, ins_m\n",
" consec_fac = 0\n",
" if insert_sdm and not dry_run:\n",
" with scaii_conn.cursor() as cur:\n",
" cur.execute(\"SELECT CONSECUTIVO FROM SFacExp WHERE FACTURAEXPO=?\", (factura,))\n",
" _rf = cur.fetchone()\n",
" consec_fac = int(_rf[0]) if _rf and _rf[0] is not None else 0\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",
" # Helpers locales para evitar NULLs: '' para strings, 0/0.0 para numericos.\n",
" def _ss(v):\n",
" r = _s(v); return '' if r is None else r\n",
" def _ff(v):\n",
" r = _f(v); return 0.0 if r is None else r\n",
" def _aa(v):\n",
" r = _adv(v); return 0 if r is None else r\n",
" _pt_val = r['PT'] if 'PT' in r and pd.notna(r.get('PT')) else ''\n",
" _np_val = r['NUMPARTE_USADO'] if pd.notna(r.get('NUMPARTE_USADO')) else ''\n",
" _comp_val = r['COMPONENTE_MP'] if pd.notna(r.get('COMPONENTE_MP')) else ''\n",
" _um_val = (r['UNIMED_SALDO'] if pd.notna(r.get('UNIMED_SALDO'))\n",
" else (r['UNIMED_BOM'] if pd.notna(r.get('UNIMED_BOM')) else ''))\n",
" _fec_val = int(r['FECHADESC_CLARION']) if pd.notna(r.get('FECHADESC_CLARION')) else 0\n",
" cur.execute(INSERT_DESC_SQL, (\n",
" next_consec, r['FACTURAEXPO'], r['FACTURAEXPO'],\n",
" r['FACTURAIMPO_SALDO'], _ss(r['PEDIMENTOIMPO']),\n",
" _ss(r['PEDIMENTOEXPO']), _ss(r['CLASE']),\n",
" _ff(r['VALORMN']), _ff(r['VALORME']),\n",
" 0.0, 0.0, # VALORIVAMN, VALORIVAME\n",
" _ff(r['PESONETO_DESC']), _ff(r['PESONETO_DESC']),\n",
" _ss(r['PAISMERCANCIA']),\n",
" _fec_val, # FECHADESC\n",
" _ss(r['TIPOFRACCION']),\n",
" '', # SECTOR\n",
" _pt_val, # NUMPARTEEXPO = PT\n",
" _np_val,\n",
" float(r['CANT_DESCARGADA']),\n",
" _um_val,\n",
" int(r['LINEA']) if pd.notna(r['LINEA']) else 0,\n",
" _comp_val, 1, '',\n",
" _ff(r['MONTOIGI']), _aa(r['ADVALOREMIMPO']), '0 PARTE',\n",
" '', '', '', '', # PAGOIMPUESTO, TIENECERT, ORDENVENTA, ACTUALREPARACION\n",
" 0, 0, '', # CONSECPARCIAL, LINEAEXPOREF, APARTADOCTM\n",
" '', 0.0)) # DESCARGASM (vacio), CANTIDADRETORNADASM\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",
" if insert_sdm:\n",
" # Helpers locales para evitar NULLs (igual que en SDescargaT).\n",
" _ssm = lambda v: '' if _s(v) is None else _s(v)\n",
" _ffm = lambda v: 0.0 if _f(v) is None else _f(v)\n",
" _ptm = r['PT'] if 'PT' in r and pd.notna(r.get('PT')) else ''\n",
" _npm = r['NUMPARTE_USADO'] if pd.notna(r.get('NUMPARTE_USADO')) else ''\n",
" _compm = r['COMPONENTE_MP'] if pd.notna(r.get('COMPONENTE_MP')) else ''\n",
" _umm = (r['UNIMED_SALDO'] if pd.notna(r.get('UNIMED_SALDO'))\n",
" else (r['UNIMED_BOM'] if pd.notna(r.get('UNIMED_BOM')) else ''))\n",
" _factimpo_m = r['FACTURAIMPO_SALDO'] if pd.notna(r.get('FACTURAIMPO_SALDO')) else ''\n",
" cur.execute(INSERT_M_SQL, (\n",
" next_consec_m, consec_fac, r['FACTURAEXPO'],\n",
" int(r['LINEA']) if pd.notna(r['LINEA']) else 0,\n",
" _ptm, _ssm(r['CLASE']),\n",
" float(r['CANT_DESCARGADA']),\n",
" _umm,\n",
" _factimpo_m, _compm,\n",
" _ffm(r['VALORMN']), _ffm(r['VALORME']),\n",
" _ssm(r['PAISMERCANCIA']),\n",
" _ffm(r['PESONETO_DESC']), _ffm(r['PESONETO_DESC']),\n",
" _ssm(r['TIPOFRACCION']), '',\n",
" '', 'TEM', _factimpo_m, 'N', '', ''))\n",
" next_consec_m += 1\n",
" ins_m += 1\n",
" next_consec += 1\n",
" inserted += 1\n",
" if not dry_run:\n",
" # Siempre actualizamos SFacExp (NATURAL y DIRIGIDA) con los campos\n",
" # de auditoria. ESTATUS solo cambia cuando viene de NA (do_update_status=True).\n",
" _hoy_cl = to_clarion(_dt.date.today())\n",
" _hora_cl = _to_clarion_time()\n",
" _coment = f\"{paso_label} {modo} - Utilerias 2.0 {_dt.date.today().isoformat()}\"\n",
" _manual = 'S' if aplica_desc_manual else 'N'\n",
" sql_upd = _build_update_facexp_sql(\n",
" cambia_estatus=bool(do_update_status), manual=_manual)\n",
" if do_update_status:\n",
" params = (_manual, _hoy_cl, _hora_cl, _hoy_cl,\n",
" _coment, _coment, factura)\n",
" else:\n",
" params = (_manual, _hoy_cl, _hora_cl,\n",
" _coment, _coment, factura)\n",
" cur.execute(sql_upd, params)\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, ins_m = cb, ib, sb, mb\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",
" if insert_sdm:\n",
" log(f' SDescargaM insertadas (espejo modo DIRIGIDA): {ins_m}')\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(\n",
" df_ins, dry_run, log, do_update_status=True, progress=progress,\n",
" insert_sdm=(modo == 'DIRIGIDA'),\n",
" aplica_desc_manual=(modo == 'DIRIGIDA'),\n",
" paso_label=paso_label, modo=modo)\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, log=log)\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(\n",
" df_to_ins, dry_run, log, do_update_status=False, progress=progress,\n",
" insert_sdm=(modo_comp == 'DIRIGIDA'),\n",
" aplica_desc_manual=(modo_comp == 'DIRIGIDA'),\n",
" paso_label=paso_label, modo=modo_comp)\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)\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}')\n",
"\n",
"\n",
"# =============================================================\n",
"# DESCARGAS UNO A UNO\n",
"# IMPO.NUMPARTE = EXPO.NUMPARTE (mismo SKU entra y sale)\n",
"# Match: NUMPARTE + FRACCION + UM.\n",
"# =============================================================\n",
"\n",
"import io as _io_u1\n",
"\n",
"def _listar_partidas_expo_uno_a_uno(fecha_ini, fecha_fin):\n",
" \"\"\"Trae partidas EXPO en el rango, excluyendo TIPOFACTURA='NODES'.\n",
" Incluye facturas NA y AC. Trae el CANT_PREV ya descargado por\n",
" (FACTURAEXPO, LINEA, NUMPARTE) para detectar las que no estan completas.\"\"\"\n",
" sql = \"\"\"\n",
" SELECT pe.FACTURAEXPO, pe.LINEA, pe.NUMPARTE,\n",
" pe.CANTEXPO, pe.UNIMED, pe.FRACCIONEXPO,\n",
" ISNULL(pe.PESONETO,0) AS PESONETO,\n",
" pe.MONTOIGIME AS MONTOIGI,\n",
" fe.FECHAFACTURA_ISO, fe.PEDIMENTOEXPO, fe.ESTATUS, fe.TIPOFACTURA\n",
" FROM SPartidasExpo pe\n",
" INNER JOIN SFacExp fe ON fe.FACTURAEXPO = pe.FACTURAEXPO\n",
" WHERE ISNULL(fe.TIPOFACTURA,'') <> 'NODES'\n",
" AND fe.FECHAFACTURA_ISO BETWEEN ? AND ?\n",
" ORDER BY fe.FECHAFACTURA_ISO, pe.FACTURAEXPO, pe.LINEA\n",
" \"\"\"\n",
" df_part = pd.read_sql(sql, scaii_conn, params=(fecha_ini, fecha_fin))\n",
" if df_part.empty:\n",
" return df_part\n",
" facturas = df_part['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, NUMPARTE,\n",
" SUM(ISNULL(CANTDESC,0)) AS CANT_PREV\n",
" FROM SDescargaT WHERE FACTEXPO IN ({ph})\n",
" GROUP BY FACTEXPO, LINEAEXPO, NUMPARTE\n",
" \"\"\", scaii_conn, params=chunk))\n",
" df_prev = pd.concat(prev_parts, ignore_index=True) if prev_parts else pd.DataFrame()\n",
" if not df_prev.empty:\n",
" df_prev['LINEA'] = df_prev['LINEA'].astype(int)\n",
" df_part['LINEA'] = df_part['LINEA'].astype(int)\n",
" if df_prev.empty:\n",
" df_part['CANT_PREV'] = 0.0\n",
" else:\n",
" df_part = df_part.merge(df_prev, on=['FACTURAEXPO','LINEA','NUMPARTE'], how='left')\n",
" df_part['CANT_PREV'] = df_part['CANT_PREV'].fillna(0.0)\n",
" df_part['CANT_FALTANTE'] = (df_part['CANTEXPO'].astype(float)\n",
" - df_part['CANT_PREV'].astype(float)).round(6)\n",
" return df_part\n",
"\n",
"\n",
"def listar_partidas_expo_uno_a_uno(fecha_ini, fecha_fin):\n",
" \"\"\"Vista para el usuario: lista partidas EXPO afectadas + resumen.\"\"\"\n",
" df = _listar_partidas_expo_uno_a_uno(fecha_ini, fecha_fin)\n",
" if df.empty:\n",
" return pd.DataFrame(), pd.DataFrame()\n",
" detalle = df[['FACTURAEXPO','LINEA','NUMPARTE','UNIMED','FRACCIONEXPO',\n",
" 'CANTEXPO','CANT_PREV','CANT_FALTANTE','FECHAFACTURA_ISO','ESTATUS']].copy()\n",
" resumen = (df.groupby(['ESTATUS'], as_index=False)\n",
" .agg(partidas=('LINEA','count'),\n",
" facturas=('FACTURAEXPO','nunique'),\n",
" cantexpo_total=('CANTEXPO','sum'),\n",
" faltante_total=('CANT_FALTANTE','sum')))\n",
" return detalle, resumen\n",
"\n",
"\n",
"def _cargar_saldos_vencidos_uno_a_uno(numpartes):\n",
" \"\"\"SSaldoTem vencidos (< hoy) y disponibles, filtrados a los NUMPARTES dados.\"\"\"\n",
" import datetime as _dt\n",
" hoy = _dt.date.today().isoformat()\n",
" nps = [str(n).strip() for n in numpartes if str(n).strip()]\n",
" if not nps:\n",
" return pd.DataFrame()\n",
" out = []\n",
" LOTE = 1000\n",
" for i in range(0, len(nps), LOTE):\n",
" chunk = nps[i:i+LOTE]\n",
" ph = ','.join(['?']*len(chunk))\n",
" out.append(pd.read_sql(f\"\"\"\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 NUMPARTE IN ({ph})\n",
" AND FECHAVENC_ISO IS NOT NULL\n",
" AND FECHAVENC_ISO < ?\n",
" AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n",
" \"\"\", scaii_conn, params=tuple(chunk) + (hoy,)))\n",
" df = pd.concat(out, ignore_index=True) if out else pd.DataFrame()\n",
" if df.empty: return df\n",
" for c in ('NUMPARTE','FRACCIONIMPO','UNIDAD_MEDIDA'):\n",
" df[c] = df[c].fillna('').astype(str).str.strip()\n",
" return df\n",
"\n",
"\n",
"def analizar_descargas_uno_a_uno(fecha_ini, fecha_fin, progress=None, log=print):\n",
" \"\"\"Paso A: arma df_comp con la misma estructura que _build_descarga_df.\n",
" Devuelve (df_comp, resumen, inconsistencias).\"\"\"\n",
" prog = _Progress(progress)\n",
" prog.setup(3, 'Listando partidas EXPO...')\n",
" df_part = _listar_partidas_expo_uno_a_uno(fecha_ini, fecha_fin)\n",
" log(f'Partidas EXPO en rango (sin NODES): {len(df_part):,}')\n",
" if df_part.empty:\n",
" prog.done('Sin partidas'); return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()\n",
" # Solo las que aun tienen faltante\n",
" pendientes = df_part[df_part['CANT_FALTANTE'] > 1e-9].copy()\n",
" log(f'Partidas con faltante: {len(pendientes):,}')\n",
" if pendientes.empty:\n",
" prog.done('Sin pendientes'); return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()\n",
" prog.step(desc='Cargando saldos vencidos...')\n",
" numpartes = pendientes['NUMPARTE'].astype(str).str.strip().unique().tolist()\n",
" saldos = _cargar_saldos_vencidos_uno_a_uno(numpartes)\n",
" log(f'Saldos vencidos disponibles para esos NUMPARTES: {len(saldos):,}')\n",
" if saldos.empty:\n",
" prog.done('Sin saldos'); return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()\n",
" # Indexar saldos por NUMPARTE para acceso rapido\n",
" saldos = saldos.sort_values(['NUMPARTE','FECHA_ENTRADA']).reset_index(drop=True)\n",
" saldos_por_np = {k: list(g.index) for k, g in saldos.groupby('NUMPARTE')}\n",
"\n",
" prog.setup(len(pendientes), 'Procesando partidas...')\n",
" all_rows = []\n",
" inconsistencias = []\n",
" for _, p in pendientes.iterrows():\n",
" factura = p['FACTURAEXPO']\n",
" linea = int(p['LINEA'])\n",
" numparte = str(p['NUMPARTE']).strip()\n",
" unimed_e = str(p.get('UNIMED','') or '').strip().upper()\n",
" fracc_e = str(p.get('FRACCIONEXPO','') or '').strip()\n",
" falta = float(p['CANT_FALTANTE'])\n",
" base_row = {'FACTURAEXPO': factura, 'FECHA_FACTURAEXPO': p['FECHAFACTURA_ISO'],\n",
" 'PEDIMENTOEXPO': p['PEDIMENTOEXPO'],\n",
" 'LINEA': linea, 'PT': numparte, 'CANT_PT': float(p['CANTEXPO']),\n",
" 'PESONETO_PARTIDA': float(p['PESONETO']),\n",
" 'PCT_BOM': 100.0, 'MONTOIGI': p.get('MONTOIGI'),\n",
" 'COMPONENTE_MP': numparte, 'UNIMED_BOM': unimed_e,\n",
" 'CANT_REQUERIDA': falta, 'CANT_PREV': float(p['CANT_PREV'])}\n",
" idxs = saldos_por_np.get(numparte, [])\n",
" # Filtrar saldos con misma fraccion y misma UM\n",
" idxs_ok = [i for i in idxs\n",
" if saldos.at[i,'FRACCIONIMPO'] == fracc_e\n",
" and saldos.at[i,'UNIDAD_MEDIDA'].upper() == unimed_e]\n",
" if not idxs_ok:\n",
" inconsistencias.append({**{k: base_row[k] for k in ('FACTURAEXPO','LINEA')},\n",
" 'NUMPARTE': numparte, 'UNIMED': unimed_e, 'FRACCIONEXPO': fracc_e,\n",
" 'CANT_FALTANTE': falta,\n",
" 'STATUS': 'SIN_SALDO_COMPATIBLE',\n",
" 'DETALLE': 'No hay saldos vencidos con misma fraccion+UM'})\n",
" # Generar fila FALTANTE para que aparezca en df_comp\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': falta, 'VALORMN': None, 'VALORME': None,\n",
" 'PESONETO_DESC': None, 'STATUS': 'FALTANTE'})\n",
" prog.step(); continue\n",
" # Consumir FIFO (saldos ya estan ordenados por FECHA_ENTRADA)\n",
" faltante, rows = _consumir(saldos, idxs_ok, falta, base_row, 'ORIGINAL', numparte)\n",
" all_rows.extend(rows)\n",
" if faltante > 1e-9:\n",
" inconsistencias.append({'FACTURAEXPO': factura, 'LINEA': linea,\n",
" 'NUMPARTE': numparte, 'UNIMED': unimed_e, 'FRACCIONEXPO': fracc_e,\n",
" 'CANT_REQUERIDA': falta,\n",
" 'CANT_DESCARGADA': falta - faltante, 'CANT_FALTANTE': faltante,\n",
" 'STATUS': 'SALDO_INSUFICIENTE',\n",
" 'DETALLE': f'Faltan {faltante:.4f} {unimed_e} sin cubrir'})\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",
" df_comp = pd.DataFrame(all_rows)\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",
" resumen = df_comp.groupby('TIPO').size().reset_index(name='filas')\n",
" prog.done('Analisis listo')\n",
" log(f'Filas calculadas: {len(df_comp):,} | Inconsistencias: {len(inconsistencias):,}')\n",
" return df_comp, resumen, pd.DataFrame(inconsistencias)\n",
"\n",
"\n",
"def ejecutar_descargas_uno_a_uno(df_comp, modo_comp='DIRIGIDA', dry_run=True,\n",
" progress=None, log=print):\n",
" \"\"\"Paso B: usa _do_inserts. No cambia ESTATUS (do_update_status=False) porque\n",
" mezcla facturas NA y AC; el usuario puede correr Paso 9 estandar despues si\n",
" quiere cerrar las NA que queden 100% cubiertas.\"\"\"\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(\n",
" df_to_ins, dry_run, log, do_update_status=False, progress=progress,\n",
" insert_sdm=(modo_comp == 'DIRIGIDA'),\n",
" aplica_desc_manual=(modo_comp == 'DIRIGIDA'),\n",
" paso_label='descargas uno a uno', modo=modo_comp)\n",
" log(f'\\n=== RESUMEN Descargas Uno a Uno ({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",
"\n",
"# =============================================================\n",
"# REINTEGRO DE DESCARGAS\n",
"# Reduce CANTDESC en SDescargaT y devuelve al saldo IMPO en SSaldoTem.\n",
"# =============================================================\n",
"\n",
"import io as _io_re\n",
"\n",
"def generar_plantilla_reintegro():\n",
" \"\"\"Plantilla inicial: NUMPARTE_MP, UNIMED, FRACCION.\"\"\"\n",
" df = pd.DataFrame([\n",
" {'NUMPARTE_MP': 'MP-ABC-001', 'UNIMED': 'KGS', 'FRACCION': '72082700'},\n",
" {'NUMPARTE_MP': 'MP-XYZ-002', 'UNIMED': 'PZA', 'FRACCION': '84149099'},\n",
" ])\n",
" buf = _io_re.BytesIO()\n",
" with pd.ExcelWriter(buf, engine='openpyxl') as w:\n",
" df.to_excel(w, sheet_name='Reintegro', index=False)\n",
" buf.seek(0)\n",
" return buf.read()\n",
"\n",
"\n",
"def cargar_excel_reintegro_inicial(path):\n",
" \"\"\"Lee Excel inicial: NUMPARTE_MP + UNIMED + FRACCION.\"\"\"\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_MP': ['NUMPARTE_MP','NUMPARTE','NUM_PARTE','COMPONENTE'],\n",
" 'UNIMED': ['UNIMED','UM','UNIDAD','UNIDAD_DE_MEDIDA'],\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:\n",
" raise ValueError(f'Falta la columna {std} (acepta: {opts})')\n",
" df2 = pd.DataFrame(out)\n",
" for c in df2.columns:\n",
" df2[c] = df2[c].fillna('').astype(str).str.strip()\n",
" df2['UNIMED'] = df2['UNIMED'].str.upper()\n",
" return df2[df2['NUMPARTE_MP'] != ''].reset_index(drop=True)\n",
"\n",
"\n",
"def listar_partidas_para_reintegro(df_mp, fecha_ini, fecha_fin, progress=None, log=print):\n",
" \"\"\"Lista descargas existentes en SDescargaT cuyas PARTE_ORIGINAL coincide con\n",
" los MP del Excel, agrupado por (FACTURAEXPO, LINEA, NUMPARTE_MP) con el PT\n",
" correspondiente. Solo facturas en el rango de fechas.\"\"\"\n",
" prog = _Progress(progress)\n",
" prog.setup(3, 'Buscando descargas...')\n",
" if df_mp is None or df_mp.empty:\n",
" log('Excel vacio.'); return pd.DataFrame()\n",
" mps = df_mp['NUMPARTE_MP'].astype(str).str.strip().unique().tolist()\n",
" if not mps:\n",
" log('Sin MPs.'); return pd.DataFrame()\n",
" LOTE = 1000\n",
" detalle = []\n",
" for i in range(0, len(mps), LOTE):\n",
" chunk = mps[i:i+LOTE]\n",
" ph = ','.join(['?'] * len(chunk))\n",
" try:\n",
" detalle.append(pd.read_sql(f\"\"\"\n",
" SELECT d.FACTEXPO AS FACTURAEXPO,\n",
" d.LINEAEXPO AS LINEA,\n",
" d.PARTEORIGINAL AS NUMPARTE_MP,\n",
" d.NUMPARTE AS NUMPARTE_USADO,\n",
" d.UNIMED AS UNIMED,\n",
" d.TIPOFRACCION AS FRACCION,\n",
" d.FACTIMPO,\n",
" d.PAISMERCANCIA,\n",
" SUM(ISNULL(d.CANTDESC,0)) AS CANTDESC_ACTUAL,\n",
" SUM(ISNULL(d.VALORMN,0)) AS VALORMN_ACTUAL,\n",
" SUM(ISNULL(d.VALORME,0)) AS VALORME_ACTUAL,\n",
" SUM(ISNULL(d.PESONETO,0)) AS PESONETO_ACTUAL,\n",
" SUM(ISNULL(d.PESOBRUTO,0)) AS PESOBRUTO_ACTUAL,\n",
" SUM(ISNULL(d.MONTOIGI,0)) AS MONTOIGI_ACTUAL,\n",
" COUNT(*) AS DESCARGAS,\n",
" f.FECHAFACTURA_ISO,\n",
" f.ESTATUS\n",
" FROM SDescargaT d\n",
" INNER JOIN SFacExp f ON f.FACTURAEXPO = d.FACTEXPO\n",
" WHERE d.PARTEORIGINAL IN ({ph})\n",
" AND f.FECHAFACTURA_ISO BETWEEN ? AND ?\n",
" AND ISNULL(d.CANTDESC,0) > 0\n",
" GROUP BY d.FACTEXPO, d.LINEAEXPO, d.PARTEORIGINAL, d.NUMPARTE, d.UNIMED,\n",
" d.TIPOFRACCION, d.FACTIMPO, d.PAISMERCANCIA, f.FECHAFACTURA_ISO, f.ESTATUS\n",
" ORDER BY f.FECHAFACTURA_ISO, d.FACTEXPO, d.LINEAEXPO\n",
" \"\"\", scaii_conn, params=tuple(chunk) + (fecha_ini, fecha_fin)))\n",
" except Exception as e:\n",
" log(f'WARN chunk MP: {e}')\n",
" df = pd.concat(detalle, ignore_index=True) if detalle else pd.DataFrame()\n",
" prog.step(desc=f'Descargas matched: {len(df)}')\n",
" if df.empty:\n",
" prog.done('Sin resultados'); return df\n",
" # Filtrar por UNIMED y FRACCION del Excel (si vienen)\n",
" df['UNIMED'] = df['UNIMED'].fillna('').astype(str).str.strip().str.upper()\n",
" df['FRACCION'] = df['FRACCION'].fillna('').astype(str).str.strip()\n",
" # join con df_mp para validar match\n",
" df_mp2 = df_mp.copy()\n",
" df_mp2.columns = [c.upper() for c in df_mp2.columns]\n",
" df_mp2['UNIMED'] = df_mp2['UNIMED'].str.upper()\n",
" df_mp2['FRACCION'] = df_mp2['FRACCION'].astype(str).str.strip()\n",
" # Solo conservamos los registros cuyo NUMPARTE_MP + UNIMED + FRACCION coincide\n",
" df = df.merge(df_mp2, on='NUMPARTE_MP', how='inner', suffixes=('','_xls'))\n",
" df = df[(df['UNIMED'] == df['UNIMED_xls']) & (df['FRACCION'] == df['FRACCION_xls'])]\n",
" df = df.drop(columns=['UNIMED_xls','FRACCION_xls']).reset_index(drop=True)\n",
" # Agregar columna vacia CANTDESC_NUEVA para que el usuario la llene\n",
" df['CANTDESC_NUEVA'] = ''\n",
" prog.done(f'{len(df)} partidas/MP afectadas')\n",
" return df\n",
"\n",
"\n",
"def exportar_excel_listado_reintegro(df, ruta):\n",
" if df is None or df.empty: return None\n",
" with pd.ExcelWriter(ruta, engine='openpyxl') as w:\n",
" df.to_excel(w, sheet_name='Reintegro_a_llenar', index=False)\n",
" return ruta\n",
"\n",
"\n",
"def cargar_excel_reintegro_lleno(path):\n",
" \"\"\"Excel con CANTDESC_NUEVA llena. Valida que sea > 0 y < CANTDESC_ACTUAL.\"\"\"\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",
" required = ['FACTURAEXPO','LINEA','NUMPARTE_MP','CANTDESC_ACTUAL','CANTDESC_NUEVA']\n",
" missing = [c for c in required if c not in df.columns]\n",
" if missing:\n",
" raise ValueError(f'Faltan columnas: {missing}')\n",
" for c in ('FACTURAEXPO','NUMPARTE_MP'):\n",
" df[c] = df[c].fillna('').astype(str).str.strip()\n",
" df['LINEA'] = pd.to_numeric(df['LINEA'], errors='coerce').fillna(0).astype(int)\n",
" df['CANTDESC_ACTUAL'] = pd.to_numeric(df['CANTDESC_ACTUAL'], errors='coerce').fillna(0)\n",
" df['CANTDESC_NUEVA'] = pd.to_numeric(df['CANTDESC_NUEVA'], errors='coerce')\n",
" df = df[df['CANTDESC_NUEVA'].notna()].reset_index(drop=True)\n",
" df = df[df['FACTURAEXPO'] != ''].reset_index(drop=True)\n",
" return df\n",
"\n",
"\n",
"def analizar_reintegro(df_xls, progress=None, log=print):\n",
" \"\"\"Para cada fila del Excel:\n",
" 1. Valida CANTDESC_NUEVA > 0 y < CANTDESC_ACTUAL.\n",
" 2. Toma todas las filas SDescargaT actuales para (FACTURAEXPO, LINEA, NUMPARTE_MP).\n",
" 3. Distribuye la reduccion proporcional al CANTDESC de cada fila.\n",
" 4. Calcula proporciones de VALORMN/VALORME/PESONETO/PESOBRUTO/MONTOIGI a devolver.\n",
" Devuelve (plan, inconsistencias).\n",
" \"\"\"\n",
" prog = _Progress(progress)\n",
" if df_xls is None or df_xls.empty:\n",
" log('Excel lleno vacio.'); return pd.DataFrame(), pd.DataFrame()\n",
" prog.setup(len(df_xls), 'Analizando filas...')\n",
" plan_rows = []\n",
" incons = []\n",
" for _, r in df_xls.iterrows():\n",
" factura = r['FACTURAEXPO']\n",
" linea = int(r['LINEA'])\n",
" mp = r['NUMPARTE_MP']\n",
" actual = float(r['CANTDESC_ACTUAL'])\n",
" nueva = float(r['CANTDESC_NUEVA'])\n",
" if nueva <= 0:\n",
" incons.append({**r.to_dict(), 'STATUS': 'NUEVA_INVALIDA',\n",
" 'DETALLE': 'CANTDESC_NUEVA debe ser mayor que 0'})\n",
" prog.step(); continue\n",
" if nueva >= actual:\n",
" incons.append({**r.to_dict(), 'STATUS': 'NUEVA_NO_REDUCE',\n",
" 'DETALLE': 'CANTDESC_NUEVA debe ser menor que CANTDESC_ACTUAL'})\n",
" prog.step(); continue\n",
" # Cargar las descargas individuales para esa combinacion\n",
" try:\n",
" df_d = pd.read_sql(\"\"\"\n",
" SELECT CONSECUTIVO, FACTIMPO, UNIMED, NUMPARTE,\n",
" ISNULL(CANTDESC,0) AS CANTDESC,\n",
" ISNULL(VALORMN,0) AS VALORMN,\n",
" ISNULL(VALORME,0) AS VALORME,\n",
" ISNULL(PESONETO,0) AS PESONETO,\n",
" ISNULL(PESOBRUTO,0) AS PESOBRUTO,\n",
" ISNULL(MONTOIGI,0) AS MONTOIGI\n",
" FROM SDescargaT\n",
" WHERE FACTEXPO=? AND LINEAEXPO=? AND PARTEORIGINAL=?\n",
" AND ISNULL(CANTDESC,0) > 0\n",
" ORDER BY CONSECUTIVO\n",
" \"\"\", scaii_conn, params=(factura, linea, mp))\n",
" except Exception as e:\n",
" incons.append({**r.to_dict(), 'STATUS': 'ERROR_QUERY',\n",
" 'DETALLE': str(e)})\n",
" prog.step(); continue\n",
" if df_d.empty:\n",
" incons.append({**r.to_dict(), 'STATUS': 'SIN_DESCARGAS',\n",
" 'DETALLE': 'No hay descargas vigentes con esa combinacion'})\n",
" prog.step(); continue\n",
" suma_actual = float(df_d['CANTDESC'].sum())\n",
" if abs(suma_actual - actual) > 0.01:\n",
" incons.append({**r.to_dict(), 'STATUS': 'DESACTUALIZADO',\n",
" 'DETALLE': f'Suma actual en BD={suma_actual} no coincide con CANTDESC_ACTUAL del Excel'})\n",
" prog.step(); continue\n",
" reduccion_total = actual - nueva\n",
" # Distribuir proporcional al CANTDESC de cada fila\n",
" for _, d in df_d.iterrows():\n",
" prop = float(d['CANTDESC']) / suma_actual if suma_actual > 1e-9 else 0\n",
" reduccion_fila = round(reduccion_total * prop, 6)\n",
" plan_rows.append({\n",
" 'FACTURAEXPO': factura, 'LINEA': linea, 'NUMPARTE_MP': mp,\n",
" 'CONSECUTIVO': int(d['CONSECUTIVO']),\n",
" 'FACTIMPO': d['FACTIMPO'], 'UNIMED': d['UNIMED'],\n",
" 'NUMPARTE_DESC': d['NUMPARTE'],\n",
" 'CANTDESC_ACTUAL': float(d['CANTDESC']),\n",
" 'CANTDESC_NUEVA': round(float(d['CANTDESC']) - reduccion_fila, 6),\n",
" 'REDUCCION_CANT': reduccion_fila,\n",
" 'DEVOLVER_VALORMN': round(float(d['VALORMN']) * prop * (reduccion_total/suma_actual), 6),\n",
" 'DEVOLVER_VALORME': round(float(d['VALORME']) * prop * (reduccion_total/suma_actual), 6),\n",
" 'DEVOLVER_PESONETO': round(float(d['PESONETO']) * prop * (reduccion_total/suma_actual), 6),\n",
" 'DEVOLVER_PESOBRUTO': round(float(d['PESOBRUTO'])* prop * (reduccion_total/suma_actual), 6),\n",
" 'DEVOLVER_MONTOIGI': round(float(d['MONTOIGI']) * prop * (reduccion_total/suma_actual), 6),\n",
" })\n",
" prog.step()\n",
" prog.done(f'Plan: {len(plan_rows)} filas | Inconsistencias: {len(incons)}')\n",
" return pd.DataFrame(plan_rows), pd.DataFrame(incons)\n",
"\n",
"\n",
"def ejecutar_reintegro(plan, dry_run=True, ajustar_sdm=False, progress=None, log=print):\n",
" \"\"\"Paso B: aplica los UPDATEs a SDescargaT y SSaldoTem. Opcionalmente\n",
" ajusta tambien SDescargaM (espejo).\"\"\"\n",
" assert isinstance(dry_run, bool), 'dry_run debe ser bool'\n",
" if plan is None or plan.empty:\n",
" log('ERROR: plan vacio.'); return\n",
" prog = _Progress(progress)\n",
" UPD_DESC = \"\"\"UPDATE SDescargaT\n",
" SET CANTDESC = ?,\n",
" VALORMN = ISNULL(VALORMN,0) - ?,\n",
" VALORME = ISNULL(VALORME,0) - ?,\n",
" PESONETO = ISNULL(PESONETO,0) - ?,\n",
" PESOBRUTO= ISNULL(PESOBRUTO,0)- ?,\n",
" MONTOIGI = ISNULL(MONTOIGI,0) - ?\n",
" WHERE CONSECUTIVO = ?\"\"\"\n",
" UPD_SAL = \"\"\"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 NUMPARTE=? AND UMEXITENCIA=?\"\"\"\n",
" UPD_SDM = \"\"\"UPDATE SDescargaM\n",
" SET CANTIDAD = ISNULL(CANTIDAD,0) - ?,\n",
" VALORIMPOMN = ISNULL(VALORIMPOMN,0) - ?,\n",
" VALORIMPOME = ISNULL(VALORIMPOME,0) - ?,\n",
" PESONETO = ISNULL(PESONETO,0) - ?,\n",
" PESOBRUTO = ISNULL(PESOBRUTO,0) - ?\n",
" WHERE FACTURAEXPO=? AND LINEA=? AND NUMPARTEMP=? AND FACTURAIMPO=?\"\"\"\n",
" prog.setup(len(plan), 'Aplicando reintegro')\n",
" upd_t = upd_s = upd_m = errores = 0\n",
" err_list = []\n",
" # Para agrupar updates a SSaldoTem (suma de devoluciones por FACTIMPO+NUMPARTE+UNIMED)\n",
" # Lo hacemos por fila para mantener consistencia y rollback fino.\n",
" for _, p in plan.iterrows():\n",
" try:\n",
" with scaii_conn.cursor() as cur:\n",
" if not dry_run:\n",
" # 1) SDescargaT: poner CANTDESC al nuevo valor y restar proporcionales\n",
" cur.execute(UPD_DESC,\n",
" float(p['CANTDESC_NUEVA']),\n",
" float(p['DEVOLVER_VALORMN']), float(p['DEVOLVER_VALORME']),\n",
" float(p['DEVOLVER_PESONETO']), float(p['DEVOLVER_PESOBRUTO']),\n",
" float(p['DEVOLVER_MONTOIGI']),\n",
" int(p['CONSECUTIVO']))\n",
" upd_t += 1\n",
" # 2) SSaldoTem: devolver al saldo IMPO\n",
" cur.execute(UPD_SAL,\n",
" float(p['REDUCCION_CANT']),\n",
" float(p['DEVOLVER_VALORMN']), float(p['DEVOLVER_VALORME']),\n",
" float(p['DEVOLVER_PESONETO']), float(p['DEVOLVER_PESOBRUTO']),\n",
" p['FACTIMPO'], p['NUMPARTE_DESC'], p['UNIMED'])\n",
" upd_s += 1\n",
" # 3) SDescargaM (opcional)\n",
" if ajustar_sdm:\n",
" cur.execute(UPD_SDM,\n",
" float(p['REDUCCION_CANT']),\n",
" float(p['DEVOLVER_VALORMN']), float(p['DEVOLVER_VALORME']),\n",
" float(p['DEVOLVER_PESONETO']), float(p['DEVOLVER_PESOBRUTO']),\n",
" p['FACTURAEXPO'], int(p['LINEA']), p['NUMPARTE_MP'], p['FACTIMPO'])\n",
" upd_m += cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0\n",
" else:\n",
" upd_t += 1; upd_s += 1\n",
" if ajustar_sdm: upd_m += 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\"{p['FACTURAEXPO']}/{p['LINEA']}/{p['NUMPARTE_MP']}/CONSEC {p['CONSECUTIVO']}\"\n",
" err_list.append((etq, str(e)))\n",
" log(f' ERROR {etq}: {e}')\n",
" prog.step()\n",
" prog.done(f'{upd_t} desc / {upd_s} saldos / {upd_m} sdm')\n",
" log(f'\\n=== RESUMEN Reintegro (DRY_RUN={dry_run}, ajustar_sdm={ajustar_sdm}) ===')\n",
" log(f' SDescargaT actualizadas: {upd_t:,}')\n",
" log(f' SSaldoTem actualizadas: {upd_s:,}')\n",
" log(f' SDescargaM actualizadas: {upd_m:,}')\n",
" log(f' Errores : {errores:,}')\n",
" for f, e in err_list[:5]: log(f' {f}: {e}')\n",
"\n",
"\n",
"# =============================================================\n",
"# DIAGNOSTICO PASO 12 (% KGS)\n",
"# Lista facturas EXPO pendientes (NA) y explica por que el Paso 12\n",
"# no las pudo descargar (en este momento). Solo lectura.\n",
"# =============================================================\n",
"\n",
"def diagnosticar_paso12_pendientes(fecha_ini=None, fecha_fin=None,\n",
" progress=None, log=print):\n",
" \"\"\"Para cada factura EXPO en NA (opcionalmente filtrada por rango de\n",
" FECHAFACTURA_ISO) revisa por que no se pudo descargar con el flujo\n",
" KG_PCT del Paso 12. Devuelve dos DataFrames: bloqueos por factura y\n",
" detalle por componente faltante.\n",
" \"\"\"\n",
" prog = _Progress(progress)\n",
" prog.setup(5, 'Cargando facturas NA...')\n",
" # 1) Facturas NA\n",
" sql_fact = (\"SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO, \"\n",
" \"ISNULL(TIPOFACTURA,'') AS TIPOFACTURA \"\n",
" \"FROM SFacExp WHERE ESTATUS = 'NA'\")\n",
" params = []\n",
" if fecha_ini:\n",
" sql_fact += ' AND FECHAFACTURA_ISO >= ?'; params.append(fecha_ini)\n",
" if fecha_fin:\n",
" sql_fact += ' AND FECHAFACTURA_ISO <= ?'; params.append(fecha_fin)\n",
" sql_fact += ' ORDER BY FECHAFACTURA_ISO'\n",
" df_fact = pd.read_sql(sql_fact, scaii_conn, params=params)\n",
" log(f'Facturas NA en rango: {len(df_fact)}')\n",
" if df_fact.empty:\n",
" prog.done('Sin facturas NA'); return pd.DataFrame(), pd.DataFrame()\n",
"\n",
" # 2) Cargar catalogos (PT con BOM, partepais, etc.)\n",
" prog.step(desc='Cargando catalogos...')\n",
" _load_catalogos()\n",
" df_part = _state.get('df_partidas_all', pd.DataFrame())\n",
" df_bom = _state.get('df_boms_all', pd.DataFrame())\n",
" saldos = _load_saldos_snapshot()\n",
"\n",
" if df_part.empty:\n",
" log('Sin SPartidasExpo en estado.'); df_part = pd.DataFrame()\n",
" if df_bom.empty:\n",
" log('Sin SMatBOM en estado.'); df_bom = pd.DataFrame()\n",
"\n",
" pts_con_bom = set(df_bom['PT'].astype(str).str.strip().unique().tolist()) if not df_bom.empty else set()\n",
" partidas_por_factura = (df_part.groupby('FACTURAEXPO')\n",
" if not df_part.empty else None)\n",
"\n",
" # 3) Saldos disponibles agrupados por (NUMPARTE, UM_KEY)\n",
" saldos_disp = saldos.groupby(['NUMPARTE','UM_KEY'])['SALDO_DISPONIBLE'].sum().to_dict() if not saldos.empty else {}\n",
" # 3.1) Mapa de sustitutos {NUMPARTE: [(alt, [um1,um2]), ...]} para emular\n",
" # el flujo real del Paso 12 (original primero, luego sustitutos).\n",
" df_sub = _state.get('df_sustitutos_all', pd.DataFrame())\n",
" sust_map = {}\n",
" if not df_sub.empty:\n",
" for _, r in df_sub.iterrows():\n",
" k = str(r['NUMPARTE']).strip()\n",
" alt = str(r['COMPONENTE_ALTERNO']).strip()\n",
" um_keys = [str(u).strip().upper() for u in [r.get('UNIDADMEDIDA1'), r.get('UNIDADMEDIDA2')] if u and str(u).strip()]\n",
" if k and alt and um_keys:\n",
" sust_map.setdefault(k, []).append((alt, um_keys))\n",
"\n",
" bloqueos = [] # uno por factura con su motivo principal\n",
" componentes = [] # detalle de cada componente faltante / problematico\n",
"\n",
" prog.setup(len(df_fact), 'Analizando facturas...')\n",
" for _, f in df_fact.iterrows():\n",
" factura = f['FACTURAEXPO']\n",
" tipof = (f['TIPOFACTURA'] or '').strip().upper()\n",
" fecha = f['FECHAFACTURA_ISO']\n",
" ped = f['PEDIMENTOEXPO']\n",
" # 3a) TIPOFACTURA NODES\n",
" if tipof == 'NODES':\n",
" bloqueos.append({'FACTURAEXPO': factura, 'PEDIMENTOEXPO': ped,\n",
" 'FECHAFACTURA_ISO': fecha, 'TIPOFACTURA': tipof,\n",
" 'PARTIDAS': 0, 'PT_SIN_BOM': 0, 'PESONETO_CERO': 0,\n",
" 'COMPONENTES_SIN_SALDO': 0,\n",
" 'MOTIVO_PRINCIPAL': 'TIPOFACTURA_NODES',\n",
" 'DETALLE': 'Las facturas NODES estan excluidas del Paso 12.'})\n",
" prog.step(); continue\n",
" # 3b) Sin partidas\n",
" if partidas_por_factura is None or factura not in partidas_por_factura.groups:\n",
" bloqueos.append({'FACTURAEXPO': factura, 'PEDIMENTOEXPO': ped,\n",
" 'FECHAFACTURA_ISO': fecha, 'TIPOFACTURA': tipof,\n",
" 'PARTIDAS': 0, 'PT_SIN_BOM': 0, 'PESONETO_CERO': 0,\n",
" 'COMPONENTES_SIN_SALDO': 0,\n",
" 'MOTIVO_PRINCIPAL': 'SIN_PARTIDAS',\n",
" 'DETALLE': 'La factura no tiene partidas EXPO cargadas.'})\n",
" prog.step(); continue\n",
" df_p = partidas_por_factura.get_group(factura)\n",
" n_partidas = len(df_p)\n",
" n_pt_sin_bom = 0\n",
" n_peso_cero = 0\n",
" n_comp_sin_saldo = 0\n",
" problemas = []\n",
" for _, p in df_p.iterrows():\n",
" pt = str(p['PT']).strip()\n",
" linea = int(p['LINEA']) if pd.notna(p['LINEA']) else 0\n",
" peso = float(p.get('PESONETO', 0) or 0)\n",
" if pt not in pts_con_bom:\n",
" n_pt_sin_bom += 1\n",
" componentes.append({'FACTURAEXPO': factura, 'LINEA': linea, 'PT': pt,\n",
" 'COMPONENTE': None, 'UNIMED': None,\n",
" 'CANT_REQUERIDA': 0, 'SALDO_DISPONIBLE': 0,\n",
" 'MOTIVO': 'PT_SIN_BOM',\n",
" 'DETALLE': f'El producto terminado {pt} no tiene BOM en SMatBOM.'})\n",
" problemas.append('PT_SIN_BOM')\n",
" continue\n",
" if peso <= 1e-9:\n",
" n_peso_cero += 1\n",
" componentes.append({'FACTURAEXPO': factura, 'LINEA': linea, 'PT': pt,\n",
" 'COMPONENTE': None, 'UNIMED': None,\n",
" 'CANT_REQUERIDA': 0, 'SALDO_DISPONIBLE': 0,\n",
" 'MOTIVO': 'PESONETO_CERO',\n",
" 'DETALLE': 'PESONETO=0 en la partida. El modo KG_PCT calcula cant_req=0 y no descarga.'})\n",
" problemas.append('PESONETO_CERO')\n",
" continue\n",
" # 3c) Revisar cada componente del BOM\n",
" df_b = df_bom[df_bom['PT'] == pt]\n",
" for _, c in df_b.iterrows():\n",
" comp = str(c['COMPONENTE_MP']).strip()\n",
" unimed = (c['UNIMED'] or '').strip().upper()\n",
" pct = float(c['CANTIDAD'] or 0)\n",
" cant_req = (pct / 100.0) * peso\n",
" if cant_req <= 1e-9:\n",
" continue\n",
" # Saldo del ORIGINAL: cualquier UM (replica _consumir con UM del BOM,\n",
" # aqui agregamos por NUMPARTE sin filtrar UM para no perder casos validos).\n",
" disp_orig = sum(v for (np_, um), v in saldos_disp.items() if np_ == comp)\n",
" # Saldo de SUSTITUTOS: solo si UM en UNIDADMEDIDA1/2 del registro de sust.\n",
" disp_sust = 0.0\n",
" lista_alt = []\n",
" for alt, um_keys in sust_map.get(comp, []):\n",
" s_alt = sum(v for (np_, um), v in saldos_disp.items()\n",
" if np_ == alt and um in um_keys)\n",
" if s_alt > 0:\n",
" disp_sust += s_alt\n",
" lista_alt.append(f'{alt}({s_alt:.2f})')\n",
" disp = disp_orig + disp_sust\n",
" if disp < cant_req:\n",
" n_comp_sin_saldo += 1\n",
" extra = f' | orig={disp_orig:.2f} sust={disp_sust:.2f}'\n",
" if lista_alt: extra += ' [' + ', '.join(lista_alt[:3]) + (']' if len(lista_alt)<=3 else ' ...]')\n",
" componentes.append({'FACTURAEXPO': factura, 'LINEA': linea, 'PT': pt,\n",
" 'COMPONENTE': comp, 'UNIMED': unimed,\n",
" 'CANT_REQUERIDA': round(cant_req, 6),\n",
" 'SALDO_DISPONIBLE': round(disp, 6),\n",
" 'SALDO_ORIGINAL': round(disp_orig, 6),\n",
" 'SALDO_SUSTITUTOS': round(disp_sust, 6),\n",
" 'N_SUSTITUTOS_CON_SALDO': len(lista_alt),\n",
" 'MOTIVO': 'COMPONENTE_SIN_SALDO',\n",
" 'DETALLE': f'Faltan {cant_req - disp:.4f} {unimed} (incluyendo sustitutos).{extra}'})\n",
" problemas.append('COMPONENTE_SIN_SALDO')\n",
" # 3d) Motivo principal\n",
" if not problemas:\n",
" motivo = 'COBERTURA_POSIBLE'\n",
" detalle = 'La factura tiene cobertura completa segun el snapshot actual. Si el Paso 12 no la pudo procesar revisa el modo NATURAL/DIRIGIDA o si hay descargas previas que la dejaron en un estado intermedio.'\n",
" else:\n",
" from collections import Counter\n",
" counter = Counter(problemas)\n",
" motivo = counter.most_common(1)[0][0]\n",
" detalle = ', '.join(f'{k}:{v}' for k, v in counter.most_common())\n",
" bloqueos.append({'FACTURAEXPO': factura, 'PEDIMENTOEXPO': ped,\n",
" 'FECHAFACTURA_ISO': fecha, 'TIPOFACTURA': tipof,\n",
" 'PARTIDAS': n_partidas, 'PT_SIN_BOM': n_pt_sin_bom,\n",
" 'PESONETO_CERO': n_peso_cero,\n",
" 'COMPONENTES_SIN_SALDO': n_comp_sin_saldo,\n",
" 'MOTIVO_PRINCIPAL': motivo,\n",
" 'DETALLE': detalle})\n",
" prog.step()\n",
" prog.done(f'{len(bloqueos)} facturas analizadas')\n",
" df_bloq = pd.DataFrame(bloqueos)\n",
" df_comp = pd.DataFrame(componentes)\n",
" # Resumen por motivo\n",
" if not df_bloq.empty:\n",
" res_motivo = df_bloq['MOTIVO_PRINCIPAL'].value_counts().reset_index()\n",
" res_motivo.columns = ['MOTIVO', 'FACTURAS']\n",
" log('\\n=== Resumen por motivo principal ===')\n",
" for _, row in res_motivo.iterrows():\n",
" log(f' {row[\"MOTIVO\"]:<30} {row[\"FACTURAS\"]:>5,}')\n",
" return df_bloq, df_comp\n",
"\n",
"\n",
"def exportar_excel_diagnostico_paso12(df_bloq, df_comp, ruta):\n",
" with pd.ExcelWriter(ruta, engine='openpyxl') as w:\n",
" if df_bloq is not None and not df_bloq.empty:\n",
" df_bloq.to_excel(w, sheet_name='Bloqueos_por_factura', index=False)\n",
" if df_comp is not None and not df_comp.empty:\n",
" df_comp.to_excel(w, sheet_name='Detalle_componentes', index=False)\n",
" return ruta\n",
"\n",
"\n",
"# DESCARGA POR FRACCIÓN + UM\n",
"# Match: LEFT(FRACCIONIMPO, 8) = LEFT(FRACCIONEXPO, 8) AND UMEXITENCIA = UNIMED\n",
"# Sin BOM — cantidad directamente de CANTEXPO en SPartidasExpo.\n",
"# ─────────────────────────────────────────────────────────────────────────────\n",
"\n",
"def _saldos_idx_fraccion(saldos, fraccion8, um_key, fecha_export):\n",
" mask = ((saldos['FRACCION8'] == fraccion8)\n",
" & (saldos['UM_KEY'] == um_key)\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",
"\n",
"def _consumir_fraccion(saldos, idx_list, faltante, base_row):\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': saldos.at[idx, 'NUMPARTE'],\n",
" 'TIPO': 'OK',\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",
" '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 _build_cobertura_fraccion(df):\n",
" df_d = df.copy()\n",
" df_d['cant_cubierta'] = df_d['CANT_DESCARGADA'].where(df_d['TIPO'] == 'OK', 0)\n",
" cob_part = df_d.groupby(\n",
" ['FACTURAEXPO', 'FECHA_FACTURAEXPO', 'LINEA', 'FRACCIONEXPO', 'UNIMED_EXPO', 'CANT_REQUERIDA'],\n",
" as_index=False, dropna=False\n",
" ).agg(cant_cubierta=('cant_cubierta', 'sum'))\n",
" cob_part['pct'] = ((cob_part['cant_cubierta'] / cob_part['CANT_REQUERIDA'])\n",
" .fillna(0) * 100).round(2).clip(upper=100)\n",
" cob_fact = cob_part.groupby(['FACTURAEXPO', 'FECHA_FACTURAEXPO'], as_index=False, dropna=False).agg(\n",
" partidas_total=('LINEA', 'count'),\n",
" partidas_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",
"\n",
"def _build_descarga_fraccion_df(facturas_df=None, partidas_df=None, progress=None):\n",
" prog = _Progress(progress)\n",
" _load_catalogos()\n",
" saldos = _load_saldos_snapshot()\n",
" saldos['FRACCION8'] = saldos['FRACCIONIMPO'].fillna('').str.strip().str[:8]\n",
"\n",
" if facturas_df is None:\n",
" facturas_df = pd.read_sql(\n",
" \"SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO FROM SFacExp WHERE ESTATUS='NA'\",\n",
" scaii_conn)\n",
" if partidas_df is None:\n",
" partidas_df = pd.read_sql(\n",
" \"SELECT CONSECUTIVO, FACTURAEXPO, LINEA, NUMPARTE, FRACCIONEXPO, UNIMED, \"\n",
" \"CANTEXPO, PESONETO, MONTOIGIME, PAISORIGEN FROM SPartidasExpo\",\n",
" scaii_conn)\n",
"\n",
" partepais_dict = (_state['df_partepais_all']\n",
" .drop_duplicates(subset=['FRACCION', 'PAIS'], keep='last')\n",
" .set_index(['FRACCION', 'PAIS'])[['TIPOFRACCION', 'TASAIM']]\n",
" .to_dict('index'))\n",
"\n",
" partidas_por_factura = partidas_df.groupby('FACTURAEXPO')\n",
" prog.setup(len(facturas_df), 'Procesando facturas (fraccion/UM)')\n",
" all_rows = []\n",
"\n",
" for _, f in facturas_df.iterrows():\n",
" factura = f['FACTURAEXPO']\n",
" fecha_exp = f['FECHAFACTURA_ISO']\n",
" ped_ex = f.get('PEDIMENTOEXPO')\n",
" fec_clarion = to_clarion(fecha_exp)\n",
"\n",
" if factura not in partidas_por_factura.groups:\n",
" prog.step(); continue\n",
"\n",
" for _, p in partidas_por_factura.get_group(factura).iterrows():\n",
" linea = int(p['LINEA']) if pd.notna(p['LINEA']) else 0\n",
" frac_expo = (p.get('FRACCIONEXPO') or '').strip()\n",
" frac8 = frac_expo[:8]\n",
" um_expo = (p.get('UNIMED') or '').strip().upper()\n",
" cant_req = float(p.get('CANTEXPO') or 0)\n",
"\n",
" base = {\n",
" 'FACTURAEXPO': factura,\n",
" 'FECHA_FACTURAEXPO': fecha_exp,\n",
" 'PEDIMENTOEXPO': ped_ex,\n",
" 'LINEA': linea,\n",
" 'FRACCIONEXPO': frac_expo,\n",
" 'UNIMED_EXPO': um_expo,\n",
" 'UNIMED_BOM': um_expo,\n",
" 'COMPONENTE_MP': frac8,\n",
" 'CANT_REQUERIDA': cant_req,\n",
" 'MONTOIGI': float(p.get('MONTOIGIME') or 0),\n",
" 'FECHADESC_CLARION': fec_clarion,\n",
" 'TIPOFRACCION': '',\n",
" 'ADVALOREMIMPO': None,\n",
" }\n",
"\n",
" _vacio = {**base, 'NUMPARTE_USADO': '', 'FACTURAIMPO_SALDO': '',\n",
" 'PEDIMENTOIMPO': '', 'CLASE': '', 'PAISMERCANCIA': '',\n",
" 'FRACCION_SALDO': frac_expo, 'UNIMED_SALDO': um_expo,\n",
" 'CANT_LOTE_ORIG': 0, 'VALORMN': 0, 'VALORME': 0, 'PESONETO_DESC': 0}\n",
"\n",
" if cant_req <= 1e-9 or not frac8:\n",
" all_rows.append({**_vacio, 'TIPO': 'SIN_CANT', 'CANT_DESCARGADA': 0, 'STATUS': 'SIN_CANT'})\n",
" continue\n",
"\n",
" idx_list = _saldos_idx_fraccion(saldos, frac8, um_expo, fecha_exp)\n",
" faltante, rows = _consumir_fraccion(saldos, idx_list, cant_req, base)\n",
"\n",
" for row in rows:\n",
" pp = partepais_dict.get((row['FRACCION_SALDO'], row['PAISMERCANCIA']), {})\n",
" row['TIPOFRACCION'] = pp.get('TIPOFRACCION', '')\n",
" row['ADVALOREMIMPO'] = _adv(pp.get('TASAIM'))\n",
" row['COMPONENTE_MP'] = row['NUMPARTE_USADO']\n",
" all_rows.extend(rows)\n",
"\n",
" if faltante > 1e-9:\n",
" all_rows.append({**_vacio, 'TIPO': 'FALTANTE',\n",
" 'CANT_DESCARGADA': faltante, 'NUMPARTE_USADO': frac8, 'STATUS': 'FALTANTE'})\n",
" prog.step()\n",
"\n",
" df = pd.DataFrame(all_rows) if all_rows else pd.DataFrame()\n",
" if not df.empty and 'FECHA_FACTURAEXPO' in df.columns:\n",
" prog.done('Pronostico fraccion/UM listo')\n",
" return df\n",
"\n",
"\n",
"def calcular_pronostico_fraccion_um(log=print, progress=None):\n",
" try:\n",
" log('Cargando facturas NA...')\n",
" facturas = pd.read_sql(\n",
" \"SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO FROM SFacExp WHERE ESTATUS='NA'\",\n",
" scaii_conn)\n",
" partidas = pd.read_sql(\n",
" \"SELECT CONSECUTIVO, FACTURAEXPO, LINEA, NUMPARTE, FRACCIONEXPO, UNIMED, \"\n",
" \"CANTEXPO, PESONETO, MONTOIGIME, PAISORIGEN FROM SPartidasExpo\",\n",
" scaii_conn)\n",
" log(f'Facturas NA: {len(facturas)} | Partidas: {len(partidas)}')\n",
" df = _build_descarga_fraccion_df(facturas_df=facturas, partidas_df=partidas, progress=progress)\n",
" _state['df_descarga_fraccion'] = df\n",
" cob = _build_cobertura_fraccion(df)\n",
" _state['cobertura_fraccion'] = cob\n",
" if cob.empty:\n",
" log('Sin facturas con partidas para procesar.'); return\n",
" total = len(cob)\n",
" al_100 = int((cob['partidas_100pct'] == cob['partidas_total']).sum())\n",
" log(f'Tiempo OK | Facturas: {total} | 100% cobertura: {al_100} | Parcial: {total - al_100}')\n",
" log(cob.head(20).to_string(index=False))\n",
" except Exception as e:\n",
" log(f'ERROR pronostico_fraccion_um: {e}')\n",
" raise\n",
"\n",
"\n",
"def ejecutar_fraccion_um(modo, dry_run, fecha_desde=None, fecha_hasta=None, log=print, progress=None):\n",
" if 'df_descarga_fraccion' not in _state:\n",
" log('ERROR: corre primero el pronostico de fraccion/UM.'); return\n",
" df_da = _state['df_descarga_fraccion']\n",
" cob = _state['cobertura_fraccion']\n",
"\n",
" if modo == 'NATURAL':\n",
" elig = cob[cob['partidas_100pct'] == cob['partidas_total']]['FACTURAEXPO'].tolist()\n",
" else:\n",
" elig = cob['FACTURAEXPO'].tolist()\n",
"\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",
"\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",
"\n",
" mask = df_da['FACTURAEXPO'].isin(elig) & (df_da['TIPO'] == 'OK')\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 FRACCION/UM ({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 fac, err in errores[:5]: log(f' {fac}: {err}')\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "logic-analisis-y-nlp",
"metadata": {
"tags": [
"hide-input"
]
},
"outputs": [],
"source": [
"def _fmt_an_df(df):\n",
" \"\"\"Formatea las tablas con separador de miles, decimales y moneda.\n",
" Solo aplica formato numerico a columnas numericas (evita ValueError en strings).\n",
" Devuelve un pandas Styler (visual) sin alterar el DataFrame original.\"\"\"\n",
" if df is None or getattr(df, 'empty', True):\n",
" return df\n",
" fmt = {}\n",
" for col in df.columns:\n",
" # Solo formatear columnas numericas\n",
" if not pd.api.types.is_numeric_dtype(df[col]):\n",
" continue\n",
" c = str(col).lower()\n",
" # Anios y lineas: enteros sin coma (decoracion)\n",
" if c in ('anio', 'anio_entrada', 'linea'):\n",
" fmt[col] = '{:.0f}'\n",
" continue\n",
" if c.startswith('ratio'):\n",
" fmt[col] = '{:,.4f}'\n",
" elif any(k in c for k in ('vmn', 'vme', 'valor', 'dif_valor')):\n",
" fmt[col] = '${:,.2f}'\n",
" elif any(k in c for k in ('partidas', 'partes', 'lotes', 'unicas', 'n_sustitutos', 'pts_', 'relaciones', 'componentes_distintos', 'n_sustitutos_con_saldo')):\n",
" fmt[col] = '{:,.0f}'\n",
" else:\n",
" fmt[col] = '{:,.2f}'\n",
" try:\n",
" sty = df.style.format(fmt, na_rep='-')\n",
" try:\n",
" sty = sty.hide(axis='index')\n",
" except Exception:\n",
" try: sty = sty.hide_index()\n",
" except Exception: pass\n",
" return sty\n",
" except Exception:\n",
" return df\n",
"\n",
"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",
"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"
]
},
{
"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",
" # Defaults: '' para strings, 0/0.0 para numericos.\n",
" _x = lambda v, d='': (d if (v is None or (isinstance(v, float) and pd.isna(v))) else v)\n",
" _xn = lambda v: 0.0 if (v is None or (isinstance(v, float) and pd.isna(v))) else float(v)\n",
" cur.execute(INSERT_M,\n",
" next_m, consec_factura_ctm, factura_ctm, linea_ctm,\n",
" _x(numparte_pt_ctm), _x(r.CLASE), _xn(m_cantidad), _x(r.UNIMED),\n",
" _x(r.FACTIMPO), _x(r.PARTEORIGINAL),\n",
" _xn(m_vmn), _xn(m_vme), _x(r.PAISMERCANCIA), _xn(m_pneto), _xn(m_pbruto),\n",
" _x(r.TIPOFRACCION), _x(r.SECTOR),\n",
" '', 'TEM', _x(r.FACTIMPO), 'N', _x(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}')"
]
},
{
"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 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.'); 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\\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'); 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\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"
]
},
{
"cell_type": "code",
"id": "logic-bom",
"metadata": {},
"outputs": [],
"execution_count": null,
"source": [
"# ============================================================================\n",
"# Logic: Analisis BOM + Sustitutos + Saldos (vigentes/vencidos)\n",
"# Pestana general - muestra composicion de PT, sus sustitutos y disponibilidad\n",
"# ============================================================================\n",
"\n",
"def cargar_bom_completo(progress=None):\n",
" \"\"\"Trae todo SMatBOM con info del PT (tipo de material si esta en SPartes).\n",
" Una fila = un componente de un PT.\"\"\"\n",
" prog = _Progress(progress)\n",
" prog.setup(2, 'Cargando SMatBOM...')\n",
" df = pd.read_sql(\"\"\"\n",
" SELECT B.NUMPARTE AS PT,\n",
" B.NUMPARTEBOM AS COMPONENTE,\n",
" B.CANTIDAD AS CANTIDAD_BOM,\n",
" B.UNIMED AS UNIMED_BOM,\n",
" P.TIPOMAT AS TIPOMAT_PT,\n",
" P.DESCRIPCIONE AS DESC_PT\n",
" FROM SMatBOM B\n",
" LEFT JOIN SPartes P ON P.NUMPARTE = B.NUMPARTE\n",
" \"\"\", scaii_conn)\n",
" prog.step(desc='SMatBOM cargado')\n",
" for c in ('PT', 'COMPONENTE', 'UNIMED_BOM', 'TIPOMAT_PT', 'DESC_PT'):\n",
" df[c] = df[c].fillna('').astype(str).str.strip()\n",
" df['CANTIDAD_BOM'] = pd.to_numeric(df['CANTIDAD_BOM'], errors='coerce').fillna(0.0)\n",
" prog.done(f'BOM: {len(df):,} relaciones PT-componente')\n",
" return df\n",
"\n",
"def cargar_sustitutos_map(progress=None):\n",
" \"\"\"Devuelve dict {NUMPARTE: [sustitutos]} desde SPartesSustitutos.\"\"\"\n",
" prog = _Progress(progress)\n",
" prog.setup(1, 'Cargando SPartesSustitutos...')\n",
" df = pd.read_sql(\"SELECT NUMPARTE, NUMPARTESUSTITUTO FROM SPartesSustitutos\", scaii_conn)\n",
" d = {}\n",
" for _, r in df.iterrows():\n",
" k = str(r['NUMPARTE']).strip()\n",
" v = str(r['NUMPARTESUSTITUTO']).strip()\n",
" if k and v:\n",
" d.setdefault(k, []).append(v)\n",
" prog.done(f'Sustitutos: {len(d):,} partes con al menos un sustituto')\n",
" return d\n",
"\n",
"def cargar_saldos_agrupados_por_parte(progress=None):\n",
" \"\"\"Saldos disponibles por NUMPARTE con sub-totales vigente / vencido.\n",
" Vigente = FECHAVENC_ISO IS NULL o > hoy. Vencido = FECHAVENC_ISO <= hoy.\"\"\"\n",
" prog = _Progress(progress)\n",
" prog.setup(1, 'Cargando saldos por parte...')\n",
" df = pd.read_sql(\"\"\"\n",
" SELECT NUMPARTE,\n",
" CASE WHEN FECHAVENC_ISO IS NULL OR FECHAVENC_ISO > CAST(GETDATE() AS DATE)\n",
" THEN 'VIG' ELSE 'VEN' END AS ESTADO,\n",
" SUM(CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_CANT,\n",
" SUM(ISNULL(VALORIMPOMN,0) - ISNULL(VALORUSADOMN,0)) AS SALDO_VMN,\n",
" SUM(ISNULL(VALORIMPOME,0) - ISNULL(VALORUSADOME,0)) AS SALDO_VME,\n",
" SUM(ISNULL(PESONETO,0) - ISNULL(PESOUSADO,0)) AS SALDO_PESO,\n",
" COUNT(*) AS LOTES\n",
" FROM SSaldoTem\n",
" WHERE (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n",
" GROUP BY NUMPARTE,\n",
" CASE WHEN FECHAVENC_ISO IS NULL OR FECHAVENC_ISO > CAST(GETDATE() AS DATE)\n",
" THEN 'VIG' ELSE 'VEN' END\n",
" \"\"\", scaii_conn)\n",
" df['NUMPARTE'] = df['NUMPARTE'].fillna('').astype(str).str.strip()\n",
" # Pivot a una fila por NUMPARTE con columnas separadas vig/ven\n",
" piv = df.pivot_table(index='NUMPARTE', columns='ESTADO',\n",
" values=['SALDO_CANT','SALDO_VMN','SALDO_VME','SALDO_PESO','LOTES'],\n",
" aggfunc='sum', fill_value=0.0)\n",
" piv.columns = [f'{a}_{b}' for a,b in piv.columns]\n",
" piv = piv.reset_index()\n",
" # Garantizar columnas (puede faltar VIG o VEN si no hay)\n",
" for col in ('SALDO_CANT_VIG','SALDO_CANT_VEN','SALDO_VMN_VIG','SALDO_VMN_VEN',\n",
" 'SALDO_VME_VIG','SALDO_VME_VEN','SALDO_PESO_VIG','SALDO_PESO_VEN',\n",
" 'LOTES_VIG','LOTES_VEN'):\n",
" if col not in piv.columns: piv[col] = 0.0\n",
" prog.done(f'Saldos: {len(piv):,} partes con saldo > 0')\n",
" return piv\n",
"\n",
"def analizar_bom_completo(progress=None):\n",
" \"\"\"Arma la tabla plana BOM con columnas:\n",
" PT, DESC_PT, TIPOMAT_PT, COMPONENTE, UNIMED_BOM, CANTIDAD_BOM,\n",
" N_SUSTITUTOS, LISTA_SUSTITUTOS,\n",
" SALDO_VIG_ORIG, SALDO_VEN_ORIG,\n",
" SALDO_VIG_SUST, SALDO_VEN_SUST,\n",
" SALDO_VIG_TOTAL, SALDO_VEN_TOTAL, SALDO_TOTAL,\n",
" ESTATUS_COBERTURA, LOTES_VIG, LOTES_VEN\n",
" \"\"\"\n",
" prog = _Progress(progress)\n",
" prog.setup(5, 'Cargando BOM...')\n",
" bom = cargar_bom_completo(progress=None)\n",
" prog.step(desc='Cargando sustitutos...')\n",
" sust = cargar_sustitutos_map(progress=None)\n",
" prog.step(desc='Cargando saldos...')\n",
" sal = cargar_saldos_agrupados_por_parte(progress=None)\n",
" prog.step(desc='Cruzando datos...')\n",
"\n",
" # Indice de saldos por NUMPARTE\n",
" sal_map = sal.set_index('NUMPARTE').to_dict('index')\n",
"\n",
" def _saldo_de(parte):\n",
" r = sal_map.get(parte)\n",
" if not r:\n",
" return (0.0, 0.0, 0, 0)\n",
" return (float(r.get('SALDO_CANT_VIG',0) or 0),\n",
" float(r.get('SALDO_CANT_VEN',0) or 0),\n",
" int(r.get('LOTES_VIG',0) or 0),\n",
" int(r.get('LOTES_VEN',0) or 0))\n",
"\n",
" rows = []\n",
" for _, b in bom.iterrows():\n",
" comp = b['COMPONENTE']\n",
" if not comp:\n",
" continue\n",
" s_vig_o, s_ven_o, lt_vig_o, lt_ven_o = _saldo_de(comp)\n",
" lista_sust = sust.get(comp, [])\n",
" s_vig_s = s_ven_s = 0.0\n",
" lt_vig_s = lt_ven_s = 0\n",
" for x in lista_sust:\n",
" v, w, lv, lw = _saldo_de(x)\n",
" s_vig_s += v; s_ven_s += w\n",
" lt_vig_s += lv; lt_ven_s += lw\n",
" vig_total = s_vig_o + s_vig_s\n",
" ven_total = s_ven_o + s_ven_s\n",
" total = vig_total + ven_total\n",
" if vig_total > 0:\n",
" estatus = 'DISPONIBLE_VIG'\n",
" elif ven_total > 0:\n",
" estatus = 'SOLO_VENCIDO'\n",
" else:\n",
" estatus = 'SIN_SALDO'\n",
" rows.append({\n",
" 'PT': b['PT'],\n",
" 'DESC_PT': b['DESC_PT'][:60],\n",
" 'TIPOMAT_PT': b['TIPOMAT_PT'],\n",
" 'COMPONENTE': comp,\n",
" 'UNIMED_BOM': b['UNIMED_BOM'],\n",
" 'CANTIDAD_BOM': b['CANTIDAD_BOM'],\n",
" 'N_SUSTITUTOS': len(lista_sust),\n",
" 'LISTA_SUSTITUTOS': ', '.join(lista_sust[:5]) + (' ...' if len(lista_sust) > 5 else ''),\n",
" 'SALDO_VIG_ORIG': s_vig_o,\n",
" 'SALDO_VEN_ORIG': s_ven_o,\n",
" 'SALDO_VIG_SUST': s_vig_s,\n",
" 'SALDO_VEN_SUST': s_ven_s,\n",
" 'SALDO_VIG_TOTAL': vig_total,\n",
" 'SALDO_VEN_TOTAL': ven_total,\n",
" 'SALDO_TOTAL': total,\n",
" 'ESTATUS_COBERTURA': estatus,\n",
" 'LOTES_VIG': lt_vig_o + lt_vig_s,\n",
" 'LOTES_VEN': lt_ven_o + lt_ven_s,\n",
" })\n",
" prog.step(desc='Tabla armada')\n",
" df_out = pd.DataFrame(rows)\n",
" if df_out.empty:\n",
" df_out = pd.DataFrame(columns=['PT','DESC_PT','TIPOMAT_PT','COMPONENTE','UNIMED_BOM',\n",
" 'CANTIDAD_BOM','N_SUSTITUTOS','LISTA_SUSTITUTOS','SALDO_VIG_ORIG','SALDO_VEN_ORIG',\n",
" 'SALDO_VIG_SUST','SALDO_VEN_SUST','SALDO_VIG_TOTAL','SALDO_VEN_TOTAL','SALDO_TOTAL',\n",
" 'ESTATUS_COBERTURA','LOTES_VIG','LOTES_VEN'])\n",
" df_out = df_out.sort_values(['ESTATUS_COBERTURA','PT','COMPONENTE']).reset_index(drop=True)\n",
" prog.done(f'BOM analizado: {len(df_out):,} filas (PT-componente)')\n",
" return df_out\n",
"\n",
"def resumen_bom(df_bom):\n",
" \"\"\"Resumen agregado: cuantos PTs totales, cuantos con problema, etc.\"\"\"\n",
" if df_bom is None or df_bom.empty:\n",
" return pd.DataFrame()\n",
" tot_pt = df_bom['PT'].nunique()\n",
" tot_comp_unicos = df_bom['COMPONENTE'].nunique()\n",
" por_estatus = df_bom.groupby('ESTATUS_COBERTURA', as_index=False).agg(\n",
" relaciones=('PT','count'),\n",
" pts_distintos=('PT','nunique'),\n",
" componentes_distintos=('COMPONENTE','nunique'))\n",
" pts_con_problema = df_bom[df_bom['ESTATUS_COBERTURA'] != 'DISPONIBLE_VIG']['PT'].nunique()\n",
" print(f'Total PTs con BOM: {tot_pt:,}')\n",
" print(f'Total componentes distintos: {tot_comp_unicos:,}')\n",
" print(f'PTs con al menos 1 problema: {pts_con_problema:,}')\n",
" print()\n",
" return por_estatus\n",
"\n",
"def exportar_excel_bom(df_bom, ruta='Analisis_BOM.xlsx'):\n",
" \"\"\"Exporta a Excel: hoja 'BOM_completo' + hoja 'Componentes_sin_saldo'.\"\"\"\n",
" ruta = Path(ruta).resolve()\n",
" with pd.ExcelWriter(ruta, engine='openpyxl') as xw:\n",
" df_bom.to_excel(xw, sheet_name='BOM_completo', index=False)\n",
" problema = df_bom[df_bom['ESTATUS_COBERTURA'] != 'DISPONIBLE_VIG']\n",
" problema.to_excel(xw, sheet_name='Componentes_sin_saldo_vig', index=False)\n",
" # Resumen por componente\n",
" if not df_bom.empty:\n",
" por_comp = df_bom.groupby(['COMPONENTE','UNIMED_BOM'], as_index=False).agg(\n",
" pts_que_lo_usan=('PT','nunique'),\n",
" saldo_vig_total=('SALDO_VIG_TOTAL','max'),\n",
" saldo_ven_total=('SALDO_VEN_TOTAL','max'),\n",
" n_sustitutos=('N_SUSTITUTOS','max'),\n",
" estatus=('ESTATUS_COBERTURA','first'))\n",
" por_comp = por_comp.sort_values(['estatus','pts_que_lo_usan'], ascending=[True, False])\n",
" por_comp.to_excel(xw, sheet_name='Por_componente', index=False)\n",
" return str(ruta)\n",
"\n",
"def filtrar_bom(df, pt_filtro='', comp_filtro='', solo_problemas=False):\n",
" \"\"\"Filtros simples sobre la tabla del BOM.\"\"\"\n",
" if df is None or df.empty:\n",
" return df\n",
" out = df.copy()\n",
" if pt_filtro:\n",
" out = out[out['PT'].str.contains(pt_filtro, case=False, regex=False, na=False)]\n",
" if comp_filtro:\n",
" out = out[out['COMPONENTE'].str.contains(comp_filtro, case=False, regex=False, na=False)]\n",
" if solo_problemas:\n",
" out = out[out['ESTATUS_COBERTURA'] != 'DISPONIBLE_VIG']\n",
" return out.reset_index(drop=True)\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<NNN>)\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<NNN>. 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<NNN>.\"\"\"\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 DISTINCT\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 DISTINCT\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",
" (substr(Q1.\"SeccionAduanera\", 1, 2) || '-' ||\n",
" Q1.\"Patente\" || '-' || Q1.\"Pedimento\") 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.\"FechaOperacionAnterior\", 3, 2) || '-' ||\n",
" substr(R7.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n",
" R7.\"PatenteAnterior\" || '-' || R7.\"PedimentoAnterior\")\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",
" # Catalogo de Unidades de Medida (clave numerica -> sigla SCAII)\n",
" # Fuente: Catalogo de UM de la Aduana Mexicana\n",
" 1: 'KGS', # Kilo\n",
" 2: 'GR', # Gramo\n",
" 3: 'MT', # Metro Lineal\n",
" 4: 'M2', # Metro Cuadrado\n",
" 5: 'M3', # Metro Cubico\n",
" 6: 'PZA', # Pieza\n",
" 7: 'PZA', # Cabeza\n",
" 8: 'LT', # Litro\n",
" 9: 'PAR', # Par\n",
" 10: '', # Kilowatt (sin UM SCAII)\n",
" 11: 'MILLR', # Millar\n",
" 12: 'JGO', # Juego\n",
" 13: '', # Kilowatt/Hora (sin UM SCAII)\n",
" 14: 'TON', # Tonelada\n",
" 15: 'BARR', # Barril\n",
" 16: '', # Gramo Neto (sin UM SCAII)\n",
" 17: 'DEC', # Decenas\n",
" 18: 'CIEN', # Cientos\n",
" 19: 'DOCE', # Docenas\n",
" 20: 'CAJA', # Caja\n",
" 21: 'PZA', # Botella\n",
" 22: 'CARAT', # 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.\"SubdivisionFraccion\" AS subdivision_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",
" (SELECT R501i.\"PesoBrutoMercancia\" 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 peso_bruto_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', 'subdivision_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<F4>-R<NNN>'.\"\"\"\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",
" # NUMERO FACTURA = pedimento a 15 digitos: SS-PPPP-NNNNNNN (sin clave)\n",
" df['ped_15'] = (df['seccion_aduanera'].astype(str).str[:2]\n",
" + '-' + df['patente'].astype(str).str.zfill(4)\n",
" + '-' + df['pedimento'].astype(str).str.zfill(7).str[-7:])\n",
" df['factura'] = df['ped_15']\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",
" # Fraccion arancelaria a 10 digitos = Fraccion (8) + SubdivisionFraccion (2)\n",
" df['fraccion_10'] = (df['fraccion'].fillna('').astype(str).str.strip().str.zfill(8)\n",
" + df['subdivision_fraccion'].fillna('').astype(str).str.strip().str.zfill(2))\n",
"\n",
" # LINEA viene de SecuenciaFraccion del Registro551 (no secuencial)\n",
" df['linea_551'] = pd.to_numeric(df['secuencia_fraccion'], errors='coerce').fillna(0).astype(int)\n",
"\n",
" # =========================================================\n",
" # PESO NETO / PESO BRUTO\n",
" # Logica:\n",
" # - Si UnidadMedidaTarifa == 1 (KGS): peso = CantidadUMTarifa\n",
" # - Si no: prorrateo del peso bruto del 501 que sobra despues de los KGS\n",
" # 1. peso_bruto_501 - sum(CantidadUMTarifa donde UMT=1) = kgs_restantes\n",
" # 2. peso_unitario = kgs_restantes / sum(CantidadUMTarifa donde UMT!=1)\n",
" # 3. peso_partida = peso_unitario * CantidadUMTarifa de la partida\n",
" # =========================================================\n",
" _ped_key = (df['patente'].astype(str) + '|' +\n",
" df['pedimento'].astype(str) + '|' +\n",
" df['seccion_aduanera'].astype(str))\n",
" df['_ped_key'] = _ped_key\n",
" _qty_tar = pd.to_numeric(df['cantidad_um_tarifa'], errors='coerce').fillna(0.0)\n",
" _umt = pd.to_numeric(df['unidad_medida_tarifa'], errors='coerce').fillna(0).astype(int)\n",
" df['_qty_tar_num'] = _qty_tar\n",
" df['_umt_int'] = _umt\n",
" df['_es_kgs'] = (_umt == 1)\n",
" # Sumas por pedimento\n",
" _agg_kgs = df.groupby('_ped_key').apply(lambda g: g.loc[g['_es_kgs'], '_qty_tar_num'].sum())\n",
" _agg_otras = df.groupby('_ped_key').apply(lambda g: g.loc[~g['_es_kgs'], '_qty_tar_num'].sum())\n",
" df['_sum_kgs'] = df['_ped_key'].map(_agg_kgs).fillna(0.0)\n",
" df['_sum_otras'] = df['_ped_key'].map(_agg_otras).fillna(0.0)\n",
" _peso_bruto_501 = pd.to_numeric(df['peso_bruto_501'], errors='coerce').fillna(0.0)\n",
" _kgs_rest = (_peso_bruto_501 - df['_sum_kgs']).clip(lower=0)\n",
" _peso_unit = np.where(df['_sum_otras'] > 0, _kgs_rest / df['_sum_otras'], 0.0)\n",
" df['peso_calc'] = np.where(df['_es_kgs'], _qty_tar, _peso_unit * _qty_tar).round(3)\n",
" # Limpieza columnas temporales\n",
" df.drop(columns=['_ped_key', '_qty_tar_num', '_umt_int', '_es_kgs', '_sum_kgs', '_sum_otras'],\n",
" inplace=True)\n",
"\n",
" out = pd.DataFrame({\n",
" 'NUMERO FACTURA': df['factura'],\n",
" 'FECHA PAGO REAL': df['fecha_pago_real'],\n",
" 'LINEA': df['linea_551'],\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': df['peso_calc'],\n",
" 'PESO BRUTO': df['peso_calc'],\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_10'],\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'<div style=\"background:{color};color:white;padding:12px 18px;border-radius:6px;\">'\n",
" f'<h2 style=\"margin:0;\">Sistema de Utilerias 2.0 SCAII</h2>'\n",
" f'<div style=\"font-size:12px;opacity:0.9;\">{CONEXION_MSG}</div></div>'\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'<b>DB actual:</b> <span style=\"color:#1565C0;\">{DB_ACTUAL or \"sin conexion\"}</span>'\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'<b>DB actual:</b> <span style=\"color:#1565C0;\">{DB_ACTUAL or \"sin conexion\"}</span>'\n",
"btn_conectar.on_click(_on_conectar)\n",
"\n",
"tab_conn = W.VBox([\n",
" W.HTML('<h3>Conexion a SQL Server</h3>'\n",
" f'<p style=\"color:#555;margin:0 0 8px 0;\">Server: <b>{SCAII_SERVER}</b> | Usuario: <b>{SCAII_USER}</b></p>'),\n",
" lbl_actual,\n",
" W.HTML('<p style=\"margin-top:10px;\">Selecciona la base de datos. El cambio aplica a todas las pestanas; '\n",
" 'el cache de pronostico/analisis se resetea al cambiar.</p>'),\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",
"# ----- Fraccion / UM -----\n",
"out_pron_frac = W.Output(layout=OUT_STYLE); bar_pron_frac = _mkbar('Pron.Frac')\n",
"out_frac = W.Output(layout=OUT_STYLE); bar_frac_um = _mkbar('Frac/UM')\n",
"btn_pron_frac = W.Button(description='Calcular pronostico (Frac/UM)', button_style='primary', icon='play', layout={'width':'280px'})\n",
"modo_frac = W.Dropdown(options=['NATURAL','DIRIGIDA'], value='NATURAL', description='Modo:', layout={'width':'250px'})\n",
"dry_frac = W.Checkbox(value=True, description='DRY_RUN (simular)')\n",
"fd_frac = W.Text(placeholder='YYYY-MM-DD', description='Desde:', layout={'width':'250px'})\n",
"fh_frac = W.Text(placeholder='YYYY-MM-DD', description='Hasta:', layout={'width':'250px'})\n",
"btn_frac_um = W.Button(description='Ejecutar descarga Frac/UM (NA->AC)', button_style='warning', icon='check', layout={'width':'280px'})\n",
"\n",
"def _on_pron_frac(_):\n",
" with out_pron_frac:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" calcular_pronostico_fraccion_um(log=print, progress=bar_pron_frac)\n",
"btn_pron_frac.on_click(_on_pron_frac)\n",
"\n",
"def _on_frac(_):\n",
" with out_frac:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" ejecutar_fraccion_um(modo_frac.value, dry_frac.value, fd_frac.value or None, fh_frac.value or None, log=print, progress=bar_frac_um)\n",
"btn_frac_um.on_click(_on_frac)\n",
"\n",
"\n",
"\n",
"\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('<h3>Paso 8 - Pronostico de cobertura</h3>'),\n",
" btn_pron, bar_pron, out_pron,\n",
" W.HTML('<h3 style=\"margin-top:18px;\">Paso 9 - Descargas pendientes (NA - AC)</h3>'),\n",
" W.HBox([modo9, dry9]), W.HBox([fd9, fh9]), btn_p9, bar_p9, out_p9,\n",
" W.HTML('<h3 style=\"margin-top:18px;\">Paso 10 - Complementaria (sobre facturas AC)</h3>'),\n",
" W.HBox([modo10, dry10]), W.HBox([fd10, fh10]), facts_obj, btn_p10, bar_p10, out_p10,\n",
" W.HTML('<h3 style=\"margin-top:18px;\">Descarga por Fraccion / UM</h3>'),\n",
" W.HTML('<p style=\"color:#555;font-size:0.9em;\">Match por primeros 8 digitos de fraccion arancelaria + unidad de medida. Sin BOM.</p>'),\n",
" btn_pron_frac, bar_pron_frac, out_pron_frac,\n",
" W.HBox([modo_frac, dry_frac]), W.HBox([fd_frac, fh_frac]), btn_frac_um, bar_frac_um, out_frac,\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('<h4>Saldo disponible por a-o (de entrada del lote)</h4>'))\n",
" display(_fmt_an_df(_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('<h4>IMPO vs EXPO por a-o</h4>'))\n",
" display(_fmt_an_df(_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('<h4>Peso por a-o - IMPO / EXPO / CONSUMIDO / DESCARGAS</h4>'))\n",
" display(_fmt_an_df(_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('<h4>Cantidades por a-o - IMPO vs EXPO</h4>'))\n",
" display(_fmt_an_df(_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('<h4>Cantidades por a-o + UM (separado por unidad de medida)</h4>'))\n",
" display(_fmt_an_df(_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('<h3>An-lisis SSaldoTem + IMPO vs EXPO</h3>'),\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('<h3>Sustitutos NLP - TF-IDF + coseno</h3>'),\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('<h3>Paso 12 - Descarga por % de KGS</h3>'\n",
" '<p style=\"margin:0 0 10px 0;color:#555;\">'\n",
" 'cant_req = (BOM.CANTIDAD / 100) - PESONETO de la partida. Mismo PEPS, UM y sustitutos.</p>'),\n",
" btn_pron12, bar_pron12, out_pron12,\n",
" W.HTML('<h3 style=\"margin-top:18px;\">Paso 12 - Descargas pendientes KG (NA - AC)</h3>'),\n",
" W.HBox([modo12, dry12]), W.HBox([fd12, fh12]), btn_p12, bar_p12, out_p12,\n",
" W.HTML('<h3 style=\"margin-top:18px;\">Paso 12 - Complementaria KG (sobre facturas AC)</h3>'),\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",
"<div style=\"background:#E3F2FD;border:1px solid #90CAF9;padding:12px;border-radius:6px;margin:8px 0;\">\n",
"<b style=\"color:#0D47A1;\">Formato esperado del Excel del cliente</b>\n",
"<p style=\"margin:6px 0;color:#333;font-size:12px;\">La primera hoja del archivo debe contener al menos estas columnas (solo las dos primeras son obligatorias):</p>\n",
"<table style=\"border-collapse:collapse;font-size:12px;\">\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><th style=\"padding:4px 10px;text-align:left;\">Obligatoria</th></tr>\n",
"<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>Facturas CTM</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">AAU112023RFR0481, NIS112023RFR0035</td><td style=\"padding:4px 10px;border:1px solid #ddd;color:#C62828;\">SI</td></tr>\n",
"<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>PEDIMENTO COMPLETO</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">75-3076-4021492</td><td style=\"padding:4px 10px;border:1px solid #ddd;color:#C62828;\">SI</td></tr>\n",
"<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\">PATENTE</td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">3076</td><td style=\"padding:4px 10px;border:1px solid #ddd;color:#888;\">no</td></tr>\n",
"<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\">ADUANA</td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">75</td><td style=\"padding:4px 10px;border:1px solid #ddd;color:#888;\">no</td></tr>\n",
"<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\">PEDIMENTO</td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">4021492</td><td style=\"padding:4px 10px;border:1px solid #ddd;color:#888;\">no</td></tr>\n",
"<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\">Operacion</td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">Importacion</td><td style=\"padding:4px 10px;border:1px solid #ddd;color:#888;\">no</td></tr>\n",
"<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\">Clave de pedimento</td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">F4</td><td style=\"padding:4px 10px;border:1px solid #ddd;color:#888;\">no</td></tr>\n",
"</table>\n",
"<p style=\"margin:8px 0 0 0;color:#555;font-size:11px;\">La celda <b>Facturas CTM</b> puede traer varias facturas separadas por coma; la herramienta las separa automaticamente.</p>\n",
"</div>\n",
"'''\n",
"\n",
"def _on_plantilla(_):\n",
" with out_ctm_log:\n",
" clear_output()\n",
" path = generar_plantilla_excel_ctm()\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><br>'\n",
" f'<small style=\"color:#555;\">Abre el archivo, agrega tus datos, guarda y luego subelo con el boton \"Subir Excel\".</small></div>'))\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('<p style=\"color:#555;margin-top:8px;font-size:12px;\"><i>En pantalla se muestran solo las primeras 50 filas del plan. '\n",
" 'Para ver el detalle completo presiona <b>Exportar Excel completo</b>.</i></p>'))\n",
" with out_ctm_tabla:\n",
" clear_output()\n",
" if 'ctm_resumen' in _state and not _state['ctm_resumen'].empty:\n",
" display(HTML('<h4>Resumen CTM (por factura + linea + componente)</h4>'))\n",
" display(_state['ctm_resumen'])\n",
" display(HTML('<h4>Plan detalle (primeras 50 filas)</h4>'))\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('<div style=\"color:#C62828;\">Corre primero <b>Analizar CTM</b>.</div>'))\n",
" return\n",
" path = exportar_excel_ctm(_state['ctm_plan'], _state['ctm_resumen'])\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:12px;border-radius:6px;margin:8px 0;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado con el detalle completo</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:13px;color:#333;\">{path}</span><br>'\n",
" f'<small style=\"color:#555;\">Contiene tres hojas: <b>Resumen</b>, <b>Plan_Detalle</b> (todas las filas, no solo 50) y <b>Faltantes</b>.</small></div>'))\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('<h3>CTM - Reasignacion de descargas desde Cambio de Regimen</h3>'\n",
" '<p style=\"margin:0 0 10px 0;color:#555;\">'\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 <b>solo lectura</b> (Paso A): no escribe en la base de datos.</p>'),\n",
" W.HTML(_html_formato),\n",
" btn_plantilla,\n",
" W.HTML('<p style=\"margin-top:14px;\"><b>Sube tu Excel con el mapeo Facturas CTM <-> Pedimento F4:</b></p>'),\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('<h4 style=\"margin-top:20px;border-top:2px solid #FFA726;padding-top:14px;\">Paso B - Ejecutar reasignacion en la base de datos</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Toma el plan calculado arriba y aplica los cambios en SDescargaT y SFacExp. <b>Corre primero con DRY_RUN activado para revisar.</b></p>'),\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",
"<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 (3 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>FACTURAIMPO</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">F1234567</td></tr>\n",
"<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>CANTIDAD_SALDO</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">100.0</td></tr>\n",
"<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>FRACCION_IMPO</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">85044010</td></tr>\n",
"</table>\n",
"<p style=\"margin:8px 0 0 0;color:#555;font-size:11px;\">Las fechas inicio/fin filtran SSaldoTem por <b>FECHAFACTURA_ISO</b>.</p>\n",
"</div>\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'<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_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('<h4>Resumen Saldos Vencidos</h4>'))\n",
" display(_state['sv_resumen'])\n",
" display(HTML('<h4>Plan detalle (primeras 50 filas)</h4>'))\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('<div style=\"color:#C62828;\">Corre primero <b>Analizar</b>.</div>'))\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'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:12px;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:13px;color:#333;\">{path}</span></div>'))\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('<h3>Saldos Vencidos - Utileria Forma 5</h3>'\n",
" '<p style=\"margin:0 0 10px 0;color:#555;\">'\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).</p>'),\n",
" W.HTML(_html_formato_sv),\n",
" btn_plantilla_sv,\n",
" W.HTML('<p style=\"margin-top:14px;\"><b>Sube tu Excel y define el rango de fechas (FECHAFACTURA_ISO):</b></p>'),\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('<h4 style=\"margin-top:20px;border-top:2px solid #FFA726;padding-top:14px;\">Paso B - Ejecutar prorrateo en la base de datos</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">UPDATE en cada SDescargaT matched + UPDATE en SSaldoTem (CANTUSADA, VALORUSADOMN/ME, PESOUSADO, PESOBRUTOUSADO). <b>Corre primero con DRY_RUN activado.</b></p>'),\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('<h4>Resumen Saldos Vencidos (busqueda automatica)</h4>'))\n",
" display(_state['sv2_resumen'])\n",
" display(HTML('<h4>Plan detalle (primeras 50 filas)</h4>'))\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('<div style=\"color:#C62828;\">Corre primero <b>Buscar saldos vencidos</b>.</div>'))\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'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:12px;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:13px;color:#333;\">{path}</span></div>'))\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('<h4 style=\"margin-top:24px;border-top:2px solid #1976D2;padding-top:14px;\">Modo Automatico - sin Excel</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">'\n",
" 'Define el rango de fechas (filtra <b>FECHAFACTURA_ISO</b>) y el sistema busca automaticamente '\n",
" 'los saldos con <b>FECHAVENC_ISO &lt; hoy</b> (vencidos) y SALDO_DISPONIBLE &gt; 0. '\n",
" 'Aplica el mismo prorrateo y los mismos UPDATEs que el modo Excel.</p>'),\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('<h4 style=\"margin-top:18px;border-top:2px solid #FFA726;padding-top:14px;\">Paso B - Ejecutar prorrateo automatico</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Mismos UPDATEs: SDescargaT por descarga + SSaldoTem por saldo. <b>Corre primero con DRY_RUN activado.</b></p>'),\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 (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",
"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",
"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",
" 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",
" W.HTML('<h4 style=\"margin-top:24px;border-top:2px solid #4CAF50;padding-top:14px;\">Saldos vencidos por anio (vista general)</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">'\n",
" '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_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",
"\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'<div style=\"background:#FFEBEE;border:1px solid #C62828;padding:10px;border-radius:6px;\">'\n",
" f'<b style=\"color:#C62828;\">Postgres no disponible.</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{DATASTAGE_MSG}</span><br>'\n",
" f'<small>Revisa que el contenedor postgres-datastage este levantado y que el .env tenga DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD.</small></div>'))\n",
" return False\n",
" display(HTML(f'<div style=\"color:#1B5E20;font-size:12px;\">{DATASTAGE_MSG}</div>'))\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('<h4>Resumen por tabla destino</h4>'))\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('<h4>Resultado por archivo</h4>'))\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('<h3>DataStage - Subir archivos .asc a Postgres</h3>'\n",
" '<p style=\"margin:0 0 10px 0;color:#555;\">'\n",
" 'Carga masiva de archivos .asc del DataStage en sus tablas <b>Registro&lt;NNN&gt;</b>. '\n",
" 'Migrado del modulo PHP <code>HOME/DATASTAGE</code>. La estructura de los archivos '\n",
" 'se infiere del nombre (<code>_NNN.asc</code> &rarr; <code>RegistroNNN</code>); '\n",
" 'el separador es <code>|</code>, el encoding es latin-1, y se omite el header.</p>'),\n",
" W.HTML('<div style=\"background:#E3F2FD;border:1px solid #1565C0;padding:12px;border-radius:6px;margin:8px 0;\">'\n",
" '<b style=\"color:#0D47A1;\">Estructura esperada de la carpeta</b>'\n",
" '<ul style=\"margin:6px 0 0 20px;font-size:13px;color:#333;\">'\n",
" '<li>Layout HONDA: <code>RAIZ/2020/*.asc</code>, <code>RAIZ/2021/*.asc</code>, ... (solo .asc directos, sin entrar a subcarpetas de meses)</li>'\n",
" '<li>Layout plano: <code>RAIZ/*.asc</code></li>'\n",
" '</ul>'\n",
" '<p style=\"margin:6px 0 0 0;font-size:11px;color:#555;\">Tablas destino <b>deben existir previamente</b> en Postgres. Esta pestania no crea tablas.</p>'\n",
" '</div>'),\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('<h4 style=\"margin-top:14px;border-top:2px solid #FFA726;padding-top:10px;\">Log de carga</h4>'),\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('<div style=\"color:#C62828;\"><b>Marca el checkbox de confirmacion antes de ejecutar.</b></div>'))\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('<h4>Resultado del TRUNCATE</h4>'))\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'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:10px;border-radius:6px;margin:6px 0;\">'\n",
" f'<b style=\"color:#1B5E20;\">Total:</b> '\n",
" f'{total_tablas} tablas | {total_con_datos} con datos | '\n",
" f'<b>{total_filas:,}</b> filas en total</div>'))\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'<h4>{tabla} (primeras {limit})</h4>'))\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('<div style=\"color:#C62828;\">No hay datos cargados. Corre \"Ver datos\" primero.</div>'))\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'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:10px;border-radius:6px;margin:6px 0;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{ruta}</span></div>'))\n",
"btn_ds_export.on_click(_on_ds_export)\n",
"\n",
"tab_datastage.children = tuple(list(tab_datastage.children) + [\n",
" W.HTML('<h4 style=\"margin-top:20px;border-top:2px solid #1976D2;padding-top:14px;\">Estadisticas de tablas Registro*</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Cuenta de filas por tabla. Equivalente al modulo PHP <code>datastage.php</code>: '\n",
" 'permite ver totales y explorar el contenido cargado.</p>'),\n",
" W.HBox([btn_ds_stats]), bar_ds_stats,\n",
" out_ds_stats,\n",
" W.HTML('<h5 style=\"margin-top:10px;\">Explorar contenido</h5>'),\n",
" W.HBox([w_ds_tabla_sel, w_ds_limit, btn_ds_ver_datos, btn_ds_export]),\n",
" out_ds_datos,\n",
" W.HTML('<h4 style=\"margin-top:20px;border-top:2px solid #C62828;padding-top:14px;\">Zona peligrosa</h4>'\n",
" '<p style=\"color:#C62828;margin:0 0 8px 0;\"><b>TRUNCATE TABLE</b> en todas las tablas Registro*. '\n",
" 'Elimina TODAS las filas (no se puede deshacer). Util para reiniciar la carga desde cero.</p>'),\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('<div style=\"color:#C62828;\">Corre Generar primero.</div>'))\n",
" return\n",
" ruta = exportar_df_a_excel(_state['ds_cat_df'], 'Estructura_CAT_Pedimentos')\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:8px;border-radius:6px;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{ruta}</span></div>'))\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('<div style=\"color:#C62828;\">Corre Generar primero.</div>'))\n",
" return\n",
" ruta = exportar_df_a_excel(_state['ds_catr_df'], 'Estructura_CAT_Pedimentos_Rectificados')\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:8px;border-radius:6px;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{ruta}</span></div>'))\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('<small style=\"color:#555;\">Tip: copia <b>Patente / Pedimento / SeccionAduanera / Anio</b> a la seccion de abajo para ver el historial recursivo.</small>'))\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('<div style=\"color:#C62828;\">Corre Listar primero.</div>'))\n",
" return\n",
" ruta = exportar_df_a_excel(_state['ds_rect_df'], 'Pedimentos_Rectificados')\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:8px;border-radius:6px;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{ruta}</span></div>'))\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('<h3>Estructuras SCAII - Reportes de Pedimentos</h3>'\n",
" '<p style=\"margin:0 0 10px 0;color:#555;\">Migracion de las opciones del menu PHP <i>Pedimentos</i>: '\n",
" 'CAT Pedimentos, CAT Pedimentos Rectificados y Rastreo de Rectificaciones (con historial recursivo). '\n",
" 'Las consultas se ejecutan sobre Postgres (tablas <code>Registro501</code> y <code>Registro701</code>).</p>'),\n",
" _ds_report_block(\n",
" '<h5>Estructura CAT Pedimentos</h5>'\n",
" '<p style=\"color:#555;font-size:12px;\">Pedimentos de <b>Registro501</b> en el rango, indicando si fueron rectificados.</p>',\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",
" '<h5 style=\"margin-top:12px;\">Estructura CAT Pedimentos Rectificados</h5>'\n",
" '<p style=\"color:#555;font-size:12px;\">Pedimentos rectificados de <b>Registro701</b> en el rango, '\n",
" 'enlazados con el tipo de operacion de Registro501.</p>',\n",
" w_ds_catr_fi, w_ds_catr_ff, btn_ds_catr_run, btn_ds_catr_export, out_ds_catr),\n",
" W.HTML('<h5 style=\"margin-top:12px;\">Rastreo de Rectificaciones</h5>'\n",
" '<p style=\"color:#555;font-size:12px;\">Pedimentos de Registro501 que fueron rectificados, con filtros y opcion de ver la cadena historica.</p>'),\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('<h6 style=\"margin-top:10px;color:#0D47A1;\">Historial recursivo de un pedimento</h6>'\n",
" '<p style=\"color:#555;font-size:11px;\">Equivalente al modal <i>obtener_historial.php</i>.</p>'),\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('<div style=\"color:#C62828;\">Corre Generar primero.</div>')); return\n",
" ruta = exportar_df_a_excel(_state['ds_fimpo_df'], 'Estructura_facturasImpo_501')\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:8px;border-radius:6px;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{ruta}</span></div>'))\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('<div style=\"color:#C62828;\">Corre Generar primero.</div>')); return\n",
" ruta = exportar_df_a_excel(_state['ds_fexpo_df'], 'Estructura_facturasExpo_501')\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:8px;border-radius:6px;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{ruta}</span></div>'))\n",
"btn_ds_fexpo_export.on_click(_on_ds_fexpo_export)\n",
"\n",
"tab_estructuras.children = tuple(list(tab_estructuras.children) + [\n",
" W.HTML('<h4 style=\"margin-top:24px;border-top:2px solid #00838F;padding-top:14px;\">Encabezado de Facturas</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Genera el encabezado de facturas de importacion (TipoOperacion=1) o exportacion '\n",
" '(TipoOperacion=2) excluyendo los pedimentos que ya fueron rectificados.</p>'),\n",
" _ds_report_block(\n",
" '<h5>Encabezado Facturas Importacion</h5>'\n",
" '<p style=\"color:#555;font-size:12px;\">Pedimentos IMPO de <b>Registro501</b> en el rango (excluye rectificados).</p>',\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",
" '<h5 style=\"margin-top:12px;\">Encabezado Facturas Exportacion</h5>'\n",
" '<p style=\"color:#555;font-size:12px;\">Pedimentos EXPO de <b>Registro501</b> en el rango (excluye rectificados).</p>',\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('<div style=\"color:#C62828;\">Corre Generar primero.</div>')); 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'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:8px;border-radius:6px;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b> (celdas con inconsistencia en rojo)<br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{ruta}</span></div>'))\n",
"btn_ds_tc_export.on_click(_on_ds_tc_export)\n",
"\n",
"tab_estructuras.children = tuple(list(tab_estructuras.children) + [\n",
" W.HTML('<h4 style=\"margin-top:24px;border-top:2px solid #EF6C00;padding-top:14px;\">Tipo de Cambio</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Estructura de Tipo de Cambio del Registro501. '\n",
" 'Detecta automaticamente <b>inconsistencias</b>: fechas que tienen mas de un valor distinto '\n",
" 'de TIPO CAMBIO en sus pedimentos (resaltadas en rojo).</p>'),\n",
" _ds_report_block(\n",
" '<h5>Estructura Tipo de Cambio 501</h5>'\n",
" '<p style=\"color:#555;font-size:12px;\">Pedimentos de <b>Registro501</b> en el rango con su Tipo de Cambio.</p>',\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('<div style=\"color:#C62828;\"><b>Marca el checkbox de confirmacion antes de ejecutar.</b></div>')); 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('<div style=\"color:#C62828;\">Corre Generar primero.</div>')); return\n",
" ruta = exportar_df_a_excel(_state['ds_pimpo_df'], 'Estructura_Partidas_Impo')\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:8px;border-radius:6px;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{ruta}</span></div>'))\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('<div style=\"color:#C62828;\">Corre Generar primero.</div>')); return\n",
" ruta = exportar_df_a_excel(_state['ds_pexpo_df'], 'Estructura_Partidas_Expo')\n",
" display(HTML(f'<div style=\"background:#E8F5E9;border:1px solid #66BB6A;padding:8px;border-radius:6px;\">'\n",
" f'<b style=\"color:#1B5E20;\">Excel generado</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;\">{ruta}</span></div>'))\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('<h4 style=\"margin-top:24px;border-top:2px solid #8E24AA;padding-top:14px;\">Catalogo base de NUMPARTES</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Catalogo del cliente con columnas <b>NUMPARTE, DESCRIPCION, UNIDAD DE MEDIDA, FRACCION</b>. '\n",
" 'Se usa para asignar NUMPARTE a las partidas del Registro551 por similitud. La subida hace <b>upsert</b> (acumula).</p>'),\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('<h4 style=\"margin-top:20px;border-top:2px solid #6A1B9A;padding-top:14px;\">Estructura de Partidas</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">Genera la estructura de partidas a partir de <b>Registro551</b>. '\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 <code>MP&lt;F4&gt;-R&lt;NNN&gt;</code>.</p>'),\n",
" W.HTML('<h5>Partidas Importacion</h5>'),\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('<h5 style=\"margin-top:12px;\">Partidas Exportacion</h5>'),\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 = ('<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 (3 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>PEDIMENTO</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">07-3429-4015540</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>VALOR_ME</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">12345.67</td></tr>'\n",
" '<tr><td style=\"padding:4px 10px;border:1px solid #ddd;\"><b>VALOR_MN</b></td><td style=\"padding:4px 10px;border:1px solid #ddd;font-family:Consolas;\">234567.89</td></tr>'\n",
" '</table>'\n",
" '<p style=\"margin:8px 0 0 0;color:#555;font-size:11px;\">Por cada pedimento se buscan sus partidas de exportacion y se prorratean los valores hasta cuadrar al 100%.</p>'\n",
" '</div>')\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'<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_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('<h4>Resumen por pedimento</h4>'))\n",
" display(_state['val_resumen'])\n",
" if 'val_plan' in _state and not _state['val_plan'].empty:\n",
" display(HTML('<h4>Plan detalle (primeras 50 filas)</h4>'))\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('<div style=\"color:#C62828;\">Corre Analizar primero.</div>')); 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'<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_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('<h3>Valores - Ajuste de VALORTOTALME / VALORTOTALMN</h3>'\n",
" '<p style=\"margin:0 0 10px 0;color:#555;\">'\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%.</p>'),\n",
" W.HTML(_html_formato_val),\n",
" btn_plantilla_val,\n",
" W.HTML('<p style=\"margin-top:14px;\"><b>Sube tu Excel:</b></p>'),\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('<h4 style=\"margin-top:20px;border-top:2px solid #FFA726;padding-top:14px;\">Paso B - Ejecutar UPDATE en SPartidasExpo</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">UPDATE VALORTOTALME y VALORTOTALMN por (FACTURAEXPO, LINEA). '\n",
" '<b>Corre primero con DRY_RUN activado.</b></p>'),\n",
" W.HBox([dry_val]),\n",
" W.HTML('<small style=\"color:#555;\">Columnas MN a actualizar:</small>'),\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('<h4>Donde esta cada pedimento</h4>'))\n",
" display(reporte)\n",
" if not resumen_bd.empty:\n",
" display(HTML('<h4>Resumen por base de datos</h4>'))\n",
" display(resumen_bd)\n",
" if faltantes:\n",
" display(HTML('<h4 style=\"color:#C62828;\">No encontrados en ninguna BD</h4>'))\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('<h4 style=\"margin-top:20px;border-top:2px solid #00838F;padding-top:14px;\">Shelter - Buscar pedimentos en otras BDs</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">'\n",
" 'Recorre todas las bases de datos de la instancia SQL Server (las que el usuario '\n",
" 'puede acceder y tienen la tabla <code>SPedimentos.PEDIMENTO</code>) y reporta en '\n",
" 'cual(es) base(s) vive cada pedimento del Excel. <b>Solo es diagnostico no '\n",
" 'modifica nada.</b> Util cuando el cliente tiene varias BDs y no sabes a cual '\n",
" 'apuntar el ajuste.</p>'),\n",
" W.HBox([btn_val_shelter]), bar_val_shelter,\n",
" out_val_shelter,\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",
"\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",
"\n",
"\n",
"# ===== Descargas Uno a Uno (en pestania Descargas) =====\n",
"import datetime as _dt_u1\n",
"w_u1_fi = W.DatePicker(description='Fecha inicio:', value=_dt_u1.date(2019,4,12), layout={'width':'260px'})\n",
"w_u1_ff = W.DatePicker(description='Fecha fin:', value=_dt_u1.date(2026,5,14), layout={'width':'260px'})\n",
"w_u1_modo = W.RadioButtons(options=[('NATURAL','NATURAL'),('DIRIGIDA','DIRIGIDA')],\n",
" value='DIRIGIDA', description='Modo:', layout={'width':'320px'})\n",
"btn_u1_listar = W.Button(description='Listar partidas EXPO', button_style='', icon='list', layout={'width':'220px'})\n",
"btn_u1_analizar = W.Button(description='Analizar', button_style='primary', icon='search', layout={'width':'180px'})\n",
"btn_u1_xlsx = W.Button(description='Exportar Excel', button_style='', icon='file-excel', layout={'width':'200px'})\n",
"dry_u1 = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n",
"btn_u1_ejecutar = W.Button(description='Ejecutar uno a uno (Paso B)', button_style='warning', icon='play', layout={'width':'280px'})\n",
"bar_u1 = _mkbar('Uno a uno')\n",
"out_u1_log = W.Output(layout=OUT_STYLE)\n",
"out_u1_tabla = W.Output(layout=OUT_STYLE)\n",
"\n",
"_html_u1 = ('<div style=\"background:#E3F2FD;border:1px solid #1565C0;padding:12px;border-radius:6px;margin:8px 0;\">'\n",
" '<b style=\"color:#0D47A1;\">Descargas Uno a Uno</b>'\n",
" '<p style=\"margin:6px 0 0 0;color:#333;font-size:12px;\">'\n",
" 'Para empresas que importan y re-exportan el mismo producto sin transformacion. '\n",
" 'Cada partida EXPO descarga el saldo IMPO del MISMO NUMPARTE, con la MISMA fraccion '\n",
" '(SSaldoTem.FRACCIONIMPO = SPartidasExpo.FRACCIONEXPO) y la MISMA unidad de medida. '\n",
" 'Solo consume saldos vencidos (FECHAVENC_ISO &lt; hoy y SALDO_DISPONIBLE &gt; 0). '\n",
" 'Omite facturas con TIPOFACTURA = NODES y respeta CANTEXPO (no sobre-descarga). '\n",
" 'Incluye facturas NA y AC con lineas faltantes.</p></div>')\n",
"\n",
"def _on_u1_listar(_):\n",
" with out_u1_tabla:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if not w_u1_fi.value or not w_u1_ff.value:\n",
" print('Define fecha inicio y fecha fin.'); return\n",
" try:\n",
" detalle, resumen = listar_partidas_expo_uno_a_uno(\n",
" str(w_u1_fi.value), str(w_u1_ff.value))\n",
" if detalle.empty:\n",
" print('No hay partidas EXPO en ese rango.'); return\n",
" print(f'Partidas EXPO (sin NODES): {len(detalle):,}')\n",
" if not resumen.empty:\n",
" display(HTML('<h4>Resumen por ESTATUS</h4>'))\n",
" display(resumen)\n",
" display(HTML('<h4>Detalle (primeras 200 filas)</h4>'))\n",
" display(detalle.head(200))\n",
" except Exception as e:\n",
" print(f'ERROR: {e}')\n",
"btn_u1_listar.on_click(_on_u1_listar)\n",
"\n",
"def _on_u1_analizar(_):\n",
" with out_u1_log:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if not w_u1_fi.value or not w_u1_ff.value:\n",
" print('Define fecha inicio y fecha fin.'); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" df_comp, resumen, incons = analizar_descargas_uno_a_uno(\n",
" str(w_u1_fi.value), str(w_u1_ff.value),\n",
" progress=bar_u1, log=print)\n",
" _state['u1_plan'] = df_comp\n",
" _state['u1_resumen'] = resumen\n",
" _state['u1_incons'] = incons\n",
" with out_u1_tabla:\n",
" clear_output()\n",
" if 'u1_resumen' in _state and not _state['u1_resumen'].empty:\n",
" display(HTML('<h4>Resumen por TIPO</h4>'))\n",
" display(_state['u1_resumen'])\n",
" if 'u1_plan' in _state and not _state['u1_plan'].empty:\n",
" display(HTML('<h4>Plan detalle (primeras 100)</h4>'))\n",
" display(_state['u1_plan'].head(100))\n",
" if 'u1_incons' in _state and not _state['u1_incons'].empty:\n",
" display(HTML('<h4 style=\"color:#C62828;\">Inconsistencias</h4>'))\n",
" display(_state['u1_incons'])\n",
"btn_u1_analizar.on_click(_on_u1_analizar)\n",
"\n",
"def _on_u1_xlsx(_):\n",
" with out_u1_log:\n",
" if 'u1_plan' not in _state:\n",
" display(HTML('<div style=\"color:#C62828;\">Corre Analizar primero.</div>')); return\n",
" path = os.path.join(os.getcwd(), 'plan_descargas_uno_a_uno.xlsx')\n",
" with pd.ExcelWriter(path, engine='openpyxl') as w:\n",
" if not _state['u1_plan'].empty:\n",
" _state['u1_plan'].to_excel(w, sheet_name='Plan_Detalle', index=False)\n",
" if 'u1_resumen' in _state and not _state['u1_resumen'].empty:\n",
" _state['u1_resumen'].to_excel(w, sheet_name='Resumen', index=False)\n",
" if 'u1_incons' in _state and not _state['u1_incons'].empty:\n",
" _state['u1_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_u1_xlsx.on_click(_on_u1_xlsx)\n",
"\n",
"def _on_u1_ejecutar(_):\n",
" with out_u1_log:\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if 'u1_plan' not in _state:\n",
" print('Corre Analizar primero.'); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" ejecutar_descargas_uno_a_uno(_state['u1_plan'], modo_comp=w_u1_modo.value,\n",
" dry_run=dry_u1.value, log=print, progress=bar_u1)\n",
"btn_u1_ejecutar.on_click(_on_u1_ejecutar)\n",
"\n",
"tab_desc.children = tuple(list(tab_desc.children) + [\n",
" W.HTML('<h3 style=\"margin-top:24px;border-top:2px solid #43A047;padding-top:14px;\">Descargas Uno a Uno</h3>'),\n",
" W.HTML(_html_u1),\n",
" W.HBox([w_u1_fi, w_u1_ff]),\n",
" W.HBox([btn_u1_listar, btn_u1_analizar, btn_u1_xlsx]), bar_u1,\n",
" out_u1_log, out_u1_tabla,\n",
" W.HTML('<h4 style=\"margin-top:20px;border-top:2px solid #FFA726;padding-top:14px;\">Paso B - Ejecutar uno a uno</h4>'\n",
" '<p style=\"color:#555;margin:0 0 8px 0;\">NATURAL excluye facturas con faltantes; '\n",
" 'DIRIGIDA inserta lo que se pudo y registra en SDescargaM espejo + APLICADESCMANUAL=S. '\n",
" '<b>Corre primero con DRY_RUN activado.</b></p>'),\n",
" W.HBox([w_u1_modo]), W.HBox([dry_u1]),\n",
" btn_u1_ejecutar,\n",
"])\n",
"\n",
"\n",
"\n",
"\n",
"# ===== Reintegro de descargas =====\n",
"upload_re_inicial = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir lista MP')\n",
"btn_re_plantilla = W.Button(description='Descargar plantilla', button_style='info', icon='download', layout={'width':'220px'})\n",
"w_re_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n",
"w_re_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n",
"btn_re_listar = W.Button(description='Listar partidas EXPO afectadas', button_style='primary', icon='search', layout={'width':'310px'})\n",
"btn_re_export_lista = W.Button(description='Exportar lista a Excel (a llenar)', button_style='', icon='file-excel', layout={'width':'280px'})\n",
"bar_re = _mkbar('Reintegro')\n",
"out_re_log = W.Output(layout=OUT_STYLE)\n",
"out_re_tabla = W.Output(layout=OUT_STYLE)\n",
"\n",
"upload_re_lleno = W.FileUpload(accept='.xlsx,.xls', multiple=False, description='Subir Excel lleno')\n",
"btn_re_analizar = W.Button(description='Analizar reintegro', button_style='primary', icon='check', layout={'width':'240px'})\n",
"btn_re_plan_xlsx = W.Button(description='Exportar plan a Excel', button_style='', icon='file-excel', layout={'width':'240px'})\n",
"chk_re_sdm = W.Checkbox(value=False, description='Ajustar tambien SDescargaM (espejo)', indent=False, layout={'width':'380px'})\n",
"dry_re = W.Checkbox(value=True, description='DRY_RUN (simular sin escribir)')\n",
"btn_re_ejecutar = W.Button(description='Ejecutar reintegro', button_style='warning', icon='play', layout={'width':'260px'})\n",
"out_re_plan = W.Output(layout=OUT_STYLE)\n",
"\n",
"_html_re = ('<div style=\"background:#E3F2FD;border:1px solid #1565C0;padding:12px;border-radius:6px;margin:8px 0;\">'\n",
" '<b style=\"color:#0D47A1;\">Reintegro de descargas</b>'\n",
" '<p style=\"margin:6px 0 0 0;color:#333;font-size:12px;\">'\n",
" 'Reduce la CANTDESC de descargas existentes y devuelve el saldo al IMPO. '\n",
" 'Sube primero un Excel con los <b>NUMPARTE_MP, UNIMED, FRACCION</b> y define '\n",
" 'el rango de fechas (FECHAFACTURA de las facturas EXPO). La utileria lista las '\n",
" 'partidas EXPO afectadas con su CANTDESC actual y una columna vacia para que '\n",
" 'indiques la NUEVA cantidad que debe quedar. La nueva cantidad debe ser '\n",
" '<b>mayor que 0 y menor que la actual</b>. La reduccion se distribuye '\n",
" '<b>proporcionalmente</b> entre las descargas reales que cubren esa partida.'\n",
" '</p></div>')\n",
"\n",
"def _on_re_plantilla(_):\n",
" with out_re_log:\n",
" clear_output()\n",
" bts = generar_plantilla_reintegro()\n",
" path = os.path.join(os.getcwd(), 'plantilla_reintegro_mp.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_re_plantilla.on_click(_on_re_plantilla)\n",
"\n",
"def _on_re_listar(_):\n",
" with out_re_tabla:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if len(upload_re_inicial.value) == 0:\n",
" print('Sube el Excel con los MPs primero.'); return\n",
" if not w_re_fi.value or not w_re_ff.value:\n",
" print('Define rango de fechas.'); return\n",
" try:\n",
" if isinstance(upload_re_inicial.value, dict):\n",
" fname = list(upload_re_inicial.value.keys())[0]\n",
" fb = upload_re_inicial.value[fname]['content']\n",
" else:\n",
" fb = upload_re_inicial.value[0]['content']\n",
" tmp = os.path.join(os.getcwd(), '_upload_re_inicial.xlsx')\n",
" with open(tmp, 'wb') as f: f.write(fb)\n",
" df_mp = cargar_excel_reintegro_inicial(tmp)\n",
" print(f'MPs en Excel: {len(df_mp):,}')\n",
" except Exception as e:\n",
" print(f'ERROR Excel: {e}'); return\n",
" df = listar_partidas_para_reintegro(df_mp, str(w_re_fi.value), str(w_re_ff.value),\n",
" progress=bar_re, log=print)\n",
" _state['re_listado'] = df\n",
" if df.empty:\n",
" print('No hay descargas que coincidan.'); return\n",
" print(f'Descargas afectadas (agrupadas por factura+linea+MP): {len(df):,}')\n",
" display(df.head(200))\n",
"btn_re_listar.on_click(_on_re_listar)\n",
"\n",
"def _on_re_export_lista(_):\n",
" with out_re_log:\n",
" if 're_listado' not in _state or _state['re_listado'].empty:\n",
" display(HTML('<div style=\"color:#C62828;\">Corre \"Listar\" primero.</div>')); return\n",
" path = os.path.join(os.getcwd(), 'lista_reintegro_para_llenar.xlsx')\n",
" exportar_excel_listado_reintegro(_state['re_listado'], 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;\">Lista generada (llena la columna CANTDESC_NUEVA)</b><br>'\n",
" f'<span style=\"font-family:Consolas;font-size:12px;color:#333;\">{path}</span></div>'))\n",
"btn_re_export_lista.on_click(_on_re_export_lista)\n",
"\n",
"def _on_re_analizar(_):\n",
" with out_re_log:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if len(upload_re_lleno.value) == 0:\n",
" print('Sube el Excel lleno primero.'); return\n",
" try:\n",
" if isinstance(upload_re_lleno.value, dict):\n",
" fname = list(upload_re_lleno.value.keys())[0]\n",
" fb = upload_re_lleno.value[fname]['content']\n",
" else:\n",
" fb = upload_re_lleno.value[0]['content']\n",
" tmp = os.path.join(os.getcwd(), '_upload_re_lleno.xlsx')\n",
" with open(tmp, 'wb') as f: f.write(fb)\n",
" df_xls = cargar_excel_reintegro_lleno(tmp)\n",
" print(f'Filas con CANTDESC_NUEVA: {len(df_xls):,}')\n",
" except Exception as e:\n",
" print(f'ERROR Excel: {e}'); return\n",
" plan, incons = analizar_reintegro(df_xls, progress=bar_re, log=print)\n",
" _state['re_plan'] = plan\n",
" _state['re_incons'] = incons\n",
" with out_re_plan:\n",
" clear_output()\n",
" if 're_plan' in _state and not _state['re_plan'].empty:\n",
" display(HTML('<h4>Plan de reintegro (primeras 100)</h4>'))\n",
" display(_state['re_plan'].head(100))\n",
" if 're_incons' in _state and not _state['re_incons'].empty:\n",
" display(HTML('<h4 style=\"color:#C62828;\">Inconsistencias</h4>'))\n",
" display(_state['re_incons'])\n",
"btn_re_analizar.on_click(_on_re_analizar)\n",
"\n",
"def _on_re_plan_xlsx(_):\n",
" with out_re_log:\n",
" if 're_plan' not in _state:\n",
" display(HTML('<div style=\"color:#C62828;\">Corre Analizar primero.</div>')); return\n",
" path = os.path.join(os.getcwd(), 'plan_reintegro.xlsx')\n",
" with pd.ExcelWriter(path, engine='openpyxl') as w:\n",
" if not _state['re_plan'].empty:\n",
" _state['re_plan'].to_excel(w, sheet_name='Plan', index=False)\n",
" if 're_incons' in _state and not _state['re_incons'].empty:\n",
" _state['re_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_re_plan_xlsx.on_click(_on_re_plan_xlsx)\n",
"\n",
"def _on_re_ejecutar(_):\n",
" with out_re_log:\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" if 're_plan' not in _state:\n",
" print('Corre Analizar primero.'); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" ejecutar_reintegro(_state['re_plan'], dry_run=dry_re.value,\n",
" ajustar_sdm=chk_re_sdm.value, progress=bar_re, log=print)\n",
"btn_re_ejecutar.on_click(_on_re_ejecutar)\n",
"\n",
"tab_desc.children = tuple(list(tab_desc.children) + [\n",
" W.HTML('<h3 style=\"margin-top:24px;border-top:2px solid #D32F2F;padding-top:14px;\">Reintegro de descargas</h3>'),\n",
" W.HTML(_html_re),\n",
" btn_re_plantilla,\n",
" W.HTML('<p style=\"margin-top:12px;\"><b>Paso 1 - Sube el Excel con NUMPARTE_MP, UNIMED, FRACCION y define el rango de fechas:</b></p>'),\n",
" W.HBox([upload_re_inicial]),\n",
" W.HBox([w_re_fi, w_re_ff]),\n",
" W.HBox([btn_re_listar, btn_re_export_lista]), bar_re,\n",
" out_re_tabla,\n",
" W.HTML('<p style=\"margin-top:12px;\"><b>Paso 2 - Sube el Excel con la columna CANTDESC_NUEVA llena:</b></p>'),\n",
" W.HBox([upload_re_lleno]),\n",
" W.HBox([btn_re_analizar, btn_re_plan_xlsx]),\n",
" out_re_plan,\n",
" W.HTML('<h4 style=\"margin-top:20px;border-top:2px solid #FFA726;padding-top:14px;\">Paso 3 - Ejecutar reintegro</h4>'),\n",
" W.HBox([chk_re_sdm]),\n",
" W.HBox([dry_re]),\n",
" btn_re_ejecutar,\n",
" out_re_log,\n",
"])\n",
"\n",
"\n",
"\n",
"\n",
"# ===== Diagnostico Paso 12 (KGS): por que no se descargo =====\n",
"w_d12_fi = W.DatePicker(description='Fecha inicio:', layout={'width':'260px'})\n",
"w_d12_ff = W.DatePicker(description='Fecha fin:', layout={'width':'260px'})\n",
"btn_d12_run = W.Button(description='Analizar facturas pendientes', button_style='primary',\n",
" icon='stethoscope', layout={'width':'320px'})\n",
"btn_d12_xlsx = W.Button(description='Exportar diagnostico a Excel', button_style='',\n",
" icon='file-excel', layout={'width':'280px'})\n",
"bar_d12 = _mkbar('Diagnostico')\n",
"out_d12_log = W.Output(layout=OUT_STYLE)\n",
"out_d12_tabla = W.Output(layout=OUT_STYLE)\n",
"\n",
"_html_d12 = ('<div style=\"background:#E3F2FD;border:1px solid #1565C0;padding:12px;border-radius:6px;margin:8px 0;\">'\n",
" '<b style=\"color:#0D47A1;\">Diagnostico Paso 12 (% KGS)</b>'\n",
" '<p style=\"margin:6px 0 0 0;color:#333;font-size:12px;\">'\n",
" 'Revisa las facturas EXPO pendientes (estatus NA) y explica por que el Paso 12 '\n",
" 'no las pudo descargar. Motivos posibles: TIPOFACTURA NODES, SIN_PARTIDAS, '\n",
" 'PT_SIN_BOM (producto sin receta), PESONETO_CERO (la partida no tiene peso), '\n",
" 'COMPONENTE_SIN_SALDO (no hay saldo IMPO suficiente). Es solo lectura: '\n",
" 'no escribe en la base.</p></div>')\n",
"\n",
"def _on_d12_run(_):\n",
" with out_d12_log:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" fi = str(w_d12_fi.value) if w_d12_fi.value else None\n",
" ff = str(w_d12_ff.value) if w_d12_ff.value else None\n",
" df_bloq, df_comp = diagnosticar_paso12_pendientes(\n",
" fecha_ini=fi, fecha_fin=ff, progress=bar_d12, log=print)\n",
" _state['d12_bloq'] = df_bloq\n",
" _state['d12_comp'] = df_comp\n",
" with out_d12_tabla:\n",
" clear_output()\n",
" if 'd12_bloq' in _state and not _state['d12_bloq'].empty:\n",
" display(HTML('<h4>Resumen por factura (motivo principal)</h4>'))\n",
" display(_state['d12_bloq'].head(200))\n",
" if 'd12_comp' in _state and not _state['d12_comp'].empty:\n",
" display(HTML('<h4>Detalle por componente faltante (primeras 200)</h4>'))\n",
" display(_state['d12_comp'].head(200))\n",
"btn_d12_run.on_click(_on_d12_run)\n",
"\n",
"def _on_d12_xlsx(_):\n",
" with out_d12_log:\n",
" if 'd12_bloq' not in _state:\n",
" display(HTML('<div style=\"color:#C62828;\">Corre Analizar primero.</div>')); return\n",
" path = os.path.join(os.getcwd(), 'diagnostico_paso12.xlsx')\n",
" exportar_excel_diagnostico_paso12(_state.get('d12_bloq'), _state.get('d12_comp'), 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_d12_xlsx.on_click(_on_d12_xlsx)\n",
"\n",
"tab_kg.children = tuple(list(tab_kg.children) + [\n",
" W.HTML('<h3 style=\"margin-top:24px;border-top:2px solid #00838F;padding-top:14px;\">Diagnostico de facturas pendientes</h3>'),\n",
" W.HTML(_html_d12),\n",
" W.HBox([w_d12_fi, w_d12_ff]),\n",
" W.HBox([btn_d12_run, btn_d12_xlsx]), bar_d12,\n",
" out_d12_log, out_d12_tabla,\n",
"])\n",
"\n",
"\n",
"# ----- Tab BOM: Analisis de composicion + sustitutos + saldos -----\n",
"out_bom_log = W.Output(layout=OUT_STYLE)\n",
"out_bom_resumen= W.Output()\n",
"out_bom_tabla = W.Output()\n",
"bar_bom = _mkbar('BOM')\n",
"tx_bom_pt = W.Text(value='', placeholder='filtro NUMPARTE PT (contiene)', layout={'width':'260px'})\n",
"tx_bom_comp = W.Text(value='', placeholder='filtro COMPONENTE (contiene)', layout={'width':'260px'})\n",
"chk_bom_prob = W.Checkbox(value=False, description='Solo con problemas (sin saldo vigente)')\n",
"btn_bom = W.Button(description='Cargar analisis BOM', button_style='primary', icon='sitemap', layout={'width':'260px'})\n",
"btn_bom_filtrar= W.Button(description='Aplicar filtros', icon='filter', layout={'width':'200px'})\n",
"btn_bom_xlsx = W.Button(description='Exportar a Excel', button_style='success', icon='file-excel-o', layout={'width':'200px'})\n",
"\n",
"def _bom_render_tabla(df):\n",
" with out_bom_tabla:\n",
" clear_output()\n",
" if df is None or df.empty:\n",
" print('Sin resultados.'); return\n",
" display(HTML(f'<p><b>{len(df):,}</b> filas (PT-componente)</p>'))\n",
" display(_fmt_an_df(df.head(500)))\n",
" if len(df) > 500:\n",
" display(HTML(f'<p><i>(mostrando primeras 500 filas de {len(df):,} -- exporta a Excel para ver todas)</i></p>'))\n",
"\n",
"def _on_bom(_):\n",
" with out_bom_log:\n",
" clear_output()\n",
" if not CONEXION_OK: print(CONEXION_MSG); return\n",
" print(f'(DB actual: {DB_ACTUAL})')\n",
" t0 = time.time()\n",
" df = analizar_bom_completo(progress=bar_bom)\n",
" _state['bom_full'] = df\n",
" print(f'Tiempo total: {time.time()-t0:.1f}s')\n",
" with out_bom_resumen:\n",
" clear_output()\n",
" display(HTML('<h4>Resumen por estatus de cobertura</h4>'))\n",
" res = resumen_bom(df)\n",
" if not res.empty: display(_fmt_an_df(res))\n",
" _bom_render_tabla(df)\n",
"\n",
"def _on_bom_filtrar(_):\n",
" with out_bom_log:\n",
" if 'bom_full' not in _state:\n",
" print('Corre primero \"Cargar analisis BOM\".'); return\n",
" df = filtrar_bom(_state['bom_full'],\n",
" pt_filtro=tx_bom_pt.value.strip(),\n",
" comp_filtro=tx_bom_comp.value.strip(),\n",
" solo_problemas=chk_bom_prob.value)\n",
" print(f'Filtro aplicado: {len(df):,} filas')\n",
" _bom_render_tabla(df)\n",
"\n",
"def _on_bom_xlsx(_):\n",
" with out_bom_log:\n",
" if 'bom_full' not in _state:\n",
" print('Corre primero \"Cargar analisis BOM\".'); return\n",
" df = filtrar_bom(_state['bom_full'],\n",
" pt_filtro=tx_bom_pt.value.strip(),\n",
" comp_filtro=tx_bom_comp.value.strip(),\n",
" solo_problemas=chk_bom_prob.value)\n",
" path = exportar_excel_bom(df, 'Analisis_BOM.xlsx')\n",
" print(f'Excel: {path}')\n",
"\n",
"btn_bom.on_click(_on_bom)\n",
"btn_bom_filtrar.on_click(_on_bom_filtrar)\n",
"btn_bom_xlsx.on_click(_on_bom_xlsx)\n",
"\n",
"tab_bom = W.VBox([\n",
" W.HTML('<div style=\"padding:8px 12px;border-top:2px solid #6A1B9A;'\n",
" 'background:#F3E5F5;margin-bottom:6px;\">'\n",
" '<h3 style=\"margin:0;color:#4A148C;\">Analisis de BOM (composicion + sustitutos + saldos)</h3>'\n",
" '<p style=\"margin:4px 0 0 0;color:#4A148C;\">Cruza SMatBOM con SPartesSustitutos y SSaldoTem '\n",
" 'para mostrar, por cada producto terminado, sus componentes, sustitutos disponibles y '\n",
" 'cobertura (vigente vs vencido). Util para entender por que una factura EXPO no descarga.</p>'\n",
" '</div>'),\n",
" W.HBox([btn_bom, bar_bom]),\n",
" W.HTML('<hr>'),\n",
" W.HTML('<b>Filtros:</b>'),\n",
" W.HBox([tx_bom_pt, tx_bom_comp, chk_bom_prob]),\n",
" W.HBox([btn_bom_filtrar, btn_bom_xlsx]),\n",
" out_bom_log,\n",
" out_bom_resumen,\n",
" out_bom_tabla,\n",
"])\n",
"\n",
"tabs = W.Tab(children=[tab_conn, tab_desc, tab_an, tab_nlp, tab_kg, tab_bom, 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, 'BOM')\n",
"tabs.set_title(6, 'CTM')\n",
"tabs.set_title(7, 'Saldos Vencidos')\n",
"tabs.set_title(8, 'DataStage')\n",
"tabs.set_title(9, 'Estructuras SCAII')\n",
"tabs.set_title(10, '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
}