Subir archivos a "/"
This commit is contained in:
@@ -0,0 +1,906 @@
|
||||
from typing import Dict, List, Optional
|
||||
from database import get_db_cursor
|
||||
from modelos import Articulo, Operario, Recurso, Relevamiento, Usuario, Tarea
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class ArticuloDAO:
|
||||
@staticmethod
|
||||
def obtener_todos() -> List[Articulo]:
|
||||
"""Obtiene todos los artículos usando consultas parametrizadas (Regla 6)."""
|
||||
query = "SELECT id_articulo, ISNULL(codigo, '') AS codigo, descripcion FROM Articulos"
|
||||
articulos: List[Articulo] = []
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
for row in cursor.fetchall():
|
||||
articulos.append(Articulo(id_articulo=row[0], codigo=row[1], descripcion=row[2]))
|
||||
except Exception:
|
||||
logger.exception("Falló la obtención de artículos.")
|
||||
return articulos
|
||||
|
||||
@staticmethod
|
||||
def obtener_por_codigo(codigo: str) -> Optional[Articulo]:
|
||||
"""
|
||||
Obtiene un artículo por su código alfanumérico usando consulta parametrizada (Regla 6).
|
||||
|
||||
Args:
|
||||
codigo: Código alfanumérico del artículo ingresado por el usuario.
|
||||
|
||||
Returns:
|
||||
Articulo si existe, None en caso contrario.
|
||||
"""
|
||||
query = "SELECT id_articulo, ISNULL(codigo, '') AS codigo, descripcion FROM Articulos WHERE codigo = ?"
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query, (codigo,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return Articulo(id_articulo=row[0], codigo=row[1], descripcion=row[2])
|
||||
except Exception:
|
||||
logger.exception(f"Error al buscar artículo por código: {codigo}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def obtener_o_crear(codigo: str, descripcion: str) -> Articulo:
|
||||
"""
|
||||
Obtiene un artículo por código o lo crea con código+descripción de forma transaccional (Regla 6).
|
||||
|
||||
Args:
|
||||
codigo: Código alfanumérico del artículo (ej. 'CHAPA-123').
|
||||
descripcion: Descripción textual del artículo.
|
||||
|
||||
Returns:
|
||||
Articulo existente o recién creado.
|
||||
|
||||
Raises:
|
||||
Exception: Si falla la inserción en la BD.
|
||||
"""
|
||||
try:
|
||||
art = ArticuloDAO.obtener_por_codigo(codigo)
|
||||
if art:
|
||||
return art
|
||||
|
||||
query = "INSERT INTO Articulos (codigo, descripcion) OUTPUT INSERTED.id_articulo VALUES (?, ?)"
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (codigo, descripcion))
|
||||
new_id = cursor.fetchone()[0]
|
||||
return Articulo(id_articulo=new_id, codigo=codigo, descripcion=descripcion)
|
||||
except Exception:
|
||||
logger.exception(f"Error al obtener o crear artículo: código={codigo}, desc={descripcion}")
|
||||
raise
|
||||
|
||||
class RecursoDAO:
|
||||
@staticmethod
|
||||
def obtener_por_tipo(tipo: str) -> List[Recurso]:
|
||||
"""
|
||||
Obtiene los recursos filtrados por tipo (MAQUINA o HM) de forma segura (Regla 4 y 6).
|
||||
"""
|
||||
query = "SELECT id_recurso, descripcion, tipo FROM Recursos WHERE tipo = ?"
|
||||
recursos = []
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query, (tipo,))
|
||||
for row in cursor.fetchall():
|
||||
recursos.append(Recurso(id_recurso=str(row[0]), nombre=row[1], tipo=row[2]))
|
||||
except Exception:
|
||||
logger.exception(f"Error al obtener recursos del tipo {tipo}.")
|
||||
return recursos
|
||||
|
||||
@staticmethod
|
||||
def obtener_por_descripcion_y_tipo(descripcion: str, tipo: str) -> Optional[Recurso]:
|
||||
"""
|
||||
Obtiene un recurso por su descripción y tipo.
|
||||
"""
|
||||
query = "SELECT id_recurso, descripcion, tipo FROM Recursos WHERE descripcion = ? AND tipo = ?"
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query, (descripcion, tipo))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return Recurso(id_recurso=str(row[0]), nombre=row[1], tipo=row[2])
|
||||
except Exception:
|
||||
logger.exception("Error al buscar recurso por descripción y tipo.")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def crear_silencioso(descripcion: str, tipo: str) -> Optional[Recurso]:
|
||||
"""
|
||||
Inserta un nuevo recurso de forma silenciosa si no existe.
|
||||
"""
|
||||
try:
|
||||
existing = RecursoDAO.obtener_por_descripcion_y_tipo(descripcion, tipo)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
query = "INSERT INTO Recursos (descripcion, tipo) OUTPUT INSERTED.id_recurso VALUES (?, ?)"
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (descripcion, tipo))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return Recurso(id_recurso=str(row[0]), nombre=descripcion, tipo=tipo)
|
||||
except Exception:
|
||||
logger.exception(f"Error al crear recurso silenciosamente: {descripcion} ({tipo})")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def borrar_por_nombre_y_tipo(descripcion: str, tipo: str) -> bool:
|
||||
"""
|
||||
Elimina un recurso por su descripción y tipo de forma transaccional.
|
||||
|
||||
Usa consulta parametrizada (Regla 6) y transacción explícita.
|
||||
Si el recurso está referenciado por Relevamientos, la BD rechazará
|
||||
la operación por integridad referencial.
|
||||
|
||||
Args:
|
||||
descripcion: Nombre/descripción del recurso a eliminar.
|
||||
tipo: Tipo del recurso ('MAQUINA' o 'HM').
|
||||
|
||||
Returns:
|
||||
True si se eliminó al menos un registro, False en caso contrario.
|
||||
|
||||
Raises:
|
||||
No lanza excepciones al exterior; registra en logger (Regla 7).
|
||||
"""
|
||||
query = "DELETE FROM Recursos WHERE descripcion = ? AND tipo = ?"
|
||||
try:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (descripcion, tipo))
|
||||
eliminado = cursor.rowcount > 0
|
||||
if eliminado:
|
||||
logger.info(f"Recurso '{descripcion}' ({tipo}) eliminado de la BD.")
|
||||
else:
|
||||
logger.warning(f"No se encontró recurso '{descripcion}' ({tipo}) para eliminar.")
|
||||
return eliminado
|
||||
except Exception:
|
||||
logger.exception(f"Error al eliminar recurso '{descripcion}' ({tipo}).")
|
||||
return False
|
||||
|
||||
class OperarioDAO:
|
||||
@staticmethod
|
||||
def obtener_todos() -> List[Operario]:
|
||||
"""
|
||||
Obtiene todos los operarios registrados.
|
||||
"""
|
||||
query = "SELECT id_operario, nombre FROM Operarios"
|
||||
operarios = []
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
for row in cursor.fetchall():
|
||||
operarios.append(Operario(id_operario=str(row[0]), nombre=row[1], legajo=""))
|
||||
except Exception:
|
||||
logger.exception("Error al obtener todos los operarios.")
|
||||
return operarios
|
||||
|
||||
@staticmethod
|
||||
def obtener_por_nombre(nombre: str) -> Optional[Operario]:
|
||||
"""
|
||||
Obtiene un operario por su nombre.
|
||||
"""
|
||||
query = "SELECT id_operario, nombre FROM Operarios WHERE nombre = ?"
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query, (nombre,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return Operario(id_operario=str(row[0]), nombre=row[1], legajo="")
|
||||
except Exception:
|
||||
logger.exception("Error al buscar operario por nombre.")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def crear_silencioso(nombre: str) -> Optional[Operario]:
|
||||
"""
|
||||
Inserta un nuevo operario de forma silenciosa si no existe.
|
||||
"""
|
||||
try:
|
||||
existing = OperarioDAO.obtener_por_nombre(nombre)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
query = "INSERT INTO Operarios (nombre) OUTPUT INSERTED.id_operario VALUES (?)"
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (nombre,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return Operario(id_operario=str(row[0]), nombre=nombre, legajo="")
|
||||
except Exception:
|
||||
logger.exception(f"Error al crear operario silenciosamente: {nombre}")
|
||||
return None
|
||||
|
||||
class RelevamientoDAO:
|
||||
@staticmethod
|
||||
def obtener_siguiente_id() -> int:
|
||||
"""
|
||||
Obtiene el siguiente ID correlativo para el relevamiento.
|
||||
"""
|
||||
query = "SELECT ISNULL(MAX(id_relevamiento), 0) + 1 FROM Relevamientos"
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return int(row[0])
|
||||
except Exception:
|
||||
logger.exception("Error al obtener el siguiente ID de relevamiento.")
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def guardar(relevamiento: 'Relevamiento') -> bool:
|
||||
"""
|
||||
Guarda un relevamiento y sus tiempos asociados en una sola transacción estricta.
|
||||
Soporta inserción (INSERT) o actualización (UPDATE) si ya existe.
|
||||
Cumple la Regla 6 (Transacciones explícitas) y la creación dinámica de entidades.
|
||||
"""
|
||||
query_insert = """
|
||||
INSERT INTO Relevamientos
|
||||
(id_articulo, operacion, id_recurso, id_hm, id_operario, fecha, postura, piezas_por_golpe, articulos_por_golpe, operarios_maquina, id_tarea)
|
||||
OUTPUT INSERTED.id_relevamiento
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
query_update = """
|
||||
UPDATE Relevamientos SET
|
||||
id_articulo = ?, operacion = ?, id_recurso = ?, id_hm = ?, id_operario = ?,
|
||||
fecha = ?, postura = ?, piezas_por_golpe = ?, articulos_por_golpe = ?, operarios_maquina = ?, id_tarea = ?
|
||||
WHERE id_relevamiento = ?
|
||||
"""
|
||||
query_delete_tiempos = """
|
||||
DELETE FROM TiemposElemento WHERE id_relevamiento = ?
|
||||
"""
|
||||
query_tiempo = """
|
||||
INSERT INTO TiemposElemento (id_relevamiento, elemento, valoracion, minutos, segundos, elementos_medidos, lote_frecuencial, suplemento)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
from database import get_db_connection
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
art_id = relevamiento.articulo.id_articulo if relevamiento.articulo else None
|
||||
|
||||
# --- Gestión dinámica de Entidades Maestras ---
|
||||
# Máquina
|
||||
rec_id = None
|
||||
maq_nombre = None
|
||||
if isinstance(relevamiento.recurso, str):
|
||||
maq_nombre = relevamiento.recurso.strip()
|
||||
elif relevamiento.recurso and hasattr(relevamiento.recurso, 'nombre'):
|
||||
maq_nombre = relevamiento.recurso.nombre.strip() if relevamiento.recurso.nombre else None
|
||||
|
||||
if maq_nombre:
|
||||
cursor.execute("SELECT id_recurso FROM Recursos WHERE descripcion = ? AND tipo = 'MAQUINA'", (maq_nombre,))
|
||||
row_maq = cursor.fetchone()
|
||||
if row_maq:
|
||||
rec_id = row_maq[0]
|
||||
else:
|
||||
cursor.execute("INSERT INTO Recursos (descripcion, tipo) OUTPUT INSERTED.id_recurso VALUES (?, 'MAQUINA')", (maq_nombre,))
|
||||
rec_id = cursor.fetchone()[0]
|
||||
|
||||
# HM (Herramienta/Matriz)
|
||||
hm_id = None
|
||||
hm_nombre = None
|
||||
if isinstance(relevamiento.hm, str):
|
||||
hm_nombre = relevamiento.hm.strip()
|
||||
elif relevamiento.hm and hasattr(relevamiento.hm, 'nombre'):
|
||||
hm_nombre = relevamiento.hm.nombre.strip() if relevamiento.hm.nombre else None
|
||||
|
||||
if hm_nombre:
|
||||
cursor.execute("SELECT id_recurso FROM Recursos WHERE descripcion = ? AND tipo = 'HM'", (hm_nombre,))
|
||||
row_hm = cursor.fetchone()
|
||||
if row_hm:
|
||||
hm_id = row_hm[0]
|
||||
else:
|
||||
cursor.execute("INSERT INTO Recursos (descripcion, tipo) OUTPUT INSERTED.id_recurso VALUES (?, 'HM')", (hm_nombre,))
|
||||
hm_id = cursor.fetchone()[0]
|
||||
|
||||
# Operario
|
||||
op_id = None
|
||||
op_nombre = None
|
||||
if isinstance(relevamiento.operario, str):
|
||||
op_nombre = relevamiento.operario.strip()
|
||||
elif relevamiento.operario and hasattr(relevamiento.operario, 'nombre'):
|
||||
op_nombre = relevamiento.operario.nombre.strip() if relevamiento.operario.nombre else None
|
||||
|
||||
if op_nombre:
|
||||
cursor.execute("SELECT id_operario FROM Operarios WHERE nombre = ?", (op_nombre,))
|
||||
row_op = cursor.fetchone()
|
||||
if row_op:
|
||||
op_id = row_op[0]
|
||||
else:
|
||||
cursor.execute("INSERT INTO Operarios (nombre) OUTPUT INSERTED.id_operario VALUES (?)", (op_nombre,))
|
||||
op_id = cursor.fetchone()[0]
|
||||
|
||||
# --- Preparación de la cabecera ---
|
||||
from datetime import date
|
||||
fecha_str = relevamiento.fecha
|
||||
if isinstance(fecha_str, date):
|
||||
fecha_str = fecha_str.strftime("%d/%m/%Y")
|
||||
|
||||
# Sanitización estricta de Foreign Key id_tarea
|
||||
id_tarea_db = relevamiento.id_tarea
|
||||
if not id_tarea_db or str(id_tarea_db).strip() == "" or str(id_tarea_db) == "0" or str(id_tarea_db).lower() == "none":
|
||||
id_tarea_db = None
|
||||
else:
|
||||
id_tarea_db = int(id_tarea_db)
|
||||
|
||||
id_rel = relevamiento.id_relevamiento
|
||||
|
||||
if id_rel is not None:
|
||||
# UPDATE cabecera
|
||||
cursor.execute(query_update, (
|
||||
art_id, relevamiento.operacion, rec_id, hm_id, op_id,
|
||||
fecha_str, relevamiento.postura,
|
||||
relevamiento.piezas_por_golpe, relevamiento.articulos_por_golpe,
|
||||
relevamiento.operarios_maquina, id_tarea_db,
|
||||
id_rel
|
||||
))
|
||||
else:
|
||||
# INSERT cabecera
|
||||
cursor.execute(query_insert, (
|
||||
art_id, relevamiento.operacion, rec_id, hm_id, op_id,
|
||||
fecha_str, relevamiento.postura,
|
||||
relevamiento.piezas_por_golpe, relevamiento.articulos_por_golpe,
|
||||
relevamiento.operarios_maquina, id_tarea_db
|
||||
))
|
||||
id_rel = cursor.fetchone()[0]
|
||||
relevamiento.id_relevamiento = id_rel
|
||||
|
||||
if not relevamiento.tiempos:
|
||||
raise ValueError("La lista de tiempos está vacía. Abortando guardado para prevenir pérdida de datos (Regla 0).")
|
||||
|
||||
# DELETE tiempos anteriores antes del bulk insert
|
||||
cursor.execute(query_delete_tiempos, (id_rel,))
|
||||
|
||||
# INSERT tiempos nuevos
|
||||
for t in relevamiento.tiempos:
|
||||
cursor.execute(query_tiempo, (
|
||||
id_rel, t.elemento, t.valoracion, t.minutos, t.segundos,
|
||||
t.elementos_medidos, t.lote_frecuencial, t.suplemento
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"Relevamiento {id_rel} guardado exitosamente en BD (Cabecera + Detalle).")
|
||||
return True
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logger.exception("Error crítico en transacción de guardado. Se ejecutó rollback.")
|
||||
raise e
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
@staticmethod
|
||||
def obtener_por_id(id_relevamiento: int) -> Optional['Relevamiento']:
|
||||
"""
|
||||
Obtiene un relevamiento por su ID directamente, sin manejo silencioso de errores (Regla 0 y Regla 7).
|
||||
"""
|
||||
from modelos import Relevamiento, Articulo, Recurso, Operario, TiemposElemento
|
||||
from database import get_db_cursor
|
||||
|
||||
query_cabecera = """
|
||||
SELECT
|
||||
r.id_relevamiento, r.operacion, r.fecha, r.postura,
|
||||
r.piezas_por_golpe, r.articulos_por_golpe, r.operarios_maquina, r.id_tarea,
|
||||
ISNULL(a.codigo, '') AS articulo_codigo,
|
||||
ISNULL(a.descripcion, '') AS articulo_desc,
|
||||
m.descripcion AS maquina_desc, m.tipo AS maquina_tipo,
|
||||
h.descripcion AS hm_desc, h.tipo AS hm_tipo,
|
||||
o.nombre AS operario_nombre
|
||||
FROM Relevamientos r
|
||||
LEFT JOIN Articulos a ON r.id_articulo = a.id_articulo
|
||||
LEFT JOIN Recursos m ON r.id_recurso = m.id_recurso
|
||||
LEFT JOIN Recursos h ON r.id_hm = h.id_recurso
|
||||
LEFT JOIN Operarios o ON r.id_operario = o.id_operario
|
||||
WHERE r.id_relevamiento = ?
|
||||
"""
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query_cabecera, (id_relevamiento,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
# Mapeo inmune a desfases utilizando nombres de columnas (Regla 0)
|
||||
columns = [column[0] for column in cursor.description]
|
||||
result_dict = dict(zip(columns, row))
|
||||
|
||||
id_rel = result_dict.get('id_relevamiento')
|
||||
operacion = result_dict.get('operacion')
|
||||
fecha_str = result_dict.get('fecha')
|
||||
postura = result_dict.get('postura')
|
||||
piezas = result_dict.get('piezas_por_golpe')
|
||||
articulos_g = result_dict.get('articulos_por_golpe')
|
||||
operarios_m = result_dict.get('operarios_maquina')
|
||||
id_tarea = result_dict.get('id_tarea')
|
||||
|
||||
art_codigo = result_dict.get('articulo_codigo')
|
||||
art_desc = result_dict.get('articulo_desc')
|
||||
maq_desc = result_dict.get('maquina_desc')
|
||||
maq_tipo = result_dict.get('maquina_tipo')
|
||||
hm_desc = result_dict.get('hm_desc')
|
||||
hm_tipo = result_dict.get('hm_tipo')
|
||||
op_nombre = result_dict.get('operario_nombre')
|
||||
|
||||
articulo_obj = None
|
||||
if art_codigo or art_desc:
|
||||
articulo_obj = Articulo(
|
||||
id_articulo="0",
|
||||
codigo=art_codigo or "",
|
||||
descripcion=art_desc or ""
|
||||
)
|
||||
|
||||
maquina_obj = None
|
||||
if maq_desc is not None:
|
||||
maquina_obj = Recurso(id_recurso="0", nombre=maq_desc, tipo=maq_tipo)
|
||||
|
||||
hm_obj = None
|
||||
if hm_desc is not None:
|
||||
hm_obj = Recurso(id_recurso="0", nombre=hm_desc, tipo=hm_tipo)
|
||||
|
||||
operario_obj = None
|
||||
if op_nombre is not None:
|
||||
operario_obj = Operario(id_operario="0", nombre=op_nombre, legajo="")
|
||||
|
||||
query_tiempos = "SELECT elemento, valoracion, minutos, segundos, elementos_medidos, lote_frecuencial, suplemento FROM TiemposElemento WHERE id_relevamiento = ? ORDER BY id_tiempo ASC"
|
||||
cursor.execute(query_tiempos, (id_rel,))
|
||||
tiempos_db = cursor.fetchall()
|
||||
|
||||
tiempos = []
|
||||
for t_row in tiempos_db:
|
||||
tiempos.append(TiemposElemento(
|
||||
elemento=t_row[0],
|
||||
valoracion=float(t_row[1]),
|
||||
minutos=float(t_row[2]),
|
||||
segundos=float(t_row[3]),
|
||||
elementos_medidos=int(t_row[4]),
|
||||
lote_frecuencial=float(t_row[5]),
|
||||
suplemento=float(t_row[6])
|
||||
))
|
||||
|
||||
from datetime import datetime, date
|
||||
parsed_date = date.today()
|
||||
if fecha_str:
|
||||
if isinstance(fecha_str, str):
|
||||
if "-" in fecha_str:
|
||||
parsed_date = datetime.strptime(fecha_str, "%Y-%m-%d").date()
|
||||
else:
|
||||
parsed_date = datetime.strptime(fecha_str, "%d/%m/%Y").date()
|
||||
else:
|
||||
parsed_date = fecha_str
|
||||
|
||||
return Relevamiento(
|
||||
id_relevamiento=id_rel,
|
||||
id_tarea=id_tarea,
|
||||
articulo=articulo_obj,
|
||||
operacion=operacion,
|
||||
recurso=maquina_obj,
|
||||
hm=hm_obj,
|
||||
operario=operario_obj,
|
||||
fecha=parsed_date,
|
||||
postura=postura if postura else "pie",
|
||||
piezas_por_golpe=int(piezas) if piezas else 1,
|
||||
articulos_por_golpe=int(articulos_g) if articulos_g else 1,
|
||||
operarios_maquina=int(operarios_m) if operarios_m else 1,
|
||||
tiempos=tiempos
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def obtener_por_tarea(id_tarea: int) -> Optional[Relevamiento]:
|
||||
"""
|
||||
Obtiene un relevamiento asociado a una tarea.
|
||||
"""
|
||||
query = "SELECT id_relevamiento FROM Relevamientos WHERE id_tarea = ?"
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query, (id_tarea,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return RelevamientoDAO.obtener_por_id(int(row[0]))
|
||||
except Exception:
|
||||
logger.exception(f"Error al obtener relevamiento por tarea: {id_tarea}")
|
||||
return None
|
||||
@staticmethod
|
||||
def obtener_todos_resumen() -> List[tuple]:
|
||||
"""
|
||||
Devuelve un resumen de todos los relevamientos para listarlos en la interfaz.
|
||||
Retorna lista de tuplas: (ID, Fecha, Artículo, Operación, Operario)
|
||||
"""
|
||||
query = """
|
||||
SELECT
|
||||
r.id_relevamiento,
|
||||
r.fecha,
|
||||
ISNULL(a.codigo, 'S/C') AS articulo_codigo,
|
||||
r.operacion,
|
||||
ISNULL(o.nombre, 'S/O') AS operario_nombre
|
||||
FROM Relevamientos r
|
||||
LEFT JOIN Articulos a ON r.id_articulo = a.id_articulo
|
||||
LEFT JOIN Operarios o ON r.id_operario = o.id_operario
|
||||
ORDER BY r.id_relevamiento DESC
|
||||
"""
|
||||
resumen = []
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
for row in cursor.fetchall():
|
||||
resumen.append((row[0], row[1], row[2], row[3], row[4]))
|
||||
except Exception:
|
||||
logger.exception("Error al obtener resumen de relevamientos.")
|
||||
return resumen
|
||||
|
||||
@staticmethod
|
||||
def buscar_historico(filtros: Dict[str, str]) -> List[tuple]:
|
||||
"""
|
||||
Busca relevamientos históricos aplicando filtros dinámicos combinados.
|
||||
|
||||
Construye una consulta SQL con LEFT JOINs a Articulos, Recursos (máquina y HM)
|
||||
y Operarios. Agrega cláusulas WHERE parametrizadas según los filtros provistos,
|
||||
utilizando LIKE para búsqueda parcial (Regla 6: consultas parametrizadas estrictas).
|
||||
|
||||
Args:
|
||||
filtros: Diccionario con claves opcionales:
|
||||
- 'articulo': Filtra por código del artículo.
|
||||
- 'operario': Filtra por nombre del operario.
|
||||
- 'maquina': Filtra por descripción de la máquina.
|
||||
- 'hm': Filtra por descripción de la herramienta/matriz.
|
||||
- 'operacion': Filtra por nombre de la operación.
|
||||
|
||||
Returns:
|
||||
Lista de tuplas con formato:
|
||||
(id_relevamiento, fecha, codigo_articulo, operacion, maquina, hm, operario).
|
||||
El id_relevamiento se usa internamente como iid del Treeview pero NO se
|
||||
muestra al usuario. El código del artículo actúa como identificador visual.
|
||||
Retorna lista vacía si ocurre un error.
|
||||
|
||||
Raises:
|
||||
No lanza excepciones al exterior; los errores se registran
|
||||
en el logger (Regla 7).
|
||||
"""
|
||||
query_base = """
|
||||
SELECT
|
||||
r.id_relevamiento,
|
||||
r.fecha,
|
||||
ISNULL(a.codigo, 'S/C') AS codigo_articulo,
|
||||
ISNULL(r.operacion, '') AS operacion,
|
||||
ISNULL(m.descripcion, 'S/M') AS maquina_desc,
|
||||
ISNULL(h.descripcion, 'S/HM') AS hm_desc,
|
||||
ISNULL(o.nombre, 'S/O') AS operario_nombre
|
||||
FROM Relevamientos r
|
||||
LEFT JOIN Articulos a ON r.id_articulo = a.id_articulo
|
||||
LEFT JOIN Recursos m ON r.id_recurso = m.id_recurso
|
||||
LEFT JOIN Recursos h ON r.id_hm = h.id_recurso
|
||||
LEFT JOIN Operarios o ON r.id_operario = o.id_operario
|
||||
"""
|
||||
|
||||
condiciones: List[str] = []
|
||||
parametros: List[str] = []
|
||||
|
||||
# Mapeo filtro -> columna SQL para construcción dinámica segura
|
||||
mapa_filtros: Dict[str, str] = {
|
||||
"articulo": "a.codigo",
|
||||
"operario": "o.nombre",
|
||||
"maquina": "m.descripcion",
|
||||
"hm": "h.descripcion",
|
||||
"operacion": "r.operacion",
|
||||
}
|
||||
|
||||
for clave, columna in mapa_filtros.items():
|
||||
valor = filtros.get(clave, "").strip()
|
||||
if valor:
|
||||
condiciones.append(f"{columna} LIKE ?")
|
||||
parametros.append(f"%{valor}%")
|
||||
|
||||
if condiciones:
|
||||
query_base += " WHERE " + " AND ".join(condiciones)
|
||||
|
||||
query_base += " ORDER BY r.id_relevamiento DESC"
|
||||
|
||||
resultados: List[tuple] = []
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query_base, tuple(parametros))
|
||||
for row in cursor.fetchall():
|
||||
resultados.append(
|
||||
(row[0], row[1], row[2], row[3], row[4], row[5], row[6])
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error al ejecutar búsqueda histórica de relevamientos.")
|
||||
return resultados
|
||||
|
||||
@staticmethod
|
||||
def borrar_por_id(id_relevamiento: int) -> bool:
|
||||
"""
|
||||
Elimina un relevamiento y sus tiempos asociados en una sola transacción.
|
||||
|
||||
Primero borra los TiemposElemento hijos y luego la cabecera del
|
||||
relevamiento, garantizando integridad referencial (Regla 6).
|
||||
|
||||
Args:
|
||||
id_relevamiento: ID del relevamiento a eliminar.
|
||||
|
||||
Returns:
|
||||
True si la cabecera fue eliminada exitosamente, False en caso contrario.
|
||||
|
||||
Raises:
|
||||
No lanza excepciones al exterior; registra en logger (Regla 7).
|
||||
"""
|
||||
from database import get_db_connection
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"DELETE FROM TiemposElemento WHERE id_relevamiento = ?",
|
||||
(id_relevamiento,)
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM Relevamientos WHERE id_relevamiento = ?",
|
||||
(id_relevamiento,)
|
||||
)
|
||||
eliminado = cursor.rowcount > 0
|
||||
conn.commit()
|
||||
if eliminado:
|
||||
logger.info(f"Relevamiento {id_relevamiento} eliminado (cabecera + tiempos).")
|
||||
else:
|
||||
logger.warning(f"No se encontró relevamiento {id_relevamiento} para eliminar.")
|
||||
return eliminado
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
logger.exception(f"Error transaccional al eliminar relevamiento {id_relevamiento}. Rollback ejecutado.")
|
||||
return False
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
|
||||
class UsuarioDAO:
|
||||
@staticmethod
|
||||
def obtener_por_username(username: str) -> Optional[Usuario]:
|
||||
"""Obtiene un usuario (con su rol) mediante consulta parametrizada."""
|
||||
query = """
|
||||
SELECT u.id_usuario, u.username, u.password_hash, r.nombre AS rol, u.nombre
|
||||
FROM Usuarios u
|
||||
INNER JOIN Roles r ON u.id_rol = r.id_rol
|
||||
WHERE u.username = ?
|
||||
"""
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query, (username,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
# En modelos.py: id_usuario, username, password_hash, rol, nombre
|
||||
from modelos import Usuario # Import local para evitar circularidad si la hubiera
|
||||
return Usuario(id_usuario=row[0], username=row[1], password_hash=row[2], rol=row[3], nombre=row[4])
|
||||
return None
|
||||
except Exception:
|
||||
logger.exception("Fallo al obtener usuario por username.")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def crear_usuario(username: str, password_hash: str, rol: str, nombre: str) -> bool:
|
||||
"""Crea un nuevo usuario asignando el id_rol correspondiente."""
|
||||
query = """
|
||||
INSERT INTO Usuarios (username, password_hash, id_rol, nombre)
|
||||
VALUES (?, ?, (SELECT id_rol FROM Roles WHERE nombre = ?), ?)
|
||||
"""
|
||||
try:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (username, password_hash, rol, nombre))
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception(f"Fallo al crear el usuario: {username}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def obtener_metodistas() -> List['Usuario']:
|
||||
"""Obtiene la lista de usuarios con rol METODISTA."""
|
||||
query = """
|
||||
SELECT u.id_usuario, u.username, u.password_hash, r.nombre AS rol, u.nombre
|
||||
FROM Usuarios u
|
||||
INNER JOIN Roles r ON u.id_rol = r.id_rol
|
||||
WHERE r.nombre = 'METODISTA'
|
||||
"""
|
||||
metodistas = []
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
from modelos import Usuario
|
||||
for row in cursor.fetchall():
|
||||
metodistas.append(Usuario(id_usuario=row[0], username=row[1], password_hash=row[2], rol=row[3], nombre=row[4]))
|
||||
except Exception:
|
||||
logger.exception("Fallo al obtener la lista de metodistas.")
|
||||
return metodistas
|
||||
|
||||
@staticmethod
|
||||
def obtener_por_nombre_completo(nombre: str) -> Optional[Usuario]:
|
||||
"""
|
||||
Obtiene un usuario buscando por su nombre completo (campo 'nombre').
|
||||
|
||||
Realiza un INNER JOIN con Roles para recuperar el nombre del rol
|
||||
asociado, idéntico al patrón de obtener_por_username.
|
||||
Utiliza consulta parametrizada (Regla 6).
|
||||
|
||||
Args:
|
||||
nombre: Nombre completo del usuario a buscar (coincidencia exacta).
|
||||
|
||||
Returns:
|
||||
Instancia de Usuario si se encuentra, None en caso contrario.
|
||||
|
||||
Raises:
|
||||
No lanza excepciones al exterior; registra en logger (Regla 7).
|
||||
"""
|
||||
query = """
|
||||
SELECT u.id_usuario, u.username, u.password_hash, r.nombre AS rol, u.nombre
|
||||
FROM Usuarios u
|
||||
INNER JOIN Roles r ON u.id_rol = r.id_rol
|
||||
WHERE u.nombre = ?
|
||||
"""
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query, (nombre,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
from modelos import Usuario
|
||||
return Usuario(
|
||||
id_usuario=row[0],
|
||||
username=row[1],
|
||||
password_hash=row[2],
|
||||
rol=row[3],
|
||||
nombre=row[4],
|
||||
)
|
||||
return None
|
||||
except Exception:
|
||||
logger.exception("Fallo al obtener usuario por nombre completo.")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def actualizar_password(id_usuario: int, nuevo_hash: str) -> bool:
|
||||
"""
|
||||
Actualiza el hash de contraseña de un usuario existente.
|
||||
|
||||
Ejecuta un UPDATE parametrizado con commit explícito (Regla 6).
|
||||
Destinado al flujo de blanqueo/recuperación de contraseña.
|
||||
|
||||
Args:
|
||||
id_usuario: ID del usuario cuya contraseña se actualiza.
|
||||
nuevo_hash: Nuevo hash bcrypt de la contraseña.
|
||||
|
||||
Returns:
|
||||
True si la actualización afectó al menos una fila, False en caso contrario.
|
||||
|
||||
Raises:
|
||||
No lanza excepciones al exterior; registra en logger (Regla 7).
|
||||
"""
|
||||
query = "UPDATE Usuarios SET password_hash = ? WHERE id_usuario = ?"
|
||||
try:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (nuevo_hash, id_usuario))
|
||||
actualizado: bool = cursor.rowcount > 0
|
||||
if actualizado:
|
||||
logger.info(f"Contraseña actualizada para id_usuario={id_usuario}.")
|
||||
else:
|
||||
logger.warning(f"No se encontró id_usuario={id_usuario} para actualizar contraseña.")
|
||||
return actualizado
|
||||
except Exception:
|
||||
logger.exception(f"Fallo al actualizar contraseña para id_usuario={id_usuario}.")
|
||||
return False
|
||||
|
||||
class TareaDAO:
|
||||
@staticmethod
|
||||
def obtener_todas() -> List['Tarea']:
|
||||
"""Obtiene todas las tareas/solicitudes de la BD."""
|
||||
# Se asume que id_usuario_asignado ya existe en BD tras el ALTER TABLE
|
||||
query = "SELECT id_tarea, titulo, descripcion, estado, fecha, informe_url, id_usuario_asignado FROM Tareas"
|
||||
tareas = []
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
from modelos import Tarea
|
||||
for row in cursor.fetchall():
|
||||
# Para prevenir crash si la columna aún no existe, capturamos el índice seguro
|
||||
try:
|
||||
id_asig = row[6]
|
||||
except IndexError:
|
||||
id_asig = None
|
||||
|
||||
tareas.append(Tarea(
|
||||
id_tarea=row[0], titulo=row[1], descripcion=row[2],
|
||||
estado=row[3], fecha=str(row[4]), informe_url=row[5] or "",
|
||||
id_usuario_asignado=id_asig
|
||||
))
|
||||
except Exception:
|
||||
logger.exception("Fallo al obtener tareas.")
|
||||
return tareas
|
||||
|
||||
@staticmethod
|
||||
def obtener_tareas_por_usuario(id_usuario: int) -> List['Tarea']:
|
||||
"""Obtiene solo las tareas asignadas a un metodista."""
|
||||
query = "SELECT id_tarea, titulo, descripcion, estado, fecha, informe_url, id_usuario_asignado FROM Tareas WHERE id_usuario_asignado = ?"
|
||||
tareas = []
|
||||
try:
|
||||
with get_db_cursor() as cursor:
|
||||
cursor.execute(query, (id_usuario,))
|
||||
from modelos import Tarea
|
||||
for row in cursor.fetchall():
|
||||
tareas.append(Tarea(
|
||||
id_tarea=row[0], titulo=row[1], descripcion=row[2],
|
||||
estado=row[3], fecha=str(row[4]), informe_url=row[5] or "",
|
||||
id_usuario_asignado=row[6]
|
||||
))
|
||||
except Exception:
|
||||
logger.exception("Fallo al obtener tareas por usuario.")
|
||||
return tareas
|
||||
|
||||
@staticmethod
|
||||
def crear_tarea(titulo: str, descripcion: str, id_usuario_asignado: int) -> bool:
|
||||
"""Crea una nueva solicitud de relevamiento (Tarea) en estado Pendiente."""
|
||||
query = """
|
||||
INSERT INTO Tareas (titulo, descripcion, id_usuario_asignado, estado)
|
||||
VALUES (?, ?, ?, 'Pendiente')
|
||||
"""
|
||||
try:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (titulo, descripcion, id_usuario_asignado))
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception("Fallo al crear la tarea.")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def borrar_tarea(id_tarea: int) -> bool:
|
||||
"""Elimina una tarea permanentemente de la BD."""
|
||||
query = "DELETE FROM Tareas WHERE id_tarea = ?"
|
||||
try:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (id_tarea,))
|
||||
return cursor.rowcount > 0
|
||||
except Exception:
|
||||
logger.exception("Fallo al borrar la tarea.")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def actualizar_estado(id_tarea: int, nuevo_estado: str) -> bool:
|
||||
"""Actualiza el estado de una tarea."""
|
||||
query = "UPDATE Tareas SET estado = ? WHERE id_tarea = ?"
|
||||
try:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (nuevo_estado, id_tarea))
|
||||
return cursor.rowcount > 0
|
||||
except Exception:
|
||||
logger.exception("Fallo al actualizar el estado de la tarea.")
|
||||
return False
|
||||
|
||||
class AuditoriaDAO:
|
||||
@staticmethod
|
||||
def insertar_log(id_usuario: Optional[int], accion: str) -> bool:
|
||||
"""Inserta un registro en LogsAuditoria de manera transaccional."""
|
||||
query = "INSERT INTO LogsAuditoria (id_usuario, accion) VALUES (?, ?)"
|
||||
try:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (id_usuario, accion))
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception("Error crítico al guardar log de auditoría.")
|
||||
return False
|
||||
@staticmethod
|
||||
def actualizar_estado(id_tarea: int, nuevo_estado: str) -> bool:
|
||||
"""Actualiza el estado de una tarea."""
|
||||
query = "UPDATE Tareas SET estado = ? WHERE id_tarea = ?"
|
||||
try:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (nuevo_estado, id_tarea))
|
||||
return cursor.rowcount > 0
|
||||
except Exception:
|
||||
logger.exception("Fallo al actualizar el estado de la tarea.")
|
||||
return False
|
||||
|
||||
class AuditoriaDAO:
|
||||
@staticmethod
|
||||
def insertar_log(id_usuario: Optional[int], accion: str) -> bool:
|
||||
"""Inserta un registro en LogsAuditoria de manera transaccional."""
|
||||
query = "INSERT INTO LogsAuditoria (id_usuario, accion) VALUES (?, ?)"
|
||||
try:
|
||||
with get_db_cursor(commit=True) as cursor:
|
||||
cursor.execute(query, (id_usuario, accion))
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception("Error crítico al guardar log de auditoría.")
|
||||
return False
|
||||
Reference in New Issue
Block a user