Subir archivos a "/"
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user