Saltar a contenido

Base de Datos

El Sistema A3 utiliza PostgreSQL 12.3+ como sistema de gestión de base de datos relacional. Esta página documenta el esquema, relaciones y consideraciones de diseño.

Esquema General

La base de datos está organizada en tablas correspondientes a las apps Django del sistema:

erDiagram
    User ||--o| Perfil : tiene
    User ||--o{ Casa : crea
    User ||--o{ Apartado : gestiona
    User ||--o{ Ticket : maneja

    Casa ||--o{ AnexoCasa : tiene
    Casa ||--o| Apartado : se_vende_en

    Cliente ||--o{ Apartado : compra
    Cliente ||--o{ AnexoCliente : tiene

    Apartado ||--o{ Obra : requiere
    Apartado ||--o| Garantia : tiene
    Apartado ||--o| Cierre : finaliza

    Ticket ||--o{ SubtareaTicket : contiene
    Ticket ||--o{ ChatTicket : tiene

    Comprobacion ||--o{ AnexoComprobacion : documenta
    Comprobacion }|--|| User : aprueba

    Perfil ||--o{ Horarios : tiene
    Perfil ||--o{ Ausencia : registra
    Perfil ||--o{ HorasExtra : acumula

Tablas Principales

auth_user (Django Auth)

Usuario base de Django, extendido por el modelo Perfil.

CREATE TABLE auth_user (
    id SERIAL PRIMARY KEY,
    username VARCHAR(150) UNIQUE NOT NULL,
    first_name VARCHAR(150),
    last_name VARCHAR(150),
    email VARCHAR(254),
    is_staff BOOLEAN DEFAULT FALSE,
    is_active BOOLEAN DEFAULT TRUE,
    is_superuser BOOLEAN DEFAULT FALSE,
    password VARCHAR(128) NOT NULL,
    last_login TIMESTAMP,
    date_joined TIMESTAMP DEFAULT NOW()
);

users_perfil

Extensión del modelo User con información adicional.

CREATE TABLE users_perfil (
    id SERIAL PRIMARY KEY,
    user_id INTEGER UNIQUE REFERENCES auth_user(id) ON DELETE CASCADE,
    tipo_perfil VARCHAR(20) NOT NULL,  -- 'ASESOR' o 'CLIENTE'
    plaza VARCHAR(50),
    telefono VARCHAR(20),
    fecha_nacimiento DATE,
    direccion TEXT,
    rfc VARCHAR(20),
    curp VARCHAR(20),
    nss VARCHAR(20),
    -- Campos específicos para asesores
    numero_empleado VARCHAR(20),
    fecha_ingreso DATE,
    salario DECIMAL(12, 2),
    -- Campos de control
    activo BOOLEAN DEFAULT TRUE,
    fecha_creacion TIMESTAMP DEFAULT NOW(),
    fecha_modificacion TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_perfil_plaza ON users_perfil(plaza);
CREATE INDEX idx_perfil_tipo ON users_perfil(tipo_perfil);

inventario_casa

Catálogo de propiedades disponibles.

CREATE TABLE inventario_casa (
    id SERIAL PRIMARY KEY,
    clave VARCHAR(50) UNIQUE NOT NULL,
    proyecto VARCHAR(100) NOT NULL,
    tipo_vivienda VARCHAR(50),
    modelo VARCHAR(50),
    plaza VARCHAR(50) NOT NULL,

    -- Ubicación
    direccion TEXT,
    colonia VARCHAR(100),
    cp VARCHAR(10),
    latitude DECIMAL(9, 6),
    longitude DECIMAL(9, 6),

    -- Características
    recamaras INTEGER,
    banos DECIMAL(3, 1),
    superficie_terreno DECIMAL(10, 2),
    superficie_construccion DECIMAL(10, 2),

    -- Precios
    precio_venta DECIMAL(12, 2),
    precio_contado DECIMAL(12, 2),
    enganche DECIMAL(12, 2),

    -- Estado
    estatus VARCHAR(50),
    disponible BOOLEAN DEFAULT TRUE,

    -- Auditoría
    usuario_creacion_id INTEGER REFERENCES auth_user(id),
    fecha_creacion TIMESTAMP DEFAULT NOW(),
    fecha_modificacion TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_casa_plaza ON inventario_casa(plaza);
CREATE INDEX idx_casa_estatus ON inventario_casa(estatus);
CREATE INDEX idx_casa_disponible ON inventario_casa(disponible);

clientes_cliente

Base de datos de clientes.

CREATE TABLE clientes_cliente (
    id SERIAL PRIMARY KEY,
    tipo_persona VARCHAR(20) NOT NULL,  -- 'FISICA' o 'MORAL'

    -- Identificación
    rfc VARCHAR(20) UNIQUE NOT NULL,
    nombre VARCHAR(100),
    apellido_paterno VARCHAR(100),
    apellido_materno VARCHAR(100),
    razon_social VARCHAR(200),

    -- Contacto
    email VARCHAR(254),
    telefono VARCHAR(20),
    celular VARCHAR(20),

    -- Dirección
    calle TEXT,
    numero_exterior VARCHAR(20),
    numero_interior VARCHAR(20),
    colonia VARCHAR(100),
    municipio VARCHAR(100),
    estado VARCHAR(50),
    cp VARCHAR(10),

    -- Metadata
    plaza VARCHAR(50),
    asesor_id INTEGER REFERENCES auth_user(id),
    fecha_creacion TIMESTAMP DEFAULT NOW(),
    activo BOOLEAN DEFAULT TRUE
);

CREATE INDEX idx_cliente_rfc ON clientes_cliente(rfc);
CREATE INDEX idx_cliente_asesor ON clientes_cliente(asesor_id);

apartados_apartado

Registros de ventas y reservaciones.

CREATE TABLE apartados_apartado (
    id SERIAL PRIMARY KEY,

    -- Relaciones
    casa_id INTEGER REFERENCES inventario_casa(id),
    cliente_id INTEGER REFERENCES clientes_cliente(id),
    asesor_id INTEGER REFERENCES auth_user(id),

    -- Tipo de venta
    tipo_venta VARCHAR(20),  -- 'CREDITO', 'CONTADO', 'C90'
    plaza VARCHAR(50),

    -- Precios finales
    precio_final DECIMAL(12, 2),
    enganche_pagado DECIMAL(12, 2),
    monto_credito DECIMAL(12, 2),

    -- Fechas
    fecha_apartado DATE,
    fecha_escrituracion DATE,
    fecha_entrega DATE,

    -- Estado
    estatus VARCHAR(50),
    activo BOOLEAN DEFAULT TRUE,

    -- Comisiones
    comision_asesor DECIMAL(12, 2),
    comision_pagada BOOLEAN DEFAULT FALSE,

    -- Auditoría
    fecha_creacion TIMESTAMP DEFAULT NOW(),
    fecha_modificacion TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_apartado_casa ON apartados_apartado(casa_id);
CREATE INDEX idx_apartado_cliente ON apartados_apartado(cliente_id);
CREATE INDEX idx_apartado_asesor ON apartados_apartado(asesor_id);
CREATE INDEX idx_apartado_estatus ON apartados_apartado(estatus);

tickets_ticket

Sistema de tickets y tareas.

CREATE TABLE tickets_ticket (
    id SERIAL PRIMARY KEY,

    -- Información básica
    titulo VARCHAR(200) NOT NULL,
    descripcion TEXT,
    servicio_id INTEGER REFERENCES tickets_servicio(id),

    -- Asignación
    creador_id INTEGER REFERENCES auth_user(id),
    asignado_id INTEGER REFERENCES auth_user(id),

    -- Prioridad y estado
    prioridad VARCHAR(20) DEFAULT 'MEDIA',
    estatus VARCHAR(20) DEFAULT 'PENDIENTE',

    -- Fechas
    fecha_limite DATE,
    fecha_inicio TIMESTAMP,
    fecha_fin TIMESTAMP,

    -- Metadata
    plaza VARCHAR(50),
    etiquetas TEXT[],  -- PostgreSQL array

    -- Auditoría
    fecha_creacion TIMESTAMP DEFAULT NOW(),
    fecha_modificacion TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_ticket_creador ON tickets_ticket(creador_id);
CREATE INDEX idx_ticket_asignado ON tickets_ticket(asignado_id);
CREATE INDEX idx_ticket_estatus ON tickets_ticket(estatus);
CREATE INDEX idx_ticket_fecha_limite ON tickets_ticket(fecha_limite);

comprobaciones_comprobacion

Gastos y comprobaciones de gastos.

CREATE TABLE comprobaciones_comprobacion (
    id SERIAL PRIMARY KEY,

    -- Identificación
    folio VARCHAR(50) UNIQUE,
    concepto TEXT NOT NULL,

    -- Montos
    monto_total DECIMAL(12, 2) NOT NULL,
    monto_autorizado DECIMAL(12, 2),

    -- Usuario
    solicitante_id INTEGER REFERENCES auth_user(id),
    plaza VARCHAR(50),

    -- Aprobaciones
    aprobado_contabilidad BOOLEAN DEFAULT FALSE,
    aprobador_contabilidad_id INTEGER REFERENCES auth_user(id),
    fecha_aprobacion_contabilidad TIMESTAMP,

    aprobado_tesoreria BOOLEAN DEFAULT FALSE,
    aprobador_tesoreria_id INTEGER REFERENCES auth_user(id),
    fecha_aprobacion_tesoreria TIMESTAMP,

    -- Estado
    estatus VARCHAR(50) DEFAULT 'PENDIENTE',

    -- Auditoría
    fecha_creacion TIMESTAMP DEFAULT NOW(),
    fecha_modificacion TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_comprob_solicitante ON comprobaciones_comprobacion(solicitante_id);
CREATE INDEX idx_comprob_estatus ON comprobaciones_comprobacion(estatus);

ch_horarios

Horarios de trabajo de empleados.

CREATE TABLE ch_horarios (
    id SERIAL PRIMARY KEY,
    perfil_id INTEGER REFERENCES users_perfil(id),

    -- Días de la semana
    lunes_entrada TIME,
    lunes_salida TIME,
    martes_entrada TIME,
    martes_salida TIME,
    -- ... (resto de días)

    -- Metadata
    activo BOOLEAN DEFAULT TRUE,
    fecha_creacion TIMESTAMP DEFAULT NOW()
);

Relaciones Clave

One-to-One

User ←→ Perfil
Apartado ←→ Garantia
Apartado ←→ Cierre

One-to-Many

User  Casas (creadas)
User  Apartados (gestionados)
User  Tickets (asignados)
Casa  AnexosCasa
Cliente  Apartados

Many-to-Many

Ticket ←→ Etiquetas (usando ArrayField)
TareaProgramada ←→ Usuarios (responsables)

Índices y Optimizaciones

Índices Implementados

-- Búsqueda por plaza (muy frecuente)
CREATE INDEX idx_plaza ON inventario_casa(plaza);
CREATE INDEX idx_apartado_plaza ON apartados_apartado(plaza);
CREATE INDEX idx_ticket_plaza ON tickets_ticket(plaza);

-- Búsqueda por estado/estatus
CREATE INDEX idx_casa_disponible ON inventario_casa(disponible);
CREATE INDEX idx_apartado_estatus ON apartados_apartado(estatus);
CREATE INDEX idx_ticket_estatus ON tickets_ticket(estatus);

-- Búsqueda por fechas
CREATE INDEX idx_ticket_fecha_limite ON tickets_ticket(fecha_limite);
CREATE INDEX idx_apartado_fecha ON apartados_apartado(fecha_apartado);

-- Foreign keys (automáticos en PostgreSQL con Django)
-- Se crean automáticamente en todas las ForeignKey

Partial Indexes

-- Solo casas disponibles (consulta muy frecuente)
CREATE INDEX idx_casas_disponibles
ON inventario_casa(plaza, precio_venta)
WHERE disponible = TRUE AND activo = TRUE;

-- Solo tickets pendientes
CREATE INDEX idx_tickets_pendientes
ON tickets_ticket(asignado_id, fecha_limite)
WHERE estatus IN ('PENDIENTE', 'EN_PROCESO');

Triggers y Constraints

Constraints de Negocio

-- El precio de contado debe ser menor al de venta
ALTER TABLE inventario_casa
ADD CONSTRAINT check_precio_contado
CHECK (precio_contado <= precio_venta);

-- El enganche no puede ser mayor al precio
ALTER TABLE apartados_apartado
ADD CONSTRAINT check_enganche
CHECK (enganche_pagado <= precio_final);

Triggers (Ejemplo conceptual)

-- Actualizar fecha_modificacion automáticamente
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.fecha_modificacion = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_casa_modtime
BEFORE UPDATE ON inventario_casa
FOR EACH ROW
EXECUTE FUNCTION update_modified_column();

Vistas Materializadas (Futuro)

Para reportes y analytics:

-- Vista de ventas por plaza (ejemplo)
CREATE MATERIALIZED VIEW reporte_ventas_plaza AS
SELECT
    plaza,
    COUNT(*) as total_ventas,
    SUM(precio_final) as monto_total,
    AVG(precio_final) as precio_promedio,
    DATE_TRUNC('month', fecha_apartado) as mes
FROM apartados_apartado
WHERE estatus = 'CERRADO'
GROUP BY plaza, mes;

-- Refrescar cada noche
CREATE INDEX ON reporte_ventas_plaza(plaza, mes);

Backups y Mantenimiento

Estrategia de Backup

  • Backups automáticos diarios (Heroku Postgres)
  • Retención: 7 días para plan Standard
  • Backup manual antes de migraciones importantes

Mantenimiento

-- Vacuum para recuperar espacio
VACUUM ANALYZE;

-- Reindex para optimizar índices
REINDEX DATABASE sistema_a3;

-- Ver tamaño de tablas
SELECT
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;

Migraciones

Django maneja las migraciones automáticamente:

# Crear migraciones
python manage.py makemigrations

# Ver SQL de migración
python manage.py sqlmigrate inventario 0001

# Aplicar migraciones
python manage.py migrate

# Ver estado de migraciones
python manage.py showmigrations

Consideraciones de Performance

Queries N+1

Problema:

# Genera N+1 queries
casas = Casa.objects.all()
for casa in casas:
    print(casa.usuario_creacion.username)  # Query por cada casa

Solución:

# Una sola query con join
casas = Casa.objects.select_related('usuario_creacion').all()
for casa in casas:
    print(casa.usuario_creacion.username)

Paginación

from django.core.paginator import Paginator

casas = Casa.objects.all()
paginator = Paginator(casas, 50)  # 50 items por página
page = paginator.get_page(1)

Caché de Queries

from django.core.cache import cache

def get_casas_disponibles(plaza):
    cache_key = f'casas_{plaza}'
    casas = cache.get(cache_key)

    if casas is None:
        casas = list(Casa.objects.filter(plaza=plaza, disponible=True))
        cache.set(cache_key, casas, 300)  # 5 minutos

    return casas

Para Más Información

  • Modelos: Documentación detallada de cada modelo
  • API: Cómo acceder a los datos vía API
  • Backend: Lógica de negocio que usa estos modelos

La base de datos es el corazón del Sistema A3, diseñada para escalar con el negocio.