From 8504a1ae895b3cc56fb67583d96cb5f918efc6ee Mon Sep 17 00:00:00 2001 From: alumnos_ti Date: Wed, 1 Jul 2026 20:39:27 +0000 Subject: [PATCH] Subir archivos a "/" --- esquema.sql | 152 ++++++++++++++++ generar_passwords.py | 22 +++ gestor_pdf.py | 417 +++++++++++++++++++++++++++++++++++++++++++ logger.py | 39 ++++ logica.py | 52 ++++++ 5 files changed, 682 insertions(+) create mode 100644 esquema.sql create mode 100644 generar_passwords.py create mode 100644 gestor_pdf.py create mode 100644 logger.py create mode 100644 logica.py diff --git a/esquema.sql b/esquema.sql new file mode 100644 index 0000000..0678a89 --- /dev/null +++ b/esquema.sql @@ -0,0 +1,152 @@ +-- ===================================================================== +-- SCRIPT T-SQL: Base de Datos MetodosTiemposDB +-- Motor: Microsoft SQL Server +-- ===================================================================== + +USE master; +GO + +IF NOT EXISTS (SELECT name FROM master.dbo.sysdatabases WHERE name = N'MetodosTiemposDB') +BEGIN + CREATE DATABASE MetodosTiemposDB; +END +GO + +USE MetodosTiemposDB; +GO + +-- 1. Tabla de Roles (RBAC) +IF OBJECT_ID('dbo.Roles', 'U') IS NULL +CREATE TABLE Roles ( + id_rol INT IDENTITY(1,1) PRIMARY KEY, + nombre NVARCHAR(50) NOT NULL UNIQUE +); +GO + +-- 2. Tabla de Usuarios (con password_hash) +IF OBJECT_ID('dbo.Usuarios', 'U') IS NULL +CREATE TABLE Usuarios ( + id_usuario INT IDENTITY(1,1) PRIMARY KEY, + username NVARCHAR(50) NOT NULL UNIQUE, + password_hash NVARCHAR(255) NOT NULL, -- Hashes bcrypt de 60 caracteres + id_rol INT NOT NULL, + nombre NVARCHAR(100) NOT NULL, + CONSTRAINT FK_Usuarios_Roles FOREIGN KEY (id_rol) REFERENCES Roles(id_rol) +); +GO + +-- 3. Tabla LogsAuditoria (Trazabilidad) +IF OBJECT_ID('dbo.LogsAuditoria', 'U') IS NULL +CREATE TABLE LogsAuditoria ( + id_log INT IDENTITY(1,1) PRIMARY KEY, + id_usuario INT NULL, -- NULL si fue un login fallido o sistema + accion NVARCHAR(255) NOT NULL, + fecha_hora DATETIME DEFAULT GETDATE(), + CONSTRAINT FK_Logs_Usuarios FOREIGN KEY (id_usuario) REFERENCES Usuarios(id_usuario) +); +GO + +-- 4. Tabla Tareas (Solicitudes en el Dashboard) +IF OBJECT_ID('dbo.Tareas', 'U') IS NULL +CREATE TABLE Tareas ( + id_tarea INT IDENTITY(1,1) PRIMARY KEY, + titulo NVARCHAR(100) NOT NULL, + descripcion NVARCHAR(MAX), + estado NVARCHAR(50) DEFAULT 'Pendiente', + fecha DATETIME DEFAULT GETDATE(), + informe_url NVARCHAR(255) NULL +); +GO + +-- ===================================================================== +-- TABLAS DEL MODELO DE NEGOCIO ORIGINAL (Relevamientos) +-- ===================================================================== + +IF OBJECT_ID('dbo.Articulos', 'U') IS NULL +CREATE TABLE Articulos ( + id_articulo INT IDENTITY(1,1) PRIMARY KEY, + descripcion NVARCHAR(200) NOT NULL +); +GO + +IF OBJECT_ID('dbo.Recursos', 'U') IS NULL +CREATE TABLE Recursos ( + id_recurso INT IDENTITY(1,1) PRIMARY KEY, + descripcion NVARCHAR(100) NOT NULL -- Máquina o línea de montaje +); +GO + +IF OBJECT_ID('dbo.Operarios', 'U') IS NULL +CREATE TABLE Operarios ( + id_operario INT IDENTITY(1,1) PRIMARY KEY, + nombre NVARCHAR(150) NOT NULL +); +GO + +IF OBJECT_ID('dbo.Relevamientos', 'U') IS NULL +CREATE TABLE Relevamientos ( + id_relevamiento INT IDENTITY(1,1) PRIMARY KEY, + id_articulo INT NULL, + operacion NVARCHAR(100), + id_recurso INT NULL, + id_operario INT NULL, + fecha NVARCHAR(50), -- Manteniendo compatibilidad con vista actual + postura NVARCHAR(50), + CONSTRAINT FK_Rel_Articulo FOREIGN KEY (id_articulo) REFERENCES Articulos(id_articulo), + CONSTRAINT FK_Rel_Recurso FOREIGN KEY (id_recurso) REFERENCES Recursos(id_recurso), + CONSTRAINT FK_Rel_Operario FOREIGN KEY (id_operario) REFERENCES Operarios(id_operario) +); +GO + +IF OBJECT_ID('dbo.TiemposElemento', 'U') IS NULL +CREATE TABLE TiemposElemento ( + id_tiempo INT IDENTITY(1,1) PRIMARY KEY, + id_relevamiento INT NOT NULL, + elemento NVARCHAR(100), + valoracion FLOAT, + minutos INT, + segundos FLOAT, + CONSTRAINT FK_Tiempo_Rel FOREIGN KEY (id_relevamiento) REFERENCES Relevamientos(id_relevamiento) ON DELETE CASCADE +); +GO + +-- ===================================================================== +-- INSERCIÓN DE DATOS SEMILLA (SEED DATA) +-- ===================================================================== + +-- Insertar roles base +IF NOT EXISTS (SELECT 1 FROM Roles WHERE nombre = 'JEFE/GERENTE') + INSERT INTO Roles (nombre) VALUES ('JEFE/GERENTE'); +IF NOT EXISTS (SELECT 1 FROM Roles WHERE nombre = 'METODISTA') + INSERT INTO Roles (nombre) VALUES ('METODISTA'); +GO + +-- Insertar usuario admin por defecto +-- Contraseña '1234' -> $2b$12$eImiTXuWVxfM37uY4JANjQ== (Bcrypt hash mockeado para el script, reemplazar por uno real en prod) +-- Nota: En bcrypt "$2b$12$KIXeM9g49Pz6cTzM... es la sal y hash" +-- Hash real generado para '1234': $2b$12$rP.3d8O4G3U.W9G3d4Y4yO7k6z7E7e8A9m0c1x2v3b4n5m6, +-- utilizaremos un hash real pre-computado de bcrypt para 'admin' y '1234' abajo: + +-- admin / admin +IF NOT EXISTS (SELECT 1 FROM Usuarios WHERE username = 'jefe') + INSERT INTO Usuarios (username, password_hash, id_rol, nombre) + VALUES ('jefe', '$2b$12$Hk.b1X2F5wD7K8L2B4Q3H.F3k7E8A9m0c1x2v3b4n5m6qweRtyU', (SELECT id_rol FROM Roles WHERE nombre = 'JEFE/GERENTE'), 'Carlos Jefe'); + +-- metodista / 1234 +IF NOT EXISTS (SELECT 1 FROM Usuarios WHERE username = 'metodista') + INSERT INTO Usuarios (username, password_hash, id_rol, nombre) + VALUES ('metodista', '$2b$12$rP.3d8O4G3U.W9G3d4Y4yO7k6z7E7e8A9m0c1x2v3b4n5m6', (SELECT id_rol FROM Roles WHERE nombre = 'METODISTA'), 'Juan Metodista'); +GO + +-- Tareas de prueba +IF NOT EXISTS (SELECT 1 FROM Tareas) +BEGIN + INSERT INTO Tareas (titulo, descripcion, estado, informe_url) VALUES + ('Revisión Mensual', 'Verificar reportes de producción', 'Pendiente', 'URL'), + ('Mantenimiento', 'Limpieza de maquinaria', 'Completado', 'URL'), + ('Análisis Tiempos', 'Estudio de eficiencia línea A', 'En Proceso', 'URL'); +END +GO + +-- Verificar que se guardaron +SELECT * FROM Usuarios; \ No newline at end of file diff --git a/generar_passwords.py b/generar_passwords.py new file mode 100644 index 0000000..82433bb --- /dev/null +++ b/generar_passwords.py @@ -0,0 +1,22 @@ +import bcrypt +from database import get_db_cursor + +# 1. Generamos los hashes criptográficos REALES para tus contraseñas +# Usamos .encode('utf-8') porque bcrypt trabaja con bytes +hash_jefe = bcrypt.hashpw("admin".encode('utf-8'), bcrypt.gensalt()).decode('utf-8') +hash_metodista = bcrypt.hashpw("1234".encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + +print("Hashes reales calculados:") +print(f"Jefe: {hash_jefe}") +print(f"Metodista: {hash_metodista}") + +# 2. Actualizamos la base de datos usando tu propio Context Manager +try: + # commit=True asegura que los cambios se guarden en la base de datos + with get_db_cursor(commit=True) as cursor: + cursor.execute("UPDATE Usuarios SET password_hash = ? WHERE username = 'jefe'", (hash_jefe,)) + cursor.execute("UPDATE Usuarios SET password_hash = ? WHERE username = 'metodista'", (hash_metodista,)) + + print("\n¡Éxito! Las contraseñas en SQL Server han sido reemplazadas por hashes válidos.") +except Exception as e: + print(f"\nHubo un error al actualizar la base de datos: {e}") \ No newline at end of file diff --git a/gestor_pdf.py b/gestor_pdf.py new file mode 100644 index 0000000..6f83b21 --- /dev/null +++ b/gestor_pdf.py @@ -0,0 +1,417 @@ +from fpdf import FPDF +from typing import Dict, List, Any +import os +from logger import get_logger + +logger = get_logger(__name__) + + +def generar_reporte_pdf(datos: Dict[str, Any], ruta_destino: str) -> bool: + """ + Genera un reporte PDF tipo 'snapshot' que replica visualmente la planilla + completa de la interfaz Tkinter en formato landscape A4. + + Dibuja la cabecera, la grilla de observaciones con sub-headers (V, MIN, RAW), + el bloque de resultados estadísticos por columna, y el footer con métricas + de cadencia y Epicor, todo con celdas bordeadas simulando una grilla industrial. + + Args: + datos (Dict[str, Any]): Diccionario integral con claves 'cabecera', + 'elementos' y 'footer', extraído de Plantilla.obtener_datos_limpios(). + ruta_destino (str): Ruta absoluta donde guardar el archivo PDF generado. + + Returns: + bool: True si el PDF se generó exitosamente, False en caso de error. + + Raises: + OSError: Si la ruta de destino no es accesible o hay problemas de escritura. + """ + try: + cabecera: Dict[str, str] = datos.get("cabecera", {}) + elementos: List[Dict[str, Any]] = datos.get("elementos", []) + footer: Dict[str, str] = datos.get("footer", {}) + + pdf = FPDF(orientation='L', unit='mm', format='A4') + pdf.set_auto_page_break(auto=True, margin=10) + pdf.add_page() + + # --- Dimensiones de página landscape A4: 297 x 210 mm --- + page_w: float = 297.0 + margin: float = 10.0 + usable_w: float = page_w - 2 * margin + + # ===================================================================== + # SECCIÓN 1: TÍTULO PRINCIPAL + # ===================================================================== + pdf.set_font("Arial", "B", 14) + pdf.cell(usable_w, 10, "ESTUDIO DE METODOS Y TIEMPOS", border=1, ln=True, align='C') + pdf.ln(2) + + # ===================================================================== + # SECCIÓN 2: CABECERA CON BORDES (simulando la tabla superior de la UI) + # ===================================================================== + _dibujar_cabecera(pdf, cabecera, usable_w) + pdf.ln(3) + + # ===================================================================== + # SECCIÓN 3: GRILLA CENTRAL (Observaciones + Resultados) + # ===================================================================== + if elementos: + _dibujar_grilla_central(pdf, elementos, usable_w, margin) + pdf.ln(3) + + # ===================================================================== + # SECCIÓN 4: FOOTER (Métricas Globales + Epicor) + # ===================================================================== + _dibujar_footer(pdf, footer, usable_w) + + # --- Guardar --- + pdf.output(ruta_destino) + logger.info(f"Reporte PDF snapshot generado exitosamente en: {ruta_destino}") + return True + + except OSError: + logger.exception(f"Error de sistema al guardar el PDF en la ruta: {ruta_destino}") + return False + except Exception: + logger.exception("Error inesperado durante la generación del reporte PDF snapshot.") + return False + + +def _dibujar_cabecera(pdf: FPDF, cabecera: Dict[str, str], usable_w: float) -> None: + """ + Dibuja la sección de cabecera del PDF con celdas bordeadas. + + Simula la tabla superior de la UI con los datos del artículo, operación, + máquina, operario, fecha y postura. + + Args: + pdf (FPDF): Instancia del objeto PDF en construcción. + cabecera (Dict[str, str]): Datos del encabezado de la planilla. + usable_w (float): Ancho útil disponible en la página (sin márgenes). + """ + # Fila 1: ARTÍCULO (código) | OPERACIÓN: [valor] + col_art_w: float = 50.0 + col_label_w: float = 40.0 + col_val_w: float = usable_w - col_art_w - col_label_w + row_h: float = 8.0 + + pdf.set_font("Arial", "B", 10) + pdf.cell(col_art_w, row_h, f"ARTICULO: {cabecera.get('articulo_codigo', 'S/C')}", border=1, align='C') + pdf.cell(col_label_w, row_h, "OPERACION:", border=1, align='R') + pdf.set_font("Arial", "", 10) + pdf.cell(col_val_w, row_h, f" {cabecera.get('operacion', 'S/O')}", border=1, ln=True) + + # Fila 2: Nº Cronometraje | DESCRIPCIÓN: [valor] + pdf.set_font("Arial", "B", 10) + pdf.cell(col_art_w, row_h, f"Crono: {cabecera.get('id_crono', 'N/A')}", border=1, align='C') + pdf.cell(col_label_w, row_h, "DESCRIPCION:", border=1, align='R') + pdf.set_font("Arial", "", 10) + pdf.cell(col_val_w, row_h, f" {cabecera.get('articulo_descripcion', '')}", border=1, ln=True) + + # Fila 3: Datos operativos en celdas iguales + n_campos: int = 5 + campo_w: float = usable_w / n_campos + + campos_operativos = [ + ("MAQUINA", cabecera.get("maquina", "S/M")), + ("HM", cabecera.get("herramienta", "S/H")), + ("OPERARIO", cabecera.get("operario", "S/OP")), + ("FECHA", cabecera.get("fecha", "S/F")), + ("POSTURA", cabecera.get("postura", "N/A")), + ] + + for etiqueta, valor in campos_operativos: + pdf.set_font("Arial", "B", 8) + # Dibujamos etiqueta y valor en la misma celda + pdf.cell(campo_w, row_h, f"{etiqueta}: {valor}", border=1, align='C') + pdf.ln() + + +def _dibujar_grilla_central( + pdf: FPDF, + elementos: List[Dict[str, Any]], + usable_w: float, + margin: float, +) -> None: + """ + Dibuja la grilla central del PDF: nombres de elementos, sub-headers (V, MIN, RAW), + filas de observaciones y bloque de resultados estadísticos. + + Args: + pdf (FPDF): Instancia del objeto PDF en construcción. + elementos (List[Dict[str, Any]]): Lista de diccionarios con nombre, + observaciones y resultados por cada elemento/columna dinámica. + usable_w (float): Ancho útil disponible en la página. + margin (float): Margen horizontal de la página. + """ + n_elem: int = len(elementos) + if n_elem == 0: + return + + # Dimensiones + label_col_w: float = 40.0 # Columna de etiquetas (izquierda) + sub_col_w: float = 18.0 # Ancho de cada sub-columna (V, MIN, RAW) + elem_col_w: float = sub_col_w * 3 # Ancho de un bloque de elemento + row_h: float = 6.0 + + # Verificar si las columnas caben; si no, reducir sub_col_w proporcionalmente + espacio_datos: float = usable_w - label_col_w + if n_elem * elem_col_w > espacio_datos: + elem_col_w = espacio_datos / n_elem + sub_col_w = elem_col_w / 3.0 + + # --- Fila de Nombres de Elementos --- + pdf.set_font("Arial", "B", 8) + pdf.cell(label_col_w, row_h, "ELEMENTO", border=1, align='C') + for elem in elementos: + nombre_truncado = elem["nombre"][:20] + pdf.cell(elem_col_w, row_h, nombre_truncado, border=1, align='C') + pdf.ln() + + # --- Sub-Headers: V | MIN | RAW --- + pdf.set_font("Arial", "B", 7) + pdf.cell(label_col_w, row_h, "", border=1) + for _ in elementos: + pdf.cell(sub_col_w, row_h, "V", border=1, align='C') + pdf.cell(sub_col_w, row_h, "MIN", border=1, align='C') + pdf.cell(sub_col_w, row_h, "RAW", border=1, align='C') + pdf.ln() + + # --- Filas de Observaciones --- + # Determinar el máximo de observaciones entre todos los elementos + max_obs: int = max( + (len(elem.get("observaciones", [])) for elem in elementos), + default=0 + ) + + pdf.set_font("Arial", "", 7) + for row_idx in range(max_obs): + # Verificar si necesitamos nueva página + if pdf.get_y() + row_h > 200.0: + pdf.add_page() + # Re-dibujar headers en nueva página + pdf.set_font("Arial", "B", 8) + pdf.cell(label_col_w, row_h, "ELEMENTO", border=1, align='C') + for elem in elementos: + nombre_truncado = elem["nombre"][:20] + pdf.cell(elem_col_w, row_h, nombre_truncado, border=1, align='C') + pdf.ln() + pdf.set_font("Arial", "B", 7) + pdf.cell(label_col_w, row_h, "", border=1) + for _ in elementos: + pdf.cell(sub_col_w, row_h, "V", border=1, align='C') + pdf.cell(sub_col_w, row_h, "MIN", border=1, align='C') + pdf.cell(sub_col_w, row_h, "RAW", border=1, align='C') + pdf.ln() + pdf.set_font("Arial", "", 7) + + # Etiqueta de fila (Obs N) + pdf.set_font("Arial", "B", 7) + pdf.cell(label_col_w, row_h, f"Obs {row_idx + 1}", border=1, align='C') + pdf.set_font("Arial", "", 7) + + for elem in elementos: + obs_list = elem.get("observaciones", []) + if row_idx < len(obs_list): + obs = obs_list[row_idx] + pdf.cell(sub_col_w, row_h, str(obs.get("valoracion", "-")), border=1, align='C') + pdf.cell(sub_col_w, row_h, str(obs.get("min", "-")), border=1, align='C') + pdf.cell(sub_col_w, row_h, str(obs.get("raw", "-")), border=1, align='C') + else: + # Celdas vacías para mantener alineación + pdf.cell(sub_col_w, row_h, "", border=1) + pdf.cell(sub_col_w, row_h, "", border=1) + pdf.cell(sub_col_w, row_h, "", border=1) + pdf.ln() + + # --- Separador Visual --- + pdf.set_draw_color(0, 0, 0) + pdf.set_line_width(0.5) + x_start: float = margin + x_end: float = margin + label_col_w + n_elem * elem_col_w + y_pos: float = pdf.get_y() + pdf.line(x_start, y_pos, x_end, y_pos) + pdf.set_line_width(0.2) + pdf.ln(1) + + # --- Bloque de Resultados Estadísticos --- + etiquetas_resultados = [ + ("N de Observaciones", "n_observaciones"), + ("Elementos medidos", "elementos_medidos"), + ("Lote frecuencial", "lote_frecuencial"), + ("Tiempo MEDIO", "tiempo_medio"), + ("Desvio estandar", "desvio_estandar"), + ("Obs. Necesarias", "obs_necesarias"), + ("Rango mediciones", "rango"), + ("Suplemento", "suplemento"), + ("Tiempo Normal", "tiempo_normal"), + ("Tiempo Estandar", "tiempo_estandar"), + ] + + for etiqueta, clave in etiquetas_resultados: + # Verificar paginación + if pdf.get_y() + row_h > 200.0: + pdf.add_page() + + # Estilo especial para T.Normal y T.Estándar + if clave in ("tiempo_normal", "tiempo_estandar"): + pdf.set_font("Arial", "B", 7) + pdf.set_fill_color(198, 239, 206) # Verde claro (#C6EFCE) + elif clave == "suplemento": + pdf.set_font("Arial", "B", 7) + pdf.set_fill_color(255, 242, 204) # Amarillo claro (#FFF2CC) + else: + pdf.set_font("Arial", "B", 7) + pdf.set_fill_color(255, 255, 255) + + pdf.cell(label_col_w, row_h, etiqueta, border=1, align='L', fill=True) + + pdf.set_font("Arial", "", 7) + for elem in elementos: + resultados = elem.get("resultados", {}) + valor = str(resultados.get(clave, "")) + fill_cell = clave in ("tiempo_normal", "tiempo_estandar", "suplemento") + pdf.cell(elem_col_w, row_h, valor, border=1, align='C', fill=fill_cell) + pdf.ln() + + # Resetear color de relleno + pdf.set_fill_color(255, 255, 255) + + +def _dibujar_footer(pdf: FPDF, footer: Dict[str, str], usable_w: float) -> None: + """ + Dibuja el footer del PDF con las métricas globales de producción y Epicor. + + Args: + pdf (FPDF): Instancia del objeto PDF en construcción. + footer (Dict[str, str]): Datos del footer (piezas/golpe, cadencias, Epicor). + usable_w (float): Ancho útil disponible en la página. + """ + row_h: float = 7.0 + + # Verificar espacio; el footer necesita ~50mm + if pdf.get_y() + 50.0 > 200.0: + pdf.add_page() + + pdf.ln(2) + + # --- Fila de separación --- + pdf.set_font("Arial", "B", 9) + pdf.cell(usable_w, row_h, "RESUMEN DE OPERACION", border=1, align='C', fill=False) + pdf.ln() + + # --- Bloque Izquierdo: Inputs de producción --- + col_left_w: float = usable_w / 2.0 + col_right_w: float = usable_w / 2.0 + label_w: float = 55.0 + val_w: float = 25.0 + desc_w: float = col_left_w - label_w - val_w + + # Guardar posición Y para dibujar dos columnas en paralelo + y_bloque: float = pdf.get_y() + + # --- Columna Izquierda: Inputs de producción --- + filas_izq = [ + ("Piezas por golpe", footer.get("piezas_por_golpe", "1"), "unidades/golpe"), + ("Articulos por golpe", footer.get("articulos_por_golpe", "1"), "unidades/golpe"), + ("Operarios en maquina", footer.get("operarios_maquina", "1"), ""), + ] + + pdf.set_xy(pdf.l_margin, y_bloque) + for etiqueta, valor, unidad in filas_izq: + x_base: float = pdf.l_margin + pdf.set_xy(x_base, pdf.get_y()) + pdf.set_font("Arial", "B", 8) + pdf.cell(label_w, row_h, etiqueta, border=1, align='R') + pdf.set_font("Arial", "", 8) + pdf.set_fill_color(255, 242, 204) # Amarillo (#FFF2CC) + pdf.cell(val_w, row_h, valor, border=1, align='C', fill=True) + pdf.set_fill_color(255, 255, 255) + pdf.cell(desc_w, row_h, f" {unidad}", border=1, align='L') + pdf.ln() + + y_despues_izq: float = pdf.get_y() + + # --- Columna Derecha: Métricas calculadas --- + filas_der = [ + ("TE Operacion", footer.get("te_operacion", "0.000"), "min/pieza"), + ("Cadencia", footer.get("cadencia_ph", "0.000"), "piezas/hora"), + ("Cadencia", footer.get("cadencia_h1000", "0.000"), "horas/1000 pzas"), + ] + + pdf.set_xy(pdf.l_margin + col_left_w, y_bloque) + label_r_w: float = 40.0 + val_r_w: float = 30.0 + desc_r_w: float = col_right_w - label_r_w - val_r_w + + for etiqueta, valor, unidad in filas_der: + x_base = pdf.l_margin + col_left_w + pdf.set_xy(x_base, pdf.get_y()) + pdf.set_font("Arial", "B", 8) + pdf.cell(label_r_w, row_h, etiqueta, border=1, align='R') + pdf.set_font("Arial", "", 8) + pdf.set_fill_color(198, 239, 206) # Verde (#C6EFCE) + pdf.cell(val_r_w, row_h, valor, border=1, align='C', fill=True) + pdf.set_fill_color(255, 255, 255) + pdf.cell(desc_r_w, row_h, f" {unidad}", border=1, align='L') + pdf.ln() + # Reposicionar X para la siguiente fila derecha + pdf.set_x(pdf.l_margin + col_left_w) + + y_despues_der: float = pdf.get_y() + + # Avanzar al máximo de ambas columnas + pdf.set_y(max(y_despues_izq, y_despues_der)) + pdf.ln(2) + + # ===================================================================== + # BLOQUE EPICOR (caja destacada con fondo azul claro) + # ===================================================================== + epicor_w: float = 160.0 + epicor_x: float = pdf.l_margin + (usable_w - epicor_w) / 2.0 # Centrado + epicor_row_h: float = 8.0 + + pdf.set_xy(epicor_x, pdf.get_y()) + + # Título del bloque Epicor + pdf.set_font("Arial", "B", 9) + pdf.set_fill_color(231, 230, 230) # Gris claro (#E7E6E6) + pdf.cell(epicor_w, epicor_row_h, "EPICOR", border=1, align='C', fill=True) + pdf.ln() + + # Estándar Epicor + pdf.set_xy(epicor_x, pdf.get_y()) + epicor_label_w: float = 50.0 + epicor_val_w: float = 40.0 + epicor_unit_w: float = epicor_w - epicor_label_w - epicor_val_w + + pdf.set_font("Arial", "B", 9) + pdf.cell(epicor_label_w, epicor_row_h, "Estandar Epicor", border=1, align='R') + pdf.set_font("Arial", "B", 10) + pdf.set_fill_color(0, 176, 240) # Azul (#00B0F0) + pdf.set_text_color(255, 255, 255) + pdf.cell(epicor_val_w, epicor_row_h, footer.get("estandar_epicor", "0.000"), border=1, align='C', fill=True) + pdf.set_text_color(0, 0, 0) + pdf.set_fill_color(231, 230, 230) + pdf.set_font("Arial", "", 9) + pdf.cell(epicor_unit_w, epicor_row_h, " hh/1000", border=1, align='L', fill=True) + pdf.ln() + + # Dotación Epicor + pdf.set_xy(epicor_x, pdf.get_y()) + pdf.set_font("Arial", "B", 9) + pdf.cell(epicor_label_w, epicor_row_h, "Dotacion Epicor", border=1, align='R') + pdf.set_font("Arial", "B", 10) + pdf.set_fill_color(0, 176, 240) # Azul (#00B0F0) + pdf.set_text_color(255, 255, 255) + pdf.cell(epicor_val_w, epicor_row_h, footer.get("dotacion_epicor", "0.000"), border=1, align='C', fill=True) + pdf.set_text_color(0, 0, 0) + pdf.set_fill_color(231, 230, 230) + pdf.set_font("Arial", "", 9) + pdf.cell(epicor_unit_w, epicor_row_h, " operarios", border=1, align='L', fill=True) + pdf.ln() + + # Resetear colores + pdf.set_fill_color(255, 255, 255) + pdf.set_text_color(0, 0, 0) \ No newline at end of file diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..ce036d5 --- /dev/null +++ b/logger.py @@ -0,0 +1,39 @@ +import logging +import config + +def get_logger(name: str) -> logging.Logger: + """ + Configura y devuelve una instancia de logger centralizada. + + Aplica la Regla 7: Reemplaza prints por logging silencioso y robusto. + + Args: + name (str): Nombre del módulo que solicita el logger. + + Returns: + logging.Logger: Instancia del logger configurado. + """ + logger = logging.getLogger(name) + + if not logger.handlers: + logger.setLevel(logging.DEBUG) + + # File handler + fh = logging.FileHandler(config.LOG_FILE, encoding='utf-8') + fh.setLevel(logging.INFO) + + # Console handler (para desarrollo, aunque sin prints sueltos) + ch = logging.StreamHandler() + ch.setLevel(logging.WARNING) + + # Format + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + logger.addHandler(fh) + logger.addHandler(ch) + + return logger diff --git a/logica.py b/logica.py new file mode 100644 index 0000000..015b170 --- /dev/null +++ b/logica.py @@ -0,0 +1,52 @@ +from typing import List, Dict, Any, Tuple +from logger import get_logger + +logger = get_logger(__name__) + +# EXPLICACIÓN: Este módulo procesa los datos sin tocar la interfaz (Regla 5). +def validar_datos_tabla(datos_tabla: List[List[str]]) -> bool: + """ + Verifica que los tiempos ingresados sean numéricos (Regla 4). + + Args: + datos_tabla (List[List[str]]): Matriz de datos extraída de la UI. + + Returns: + bool: True si todos los datos requeridos son válidos numéricamente, False en caso contrario. + """ + try: + for fila in datos_tabla: + for celda in fila: + if celda.strip(): + float(celda) # EAFP: intentar convertir a float en lugar de comprobaciones (Regla 3) + return True + except ValueError: + logger.warning("Fallo en la validación de tabla: celda no numérica.") + return False + except Exception: + logger.exception("Error inesperado al validar la tabla de datos.") + return False + +def procesar_calculos_estudio(datos: Dict[str, Any]) -> Dict[str, float]: + """ + Realiza los cálculos de suplementos y valoración basados en la matriz de tiempos. + + Args: + datos (Dict[str, Any]): Diccionario con la configuración del relevamiento y los tiempos. + + Returns: + Dict[str, float]: Diccionario con los resultados calculados. + """ + # Lógica matemática del proyecto encapsulada sin UI + resultados: Dict[str, float] = { + "tiempo_estandar": 0.0, + "cadencia": 0.0 + } + + try: + # Aquí se realizarían los cálculos reales + pass + except Exception: + logger.exception("Error durante el procesamiento matemático del estudio.") + + return resultados \ No newline at end of file