Files
checklist/backend/app/models.py
gitea ed3f513075 Backend v1.0.71:
- Implementado soft delete para preguntas
- Nuevas columnas: is_deleted (boolean), updated_at (timestamp)
- Migración SQL: add_soft_delete_to_questions.sql
- Endpoint DELETE marca preguntas como eliminadas en lugar de borrarlas
- GET /api/checklists/{id} filtra preguntas eliminadas (is_deleted=false)
- Validación de subpreguntas activas antes de eliminar
- Índices agregados para optimizar queries
- Mantiene integridad de respuestas históricas y PDFs generados
- Permite limpiar checklists sin afectar inspecciones completadas
2025-11-27 15:32:56 -03:00

256 lines
11 KiB
Python

from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, JSON, Float
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(100), unique=True, index=True)
password_hash = Column(String(255), nullable=False)
role = Column(String(20), nullable=False) # admin, mechanic, asesor
full_name = Column(String(100))
employee_code = Column(String(50)) # Nro Operario - código de otro sistema
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
checklists_created = relationship("Checklist", back_populates="creator")
inspections = relationship("Inspection", back_populates="mechanic")
api_tokens = relationship("APIToken", back_populates="user", cascade="all, delete-orphan")
class APIToken(Base):
__tablename__ = "api_tokens"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
token = Column(String(100), unique=True, index=True, nullable=False)
description = Column(String(200))
is_active = Column(Boolean, default=True)
last_used_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationship
user = relationship("User", back_populates="api_tokens")
class Checklist(Base):
__tablename__ = "checklists"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(200), nullable=False)
description = Column(Text)
ai_mode = Column(String(20), default="off") # off, assisted, copilot
scoring_enabled = Column(Boolean, default=True)
max_score = Column(Integer, default=0)
logo_url = Column(String(500))
is_active = Column(Boolean, default=True)
created_by = Column(Integer, ForeignKey("users.id"))
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
creator = relationship("User", back_populates="checklists_created")
questions = relationship("Question", back_populates="checklist", cascade="all, delete-orphan")
inspections = relationship("Inspection", back_populates="checklist")
permissions = relationship("ChecklistPermission", back_populates="checklist", cascade="all, delete-orphan")
class Question(Base):
__tablename__ = "questions"
id = Column(Integer, primary_key=True, index=True)
checklist_id = Column(Integer, ForeignKey("checklists.id"), nullable=False)
section = Column(String(100)) # Sistema eléctrico, Frenos, etc
text = Column(Text, nullable=False)
type = Column(String(30), nullable=False) # boolean, single_choice, multiple_choice, scale, text, number, date, time
points = Column(Integer, default=1)
options = Column(JSON) # Configuración flexible según tipo de pregunta
order = Column(Integer, default=0)
allow_photos = Column(Boolean, default=True)
max_photos = Column(Integer, default=3)
requires_comment_on_fail = Column(Boolean, default=False)
send_notification = Column(Boolean, default=False)
# Conditional logic - Subpreguntas anidadas hasta 5 niveles
parent_question_id = Column(Integer, ForeignKey("questions.id"), nullable=True)
show_if_answer = Column(String(50), nullable=True) # Valor que dispara esta pregunta
depth_level = Column(Integer, default=0) # 0=principal, 1-5=subpreguntas anidadas
# AI Analysis
ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta
# Soft Delete
is_deleted = Column(Boolean, default=False) # Soft delete: mantiene integridad de respuestas históricas
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
checklist = relationship("Checklist", back_populates="questions")
answers = relationship("Answer", back_populates="question")
subquestions = relationship("Question", backref="parent", remote_side=[id])
class Inspection(Base):
__tablename__ = "inspections"
id = Column(Integer, primary_key=True, index=True)
checklist_id = Column(Integer, ForeignKey("checklists.id"), nullable=False)
mechanic_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Datos de la OR
or_number = Column(String(50))
work_order_number = Column(String(50))
# Datos del vehículo
vehicle_plate = Column(String(20), nullable=False, index=True)
vehicle_brand = Column(String(50))
vehicle_model = Column(String(100))
vehicle_km = Column(Integer)
order_number = Column(String(200)) # Nº de Pedido
# Datos del mecánico
mechanic_employee_code = Column(String(50)) # Código de operario del mecánico
# Scoring
score = Column(Integer, default=0)
max_score = Column(Integer, default=0)
percentage = Column(Float, default=0.0)
flagged_items_count = Column(Integer, default=0)
# Estado
status = Column(String(20), default="draft") # draft, completed, inactive
is_active = Column(Boolean, default=True)
# Firma
signature_data = Column(Text) # Base64 de la firma
signed_at = Column(DateTime(timezone=True))
# Timestamps
started_at = Column(DateTime(timezone=True), server_default=func.now())
completed_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
pdf_url = Column(String(500)) # URL del PDF en S3
# Relationships
checklist = relationship("Checklist", back_populates="inspections")
mechanic = relationship("User", back_populates="inspections")
answers = relationship("Answer", back_populates="inspection", cascade="all, delete-orphan")
class Answer(Base):
__tablename__ = "answers"
id = Column(Integer, primary_key=True, index=True)
inspection_id = Column(Integer, ForeignKey("inspections.id"), nullable=False)
question_id = Column(Integer, ForeignKey("questions.id"), nullable=False)
answer_value = Column(Text) # La respuesta del mecánico
status = Column(String(20), default="ok") # ok, warning, critical, info
points_earned = Column(Integer, default=0)
comment = Column(Text) # Comentarios adicionales
ai_analysis = Column(JSON) # Análisis de IA si aplica
is_flagged = Column(Boolean, default=False) # Si requiere atención
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
inspection = relationship("Inspection", back_populates="answers")
question = relationship("Question", back_populates="answers")
media_files = relationship("MediaFile", back_populates="answer", cascade="all, delete-orphan")
class MediaFile(Base):
__tablename__ = "media_files"
id = Column(Integer, primary_key=True, index=True)
answer_id = Column(Integer, ForeignKey("answers.id"), nullable=False)
file_path = Column(String(500), nullable=False)
file_type = Column(String(20), default="image") # image, video
caption = Column(Text)
order = Column(Integer, default=0)
uploaded_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
answer = relationship("Answer", back_populates="media_files")
class AIConfiguration(Base):
__tablename__ = "ai_configurations"
id = Column(Integer, primary_key=True, index=True)
provider = Column(String(50), nullable=False) # openai, gemini
api_key = Column(Text, nullable=False)
model_name = Column(String(100), nullable=True)
logo_url = Column(Text, nullable=True) # URL del logo configurable
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class ChecklistPermission(Base):
"""Tabla intermedia para permisos de checklist por mecánico"""
__tablename__ = "checklist_permissions"
id = Column(Integer, primary_key=True, index=True)
checklist_id = Column(Integer, ForeignKey("checklists.id", ondelete="CASCADE"), nullable=False)
mechanic_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
checklist = relationship("Checklist", back_populates="permissions")
mechanic = relationship("User")
class QuestionAuditLog(Base):
"""Registro de auditoría para cambios en preguntas de checklists"""
__tablename__ = "question_audit_log"
id = Column(Integer, primary_key=True, index=True)
question_id = Column(Integer, ForeignKey("questions.id", ondelete="CASCADE"), nullable=False)
checklist_id = Column(Integer, ForeignKey("checklists.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
action = Column(String(50), nullable=False) # created, updated, deleted
field_name = Column(String(100), nullable=True) # Campo modificado
old_value = Column(Text, nullable=True) # Valor anterior
new_value = Column(Text, nullable=True) # Valor nuevo
comment = Column(Text, nullable=True) # Comentario del cambio
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
question = relationship("Question")
checklist = relationship("Checklist")
user = relationship("User")
class InspectionAuditLog(Base):
"""Registro de auditoría para cambios en inspecciones y respuestas"""
__tablename__ = "inspection_audit_log"
id = Column(Integer, primary_key=True, index=True)
inspection_id = Column(Integer, ForeignKey("inspections.id", ondelete="CASCADE"), nullable=False)
answer_id = Column(Integer, ForeignKey("answers.id", ondelete="CASCADE"), nullable=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
action = Column(String(50), nullable=False) # created, updated, deleted, status_changed
entity_type = Column(String(50), nullable=False) # inspection, answer
field_name = Column(String(100), nullable=True) # Campo modificado
old_value = Column(Text, nullable=True) # Valor anterior
new_value = Column(Text, nullable=True) # Valor nuevo
comment = Column(Text, nullable=True) # Comentario del cambio
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationships
inspection = relationship("Inspection")
answer = relationship("Answer")
user = relationship("User")