{ "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, PESONETO, PESOBRUTO, PAISMERCANCIA, FECHADESC, TIPOFRACCION,\n", " NUMPARTE, CANTDESC, UNIMED, LINEAEXPO, PARTEORIGINAL, PORUTILERIA, TIPOMATEXPO,\n", " MONTOIGI, ADVALOREMIMPO, TIPODESC)\n", " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\"\"\"\n", "UPDATE_FACEXP_SQL = \"UPDATE SFacExp SET ESTATUS='AC' WHERE FACTURAEXPO=?\"\n", "UPDATE_SALDO_SQL = \"\"\"UPDATE SSaldoTem\n", " SET CANTUSADA=ISNULL(CANTUSADA,0)+?, VALORUSADOMN=ISNULL(VALORUSADOMN,0)+?,\n", " VALORUSADOME=ISNULL(VALORUSADOME,0)+?, PESOUSADO=ISNULL(PESOUSADO,0)+?,\n", " PESOBRUTOUSADO=ISNULL(PESOBRUTOUSADO,0)+?\n", " WHERE FACTURAIMPO=? AND NUMPARTE=? AND UMEXITENCIA=?\"\"\"\n", "\n", "def _build_descarga_df(modo_calc='STANDARD', facturas_df=None, partidas_df=None, prev_dict=None, progress=None):\n", " prog = _Progress(progress)\n", " _load_catalogos()\n", " saldos = _load_saldos_snapshot()\n", " f_use = facturas_df if facturas_df is not None else _state['df_facturas_NA']\n", " p_use = partidas_df if partidas_df is not None else _state['df_partidas_all']\n", " b_all, s_all, pp_all = _state['df_boms_all'], _state['df_sustitutos_all'], _state['df_partepais_all']\n", " partidas_por_factura = p_use.groupby('FACTURAEXPO')\n", " boms_por_pt = b_all.groupby('PT')\n", " sustitutos_por_comp = s_all.groupby('NUMPARTE')\n", " partida_montoigi = p_use.set_index(['FACTURAEXPO','LINEA'])['MONTOIGIME'].to_dict()\n", " partepais_dict = pp_all.drop_duplicates(subset=['FRACCION','PAIS'], keep='last').set_index(['FRACCION','PAIS'])[['TIPOFRACCION','TASAIM']].to_dict('index')\n", " prog.setup(len(f_use), 'Procesando facturas')\n", " all_rows = []\n", " for i, (_, f) in enumerate(f_use.iterrows()):\n", " factura, fecha_export, pedimento_ex = f['FACTURAEXPO'], f['FECHAFACTURA_ISO'], f.get('PEDIMENTOEXPO')\n", " if factura not in partidas_por_factura.groups:\n", " prog.step(); continue\n", " for _, p in partidas_por_factura.get_group(factura).iterrows():\n", " if p['PT'] not in boms_por_pt.groups: continue\n", " linea = int(p['LINEA']) if pd.notna(p['LINEA']) else 0\n", " montoigi = partida_montoigi.get((factura, p['LINEA']))\n", " peso_neto = float(p.get('PESONETO', 0) or 0)\n", " for _, c in boms_por_pt.get_group(p['PT']).iterrows():\n", " comp = c['COMPONENTE_MP']\n", " unimed_bom = (c['UNIMED'] or '').strip().upper()\n", " if modo_calc == 'KG_PCT':\n", " cant_req_total = (float(c['CANTIDAD']) / 100.0) * peso_neto\n", " else:\n", " cant_req_total = float(p['CANTEXPO']) * float(c['CANTIDAD'])\n", " cant_prev = 0.0\n", " if prev_dict is not None:\n", " cant_prev = float(prev_dict.get((factura, linea, comp), 0.0))\n", " cant_pend = cant_req_total - cant_prev\n", " if prev_dict is not None and cant_pend <= 1e-9: continue\n", " cant_req = cant_pend if prev_dict is not None else cant_req_total\n", " base_row = {'FACTURAEXPO': factura, 'FECHA_FACTURAEXPO': fecha_export, 'PEDIMENTOEXPO': pedimento_ex,\n", " 'LINEA': linea, 'PT': p['PT'], 'CANT_PT': float(p['CANTEXPO']),\n", " 'PESONETO_PARTIDA': peso_neto, 'PCT_BOM': float(c['CANTIDAD']),\n", " 'MONTOIGI': montoigi, 'COMPONENTE_MP': comp, 'UNIMED_BOM': unimed_bom,\n", " 'CANT_REQUERIDA': cant_req, 'CANT_PREV': cant_prev}\n", " faltante = cant_req\n", " faltante, r = _consumir(saldos, _saldos_idx(saldos, comp, unimed_bom, fecha_export), faltante, base_row, 'ORIGINAL', comp)\n", " all_rows.extend(r)\n", " if faltante > 1e-9 and comp in sustitutos_por_comp.groups:\n", " for _, sub in sustitutos_por_comp.get_group(comp).iterrows():\n", " if faltante <= 1e-9: break\n", " um_keys = [str(u).strip().upper() for u in [sub.get('UNIDADMEDIDA1'), sub.get('UNIDADMEDIDA2')] if u and str(u).strip()]\n", " if not um_keys: continue\n", " faltante, r = _consumir(saldos, _saldos_idx(saldos, sub['COMPONENTE_ALTERNO'], um_keys, fecha_export),\n", " faltante, base_row, 'SUSTITUTO', sub['COMPONENTE_ALTERNO'])\n", " all_rows.extend(r)\n", " if faltante > 1e-9:\n", " all_rows.append({**base_row, 'NUMPARTE_USADO': None, 'TIPO': 'FALTANTE',\n", " 'FACTURAIMPO_SALDO': None, 'PEDIMENTOIMPO': None, 'CLASE': None,\n", " 'PAISMERCANCIA': None, 'FRACCION_SALDO': None, 'UNIMED_SALDO': None,\n", " 'FECHA_ENTRADA': None, 'FECHA_EXPIRACION': None, 'CANT_LOTE_ORIG': None,\n", " 'CANT_DESCARGADA': faltante, 'VALORMN': None, 'VALORME': None, 'PESONETO_DESC': None,\n", " 'STATUS': 'FALTANTE'})\n", " prog.step()\n", " df = pd.DataFrame(all_rows)\n", " if not df.empty:\n", " def _pp(row):\n", " info = partepais_dict.get((row['FRACCION_SALDO'], row['PAISMERCANCIA']))\n", " if info is None: return pd.Series([None, None])\n", " return pd.Series([info.get('TIPOFRACCION'), info.get('TASAIM')])\n", " df[['TIPOFRACCION','ADVALOREMIMPO']] = df.apply(_pp, axis=1)\n", " df['FECHADESC_CLARION'] = df['FECHA_FACTURAEXPO'].apply(to_clarion)\n", " prog.done('Pronostico listo')\n", " return df\n", "\n", "def _build_cobertura(df):\n", " if df is None or df.empty or 'CANT_DESCARGADA' not in df.columns:\n", " return pd.DataFrame(columns=['FACTURAEXPO','FECHA_FACTURAEXPO',\n", " 'componentes_total','componentes_100pct','pct_promedio'])\n", " df_d = df.copy()\n", " df_d['cant_cubierta'] = df_d['CANT_DESCARGADA'].where(df_d['TIPO'].isin(['ORIGINAL','SUSTITUTO']), 0)\n", " cob_comp = df_d.groupby(['FACTURAEXPO','FECHA_FACTURAEXPO','LINEA','PT','COMPONENTE_MP','UNIMED_BOM','CANT_REQUERIDA'],\n", " as_index=False, dropna=False).agg(cant_cubierta=('cant_cubierta','sum'))\n", " cob_comp['pct'] = ((cob_comp['cant_cubierta'] / cob_comp['CANT_REQUERIDA']).fillna(0) * 100).round(2).clip(upper=100)\n", " cob_fact = cob_comp.groupby(['FACTURAEXPO','FECHA_FACTURAEXPO'], as_index=False, dropna=False).agg(\n", " componentes_total=('COMPONENTE_MP','count'),\n", " componentes_100pct=('pct', lambda s: (s >= 99.99).sum()),\n", " pct_promedio=('pct','mean'))\n", " cob_fact['pct_promedio'] = cob_fact['pct_promedio'].round(2)\n", " return cob_fact\n", "\n", "def calcular_pronostico_paso8(progress=None):\n", " df = _build_descarga_df(modo_calc='STANDARD', progress=progress)\n", " cob = _build_cobertura(df)\n", " _state['df_descarga_all'] = df\n", " _state['cobertura_factura'] = cob\n", " return df, cob\n", "\n", "def calcular_pronostico_paso12_kg(progress=None):\n", " df = _build_descarga_df(modo_calc='KG_PCT', progress=progress)\n", " cob = _build_cobertura(df)\n", " _state['df_descarga_kg'] = df\n", " _state['cobertura_factura_kg']= cob\n", " return df, cob\n", "\n", "def _do_inserts(df_ins, dry_run, log, do_update_status=True, progress=None):\n", " prog = _Progress(progress)\n", " if df_ins.empty:\n", " log('Nada que insertar.'); prog.done('Nada que insertar'); return 0, 0, 0, []\n", " with scaii_conn.cursor() as cur:\n", " cur.execute(\"SELECT ISNULL(MAX(CONSECUTIVO),0) FROM SDescargaT\")\n", " next_consec = int(cur.fetchone()[0]) + 1\n", " log(f'Proximo CONSECUTIVO: {next_consec}')\n", " grupos = list(df_ins.groupby('FACTURAEXPO'))\n", " prog.setup(len(grupos), 'Insertando facturas')\n", " inserted, updated, saldos_upd, errores = 0, 0, 0, []\n", " for factura, df_f in grupos:\n", " cb, ib, sb = next_consec, inserted, saldos_upd\n", " ok = True\n", " try:\n", " with scaii_conn.cursor() as cur:\n", " for _, r in df_f.iterrows():\n", " if not dry_run:\n", " cur.execute(INSERT_DESC_SQL, (\n", " next_consec, r['FACTURAEXPO'], r['FACTURAEXPO'],\n", " r['FACTURAIMPO_SALDO'], _s(r['PEDIMENTOIMPO']),\n", " _s(r['PEDIMENTOEXPO']), _s(r['CLASE']),\n", " _f(r['VALORMN']), _f(r['VALORME']),\n", " _f(r['PESONETO_DESC']), _f(r['PESONETO_DESC']),\n", " _s(r['PAISMERCANCIA']),\n", " int(r['FECHADESC_CLARION']) if pd.notna(r['FECHADESC_CLARION']) else None,\n", " _s(r['TIPOFRACCION']), r['NUMPARTE_USADO'],\n", " float(r['CANT_DESCARGADA']),\n", " r['UNIMED_SALDO'] if pd.notna(r['UNIMED_SALDO']) else r['UNIMED_BOM'],\n", " int(r['LINEA']) if pd.notna(r['LINEA']) else 0,\n", " r['COMPONENTE_MP'], 1, '',\n", " _f(r['MONTOIGI']), _adv(r['ADVALOREMIMPO']), '0 PARTE'))\n", " cur.execute(UPDATE_SALDO_SQL, (\n", " float(r['CANT_DESCARGADA']),\n", " _f0(r['VALORMN']), _f0(r['VALORME']),\n", " _f0(r['PESONETO_DESC']), _f0(r['PESONETO_DESC']),\n", " r['FACTURAIMPO_SALDO'], r['NUMPARTE_USADO'],\n", " r['UNIMED_SALDO'] if pd.notna(r['UNIMED_SALDO']) else r['UNIMED_BOM']))\n", " saldos_upd += 1\n", " next_consec += 1\n", " inserted += 1\n", " if not dry_run and do_update_status:\n", " cur.execute(UPDATE_FACEXP_SQL, (factura,))\n", " if not dry_run: scaii_conn.commit()\n", " except Exception as e:\n", " ok = False\n", " if not dry_run: scaii_conn.rollback()\n", " next_consec, inserted, saldos_upd = cb, ib, sb\n", " errores.append((factura, str(e)))\n", " if ok and do_update_status: updated += 1\n", " prog.step()\n", " prog.done(f'{inserted} insertados')\n", " return inserted, updated, saldos_upd, errores\n", "\n", "def _ejecutar_descarga_NA(modo, dry_run, fecha_desde, fecha_hasta, log, key_df, key_cob, paso_label, progress=None):\n", " if key_df not in _state:\n", " log(f'ERROR: corre primero el pronostico del {paso_label}.'); return\n", " df_da, cob = _state[key_df], _state[key_cob]\n", " if modo == 'NATURAL':\n", " elig = cob[cob['componentes_100pct'] == cob['componentes_total']]['FACTURAEXPO'].tolist()\n", " else:\n", " elig = cob['FACTURAEXPO'].tolist()\n", " if fecha_desde or fecha_hasta:\n", " df_cf = cob.copy()\n", " df_cf['_f'] = pd.to_datetime(df_cf['FECHA_FACTURAEXPO'], errors='coerce')\n", " if fecha_desde: df_cf = df_cf[df_cf['_f'] >= pd.to_datetime(fecha_desde)]\n", " if fecha_hasta: df_cf = df_cf[df_cf['_f'] <= pd.to_datetime(fecha_hasta)]\n", " rango = set(df_cf['FACTURAEXPO']); elig = [f for f in elig if f in rango]\n", " df_na = pd.read_sql(\"SELECT FACTURAEXPO FROM SFacExp WHERE ESTATUS='NA'\", scaii_conn)\n", " set_na = set(df_na['FACTURAEXPO'].astype(str).str.strip())\n", " omitidas = [f for f in elig if str(f).strip() not in set_na]\n", " elig = [f for f in elig if str(f).strip() in set_na]\n", " if omitidas: log(f' Omitidas (ya AC): {len(omitidas)}')\n", " log(f'Modo: {modo} | DRY_RUN: {dry_run} | Facturas elegibles: {len(elig)}')\n", " mask = df_da['FACTURAEXPO'].isin(elig) & df_da['TIPO'].isin(['ORIGINAL','SUSTITUTO'])\n", " df_ins = df_da[mask].copy()\n", " log(f'Filas a insertar: {len(df_ins)}')\n", " inserted, updated, saldos_upd, errores = _do_inserts(df_ins, dry_run, log, do_update_status=True, progress=progress)\n", " log(f'\\n=== RESUMEN {paso_label} ({modo}, DRY_RUN={dry_run}) ===')\n", " log(f' Insertados : {inserted}')\n", " log(f' Updates SSaldoTem : {saldos_upd}')\n", " log(f' Facturas a AC : {updated}')\n", " log(f' Errores : {len(errores)}')\n", " for f, e in errores[:5]: log(f' {f}: {e}')\n", "\n", "def ejecutar_paso9(modo, dry_run, fecha_desde=None, fecha_hasta=None, log=print, progress=None):\n", " _ejecutar_descarga_NA(modo, dry_run, fecha_desde, fecha_hasta, log,\n", " 'df_descarga_all', 'cobertura_factura', 'paso 9', progress=progress)\n", "\n", "def ejecutar_paso12_kg(modo, dry_run, fecha_desde=None, fecha_hasta=None, log=print, progress=None):\n", " _ejecutar_descarga_NA(modo, dry_run, fecha_desde, fecha_hasta, log,\n", " 'df_descarga_kg', 'cobertura_factura_kg', 'paso 12 (% KGS)', progress=progress)\n", "\n", "def _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, modo_calc, paso_label, progress=None):\n", " prog = _Progress(progress)\n", " prog.setup(1, 'Cargando facturas AC...')\n", " sql_fact = \"SELECT FACTURAEXPO, PEDIMENTOEXPO, FECHAFACTURA_ISO FROM SFacExp WHERE ESTATUS='AC'\"\n", " params = []\n", " if facturas_objetivo:\n", " sql_fact += f\" AND FACTURAEXPO IN ({','.join(['?']*len(facturas_objetivo))})\"\n", " params.extend(list(facturas_objetivo))\n", " if fecha_desde:\n", " sql_fact += \" AND FECHAFACTURA_ISO >= ?\"; params.append(fecha_desde)\n", " if fecha_hasta:\n", " sql_fact += \" AND FECHAFACTURA_ISO <= ?\"; params.append(fecha_hasta)\n", " sql_fact += \" ORDER BY FECHAFACTURA_ISO\"\n", " df_fact_ac = pd.read_sql(sql_fact, scaii_conn, params=params)\n", " log(f'Facturas AC: {len(df_fact_ac)}')\n", " if df_fact_ac.empty: prog.done('Sin facturas'); return\n", " factura_list = df_fact_ac['FACTURAEXPO'].tolist()\n", " prev_parts = []\n", " for i in range(0, len(factura_list), 500):\n", " chunk = factura_list[i:i+500]\n", " ph = ','.join(['?']*len(chunk))\n", " prev_parts.append(pd.read_sql(f\"\"\"\n", " SELECT FACTEXPO AS FACTURAEXPO, LINEAEXPO AS LINEA, PARTEORIGINAL AS COMPONENTE_MP,\n", " SUM(CANTDESC) AS CANT_PREV\n", " FROM SDescargaT WHERE FACTEXPO IN ({ph})\n", " GROUP BY FACTEXPO, LINEAEXPO, PARTEORIGINAL\n", " \"\"\", scaii_conn, params=chunk))\n", " df_desc_prev = pd.concat(prev_parts) if prev_parts else pd.DataFrame(columns=['FACTURAEXPO','LINEA','COMPONENTE_MP','CANT_PREV'])\n", " df_desc_prev['LINEA'] = df_desc_prev['LINEA'].astype(int)\n", " prev_dict = df_desc_prev.set_index(['FACTURAEXPO','LINEA','COMPONENTE_MP'])['CANT_PREV'].to_dict()\n", " _load_catalogos()\n", " df_comp = _build_descarga_df(modo_calc=modo_calc, facturas_df=df_fact_ac, prev_dict=prev_dict, progress=progress)\n", " log(f'Filas calculadas: {len(df_comp)}')\n", " if df_comp.empty: return\n", " if modo_comp == 'NATURAL':\n", " bad = set(df_comp[df_comp['TIPO']=='FALTANTE']['FACTURAEXPO'])\n", " df_to_ins = df_comp[~df_comp['FACTURAEXPO'].isin(bad) & df_comp['TIPO'].isin(['ORIGINAL','SUSTITUTO'])]\n", " log(f'Excluidas en NATURAL (con faltante): {len(bad)}')\n", " else:\n", " df_to_ins = df_comp[df_comp['TIPO'].isin(['ORIGINAL','SUSTITUTO'])]\n", " log(f'Filas a insertar: {len(df_to_ins)}')\n", " inserted, _, saldos_upd, errores = _do_inserts(df_to_ins, dry_run, log, do_update_status=False, progress=progress)\n", " log(f'\\n=== RESUMEN {paso_label} ({modo_comp}, DRY_RUN={dry_run}) ===')\n", " log(f' Insertados : {inserted}')\n", " log(f' Updates SSaldoTem : {saldos_upd}')\n", " log(f' Errores : {len(errores)}')\n", " for f, e in errores[:5]: log(f' {f}: {e}')\n", "\n", "def ejecutar_paso10(modo_comp, dry_run, fecha_desde=None, fecha_hasta=None, facturas_objetivo=None, log=print, progress=None):\n", " _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, 'STANDARD', 'paso 10', progress=progress)\n", "\n", "def ejecutar_paso12_complementaria_kg(modo_comp, dry_run, fecha_desde=None, fecha_hasta=None, facturas_objetivo=None, log=print, progress=None):\n", " _ejecutar_complementaria(modo_comp, dry_run, fecha_desde, fecha_hasta, facturas_objetivo, log, 'KG_PCT', 'paso 12 complementaria (% KGS)', progress=progress)\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", "# 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 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", " cur.execute(INSERT_M,\n", " next_m, consec_factura_ctm, factura_ctm, linea_ctm,\n", " numparte_pt_ctm, r.CLASE, m_cantidad, r.UNIMED,\n", " r.FACTIMPO, r.PARTEORIGINAL,\n", " m_vmn, m_vme, r.PAISMERCANCIA, m_pneto, m_pbruto,\n", " r.TIPOFRACCION, r.SECTOR,\n", " '', 'TEM', r.FACTIMPO, 'N', r.ORDENVENTA, '')\n", " next_m += 1; ins_m += 1\n", "\n", " if not dry_run:\n", " cur.execute(UPDATE_SFACEXP, factura_ctm)\n", " stat += 1\n", " if not dry_run:\n", " scaii_conn.commit()\n", " except Exception as e:\n", " if not dry_run: scaii_conn.rollback()\n", " errores.append((factura_ctm, str(e)))\n", " log(f' ERROR {factura_ctm}: {e}')\n", " prog.step()\n", "\n", " prog.done(f'{ins_m} mirror ops')\n", " log(f'\\n=== RESUMEN Paso B ({modo}, DRY_RUN={dry_run}) ===')\n", " log(f' SDescargaM insertadas (espejo) : {ins_m:,}')\n", " log(f' Facturas CTM -> ESTATUS=AC + APLICADESCMANUAL=S + CANT_PARTIDAS: {stat:,}')\n", " log(f' SDescargaT NO se modifica (solo se usa como referencia)')\n", " log(f' Errores : {len(errores):,}')\n", " for f, e in errores[:5]:\n", " log(f' {f}: {e}')\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "id": "logic-saldos-vencidos", "metadata": {}, "outputs": [], "source": [ "# Fix completo para SaldosVencidos: usa PK de 6 campos en SSaldoTem\n", "# y match por PAISMERCANCIA en SDescargaT.\n", "\n", "# Las funciones aqui REEMPLAZAN las de la celda logic-saldos-vencidos.\n", "\n", "import io as _io_sv\n", "import datetime as _dt_sv\n", "\n", "def cargar_excel_saldos_vencidos(path):\n", " \"\"\"Lee Excel con 3 columnas: FACTURAIMPO, CANTIDAD_SALDO, FRACCION_IMPO.\"\"\"\n", " df = pd.read_excel(path, dtype=str)\n", " norm = {c: c.strip().upper().replace(' ', '_') for c in df.columns}\n", " df = df.rename(columns=norm)\n", " aliases = {\n", " 'FACTURAIMPO': ['FACTURAIMPO', 'FACTURA_IMPO', 'FACTURA'],\n", " 'CANTIDAD_SALDO': ['CANTIDAD_SALDO', 'CANT_SALDO', 'CANTIDAD', 'SALDO'],\n", " 'FRACCION_IMPO': ['FRACCION_IMPO', 'FRACCIONIMPO', 'FRACCION'],\n", " }\n", " out = {}\n", " for std, opts in aliases.items():\n", " for o in opts:\n", " if o in df.columns:\n", " out[std] = df[o]; break\n", " if std not in out:\n", " raise ValueError(f'Falta la columna {std} (acepta: {opts})')\n", " df2 = pd.DataFrame(out)\n", " df2['FACTURAIMPO'] = df2['FACTURAIMPO'].astype(str).str.strip()\n", " df2['FRACCION_IMPO'] = df2['FRACCION_IMPO'].astype(str).str.strip()\n", " df2['CANTIDAD_SALDO'] = pd.to_numeric(df2['CANTIDAD_SALDO'], errors='coerce').fillna(0)\n", " df2 = df2[df2['CANTIDAD_SALDO'] > 0]\n", " return df2.reset_index(drop=True)\n", "\n", "\n", "def generar_plantilla_excel_saldos_vencidos():\n", " df = pd.DataFrame([\n", " {'FACTURAIMPO': 'F1234567', 'CANTIDAD_SALDO': 100.0, 'FRACCION_IMPO': '85044010'},\n", " {'FACTURAIMPO': 'F1234568', 'CANTIDAD_SALDO': 50.5, 'FRACCION_IMPO': '85044010'},\n", " {'FACTURAIMPO': 'F1234569', 'CANTIDAD_SALDO': 25.0, 'FRACCION_IMPO': '73181500'},\n", " ])\n", " buf = _io_sv.BytesIO()\n", " with pd.ExcelWriter(buf, engine='openpyxl') as w:\n", " df.to_excel(w, sheet_name='SaldosVencidos', index=False)\n", " buf.seek(0)\n", " return buf.read()\n", "\n", "\n", "# PK de 6 campos para identificar univocamente una fila de SSaldoTem\n", "_PK_SALDO = ['FACTURAIMPO', 'PEDIMENTOIMPO', 'FRACCIONIMPO', 'NUMPARTE', 'UMEXITENCIA', 'PAISORIGEN']\n", "\n", "# Llave de match con SDescargaT (las descargas no traen PEDIMENTOIMPO/FRACCIONIMPO\n", "# necesariamente alineados con SSaldoTem; matchamos por los 4 campos que SI son comparables).\n", "_MATCH_DESC = ['FACTIMPO', 'NUMPARTE', 'UNIMED', 'PAISMERCANCIA']\n", "\n", "\n", "def _enriquecer_saldos_con_tasas(df):\n", " if df.empty: return df\n", " if 'CANTIDAD_SALDO' in df.columns:\n", " df['SALDO_APLICABLE'] = df[['CANTIDAD_SALDO', 'SALDO_DISPONIBLE']].min(axis=1)\n", " else:\n", " df['SALDO_APLICABLE'] = df['SALDO_DISPONIBLE']\n", " denom = df['CANTEXITENCIA'].replace(0, np.nan)\n", " df['TASA_VMN'] = (df['VALORIMPOMN'] / denom).fillna(0)\n", " df['TASA_VME'] = (df['VALORIMPOME'] / denom).fillna(0)\n", " df['TASA_PNETO'] = (df['PESONETO'] / denom).fillna(0)\n", " df['TASA_PBRUTO'] = (df['PESOBRUTO'] / denom).fillna(0)\n", " # Normalizar claves a string strip\n", " for k in _PK_SALDO:\n", " if k in df.columns:\n", " df[k] = df[k].astype(str).str.strip()\n", " return df.reset_index(drop=True)\n", "\n", "\n", "def cargar_saldos_vencidos_ssaldotem(df_excel, fecha_ini, fecha_fin):\n", " \"\"\"Modo Excel: filtra SSaldoTem por rango FECHAFACTURA_ISO y matchea con Excel\n", " por (FACTURAIMPO, FRACCIONIMPO). Trae los 6 campos de PK.\"\"\"\n", " sql = \"\"\"\n", " SELECT FACTURAIMPO, PEDIMENTOIMPO, FRACCIONIMPO, NUMPARTE, UMEXITENCIA, PAISORIGEN,\n", " FECHAFACTURA_ISO, FECHAVENC_ISO,\n", " CANTEXITENCIA,\n", " ISNULL(CANTUSADA,0) AS CANTUSADA,\n", " ISNULL(CANTUSADADESP,0) AS CANTUSADADESP,\n", " (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_DISPONIBLE,\n", " ISNULL(VALORIMPOMN,0) AS VALORIMPOMN,\n", " ISNULL(VALORIMPOME,0) AS VALORIMPOME,\n", " ISNULL(PESONETO,0) AS PESONETO,\n", " ISNULL(PESOBRUTO,0) AS PESOBRUTO\n", " FROM SSaldoTem\n", " WHERE FECHAFACTURA_ISO BETWEEN ? AND ?\n", " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", " \"\"\"\n", " df = pd.read_sql(sql, scaii_conn, params=(fecha_ini, fecha_fin))\n", " if df.empty: return df\n", " pares = df_excel[['FACTURAIMPO', 'FRACCION_IMPO', 'CANTIDAD_SALDO']].copy()\n", " pares = pares.rename(columns={'FRACCION_IMPO': 'FRACCIONIMPO'})\n", " pares['FACTURAIMPO'] = pares['FACTURAIMPO'].astype(str).str.strip()\n", " pares['FRACCIONIMPO'] = pares['FRACCIONIMPO'].astype(str).str.strip()\n", " df['FACTURAIMPO'] = df['FACTURAIMPO'].astype(str).str.strip()\n", " df['FRACCIONIMPO'] = df['FRACCIONIMPO'].astype(str).str.strip()\n", " df = df.merge(pares, on=['FACTURAIMPO', 'FRACCIONIMPO'], how='inner')\n", " return _enriquecer_saldos_con_tasas(df)\n", "\n", "\n", "def cargar_saldos_vencidos_auto(fecha_ini, fecha_fin, fecha_corte=None):\n", " \"\"\"Modo Automatico: SALDO_DISPONIBLE > 0 + FECHAVENC_ISO < fecha_corte (default hoy).\"\"\"\n", " if fecha_corte is None:\n", " fecha_corte = _dt_sv.date.today().isoformat()\n", " sql = \"\"\"\n", " SELECT FACTURAIMPO, PEDIMENTOIMPO, FRACCIONIMPO, NUMPARTE, UMEXITENCIA, PAISORIGEN,\n", " FECHAFACTURA_ISO, FECHAVENC_ISO,\n", " CANTEXITENCIA,\n", " ISNULL(CANTUSADA,0) AS CANTUSADA,\n", " ISNULL(CANTUSADADESP,0) AS CANTUSADADESP,\n", " (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_DISPONIBLE,\n", " ISNULL(VALORIMPOMN,0) AS VALORIMPOMN,\n", " ISNULL(VALORIMPOME,0) AS VALORIMPOME,\n", " ISNULL(PESONETO,0) AS PESONETO,\n", " ISNULL(PESOBRUTO,0) AS PESOBRUTO\n", " FROM SSaldoTem\n", " WHERE FECHAFACTURA_ISO BETWEEN ? AND ?\n", " AND FECHAVENC_ISO IS NOT NULL\n", " AND FECHAVENC_ISO < ?\n", " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", " \"\"\"\n", " df = pd.read_sql(sql, scaii_conn, params=(fecha_ini, fecha_fin, fecha_corte))\n", " return _enriquecer_saldos_con_tasas(df)\n", "\n", "\n", "def cargar_descargas_candidatas_sv(df_saldos):\n", " \"\"\"SDescargaT por las FACTIMPO involucradas; trae PAISMERCANCIA para match completo.\"\"\"\n", " if df_saldos.empty: return pd.DataFrame()\n", " facturas = df_saldos['FACTURAIMPO'].astype(str).str.strip().unique().tolist()\n", " if not facturas: return pd.DataFrame()\n", " out = []\n", " LOTE = 1000\n", " for i in range(0, len(facturas), LOTE):\n", " sub = facturas[i:i+LOTE]\n", " placeholders = ','.join(['?'] * len(sub))\n", " sql = f\"\"\"\n", " SELECT CONSECUTIVO, FACTIMPO, NUMPARTE, UNIMED, PAISMERCANCIA, FACTEXPO,\n", " ISNULL(CANTDESC,0) AS CANTDESC\n", " FROM SDescargaT\n", " WHERE FACTIMPO IN ({placeholders})\n", " AND ISNULL(CANTDESC,0) > 0\n", " \"\"\"\n", " out.append(pd.read_sql(sql, scaii_conn, params=sub))\n", " df = pd.concat(out, ignore_index=True) if out else pd.DataFrame()\n", " if df.empty: return df\n", " for k in ['FACTIMPO', 'NUMPARTE', 'UNIMED', 'PAISMERCANCIA']:\n", " df[k] = df[k].fillna('').astype(str).str.strip()\n", " return df\n", "\n", "\n", "def _prorratear_saldos(df_saldos, df_desc, prog):\n", " \"\"\"Motor de prorrateo. Match con descargas por (FACTIMPO, NUMPARTE, UNIMED, PAISMERCANCIA).\n", " Cada fila del plan propaga los 6 campos PK del saldo.\"\"\"\n", " plan_rows = []\n", " resumen_rows = []\n", " desc_by_key = {}\n", " if not df_desc.empty:\n", " for key, g in df_desc.groupby(_MATCH_DESC):\n", " desc_by_key[key] = g\n", " for _, s in df_saldos.iterrows():\n", " fimpo = str(s['FACTURAIMPO']).strip()\n", " pedimpo = str(s['PEDIMENTOIMPO']).strip()\n", " fraccion = str(s['FRACCIONIMPO']).strip()\n", " numparte = str(s['NUMPARTE']).strip()\n", " um = str(s['UMEXITENCIA']).strip()\n", " pais = str(s['PAISORIGEN']).strip()\n", " candidatas = desc_by_key.get((fimpo, numparte, um, pais))\n", " base_resumen = {\n", " 'FACTURAIMPO': fimpo, 'PEDIMENTOIMPO': pedimpo, 'FRACCIONIMPO': fraccion,\n", " 'NUMPARTE': numparte, 'UMEXITENCIA': um, 'PAISORIGEN': pais,\n", " 'FECHAFACTURA_ISO': s.get('FECHAFACTURA_ISO'),\n", " 'FECHAVENC_ISO': s.get('FECHAVENC_ISO'),\n", " 'SALDO_APLICABLE': float(s['SALDO_APLICABLE']),\n", " }\n", " if candidatas is None or candidatas.empty:\n", " resumen_rows.append({**base_resumen, 'CANT_DESCARGAS': 0, 'STATUS': 'SIN_DESCARGAS'})\n", " continue\n", " total_cantdesc = float(candidatas['CANTDESC'].sum())\n", " if total_cantdesc <= 1e-9:\n", " resumen_rows.append({**base_resumen, 'CANT_DESCARGAS': 0, 'STATUS': 'CANTDESC_CERO'})\n", " continue\n", " saldo_apl = float(s['SALDO_APLICABLE'])\n", " tasa_vmn = float(s['TASA_VMN'])\n", " tasa_vme = float(s['TASA_VME'])\n", " tasa_pn = float(s['TASA_PNETO'])\n", " tasa_pb = float(s['TASA_PBRUTO'])\n", " for _, d in candidatas.iterrows():\n", " prop = float(d['CANTDESC']) / total_cantdesc\n", " pc = saldo_apl * prop\n", " plan_rows.append({\n", " 'FACTURAIMPO': fimpo, 'PEDIMENTOIMPO': pedimpo, 'FRACCIONIMPO': fraccion,\n", " 'NUMPARTE': numparte, 'UMEXITENCIA': um, 'PAISORIGEN': pais,\n", " 'CONSECUTIVO_DESC': int(d['CONSECUTIVO']),\n", " 'FACTEXPO': d['FACTEXPO'],\n", " 'CANTDESC_ACTUAL': float(d['CANTDESC']),\n", " 'PROPORCION': prop,\n", " 'PORCION_CANT': pc,\n", " 'PORCION_VMN': pc * tasa_vmn,\n", " 'PORCION_VME': pc * tasa_vme,\n", " 'PORCION_PNETO': pc * tasa_pn,\n", " 'PORCION_PBRUTO': pc * tasa_pb,\n", " })\n", " resumen_rows.append({**base_resumen, 'CANT_DESCARGAS': len(candidatas), 'STATUS': 'PRORRATEADO'})\n", " if prog is not None: prog.done('Analisis listo')\n", " return pd.DataFrame(plan_rows), pd.DataFrame(resumen_rows)\n", "\n", "\n", "def analizar_saldos_vencidos(df_excel, fecha_ini, fecha_fin, progress=None):\n", " prog = _Progress(progress)\n", " prog.setup(3, 'Cargando saldos SSaldoTem...')\n", " df_saldos = cargar_saldos_vencidos_ssaldotem(df_excel, fecha_ini, fecha_fin)\n", " prog.step(desc=f'Saldos matched: {len(df_saldos)}')\n", " df_desc = cargar_descargas_candidatas_sv(df_saldos)\n", " prog.step(desc=f'Descargas candidatas: {len(df_desc)}')\n", " return _prorratear_saldos(df_saldos, df_desc, prog)\n", "\n", "\n", "def analizar_saldos_vencidos_auto(fecha_ini, fecha_fin, fecha_corte=None, progress=None):\n", " prog = _Progress(progress)\n", " prog.setup(3, 'Buscando saldos vencidos en SSaldoTem...')\n", " df_saldos = cargar_saldos_vencidos_auto(fecha_ini, fecha_fin, fecha_corte)\n", " prog.step(desc=f'Saldos vencidos: {len(df_saldos)}')\n", " df_desc = cargar_descargas_candidatas_sv(df_saldos)\n", " prog.step(desc=f'Descargas candidatas: {len(df_desc)}')\n", " return _prorratear_saldos(df_saldos, df_desc, prog)\n", "\n", "\n", "def exportar_excel_saldos_vencidos(plan, resumen, ruta):\n", " with pd.ExcelWriter(ruta, engine='openpyxl') as w:\n", " if not plan.empty: plan.to_excel(w, sheet_name='Plan_Detalle', index=False)\n", " if not resumen.empty: resumen.to_excel(w, sheet_name='Resumen', index=False)\n", "\n", "\n", "def ejecutar_saldos_vencidos(plan, dry_run=True, progress=None, log=print):\n", " \"\"\"Paso B: UPDATE SDescargaT por descarga + UPDATE SSaldoTem por saldo.\n", " Usa los 6 campos PK del saldo para identificar univocamente la fila.\"\"\"\n", " assert isinstance(dry_run, bool), 'dry_run debe ser bool'\n", " prog = _Progress(progress)\n", " if plan is None or plan.empty:\n", " log('ERROR: plan vacio, corre primero \"Analizar\".')\n", " return\n", " UPD_DESC = \"\"\"UPDATE SDescargaT\n", " SET CANTDESC = ISNULL(CANTDESC,0) + ?,\n", " VALORMN = ISNULL(VALORMN,0) + ?,\n", " VALORME = ISNULL(VALORME,0) + ?,\n", " PESONETO = ISNULL(PESONETO,0) + ?,\n", " PESOBRUTO= ISNULL(PESOBRUTO,0)+ ?\n", " WHERE CONSECUTIVO = ?\"\"\"\n", " SEL_SALDO = \"\"\"SELECT (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0))\n", " FROM SSaldoTem\n", " WHERE FACTURAIMPO=? AND PEDIMENTOIMPO=? AND FRACCIONIMPO=?\n", " AND NUMPARTE=? AND UMEXITENCIA=? AND PAISORIGEN=?\"\"\"\n", " UPD_SALDO = \"\"\"UPDATE SSaldoTem\n", " SET CANTUSADA = ISNULL(CANTUSADA,0) + ?,\n", " VALORUSADOMN = ISNULL(VALORUSADOMN,0) + ?,\n", " VALORUSADOME = ISNULL(VALORUSADOME,0) + ?,\n", " PESOUSADO = ISNULL(PESOUSADO,0) + ?,\n", " PESOBRUTOUSADO = ISNULL(PESOBRUTOUSADO,0) + ?\n", " WHERE FACTURAIMPO=? AND PEDIMENTOIMPO=? AND FRACCIONIMPO=?\n", " AND NUMPARTE=? AND UMEXITENCIA=? AND PAISORIGEN=?\"\"\"\n", " saldos = plan[_PK_SALDO].drop_duplicates().reset_index(drop=True)\n", " prog.setup(len(saldos), 'Procesando saldos')\n", " upd_desc = upd_saldo = errores = capados = 0\n", " err_list = []\n", " for _, s in saldos.iterrows():\n", " pk = (s['FACTURAIMPO'], s['PEDIMENTOIMPO'], s['FRACCIONIMPO'],\n", " s['NUMPARTE'], s['UMEXITENCIA'], s['PAISORIGEN'])\n", " etiqueta = f\"{pk[0]}/{pk[1]}/{pk[2]}/{pk[3]}/{pk[4]}/{pk[5]}\"\n", " mask = (plan['FACTURAIMPO']==pk[0]) & (plan['PEDIMENTOIMPO']==pk[1]) & \\\n", " (plan['FRACCIONIMPO']==pk[2]) & (plan['NUMPARTE']==pk[3]) & \\\n", " (plan['UMEXITENCIA']==pk[4]) & (plan['PAISORIGEN']==pk[5])\n", " fil = plan[mask].copy()\n", " if fil.empty: prog.step(); continue\n", " sum_c = float(fil['PORCION_CANT'].sum())\n", " try:\n", " with scaii_conn.cursor() as cur:\n", " cur.execute(SEL_SALDO, *pk)\n", " row = cur.fetchone()\n", " if row is None:\n", " log(f' WARN saldo {etiqueta} no existe; se salta')\n", " prog.step(); continue\n", " disp = float(row[0] or 0)\n", " if sum_c > disp + 1e-6:\n", " factor = disp / sum_c if sum_c > 0 else 0\n", " log(f' CAPEO {etiqueta}: sum={sum_c:.4f} > disp={disp:.4f} (factor={factor:.4f})')\n", " for col in ['PORCION_CANT','PORCION_VMN','PORCION_VME','PORCION_PNETO','PORCION_PBRUTO']:\n", " fil[col] = fil[col] * factor\n", " capados += 1\n", " sum_c = float(fil['PORCION_CANT'].sum())\n", " sum_vmn = float(fil['PORCION_VMN'].sum())\n", " sum_vme = float(fil['PORCION_VME'].sum())\n", " sum_pn = float(fil['PORCION_PNETO'].sum())\n", " sum_pb = float(fil['PORCION_PBRUTO'].sum())\n", " if sum_c <= 1e-9:\n", " log(f' SKIP {etiqueta}: factor=0, no hay nada que aplicar')\n", " prog.step(); continue\n", " for _, p in fil.iterrows():\n", " if not dry_run:\n", " cur.execute(UPD_DESC,\n", " float(p['PORCION_CANT']), float(p['PORCION_VMN']), float(p['PORCION_VME']),\n", " float(p['PORCION_PNETO']), float(p['PORCION_PBRUTO']),\n", " int(p['CONSECUTIVO_DESC']))\n", " upd_desc += 1\n", " if not dry_run:\n", " cur.execute(UPD_SALDO, sum_c, sum_vmn, sum_vme, sum_pn, sum_pb, *pk)\n", " upd_saldo += 1\n", " if not dry_run: scaii_conn.commit()\n", " except Exception as e:\n", " if not dry_run: scaii_conn.rollback()\n", " errores += 1\n", " err_list.append((etiqueta, str(e)))\n", " log(f' ERROR {etiqueta}: {e}')\n", " prog.step()\n", " prog.done(f'{upd_desc} desc / {upd_saldo} saldos')\n", " log(f'\\n=== RESUMEN Saldos Vencidos (DRY_RUN={dry_run}) ===')\n", " log(f' SDescargaT actualizadas: {upd_desc:,}')\n", " log(f' SSaldoTem actualizadas: {upd_saldo:,}')\n", " log(f' Saldos capeados al 100%: {capados:,}')\n", " log(f' Errores : {errores:,}')\n", " for f, e in err_list[:5]:\n", " log(f' {f}: {e}')\n", "\n", "\n", "def cargar_saldos_vencidos_por_anio(fecha_corte=None, eje='VENCIMIENTO'):\n", " if fecha_corte is None:\n", " fecha_corte = _dt_sv.date.today().isoformat()\n", " col_fecha = 'FECHAFACTURA_ISO' if eje == 'FACTURA' else 'FECHAVENC_ISO'\n", " alias_anio = 'ANIO_FACTURA' if eje == 'FACTURA' else 'ANIO_VENC'\n", " sql = f\"\"\"\n", " SELECT YEAR(CAST({col_fecha} AS DATE)) AS {alias_anio},\n", " COUNT(*) AS LOTES,\n", " COUNT(DISTINCT FACTURAIMPO) AS FACTURAS,\n", " SUM(CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) AS SALDO_CANT,\n", " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", " CASE WHEN CANTEXITENCIA > 0\n", " THEN (ISNULL(VALORIMPOMN,0) * 1.0 / CANTEXITENCIA)\n", " ELSE 0 END) AS SALDO_VMN,\n", " SUM((CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) *\n", " CASE WHEN CANTEXITENCIA > 0\n", " THEN (ISNULL(VALORIMPOME,0) * 1.0 / CANTEXITENCIA)\n", " ELSE 0 END) AS SALDO_VME\n", " FROM SSaldoTem\n", " WHERE FECHAVENC_ISO IS NOT NULL\n", " AND FECHAVENC_ISO < ?\n", " AND {col_fecha} IS NOT NULL\n", " AND (CANTEXITENCIA - ISNULL(CANTUSADA,0) - ISNULL(CANTUSADADESP,0)) > 0\n", " GROUP BY YEAR(CAST({col_fecha} AS DATE))\n", " ORDER BY {alias_anio}\n", " \"\"\"\n", " return pd.read_sql(sql, scaii_conn, params=(fecha_corte,))\n", "\n", "\n", "def 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", "execution_count": null, "id": "logic-valores", "metadata": {}, "outputs": [], "source": [ "# =============================================================\n", "# VALORES - Ajuste de VALORTOTALME / VALORTOTALMN en SPartidasExpo\n", "# Prorrateo proporcional al valor actual de cada partida.\n", "# =============================================================\n", "\n", "import io as _io_val\n", "\n", "def cargar_excel_valores(path):\n", " \"\"\"Lee Excel con 3 columnas: PEDIMENTO, VALOR_ME, VALOR_MN.\"\"\"\n", " df = pd.read_excel(path, dtype=str)\n", " norm = {c: c.strip().upper().replace(' ', '_') for c in df.columns}\n", " df = df.rename(columns=norm)\n", " aliases = {\n", " 'PEDIMENTO': ['PEDIMENTO', 'PEDIMENTOEXPO', 'PEDIMENTO_EXPO'],\n", " 'VALOR_ME': ['VALOR_ME', 'VALORME', 'VALOR_M_E', 'VALORTOTALME'],\n", " 'VALOR_MN': ['VALOR_MN', 'VALORMN', 'VALOR_M_N', 'VALORTOTALMN'],\n", " }\n", " out = {}\n", " for std, opts in aliases.items():\n", " for o in opts:\n", " if o in df.columns:\n", " out[std] = df[o]; break\n", " if std not in out:\n", " raise ValueError(f'Falta la columna {std} (acepta: {opts})')\n", " df2 = pd.DataFrame(out)\n", " df2['PEDIMENTO'] = df2['PEDIMENTO'].astype(str).str.strip()\n", " df2['VALOR_ME'] = pd.to_numeric(df2['VALOR_ME'], errors='coerce').fillna(0)\n", " df2['VALOR_MN'] = pd.to_numeric(df2['VALOR_MN'], errors='coerce').fillna(0)\n", " df2 = df2[df2['PEDIMENTO'] != ''].reset_index(drop=True)\n", " return df2\n", "\n", "\n", "def generar_plantilla_excel_valores():\n", " df = pd.DataFrame([\n", " {'PEDIMENTO': '07-3429-4015540', 'VALOR_ME': 12345.67, 'VALOR_MN': 234567.89},\n", " {'PEDIMENTO': '07-3429-4015541', 'VALOR_ME': 8000.00, 'VALOR_MN': 152000.00},\n", " ])\n", " buf = _io_val.BytesIO()\n", " with pd.ExcelWriter(buf, engine='openpyxl') as w:\n", " df.to_excel(w, sheet_name='Valores', index=False)\n", " buf.seek(0)\n", " return buf.read()\n", "\n", "\n", "def cargar_partidas_expo_pedimento(pedimento, db=None, esquema=None):\n", " \"\"\"Trae partidas SPartidasExpo de un pedimento via SFacExp.\n", " Si se pasan db y esquema, usa 3-part naming [db].[esquema].SPartidasExpo\n", " para apuntar a otra BD de la misma instancia SQL Server.\"\"\"\n", " if db and esquema:\n", " prefix = f'[{db}].[{esquema}]'\n", " else:\n", " prefix = ''\n", " pa = f'{prefix}.SPartidasExpo' if prefix else 'SPartidasExpo'\n", " fa = f'{prefix}.SFacExp' if prefix else 'SFacExp'\n", " sql = f\"\"\"\n", " SELECT se.FACTURAEXPO, se.LINEA, spe.TIPOCAMBIO,\n", " ISNULL(se.VALORTOTALME,0) AS VALORTOTALME,\n", " ISNULL(se.VALORTOTALMN,0) AS VALORTOTALMN,\n", " ISNULL(se.COSTOUNITARIOME,0) AS COSTOUNITARIOME,\n", " ISNULL(se.CANTEXPO,0) AS CANTEXPO\n", " FROM {pa} se\n", " INNER JOIN {fa} spe ON spe.FACTURAEXPO = se.FACTURAEXPO\n", " WHERE spe.PEDIMENTOEXPO = ?\n", " ORDER BY se.FACTURAEXPO, se.LINEA\n", " \"\"\"\n", " return pd.read_sql(sql, scaii_conn, params=(str(pedimento).strip(),))\n", "\n", "\n", "def analizar_valores(df_excel, modo='APLICAR_SIEMPRE', umbral_pct=50.0,\n", " usar_shelter=False, progress=None, log=print):\n", " \"\"\"Paso A: arma el plan de prorrateo. Devuelve (plan, resumen).\n", " plan: una fila por partida con AJUSTE_ME, AJUSTE_MN, NUEVO_ME, NUEVO_MN.\n", " resumen: una fila por pedimento con STATUS, factor, diferencia, etc.\n", "\n", " modo='APLICAR_SIEMPRE': escala todo sin importar la magnitud.\n", " modo='USAR_UMBRAL': si |factor - 1| > umbral_pct/100, marca FUERA_DE_UMBRAL y no se aplica.\n", " \"\"\"\n", " prog = _Progress(progress)\n", " if df_excel.empty:\n", " log('Excel vacio.'); return pd.DataFrame(), pd.DataFrame()\n", " # Localizar pedimentos en otras BDs si se pidio shelter.\n", " ubic_map = {} # pedimento -> [(db, esquema), ...]\n", " if usar_shelter:\n", " try:\n", " log('Buscando pedimentos en todas las BDs de la instancia...')\n", " reporte, _, _ = buscar_pedimentos_en_bds(df_excel, progress=progress, log=log)\n", " # Reconstruir mapping db/esquema (uno por (pedimento, BD))\n", " sub = pd.read_sql(\"\"\"\n", " SELECT name FROM sys.databases\n", " WHERE database_id > 4 AND state_desc = 'ONLINE' AND HAS_DBACCESS(name) = 1\n", " \"\"\", scaii_conn)\n", " sub_set = set(sub['name'].astype(str).tolist())\n", " for _, rr in reporte.iterrows():\n", " if not rr['BasesEncontradas']: continue\n", " ped = str(rr['PEDIMENTO']).strip()\n", " for tok in str(rr['BasesEncontradas']).split(','):\n", " tok = tok.strip()\n", " if '.' in tok:\n", " db, esq = tok.rsplit('.', 1)\n", " if db in sub_set:\n", " ubic_map.setdefault(ped, []).append((db, esq))\n", " except Exception as e:\n", " log(f'WARN shelter fallo: {e}. Usando BD actual.')\n", " ubic_map = {}\n", " prog.setup(len(df_excel), 'Analizando pedimentos...')\n", " plan_rows = []\n", " resumen_rows = []\n", " umb = float(umbral_pct) / 100.0\n", " for _, r in df_excel.iterrows():\n", " pedimento = str(r['PEDIMENTO']).strip()\n", " v_me_esp = float(r['VALOR_ME'] or 0)\n", " v_mn_esp = float(r['VALOR_MN'] or 0)\n", " # Lista de (db, esquema) a procesar para este pedimento\n", " if usar_shelter:\n", " destinos = ubic_map.get(pedimento, [])\n", " if not destinos:\n", " resumen_rows.append({'PEDIMENTO': pedimento, 'BaseDeDatos': '', 'Esquema': '',\n", " 'SUM_ME_ACTUAL': 0, 'SUM_MN_ACTUAL': 0,\n", " 'VALOR_ME_ESPERADO': v_me_esp, 'VALOR_MN_ESPERADO': v_mn_esp,\n", " 'PARTIDAS': 0, 'FACTOR_ME': 0, 'FACTOR_MN': 0,\n", " 'STATUS': 'NO_ENCONTRADO_EN_BDS'})\n", " prog.step(desc=pedimento[:30]); continue\n", " else:\n", " destinos = [(None, None)]\n", " # Procesar cada destino\n", " for (db_dest, esq_dest) in destinos:\n", " try:\n", " df_part = cargar_partidas_expo_pedimento(pedimento, db=db_dest, esquema=esq_dest)\n", " except Exception as e:\n", " log(f' ERROR query {pedimento} [{db_dest or DB_ACTUAL}]: {e}')\n", " resumen_rows.append({'PEDIMENTO': pedimento,\n", " 'BaseDeDatos': db_dest or DB_ACTUAL, 'Esquema': esq_dest or '',\n", " 'SUM_ME_ACTUAL': 0, 'SUM_MN_ACTUAL': 0,\n", " 'VALOR_ME_ESPERADO': v_me_esp, 'VALOR_MN_ESPERADO': v_mn_esp,\n", " 'PARTIDAS': 0, 'FACTOR_ME': 0, 'FACTOR_MN': 0,\n", " 'STATUS': f'ERROR: {e}'})\n", " continue\n", " if df_part.empty:\n", " resumen_rows.append({'PEDIMENTO': pedimento,\n", " 'BaseDeDatos': db_dest or DB_ACTUAL, 'Esquema': esq_dest or '',\n", " 'SUM_ME_ACTUAL': 0, 'SUM_MN_ACTUAL': 0,\n", " 'VALOR_ME_ESPERADO': v_me_esp, 'VALOR_MN_ESPERADO': v_mn_esp,\n", " 'PARTIDAS': 0, 'FACTOR_ME': 0, 'FACTOR_MN': 0,\n", " 'STATUS': 'SIN_PARTIDAS'})\n", " continue\n", " sum_me = float(df_part['VALORTOTALME'].astype(float).sum())\n", " sum_mn = float(df_part['VALORTOTALMN'].astype(float).sum())\n", " if abs(sum_me) < 1e-9 and v_me_esp > 0:\n", " status = 'SIN_BASE_ME'\n", " elif abs(sum_mn) < 1e-9 and v_mn_esp > 0:\n", " status = 'SIN_BASE_MN'\n", " else:\n", " f_me_chk = (v_me_esp / sum_me) if sum_me > 1e-9 else 0.0\n", " f_mn_chk = (v_mn_esp / sum_mn) if sum_mn > 1e-9 else 0.0\n", " if modo == 'USAR_UMBRAL':\n", " if abs(f_me_chk - 1.0) > umb or abs(f_mn_chk - 1.0) > umb:\n", " status = 'FUERA_DE_UMBRAL'\n", " else:\n", " status = 'AJUSTAR'\n", " else:\n", " status = 'AJUSTAR'\n", "\n", " f_me = (v_me_esp / sum_me) if sum_me > 1e-9 else 0.0\n", " f_mn = (v_mn_esp / sum_mn) if sum_mn > 1e-9 else 0.0\n", "\n", " if status == 'AJUSTAR':\n", " df_part = df_part.copy()\n", " df_part['NUEVO_ME'] = (df_part['VALORTOTALME'].astype(float) * f_me).round(6)\n", " df_part['NUEVO_MN'] = (df_part['VALORTOTALMN'].astype(float) * f_mn).round(6)\n", " diff_me = round(v_me_esp - df_part['NUEVO_ME'].sum(), 6)\n", " diff_mn = round(v_mn_esp - df_part['NUEVO_MN'].sum(), 6)\n", " if abs(diff_me) > 1e-9:\n", " idx_last = df_part.index[-1]\n", " df_part.at[idx_last, 'NUEVO_ME'] = round(df_part.at[idx_last, 'NUEVO_ME'] + diff_me, 6)\n", " if abs(diff_mn) > 1e-9:\n", " idx_last = df_part.index[-1]\n", " df_part.at[idx_last, 'NUEVO_MN'] = round(df_part.at[idx_last, 'NUEVO_MN'] + diff_mn, 6)\n", " df_part['AJUSTE_ME'] = (df_part['NUEVO_ME'] - df_part['VALORTOTALME'].astype(float)).round(6)\n", " df_part['AJUSTE_MN'] = (df_part['NUEVO_MN'] - df_part['VALORTOTALMN'].astype(float)).round(6)\n", " df_part['PEDIMENTO'] = pedimento\n", " for _, p in df_part.iterrows():\n", " plan_rows.append({\n", " 'PEDIMENTO': pedimento,\n", " 'BaseDeDatos': db_dest or DB_ACTUAL,\n", " 'Esquema': esq_dest or '',\n", " 'FACTURAEXPO': p['FACTURAEXPO'],\n", " 'LINEA': int(p['LINEA']),\n", " 'CANTEXPO': float(p['CANTEXPO']),\n", " 'TIPOCAMBIO': float(p['TIPOCAMBIO'] or 0),\n", " 'VALORTOTALME_ACTUAL': float(p['VALORTOTALME']),\n", " 'VALORTOTALMN_ACTUAL': float(p['VALORTOTALMN']),\n", " 'AJUSTE_ME': float(p['AJUSTE_ME']),\n", " 'AJUSTE_MN': float(p['AJUSTE_MN']),\n", " 'NUEVO_ME': float(p['NUEVO_ME']),\n", " 'NUEVO_MN': float(p['NUEVO_MN']),\n", " })\n", "\n", " resumen_rows.append({\n", " 'PEDIMENTO': pedimento,\n", " 'BaseDeDatos': db_dest or DB_ACTUAL,\n", " 'Esquema': esq_dest or '',\n", " 'SUM_ME_ACTUAL': round(sum_me, 6),\n", " 'SUM_MN_ACTUAL': round(sum_mn, 6),\n", " 'VALOR_ME_ESPERADO': v_me_esp,\n", " 'VALOR_MN_ESPERADO': v_mn_esp,\n", " 'PARTIDAS': len(df_part),\n", " 'FACTOR_ME': round(f_me, 6),\n", " 'FACTOR_MN': round(f_mn, 6),\n", " 'STATUS': status,\n", " })\n", " prog.step(desc=pedimento[:30])\n", " prog.done('Analisis listo')\n", " return pd.DataFrame(plan_rows), pd.DataFrame(resumen_rows)\n", "\n", "\n", "def exportar_excel_valores(plan, resumen, ruta):\n", " with pd.ExcelWriter(ruta, engine='openpyxl') as w:\n", " if not plan.empty: plan.to_excel(w, sheet_name='Plan_Detalle', index=False)\n", " if not resumen.empty: resumen.to_excel(w, sheet_name='Resumen', index=False)\n", "\n", "\n", "def ejecutar_valores(plan, dry_run=True, aplicar_vtmn=True, aplicar_mptemp=False, progress=None, log=print):\n", " \"\"\"Paso B: UPDATE SPartidasExpo por cada partida del plan, usando 3-part\n", " naming si la fila trae BaseDeDatos/Esquema (cuando se uso shelter).\n", " Transaccion por (pedimento, BD) rollback si alguna partida falla.\"\"\"\n", " assert isinstance(dry_run, bool), 'dry_run debe ser bool'\n", " prog = _Progress(progress)\n", " if plan is None or plan.empty:\n", " log('ERROR: plan vacio, corre primero \"Analizar\".')\n", " return\n", " # Si el plan no trae las columnas (compat), las agregamos vacias\n", " if 'BaseDeDatos' not in plan.columns:\n", " plan = plan.copy(); plan['BaseDeDatos'] = ''\n", " if 'Esquema' not in plan.columns:\n", " plan = plan.copy(); plan['Esquema'] = ''\n", " # Agrupar por (pedimento, BD, esquema)\n", " grupos = plan.groupby(['PEDIMENTO', 'BaseDeDatos', 'Esquema'], dropna=False)\n", " prog.setup(len(grupos), 'Procesando pedimentos')\n", " upd = errores = 0\n", " err_list = []\n", " for (ped, db, esq), fil in grupos:\n", " if db and esq:\n", " target = f'[{db}].[{esq}].SPartidasExpo'\n", " else:\n", " target = 'SPartidasExpo'\n", " sets, params_tmpl = ['VALORTOTALME = ?'], ['ME']\n", " if aplicar_vtmn:\n", " sets.append('VALORTOTALMN = ?'); params_tmpl.append('MN')\n", " if aplicar_mptemp:\n", " sets.append('ValorMPTempMN = ?'); params_tmpl.append('MN')\n", " upd_sql = f\"UPDATE {target} SET {', '.join(sets)} WHERE FACTURAEXPO = ? AND LINEA = ?\"\n", " try:\n", " with scaii_conn.cursor() as cur:\n", " for _, p in fil.iterrows():\n", " if not dry_run:\n", " vals = []\n", " for t in params_tmpl:\n", " vals.append(float(p['NUEVO_ME']) if t == 'ME' else float(p['NUEVO_MN']))\n", " vals.extend([p['FACTURAEXPO'], int(p['LINEA'])])\n", " cur.execute(upd_sql, *vals)\n", " upd += 1\n", " if not dry_run: scaii_conn.commit()\n", " except Exception as e:\n", " if not dry_run: scaii_conn.rollback()\n", " errores += 1\n", " etq = f'{ped} [{db or DB_ACTUAL}]' if db else str(ped)\n", " err_list.append((etq, str(e)))\n", " log(f' ERROR {etq}: {e}')\n", " prog.step(desc=str(ped)[:30])\n", " prog.done(f'{upd} partidas')\n", " bds = plan['BaseDeDatos'].replace('', pd.NA).dropna().unique().tolist()\n", " log(f'\\n=== RESUMEN Valores (DRY_RUN={dry_run}) ===')\n", " log(f' Partidas actualizadas: {upd:,}')\n", " log(f' Pedimentos procesados: {plan[\"PEDIMENTO\"].nunique():,}')\n", " log(f' BDs tocadas : {bds if bds else \"(solo la actual)\"}')\n", " log(f' Errores : {errores:,}')\n", " for f, e in err_list[:5]:\n", " log(f' {f}: {e}')\n", "\n", "\n", "# =============================================================\n", "# SHELTER - Busca pedimentos en todas las BDs de la instancia\n", "# (todas las que tengan SPedimentos.PEDIMENTO)\n", "# =============================================================\n", "\n", "def buscar_pedimentos_en_bds(df_excel, progress=None, log=print):\n", " \"\"\"Recorre sys.databases, detecta BDs con SPedimentos.PEDIMENTO y busca\n", " los pedimentos del Excel en cada una. Devuelve (reporte, resumen_bd, faltantes).\"\"\"\n", " prog = _Progress(progress)\n", " if df_excel is None or df_excel.empty:\n", " log('Excel vacio.')\n", " return pd.DataFrame(), pd.DataFrame(), []\n", " pedimentos = [str(p).strip() for p in df_excel['PEDIMENTO'].astype(str).tolist() if str(p).strip()]\n", " if not pedimentos:\n", " log('Sin pedimentos validos.')\n", " return pd.DataFrame(), pd.DataFrame(), []\n", "\n", " # 1) BDs online accesibles\n", " try:\n", " df_dbs = pd.read_sql(\"\"\"\n", " SELECT name FROM sys.databases\n", " WHERE database_id > 4 AND state_desc = 'ONLINE' AND HAS_DBACCESS(name) = 1\n", " ORDER BY name\n", " \"\"\", scaii_conn)\n", " except Exception as e:\n", " log(f'ERROR listando BDs: {e}')\n", " return pd.DataFrame(), pd.DataFrame(), []\n", " log(f'BDs online accesibles: {len(df_dbs)}')\n", " if df_dbs.empty:\n", " return pd.DataFrame(), pd.DataFrame(), pedimentos\n", "\n", " # 2) Por cada BD, detectar SPedimentos.PEDIMENTO + buscar\n", " prog.setup(len(df_dbs), 'Recorriendo BDs...')\n", " rows = []\n", " LOTE = 1000\n", " for _, r in df_dbs.iterrows():\n", " db = str(r['name'])\n", " try:\n", " sql_check = f\"\"\"\n", " SELECT s.name AS Esquema\n", " FROM [{db}].sys.tables t\n", " JOIN [{db}].sys.schemas s ON s.schema_id = t.schema_id\n", " JOIN [{db}].sys.columns c ON c.object_id = t.object_id\n", " WHERE t.name = 'SPedimentos' AND c.name = 'PEDIMENTO'\n", " \"\"\"\n", " df_check = pd.read_sql(sql_check, scaii_conn)\n", " except Exception as e:\n", " log(f' [{db}] saltada (metadata): {e}')\n", " prog.step(desc=db[:30]); continue\n", " if df_check.empty:\n", " prog.step(desc=db[:30]); continue\n", " for _, ec in df_check.iterrows():\n", " esquema = str(ec['Esquema'])\n", " for i in range(0, len(pedimentos), LOTE):\n", " sub = pedimentos[i:i+LOTE]\n", " placeholders = ','.join(['?'] * len(sub))\n", " sql_search = (f\"SELECT DISTINCT PEDIMENTO \"\n", " f\"FROM [{db}].[{esquema}].SPedimentos \"\n", " f\"WHERE PEDIMENTO IN ({placeholders})\")\n", " try:\n", " df_found = pd.read_sql(sql_search, scaii_conn, params=tuple(sub))\n", " for _, f in df_found.iterrows():\n", " rows.append({\n", " 'PEDIMENTO': str(f['PEDIMENTO']).strip(),\n", " 'BaseDeDatos': db,\n", " 'Esquema': esquema,\n", " })\n", " except Exception as e:\n", " log(f' [{db}].[{esquema}] saltada (busqueda): {e}')\n", " break\n", " prog.step(desc=db[:30])\n", " df_result = pd.DataFrame(rows)\n", "\n", " # 3) Reporte por pedimento\n", " df_in = pd.DataFrame({'PEDIMENTO': pedimentos}).drop_duplicates().reset_index(drop=True)\n", " if df_result.empty:\n", " reporte = df_in.assign(BasesEncontradas='', NumDBs=0)\n", " else:\n", " df_result['Ubicacion'] = df_result['BaseDeDatos'] + '.' + df_result['Esquema']\n", " agg = (df_result.groupby('PEDIMENTO', as_index=False)\n", " .agg(BasesEncontradas=('Ubicacion', lambda s: ', '.join(sorted(set(s)))),\n", " NumDBs=('Ubicacion', 'nunique')))\n", " reporte = df_in.merge(agg, on='PEDIMENTO', how='left')\n", " reporte['BasesEncontradas'] = reporte['BasesEncontradas'].fillna('')\n", " reporte['NumDBs'] = reporte['NumDBs'].fillna(0).astype(int)\n", "\n", " # 4) Resumen por BD\n", " if df_result.empty:\n", " resumen_bd = pd.DataFrame(columns=['BaseDeDatos', 'Esquema', 'PedimentosEncontrados'])\n", " else:\n", " resumen_bd = (df_result.groupby(['BaseDeDatos', 'Esquema'])\n", " .size().reset_index(name='PedimentosEncontrados')\n", " .sort_values('PedimentosEncontrados', ascending=False))\n", "\n", " # 5) Faltantes\n", " faltantes = reporte.loc[reporte['NumDBs'] == 0, 'PEDIMENTO'].tolist()\n", "\n", " prog.done(f'{len(df_result)} hits en {df_result[\"BaseDeDatos\"].nunique() if not df_result.empty else 0} BDs')\n", " return reporte, resumen_bd, faltantes" ] }, { "cell_type": "code", "execution_count": null, "id": "logic-datastage", "metadata": {}, "outputs": [], "source": [ "# =============================================================\n", "# DATASTAGE - Carga de archivos .asc en SQLite (tablas Registro)\n", "# Migrado de PHP/Postgres a SQLite local.\n", "# =============================================================\n", "\n", "import re as _re_ds\n", "import csv as _csv_ds\n", "from pathlib import Path as _Path_ds\n", "\n", "# En SQLite los tipos relevantes para limpiar valores numericos\n", "_TIPOS_NUMERICOS_SQ = {'numeric', 'integer', 'real', 'float'}\n", "\n", "_BATCH_DS = 200\n", "\n", "\n", "def _ds_conn():\n", " \"\"\"Devuelve una conexion sqlite3 fresca a la base local de DataStage.\"\"\"\n", " if not DATASTAGE_OK:\n", " raise RuntimeError(DATASTAGE_MSG or 'SQLite no disponible')\n", " return _sqlite3.connect(DATASTAGE_DB)\n", "\n", "\n", "def extraer_tipo_registro(nombre_archivo):\n", " \"\"\"'725199_501.asc' -> '501' | '725199_Inci.asc' -> 'Inci'.\"\"\"\n", " m = _re_ds.search(r'_(\\d{3})\\.asc$', nombre_archivo, flags=_re_ds.IGNORECASE)\n", " if m: return m.group(1)\n", " m = _re_ds.search(r'_(\\w+)\\.asc$', nombre_archivo, flags=_re_ds.IGNORECASE)\n", " if m: return m.group(1)\n", " return None\n", "\n", "\n", "def obtener_esquema_tabla(conn, tabla):\n", " \"\"\"Columnas y tipos de una tabla SQLite via PRAGMA table_info.\"\"\"\n", " cur = conn.execute(f'PRAGMA table_info(\"{tabla}\")')\n", " rows = cur.fetchall()\n", " # PRAGMA table_info: (cid, name, type, notnull, dflt_value, pk)\n", " return [{'column_name': r[1], 'data_type': (r[2] or '').lower()} for r in rows]\n", "\n", "\n", "def listar_tablas_registro(conn):\n", " \"\"\"Tablas que empiezan con 'Registro' (case-insensitive).\"\"\"\n", " cur = conn.execute(\n", " \"SELECT name FROM sqlite_master \"\n", " \"WHERE type='table' AND lower(name) LIKE 'registro%' \"\n", " \"ORDER BY name\"\n", " )\n", " return [r[0] for r in cur.fetchall()]\n", "\n", "\n", "def listar_archivos_asc(ruta_raiz):\n", " \"\"\"Devuelve lista de Path con todos los .asc encontrados.\n", " Soporta dos layouts: ruta/YYYY/*.asc (HONDA) o ruta/*.asc (plano).\"\"\"\n", " raiz = _Path_ds(ruta_raiz)\n", " if not raiz.exists() or not raiz.is_dir():\n", " return []\n", " archivos = []\n", " subdirs_anio = [d for d in raiz.iterdir() if d.is_dir() and d.name.isdigit()]\n", " if subdirs_anio:\n", " for d in sorted(subdirs_anio):\n", " for f in sorted(d.glob('*.asc')):\n", " if f.is_file():\n", " archivos.append(f)\n", " for f in sorted(raiz.glob('*.asc')):\n", " if f.is_file():\n", " archivos.append(f)\n", " return archivos\n", "\n", "\n", "def _limpiar_valor(valor, tipo_dato):\n", " v = (valor or '').strip()\n", " # Filtrar NUL y caracteres de control\n", " v = v.replace('\\x00', '').replace('\\r', '')\n", " if v == '': return None\n", " tipo = (tipo_dato or '').lower()\n", " if tipo in _TIPOS_NUMERICOS_SQ:\n", " v = v.replace(',', '.')\n", " v = _re_ds.sub(r'[^0-9.\\-]', '', v)\n", " if v in ('', '-'): return None\n", " return v\n", "\n", "\n", "def cargar_archivo_asc(conn, ruta_archivo):\n", " \"\"\"Carga un .asc en su tabla Registro. Usa SAVEPOINT por archivo:\n", " un error no rompe los archivos anteriores. Devuelve dict con resultado.\"\"\"\n", " p = _Path_ds(ruta_archivo)\n", " nombre = p.name\n", " registro = extraer_tipo_registro(nombre)\n", " if registro is None:\n", " return {'archivo': nombre, 'tabla': None, 'filas': 0,\n", " 'estatus': 'SKIP', 'mensaje': 'No se reconoce la estructura'}\n", " tabla = f'Registro{registro}'\n", " columnas = obtener_esquema_tabla(conn, tabla)\n", " if not columnas:\n", " return {'archivo': nombre, 'tabla': tabla, 'filas': 0,\n", " 'estatus': 'SKIP', 'mensaje': f\"Tabla '{tabla}' no existe\"}\n", " nombres_cols = [c['column_name'] for c in columnas]\n", " tipos_cols = {c['column_name']: c['data_type'] for c in columnas}\n", " n = len(nombres_cols)\n", " cols_sql = ', '.join(f'\"{c}\"' for c in nombres_cols)\n", " placeholders = ', '.join(['?'] * n)\n", " insert_sql = f'INSERT INTO \"{tabla}\" ({cols_sql}) VALUES ({placeholders})'\n", " filas_ins = 0\n", " conn.execute('SAVEPOINT archivo_sp')\n", " try:\n", " with open(p, 'r', encoding='latin-1') as f:\n", " reader = _csv_ds.reader(f, delimiter='|')\n", " next(reader, None) # header\n", " batch = []\n", " for fila in reader:\n", " fila = fila[:n]\n", " while len(fila) < n:\n", " fila.append('')\n", " valores = [_limpiar_valor(v, tipos_cols[nombres_cols[i]])\n", " for i, v in enumerate(fila)]\n", " batch.append(tuple(valores))\n", " if len(batch) >= _BATCH_DS:\n", " conn.executemany(insert_sql, batch)\n", " filas_ins += len(batch)\n", " batch = []\n", " if batch:\n", " conn.executemany(insert_sql, batch)\n", " filas_ins += len(batch)\n", " conn.execute('RELEASE SAVEPOINT archivo_sp')\n", " return {'archivo': nombre, 'tabla': tabla, 'filas': filas_ins,\n", " 'estatus': 'OK', 'mensaje': f'{filas_ins:,} filas insertadas'}\n", " except Exception as e:\n", " conn.execute('ROLLBACK TO SAVEPOINT archivo_sp')\n", " return {'archivo': nombre, 'tabla': tabla, 'filas': filas_ins,\n", " 'estatus': 'ERROR', 'mensaje': str(e)}\n", "\n", "\n", "def cargar_directorio_datastage(ruta_raiz, progress=None, log=print):\n", " \"\"\"Itera todos los .asc encontrados y carga en su tabla Registro.\"\"\"\n", " prog = _Progress(progress)\n", " archivos = listar_archivos_asc(ruta_raiz)\n", " if not archivos:\n", " log(f'No se encontraron .asc en: {ruta_raiz}')\n", " return pd.DataFrame()\n", " log(f'Encontrados {len(archivos):,} archivos .asc')\n", " prog.setup(len(archivos), 'Cargando .asc')\n", " conn = _ds_conn()\n", " # Control manual de transaccion para usar SAVEPOINT por archivo\n", " conn.isolation_level = None\n", " conn.execute('BEGIN')\n", " resultados = []\n", " ok = sk = er = 0\n", " total_filas = 0\n", " try:\n", " for p in archivos:\n", " r = cargar_archivo_asc(conn, p)\n", " resultados.append(r)\n", " if r['estatus'] == 'OK':\n", " ok += 1; total_filas += r['filas']\n", " elif r['estatus'] == 'SKIP':\n", " sk += 1\n", " else:\n", " er += 1\n", " log(f\" ERROR {r['archivo']}: {r['mensaje']}\")\n", " prog.step(desc=f\"{r['estatus']} {r['archivo'][:25]}\")\n", " conn.execute('COMMIT')\n", " except Exception as e:\n", " try: conn.execute('ROLLBACK')\n", " except Exception: pass\n", " log(f'EXCEPCION GLOBAL: {e}')\n", " finally:\n", " conn.close()\n", " prog.done(f'{ok} OK / {sk} SKIP / {er} ERR')\n", " log(f'\\n=== RESUMEN DataStage ===')\n", " log(f' Archivos OK : {ok:,}')\n", " log(f' Archivos SKIP : {sk:,}')\n", " log(f' Archivos ERROR : {er:,}')\n", " log(f' Filas insertadas: {total_filas:,}')\n", " return pd.DataFrame(resultados)\n", "\n", "\n", "def previsualizar_archivos_ds(ruta_raiz):\n", " \"\"\"Devuelve DataFrame con archivos detectados + tabla destino + tamanio.\"\"\"\n", " archivos = listar_archivos_asc(ruta_raiz)\n", " rows = []\n", " for p in archivos:\n", " reg = extraer_tipo_registro(p.name)\n", " rows.append({\n", " 'archivo': p.name,\n", " 'tabla_destino': f'Registro{reg}' if reg else '(no detectado)',\n", " 'tamanio_kb': round(p.stat().st_size / 1024, 1),\n", " 'ruta': str(p),\n", " })\n", " return pd.DataFrame(rows)\n", "\n", "\n", "def truncar_tablas_registro(progress=None, log=print):\n", " \"\"\"DELETE FROM en todas las tablas Registro* (SQLite no tiene TRUNCATE).\n", " Devuelve dict {tabla: filas_antes}.\"\"\"\n", " prog = _Progress(progress)\n", " conn = _ds_conn()\n", " resultado = {}\n", " try:\n", " tablas = listar_tablas_registro(conn)\n", " if not tablas:\n", " log('No hay tablas Registro* en la base.')\n", " return resultado\n", " prog.setup(len(tablas), 'Truncando tablas')\n", " for t in tablas:\n", " antes = conn.execute(f'SELECT COUNT(*) FROM \"{t}\"').fetchone()[0]\n", " conn.execute(f'DELETE FROM \"{t}\"')\n", " resultado[t] = antes\n", " log(f' DELETE FROM {t}: {antes:,} filas eliminadas')\n", " prog.step(desc=t[:30])\n", " conn.commit()\n", " prog.done(f'{len(tablas)} tablas truncadas')\n", " log(f'\\nTotal: {sum(resultado.values()):,} filas eliminadas en {len(tablas)} tablas.')\n", " except Exception as e:\n", " try: conn.rollback()\n", " except Exception: pass\n", " log(f'ERROR: {e}')\n", " prog.error('Error')\n", " finally:\n", " conn.close()\n", " return resultado\n", "\n", "\n", "def estadisticas_tablas_registro(progress=None):\n", " \"\"\"Cuenta filas por tabla Registro*. Devuelve DataFrame columnas: tabla, filas.\"\"\"\n", " prog = _Progress(progress)\n", " conn = _ds_conn()\n", " try:\n", " tablas = listar_tablas_registro(conn)\n", " if not tablas:\n", " return pd.DataFrame(columns=['tabla', 'filas'])\n", " prog.setup(len(tablas), 'Contando filas')\n", " rows = []\n", " for t in tablas:\n", " n = conn.execute(f'SELECT COUNT(*) FROM \"{t}\"').fetchone()[0]\n", " rows.append({'tabla': t, 'filas': n})\n", " prog.step(desc=t[:30])\n", " prog.done('Listo')\n", " return pd.DataFrame(rows)\n", " finally:\n", " conn.close()\n", "\n", "\n", "def obtener_muestra_tabla(tabla, limit=100, offset=0):\n", " \"\"\"Devuelve hasta `limit` filas de la tabla (con OFFSET) como DataFrame.\"\"\"\n", " conn = _ds_conn()\n", " try:\n", " sql = f'SELECT * FROM \"{tabla}\" LIMIT ? OFFSET ?'\n", " return pd.read_sql(sql, conn, params=(int(limit), int(offset)))\n", " finally:\n", " conn.close()\n", "\n", "\n", "# ============================================================\n", "# REPORTES DataStage (migrados de Postgres a SQLite)\n", "# Cambios respecto al SQL original:\n", "# - LEFT(x, n) -> substr(x, 1, n)\n", "# - RIGHT(LEFT(x::text,4),2) -> substr(x, 3, 2) (extrae YY)\n", "# - col::text -> CAST(col AS TEXT) o se omite\n", "# - EXTRACT(YEAR FROM x) -> CAST(strftime('%Y', x) AS INTEGER)\n", "# - LATERAL JOIN -> subconsultas correlacionadas\n", "# - %s -> ?\n", "# ============================================================\n", "\n", "def cat_pedimentos_ds(fecha_ini, fecha_fin):\n", " \"\"\"Estructura CAT Pedimentos. Toma Registro501 en el rango y marca si fue\n", " rectificado (existe en Registro701 como pedimento anterior).\n", " Migrado de generar_estructuracat.php.\"\"\"\n", " sql = \"\"\"\n", " WITH RECURSIVE historial_rect AS (\n", " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", " FROM \"Registro701\" R7\n", " UNION ALL\n", " SELECT R7.\"Patente\", R7.\"PatenteAnterior\", R7.\"Pedimento\",\n", " R7.\"SeccionAduanera\", R7.\"SeccionAduaneraAnterior\",\n", " R7.\"PedimentoAnterior\", R7.\"DocumentoAnterior\",\n", " R7.\"FechaOperacionAnterior\", R7.\"FechaPagoReal\"\n", " FROM \"Registro701\" R7\n", " INNER JOIN historial_rect HR ON\n", " (substr(R7.\"FechaOperacionAnterior\", 3, 2) || '-' ||\n", " substr(R7.\"SeccionAduaneraAnterior\", 1, 2) || '-' ||\n", " R7.\"PatenteAnterior\" || '-' || R7.\"PedimentoAnterior\")\n", " =\n", " (substr(HR.\"FechaPagoReal\", 3, 2) || '-' ||\n", " substr(HR.\"SeccionAduanera\", 1, 2) || '-' ||\n", " HR.\"Patente\" || '-' || HR.\"Pedimento\")\n", " )\n", " SELECT 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-R'.\"\"\"\n", " assert int(tipo_op) in (1, 2), 'tipo_op debe ser 1 (Impo) o 2 (Expo)'\n", " from sklearn.feature_extraction.text import TfidfVectorizer\n", " from sklearn.metrics.pairwise import cosine_similarity\n", " prog = _Progress(progress)\n", " prog.setup(5, 'Cargando Registro551...')\n", " df = _cargar_partidas_551(fecha_ini, fecha_fin, tipo_op)\n", " log(f'Partidas Registro551 ({\"IMPO\" if int(tipo_op)==1 else \"EXPO\"}): {len(df):,}')\n", " if df.empty:\n", " prog.done('Sin datos'); return df\n", " prog.step(desc='Preparando textos...')\n", "\n", " df['um_sigla'] = df['unidad_medida_comercial'].apply(_ds_um_sigla)\n", " df['fraccion4'] = df['fraccion'].fillna('').astype(str).str[:4]\n", " df['desc_norm'] = df['descripcion_mercancia'].apply(_ds_limpiar_texto)\n", " df['NUMERO_PARTE'] = ''\n", " df['MATCH_TIPO'] = ''\n", "\n", " try:\n", " df_base = listar_base_numpartes(limit=10_000_000)\n", " except Exception as e:\n", " log(f'WARN cargando base_numpartes: {e}')\n", " df_base = pd.DataFrame(columns=['numparte','descripcion','unimed','fraccion'])\n", " if not df_base.empty:\n", " df_base['fraccion4'] = df_base['fraccion'].fillna('').astype(str).str[:4]\n", " df_base['um_sigla'] = df_base['unimed'].fillna('').astype(str).str.upper().str.strip()\n", " df_base['desc_norm'] = df_base['descripcion'].apply(_ds_limpiar_texto)\n", " log(f'Catalogo base_numpartes: {len(df_base):,}')\n", "\n", " secuenciales_por_f4 = {}\n", " grupos = list(df.groupby(['fraccion4', 'um_sigla'], dropna=False))\n", " total_grupos = len(grupos)\n", " prog.setup(total_grupos, 'Procesando grupos...')\n", "\n", " for procesados, ((f4, um), g) in enumerate(grupos, start=1):\n", " idxs_g = list(g.index)\n", " textos_g = g['desc_norm'].tolist()\n", " base_sub = df_base[(df_base['fraccion4'] == f4) & (df_base['um_sigla'] == um)] \\\n", " if not df_base.empty else df_base\n", " sin_base_idx = []\n", " if not base_sub.empty and any(t for t in base_sub['desc_norm'].tolist()):\n", " try:\n", " vec = TfidfVectorizer(ngram_range=(1,2), sublinear_tf=True,\n", " min_df=1, max_features=20000)\n", " vec.fit(pd.concat([base_sub['desc_norm'],\n", " pd.Series(textos_g)], ignore_index=True))\n", " base_mat = vec.transform(base_sub['desc_norm'].tolist())\n", " grp_mat = vec.transform(textos_g)\n", " sims = cosine_similarity(grp_mat, base_mat)\n", " for j, idx in enumerate(idxs_g):\n", " best_j = int(sims[j].argmax())\n", " best_s = float(sims[j][best_j])\n", " if best_s >= umbral_sim and textos_g[j]:\n", " df.at[idx, 'NUMERO_PARTE'] = str(base_sub.iloc[best_j]['numparte'])\n", " df.at[idx, 'MATCH_TIPO'] = f'BASE ({best_s:.2f})'\n", " else:\n", " sin_base_idx.append(idx)\n", " except Exception as e:\n", " log(f' WARN TF-IDF base en grupo ({f4},{um}): {e}')\n", " sin_base_idx.extend(idxs_g)\n", " else:\n", " sin_base_idx.extend(idxs_g)\n", "\n", " if sin_base_idx:\n", " textos_h = [df.at[i, 'desc_norm'] for i in sin_base_idx]\n", " no_vacios = [(i, t) for i, t in zip(sin_base_idx, textos_h) if t]\n", " if no_vacios:\n", " idxs_h = [x[0] for x in no_vacios]\n", " txts_h = [x[1] for x in no_vacios]\n", " try:\n", " vec = TfidfVectorizer(ngram_range=(1,2), sublinear_tf=True,\n", " min_df=1, max_features=20000)\n", " mat = vec.fit_transform(txts_h)\n", " sims = cosine_similarity(mat, mat)\n", " parent = list(range(len(idxs_h)))\n", " def _find(x):\n", " while parent[x] != x:\n", " parent[x] = parent[parent[x]]; x = parent[x]\n", " return x\n", " for a in range(len(idxs_h)):\n", " for b in range(a+1, len(idxs_h)):\n", " if sims[a][b] >= umbral_sim:\n", " ra, rb = _find(a), _find(b)\n", " if ra != rb: parent[rb] = ra\n", " grupos_loc = {}\n", " for a in range(len(idxs_h)):\n", " grupos_loc.setdefault(_find(a), []).append(idxs_h[a])\n", " for miembros in grupos_loc.values():\n", " secuenciales_por_f4[f4] = secuenciales_por_f4.get(f4, 0) + 1\n", " nuevo_np = f\"MP{f4 or 'XXXX'}-R{secuenciales_por_f4[f4]:03d}\"\n", " for idx in miembros:\n", " df.at[idx, 'NUMERO_PARTE'] = nuevo_np\n", " df.at[idx, 'MATCH_TIPO'] = 'AUTO'\n", " except Exception as e:\n", " log(f' WARN union-find grupo ({f4},{um}): {e}')\n", " for idx in idxs_h:\n", " secuenciales_por_f4[f4] = secuenciales_por_f4.get(f4, 0) + 1\n", " df.at[idx, 'NUMERO_PARTE'] = f\"MP{f4 or 'XXXX'}-R{secuenciales_por_f4[f4]:03d}\"\n", " df.at[idx, 'MATCH_TIPO'] = 'AUTO'\n", " for idx in sin_base_idx:\n", " if df.at[idx, 'NUMERO_PARTE'] == '':\n", " secuenciales_por_f4[f4] = secuenciales_por_f4.get(f4, 0) + 1\n", " df.at[idx, 'NUMERO_PARTE'] = f\"MP{f4 or 'XXXX'}-R{secuenciales_por_f4[f4]:03d}\"\n", " df.at[idx, 'MATCH_TIPO'] = 'AUTO_SINDESC'\n", " prog.step(desc=f'{procesados}/{total_grupos}')\n", "\n", " df['anio_corto'] = pd.to_datetime(df['fecha_pago_real'], errors='coerce') \\\n", " .dt.year.astype(str).str[-2:]\n", " df['ped_full'] = (df['anio_corto'] + '-' + df['seccion_aduanera'].astype(str).str[:2]\n", " + '-' + df['patente'].astype(str).str.zfill(4)\n", " + '-' + df['pedimento'].astype(str).str.zfill(7))\n", " df['clave_eff'] = df.apply(\n", " lambda r: 'R1' if r['rectificado'] == 1\n", " else (r['clave_documento_501'] or r['clave_documento_551'] or ''),\n", " axis=1)\n", " # 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'
'\n", " f'

Sistema de Utilerias 2.0 SCAII

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

Conexion a SQL Server

'\n", " f'

Server: {SCAII_SERVER} | Usuario: {SCAII_USER}

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

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

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

Paso 8 - Pronostico de cobertura

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

Paso 9 - Descargas pendientes (NA - AC)

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

Paso 10 - Complementaria (sobre facturas AC)

'),\n", " W.HBox([modo10, dry10]), W.HBox([fd10, fh10]), facts_obj, btn_p10, bar_p10, out_p10,\n", " W.HTML('

Descarga por Fraccion / UM

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

Match por primeros 8 digitos de fraccion arancelaria + unidad de medida. Sin BOM.

'),\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, bar_frac, 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('

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

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

IMPO vs EXPO por a-o

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

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

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

Cantidades por a-o - IMPO vs EXPO

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

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

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

An-lisis SSaldoTem + IMPO vs EXPO

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

Sustitutos NLP - TF-IDF + coseno

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

Paso 12 - Descarga por % de KGS

'\n", " '

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

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

Paso 12 - Descargas pendientes KG (NA - AC)

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

Paso 12 - Complementaria KG (sobre facturas AC)

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

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

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

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

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

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

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

Resumen CTM (por factura + linea + componente)

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

Plan detalle (primeras 50 filas)

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

CTM - Reasignacion de descargas desde Cambio de Regimen

'\n", " '

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

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

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

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

Paso B - Ejecutar reasignacion en la base de datos

'\n", " '

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

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

Las fechas inicio/fin filtran SSaldoTem por FECHAFACTURA_ISO.

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

Resumen Saldos Vencidos

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

Plan detalle (primeras 50 filas)

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

Saldos Vencidos - Utileria Forma 5

'\n", " '

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

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

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

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

Paso B - Ejecutar prorrateo en la base de datos

'\n", " '

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

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

Resumen Saldos Vencidos (busqueda automatica)

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

Plan detalle (primeras 50 filas)

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

Modo Automatico - sin Excel

'\n", " '

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

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

Paso B - Ejecutar prorrateo automatico

'\n", " '

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

'),\n", " W.HBox([dry_sv2]),\n", " btn_sv2_ejecutar, bar_sv2_b,\n", " out_sv2_ejec,\n", "])\n", "\n", "\n", "# ===== Saldos Vencidos - Grafica por anio =====\n", "w_sv_metric = W.Dropdown(\n", " options=[('Valor MN ($)','SALDO_VMN'),('Valor ME (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('

Saldos vencidos por anio (vista general)

'\n", " '

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

'),\n", " W.HBox([w_sv_agrup, w_sv_metric]),\n", " W.HBox([w_sv_eje, w_sv_fdig, w_sv_topn]),\n", " W.HTML('Eje X aplica solo a \"Por Anio\". Fraccion digitos y Top N aplican solo a \"Por Fraccion\".'),\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'
'\n", " f'Postgres no disponible.
'\n", " f'{DATASTAGE_MSG}
'\n", " f'Revisa que el contenedor postgres-datastage este levantado y que el .env tenga DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD.
'))\n", " return False\n", " display(HTML(f'
{DATASTAGE_MSG}
'))\n", " return True\n", "\n", "def _on_ds_listar(_):\n", " with out_ds_lista:\n", " clear_output()\n", " if not _ds_msg_conexion(): return\n", " ruta = (w_ds_ruta.value or '').strip()\n", " if not ruta:\n", " print('ERROR: define la ruta de la carpeta con los .asc')\n", " return\n", " df = previsualizar_archivos_ds(ruta)\n", " if df.empty:\n", " print(f'No se encontraron .asc en: {ruta}')\n", " return\n", " print(f'Archivos detectados: {len(df):,}')\n", " display(df.head(200))\n", " if len(df) > 200:\n", " print(f'(mostrando 200 de {len(df)})')\n", " # Resumen por tabla destino\n", " resumen = (df.groupby('tabla_destino', as_index=False)\n", " .agg(archivos=('archivo','count'),\n", " tamanio_kb=('tamanio_kb','sum')))\n", " display(HTML('

Resumen por tabla destino

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

Resultado por archivo

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

DataStage - Subir archivos .asc a Postgres

'\n", " '

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

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

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

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

Log de carga

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

Resultado del TRUNCATE

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

{tabla} (primeras {limit})

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

Estadisticas de tablas Registro*

'\n", " '

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

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

Zona peligrosa

'\n", " '

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

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

Estructuras SCAII - Reportes de Pedimentos

'\n", " '

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

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

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

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

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

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

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

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

Equivalente al modal obtener_historial.php.

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

Encabezado de Facturas

'\n", " '

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

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

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

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

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

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

Tipo de Cambio

'\n", " '

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

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

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

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

Catalogo base de NUMPARTES

'\n", " '

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

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

Estructura de Partidas

'\n", " '

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

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

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

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

Resumen por pedimento

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

Plan detalle (primeras 50 filas)

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

Valores - Ajuste de VALORTOTALME / VALORTOTALMN

'\n", " '

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

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

Sube tu Excel:

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

Paso B - Ejecutar UPDATE en SPartidasExpo

'\n", " '

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

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

Donde esta cada pedimento

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

Resumen por base de datos

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

No encontrados en ninguna BD

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

Shelter - Buscar pedimentos en otras BDs

'\n", " '

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

'),\n", " W.HBox([btn_val_shelter]), bar_val_shelter,\n", " out_val_shelter,\n", "])\n", "\n", "\n", "\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 = ('
'\n", " 'Formato esperado del Excel (6 columnas)'\n", " ''\n", " ''\n", " ''\n", " ''\n", " ''\n", " ''\n", " ''\n", " ''\n", " '
ColumnaEjemplo
FACTURA_EXPOEXP240001
LINEA_EXPO1
NUMPARTEABC-12345
UNIMEDPZA
LINEA_EXPO_NUEVA2
FACTURA_NUEVAEXP240002
'\n", " '

Las 4 primeras identifican la descarga origen (debe ser UNICA en SDescargaT). Las 2 ultimas son el destino (debe existir en SFacExp+SPartidasExpo).

'\n", " '
')\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'
'\n", " f'Plantilla generada
'\n", " f'{path}
'))\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('

Plan OK (se aplicaran en Paso B)

'))\n", " display(_state['reasig_plan_ok'])\n", " if 'reasig_incons' in _state and not _state['reasig_incons'].empty:\n", " display(HTML('

Inconsistencias (NO se aplican)

'))\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('
Corre Analizar primero.
')); 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'
'\n", " f'Excel generado
'\n", " f'{path}
'))\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('

Reasignacion de Lineas EXPO

'\n", " '

Sube un Excel con las descargas a reasignar. La utileria '\n", " 'cambia FACTEXPO, FACREFERENCIA y LINEAEXPO 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.

'),\n", " W.HTML(_html_formato_reasig),\n", " btn_reasig_plantilla,\n", " W.HTML('

Sube tu Excel:

'),\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('

Paso B - Ejecutar reasignacion

'\n", " '

Solo aplica las filas del Plan OK. '\n", " 'Las inconsistencias se ignoran. Corre primero con DRY_RUN activado.

'),\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 = ('
'\n", " 'Formato esperado del Excel (4 columnas)'\n", " ''\n", " ''\n", " ''\n", " ''\n", " ''\n", " ''\n", " '
ColumnaEjemplo
NUMPARTE_COMPONENTECOMP-001
PCT_MATERIAL8.5
NUMPARTE_PTPT-ABC-123
UNIMEDKGS
'\n", " '

'\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", " '

')\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'
'\n", " f'Plantilla generada
'\n", " f'{path}
'))\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('

Resumen por PT

'))\n", " display(resumen)\n", " if not detalle.empty:\n", " display(HTML('

Partidas EXPO (primeras 200)

'))\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('

Resumen por TIPO

'))\n", " display(_state['csv_resumen'])\n", " if 'csv_plan' in _state and not _state['csv_plan'].empty:\n", " display(HTML('

Plan detalle (primeras 100)

'))\n", " display(_state['csv_plan'].head(100))\n", " if 'csv_incons' in _state and not _state['csv_incons'].empty:\n", " display(HTML('

Inconsistencias

'))\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('
Corre Analizar primero.
')); 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'
'\n", " f'Excel generado
'\n", " f'{path}
'))\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('

Consumo de Saldos Vencidos por Rango

'\n", " '

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 descargas complementarias usando el mismo motor del Paso 10/12.

'),\n", " W.HTML(_html_csv),\n", " btn_csv_plantilla,\n", " W.HTML('

Sube tu Excel y define el rango de vencimiento:

'),\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('

Paso B - Ejecutar consumo

'\n", " '

Inserta las descargas como complementarias. '\n", " 'NATURAL excluye facturas con faltantes; DIRIGIDA inserta lo que se pudo cubrir. '\n", " 'Corre primero con DRY_RUN activado.

'),\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('

Tabla detalle por anio y fraccion

'))\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('

Saldo IMPO disponible vs Descargas por anio y fraccion

'\n", " '

Cruza SSaldoTem (saldo IMPO disponible) y '\n", " 'SDescargaT (descargas) agrupando por anio y fraccion arancelaria. '\n", " 'Muestra solo las fracciones con mas actividad (Top N).

'),\n", " W.HBox([w_frac_topn]),\n", " W.HBox([w_frac_dig]),\n", " W.HBox([w_frac_met, btn_frac]),\n", " bar_frac, out_frac_log, out_frac_tabla,\n", "])\n", "\n", "\n", "tabs = W.Tab(children=[tab_conn, tab_desc, tab_an, tab_nlp, tab_kg, tab_ctm, tab_sv, tab_datastage, tab_estructuras, tab_valores])\n", "tabs.set_title(0, 'Conexion')\n", "tabs.set_title(1, 'Descargas')\n", "tabs.set_title(2, 'Analisis Saldos')\n", "tabs.set_title(3, 'Sustitutos NLP')\n", "tabs.set_title(4, 'Descarga % KGS')\n", "tabs.set_title(5, 'CTM')\n", "tabs.set_title(6, 'Saldos Vencidos')\n", "tabs.set_title(7, 'DataStage')\n", "tabs.set_title(8, 'Estructuras SCAII')\n", "tabs.set_title(9, 'Valores')\n", "display(header, tabs)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11" } }, "nbformat": 4, "nbformat_minor": 5 }