Subir archivos a "/"

This commit is contained in:
2026-07-01 20:39:27 +00:00
parent c9940ec377
commit 8504a1ae89
5 changed files with 682 additions and 0 deletions
+152
View File
@@ -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;
+22
View File
@@ -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}")
+417
View File
@@ -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)
+39
View File
@@ -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
+52
View File
@@ -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