Subir archivos a "/"

This commit is contained in:
2026-07-01 20:40:09 +00:00
parent 8504a1ae89
commit 3ea6891cd3
5 changed files with 3538 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
import tkinter as tk
from tkinter import messagebox, filedialog
from typing import Dict, Type, Any
import gestor_pdf
import vistas
import config
from logger import get_logger
from controlador import Controlador
logger = get_logger(__name__)
class Aplicacion(tk.Tk):
def __init__(self) -> None:
super().__init__()
self.controlador = Controlador(self)
self.title(config.TITULO_APP)
# 1. Aplicamos minsize para evitar el colapso responsivo
self.minsize(width=900, height=650)
try:
self.iconbitmap(config.ICONO_PATH)
except Exception:
logger.warning("No se pudo cargar el icono de la aplicación.")
self.geometry(config.GEOMETRIA)
# BARRA DE MENÚ NATIVA (nace oculta hasta el login exitoso)
self.barra_menus = tk.Menu(self)
self.menu_archivo = tk.Menu(self.barra_menus, tearoff=0)
self.menu_archivo.add_command(label="Nueva plantilla", command=self.nueva_plantilla)
self.menu_archivo.add_command(label="Importar desde Excel...", command=self._abrir_filedialog_excel)
self.menu_archivo.add_separator()
self.menu_archivo.add_command(label="Volver al Home", command=lambda: self.mostrar_frame(vistas.PaginaTareas))
self.menu_archivo.add_separator()
self.menu_archivo.add_command(label="Cerrar Sesión", command=self.controlador.cerrar_sesion)
self.barra_menus.add_cascade(label="Archivo", menu=self.menu_archivo)
# Menú "Herramientas" — Backup BD
self.menu_herramientas = tk.Menu(self.barra_menus, tearoff=0)
self.menu_herramientas.add_command(label="Generar Backup de Base de Datos", command=self.controlador.crear_backup_bd)
self.barra_menus.add_cascade(label="Herramientas", menu=self.menu_herramientas)
# Botón directo "Manual de Usuario" en la barra superior
self.barra_menus.add_command(label="Manual de Usuario", command=lambda: self.mostrar_frame(vistas.PaginaManual))
# CONTENEDOR
self.contenedor = tk.Frame(self, bg="white")
self.contenedor.pack(side="top", fill="both", expand=True)
self.contenedor.columnconfigure(0, weight=1) # Permite que la columna 0 crezca
self.contenedor.rowconfigure(0, weight=1) # Permite que la fila 0 crezca
self.frames: Dict[Type[tk.Frame], tk.Frame] = {}
for Pagina in (vistas.PaginaLogin, vistas.PaginaTareas, vistas.PaginaRegistroUsuario, vistas.Plantilla, vistas.PaginaBuscadorHistorico, vistas.PaginaManual):
frame = Pagina(self.contenedor, self.controlador)
self.frames[Pagina] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.mostrar_frame(vistas.PaginaLogin)
# FUNCIONES DE NAVEGACIÓN
def mostrar_frame(self, nombre_clase: Type[tk.Frame]) -> None:
frame = self.frames[nombre_clase]
frame.tkraise()
# Forzar actualización de la vista al mostrarla (Corrige Bug de RBAC en Tkinter)
if hasattr(frame, "actualizar_vista"):
frame.actualizar_vista()
def nueva_plantilla(self) -> None:
self.controlador.nueva_plantilla()
def abrir_archivo(self) -> None:
self.controlador.abrir_archivo()
def guardar_archivo(self) -> bool:
return self.controlador.guardar_archivo()
def _abrir_filedialog_excel(self) -> None:
"""Abre un diálogo para seleccionar un archivo Excel e invoca la importación."""
ruta = filedialog.askopenfilename(
title="Seleccionar archivo Excel",
filetypes=(("Archivos Excel", "*.xlsx *.xlsm"), ("Todos los archivos", "*.*"))
)
if ruta:
self.controlador.importar_desde_excel(ruta)
if __name__ == "__main__":
app = Aplicacion()
app.mainloop()
+118
View File
@@ -0,0 +1,118 @@
import pyodbc
import config
from logger import get_logger
logger = get_logger(__name__)
def run_migration() -> bool:
"""
Ejecuta la migración de base de datos para agregar las columnas necesarias a SQL Server.
"""
conn_str = (
f"DRIVER={config.DB_DRIVER};"
f"SERVER={config.DB_SERVER};"
f"DATABASE={config.DB_NAME};"
f"Trusted_Connection=yes;"
)
conn = None
try:
conn = pyodbc.connect(conn_str)
cursor = conn.cursor()
# 1. Add 'tipo' column to Recursos if it doesn't exist
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'Recursos' AND column_name = 'tipo'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE Recursos ADD tipo NVARCHAR(50) NOT NULL DEFAULT 'MAQUINA'")
logger.info("Columna 'tipo' agregada a la tabla 'Recursos'.")
print("Agregada columna 'tipo' a 'Recursos'.")
# 2. Add 'id_hm' column to Relevamientos if it doesn't exist
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'Relevamientos' AND column_name = 'id_hm'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE Relevamientos ADD id_hm INT NULL CONSTRAINT FK_Rel_HM FOREIGN KEY (id_hm) REFERENCES Recursos(id_recurso)")
logger.info("Columna 'id_hm' agregada a la tabla 'Relevamientos'.")
print("Agregada columna 'id_hm' a 'Relevamientos'.")
# 3. Add 'piezas_por_golpe' column to Relevamientos if it doesn't exist
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'Relevamientos' AND column_name = 'piezas_por_golpe'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE Relevamientos ADD piezas_por_golpe INT NOT NULL DEFAULT 1")
logger.info("Columna 'piezas_por_golpe' agregada a la tabla 'Relevamientos'.")
print("Agregada columna 'piezas_por_golpe' a 'Relevamientos'.")
# 4. Add 'articulos_por_golpe' column to Relevamientos if it doesn't exist
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'Relevamientos' AND column_name = 'articulos_por_golpe'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE Relevamientos ADD articulos_por_golpe INT NOT NULL DEFAULT 1")
logger.info("Columna 'articulos_por_golpe' agregada a la tabla 'Relevamientos'.")
print("Agregada columna 'articulos_por_golpe' a 'Relevamientos'.")
# 5. Add 'operarios_maquina' column to Relevamientos if it doesn't exist
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'Relevamientos' AND column_name = 'operarios_maquina'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE Relevamientos ADD operarios_maquina INT NOT NULL DEFAULT 1")
logger.info("Columna 'operarios_maquina' agregada a la tabla 'Relevamientos'.")
print("Agregada columna 'operarios_maquina' a 'Relevamientos'.")
# 6. Add 'id_tarea' column to Relevamientos if it doesn't exist
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'Relevamientos' AND column_name = 'id_tarea'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE Relevamientos ADD id_tarea INT NULL CONSTRAINT FK_Rel_Tarea FOREIGN KEY (id_tarea) REFERENCES Tareas(id_tarea)")
logger.info("Columna 'id_tarea' agregada a la tabla 'Relevamientos'.")
print("Agregada columna 'id_tarea' a 'Relevamientos'.")
# 7. Add 'elementos_medidos' column to TiemposElemento if it doesn't exist
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'TiemposElemento' AND column_name = 'elementos_medidos'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE TiemposElemento ADD elementos_medidos INT NOT NULL DEFAULT 1")
logger.info("Columna 'elementos_medidos' agregada a la tabla 'TiemposElemento'.")
print("Agregada columna 'elementos_medidos' a 'TiemposElemento'.")
# 8. Add 'lote_frecuencial' column to TiemposElemento if it doesn't exist
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'TiemposElemento' AND column_name = 'lote_frecuencial'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE TiemposElemento ADD lote_frecuencial FLOAT NOT NULL DEFAULT 1.0")
logger.info("Columna 'lote_frecuencial' agregada a la tabla 'TiemposElemento'.")
print("Agregada columna 'lote_frecuencial' a 'TiemposElemento'.")
# 9. Add 'suplemento' column to TiemposElemento if it doesn't exist
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'TiemposElemento' AND column_name = 'suplemento'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE TiemposElemento ADD suplemento FLOAT NOT NULL DEFAULT 0.0")
logger.info("Columna 'suplemento' agregada a la tabla 'TiemposElemento'.")
print("Agregada columna 'suplemento' a 'TiemposElemento'.")
# 10. ALTER COLUMN minutos de INT a FLOAT en TiemposElemento (BUG 1: Truncamiento SQL)
cursor.execute("""
SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'TiemposElemento' AND COLUMN_NAME = 'minutos'
""")
col_type_row = cursor.fetchone()
if col_type_row and col_type_row[0].lower() != 'float':
cursor.execute("ALTER TABLE TiemposElemento ALTER COLUMN minutos FLOAT")
logger.info("Columna 'minutos' de TiemposElemento migrada de INT a FLOAT.")
print("Migrada columna 'minutos' a FLOAT en 'TiemposElemento'.")
# 11. ADD columna 'codigo' a Articulos (BUG 2: Desfase de Artículo)
cursor.execute("SELECT column_name FROM information_schema.columns WHERE table_name = 'Articulos' AND column_name = 'codigo'")
if not cursor.fetchone():
cursor.execute("ALTER TABLE Articulos ADD codigo NVARCHAR(50) NULL")
logger.info("Columna 'codigo' agregada a la tabla 'Articulos'.")
print("Agregada columna 'codigo' a 'Articulos'.")
conn.commit()
logger.info("Migración de base de datos completada exitosamente.")
print("Migración completada exitosamente.")
return True
except Exception as e:
if conn:
conn.rollback()
logger.exception("Error crítico durante la migración de base de datos.")
print(f"Error de migración: {e}")
return False
finally:
if conn:
conn.close()
if __name__ == "__main__":
run_migration()
+81
View File
@@ -0,0 +1,81 @@
from dataclasses import dataclass, field
from datetime import date
from typing import Optional, List
@dataclass
class Articulo:
id_articulo: str
codigo: str
descripcion: str
@dataclass
class Recurso:
id_recurso: str
nombre: str
tipo: str # e.g., 'MAQUINA', 'HM'
@dataclass
class Operario:
id_operario: str
nombre: str
legajo: str
@dataclass
class TiemposElemento:
elemento: str
valoracion: float
minutos: float
segundos: float
elementos_medidos: int = 1
lote_frecuencial: float = 1.0
suplemento: float = 0.0
@dataclass
class Relevamiento:
id_relevamiento: Optional[str] = None
id_tarea: Optional[int] = None
articulo: Optional[Articulo] = None
operacion: str = ""
recurso: Optional[Recurso] = None
hm: Optional[Recurso] = None
operario: Optional[Operario] = None
fecha: date = field(default_factory=date.today)
postura: str = "pie" # pie o sentado
piezas_por_golpe: int = 1
articulos_por_golpe: int = 1
operarios_maquina: int = 1
tiempos: List[TiemposElemento] = field(default_factory=list)
@dataclass
class Informe:
id_informe: Optional[str] = None
relevamiento: Optional[Relevamiento] = None
tiempo_estandar_op: float = 0.0
cadencia_ph: float = 0.0
cadencia_h1000: float = 0.0
estandar_epicor: float = 0.0
dotacion_epicor: int = 1
@dataclass
class Usuario:
"""
Representa un usuario del sistema para autenticación y RBAC.
"""
id_usuario: int
username: str
password_hash: str
rol: str # "METODISTA" o "JEFE/GERENTE"
nombre: str
@dataclass
class Tarea:
"""
Representa una tarea o solicitud de relevamiento a mostrar en el Dashboard.
"""
id_tarea: int
titulo: str
descripcion: str
estado: str # "Pendiente", "En Proceso", "Completado", "Finalizado", "Cerrado"
fecha: str
informe_url: str
id_usuario_asignado: Optional[int] = None
+49
View File
@@ -0,0 +1,49 @@
import bcrypt
from logger import get_logger
from dao import AuditoriaDAO
logger = get_logger(__name__)
class Seguridad:
"""
Clase utilitaria para el manejo de la criptografía de contraseñas
y auditoría de la aplicación.
"""
@staticmethod
def generar_hash(password_plano: str) -> str:
"""
Genera un hash seguro utilizando bcrypt.
"""
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_plano.encode('utf-8'), salt)
return hashed.decode('utf-8')
@staticmethod
def verificar_password(password_plano: str, password_hash: str) -> bool:
"""
Verifica una contraseña en texto plano contra su hash almacenado.
"""
try:
return bcrypt.checkpw(password_plano.encode('utf-8'), password_hash.encode('utf-8'))
except Exception:
logger.exception("Error al verificar hash bcrypt (posible formato inválido).")
return False
@staticmethod
def registrar_auditoria(id_usuario: int, accion: str) -> bool:
"""
Registra una acción en la tabla LogsAuditoria.
Args:
id_usuario: ID del usuario que realiza la acción.
accion: Descripción de la acción realizada.
Returns:
bool: True si el registro fue exitoso.
"""
try:
return AuditoriaDAO.insertar_log(id_usuario, accion)
except Exception:
logger.exception("Fallo al registrar auditoría en la capa de seguridad.")
return False
+3199
View File
File diff suppressed because it is too large Load Diff