Backend v1.2.11:
Nueva Funcionalidad - Control de Generación de PDF:
Campo nuevo: generate_pdf en modelo Checklist (Boolean, default: True)
Lógica modificada: Al completar inspección se verifica si el checklist tiene habilitada la generación de PDF
Comportamiento:
Si generate_pdf = True → Se genera y guarda el PDF automáticamente
Si generate_pdf = False → No se genera PDF, pdf_url queda en NULL
Logs informativos: Muestra en consola si el PDF se generó o se omitió
Frontend v1.3.8:
Interfaz para Control de PDF:
Checkbox nuevo en modal de edición de checklist: "Generar PDF automáticamente al completar inspección"
Estado por defecto: Activado (mantiene comportamiento actual)
Persistencia: El valor se guarda en la base de datos al editar checklist
Dónde está:
Admin → Checklists → Click en "✏️ Editar" de cualquier checklist
Debajo del checkbox de "Habilitar sistema de puntuación"
259 lines
11 KiB
Python
259 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))
|
|
generate_pdf = Column(Boolean, default=True) # Controla si se genera PDF al completar
|
|
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) # DEPRECATED: usar photo_requirement
|
|
photo_requirement = Column(String(20), default='optional') # none, optional, required
|
|
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="incomplete") # incomplete, 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
|
|
chat_history = Column(JSON) # Historial de chat con AI Assistant (para tipo ai_assistant)
|
|
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")
|