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