Saltar a contenido

Arquitectura Backend

El backend del Sistema A3 está construido con Django 3.2, siguiendo una arquitectura multi-app modular que separa las responsabilidades del negocio en aplicaciones independientes pero interconectadas.

Estructura de Apps Django

sistema-a3/
├── a3ceros/              # App principal (settings, urls, core)
├── anexos/               # Sistema de archivos adjuntos
├── apartados/            # Módulo de ventas
├── api/                  # REST API endpoints
├── asistenteai/          # Asistente con IA
├── blogs/                # Blog interno
├── ch/                   # Capital Humano (RRHH)
├── clientes/             # Gestión de clientes
├── cobranza/             # Seguimiento de cobranza
├── comprobaciones/       # Gastos y provisiones
├── crm/                  # CRM
├── dashboard/            # Dashboards y KPIs
├── inventario/           # Catálogo de propiedades
├── perfil/               # Perfiles de usuario
├── prospectos/           # Leads y prospectos
├── proyectos/            # Gestión de proyectos
├── push/                 # Web Push notifications
├── reportes/             # Generación de reportes
├── services/             # Servicios compartidos
├── tickets/              # Tickets y acuerdos
├── users/                # Usuarios y permisos
└── vendofacil/           # Interfaz de ventas simplificada

App Core: a3ceros

Responsabilidades

  • Configuración global (settings.py)
  • URL routing principal (urls.py)
  • Context processors
  • Middleware personalizado
  • Utilidades compartidas

settings.py

# a3ceros/settings.py

# Apps instaladas
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    # ...
    'rest_framework',
    'django_filters',
    'notifications',
    # Apps del proyecto
    'users',
    'inventario',
    'apartados',
    'clientes',
    'tickets',
    # ...
]

# Configuración de base de datos
DATABASES = {
    'default': dj_database_url.config(
        conn_max_age=600,
        conn_health_checks=True,
    )
}

# Configuración de archivos estáticos
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# AWS S3 para media files
if USE_S3:
    DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
    AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME')

urls.py

# a3ceros/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('dashboard.urls')),
    path('inventario/', include('inventario.urls')),
    path('apartados/', include('apartados.urls')),
    path('clientes/', include('clientes.urls')),
    path('tickets/', include('tickets.urls')),
    path('api/', include('api.urls')),
    # ... más URLs
]

Patrón de Organización por App

Cada app Django sigue una estructura estándar:

nombre_app/
├── __init__.py
├── models.py           # Modelos de datos
├── views.py            # Vistas (lógica de presentación)
├── urls.py             # URLs de la app
├── forms.py            # Formularios Django
├── serializers.py      # Serializers DRF
├── viewsets.py         # ViewSets DRF
├── admin.py            # Configuración del admin
├── apps.py             # Configuración de la app
├── utils.py            # Utilidades específicas
├── signals.py          # Django signals
├── tests.py            # Tests
├── migrations/         # Migraciones de BD
└── templates/          # Templates específicos
    └── nombre_app/

Capa de Modelos

Ejemplo: Modelo Casa (Inventario)

# inventario/models.py
from django.db import models
from django.contrib.auth.models import User

class Casa(models.Model):
    """Modelo para viviendas en el inventario"""

    # Campos básicos
    clave = models.CharField(max_length=50, unique=True)
    proyecto = models.CharField(max_length=100)
    tipo_vivienda = models.CharField(max_length=50)

    # Ubicación
    direccion = models.TextField()
    plaza = models.CharField(max_length=50)
    latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True)
    longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True)

    # Estado
    estatus = models.CharField(max_length=50)
    disponible = models.BooleanField(default=True)

    # Precios
    precio_venta = models.DecimalField(max_digits=12, decimal_places=2)
    precio_contado = models.DecimalField(max_digits=12, decimal_places=2)

    # Metadatos
    fecha_creacion = models.DateTimeField(auto_now_add=True)
    fecha_modificacion = models.DateTimeField(auto_now=True)
    usuario_creacion = models.ForeignKey(User, on_delete=models.PROTECT)

    class Meta:
        db_table = 'inventario_casa'
        verbose_name = 'Casa'
        verbose_name_plural = 'Casas'
        ordering = ['-fecha_creacion']

    def __str__(self):
        return f"{self.clave} - {self.proyecto}"

Patrones de Modelo Comunes

1. TimeStampedModel

from model_utils.models import TimeStampedModel

class MiModelo(TimeStampedModel):
    # Automáticamente agrega: created, modified
    pass

2. Soft Delete

class MiModelo(models.Model):
    activo = models.BooleanField(default=True)
    cancelado = models.BooleanField(default=False)

    def soft_delete(self):
        self.activo = False
        self.cancelado = True
        self.save()

3. Multi-tenancy (Plaza-based)

class MiModelo(models.Model):
    plaza = models.CharField(max_length=50)

    @classmethod
    def para_plaza(cls, plaza):
        return cls.objects.filter(plaza=plaza)

Capa de Vistas

Function-Based Views (FBV)

# inventario/views.py
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from .models import Casa

@login_required
def listado_casas(request):
    plaza = request.user.perfil.plaza
    casas = Casa.objects.filter(plaza=plaza, disponible=True)

    context = {
        'casas': casas,
        'plaza': plaza
    }
    return render(request, 'inventario/listado.html', context)

@login_required
def detalle_casa(request, clave):
    casa = get_object_or_404(Casa, clave=clave)
    return render(request, 'inventario/detalle.html', {'casa': casa})

Class-Based Views (CBV)

from django.views.generic import ListView, DetailView

class CasaListView(LoginRequiredMixin, ListView):
    model = Casa
    template_name = 'inventario/listado.html'
    context_object_name = 'casas'
    paginate_by = 20

    def get_queryset(self):
        plaza = self.request.user.perfil.plaza
        return Casa.objects.filter(plaza=plaza, disponible=True)

API REST con Django REST Framework

Serializers

# inventario/serializers.py
from rest_framework import serializers
from .models import Casa

class CasaSerializer(serializers.ModelSerializer):
    class Meta:
        model = Casa
        fields = ['id', 'clave', 'proyecto', 'tipo_vivienda',
                  'precio_venta', 'estatus', 'disponible']
        read_only_fields = ['id', 'fecha_creacion']

    def validate_precio_venta(self, value):
        if value <= 0:
            raise serializers.ValidationError("El precio debe ser mayor a 0")
        return value

ViewSets

# inventario/viewsets.py
from rest_framework import viewsets, filters
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from .models import Casa
from .serializers import CasaSerializer

class CasaViewSet(viewsets.ModelViewSet):
    queryset = Casa.objects.all()
    serializer_class = CasaSerializer
    permission_classes = [IsAuthenticated]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter]
    filterset_fields = ['plaza', 'estatus', 'disponible']
    search_fields = ['clave', 'proyecto']

    def get_queryset(self):
        user = self.request.user
        if user.groups.filter(name='Corporativo').exists():
            return Casa.objects.all()
        return Casa.objects.filter(plaza=user.perfil.plaza)

Router

# api/urls.py
from rest_framework import routers
from inventario.viewsets import CasaViewSet

router = routers.DefaultRouter()
router.register('casas', CasaViewSet)

urlpatterns = router.urls

Capa de Servicios

# services/inventario_service.py
from inventario.models import Casa
from services.sap_service import SAPService

class InventarioService:
    """Lógica de negocio para inventario"""

    @staticmethod
    def crear_casa(data, usuario):
        # Validación de negocio
        if Casa.objects.filter(clave=data['clave']).exists():
            raise ValueError("La clave ya existe")

        # Crear en BD local
        casa = Casa.objects.create(
            **data,
            usuario_creacion=usuario
        )

        # Sincronizar con SAP
        try:
            SAPService.enviar_casa(casa)
        except Exception as e:
            # Log del error pero no fallar
            logging.error(f"Error al sincronizar con SAP: {e}")

        return casa

    @staticmethod
    def cambiar_estatus(casa, nuevo_estatus, usuario):
        # Validar transición de estado
        if not validar_cambio_estatus(casa.estatus, nuevo_estatus):
            raise ValueError("Cambio de estatus no permitido")

        casa.estatus = nuevo_estatus
        casa.save()

        # Notificar a usuarios relevantes
        notificar_cambio_estatus(casa, usuario)

        return casa

Django Signals

# tickets/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from notifications.signals import notify
from .models import Ticket

@receiver(post_save, sender=Ticket)
def notificar_ticket_creado(sender, instance, created, **kwargs):
    if created:
        # Notificar al asignado
        notify.send(
            sender=instance.creador,
            recipient=instance.asignado,
            verb='te asignó un ticket',
            action_object=instance,
            description=instance.titulo
        )

Permisos y Autorizació

n

# tickets/permissions.py
from rest_framework import permissions

class EsPropietarioOCorporativo(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        # Corporativo puede todo
        if request.user.groups.filter(name='Corporativo').exists():
            return True

        # El propietario puede ver/editar
        return obj.creador == request.user or obj.asignado == request.user

Middleware Personalizado

# a3ceros/middleware.py
class PlazaMiddleware:
    """Agrega plaza del usuario al request"""

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.user.is_authenticated:
            request.plaza = request.user.perfil.plaza
        else:
            request.plaza = None

        response = self.get_response(request)
        return response

Management Commands

# tickets/management/commands/generar_instancias_recurrentes.py
from django.core.management.base import BaseCommand
from tickets.models import TareaProgramada

class Command(BaseCommand):
    help = 'Genera instancias de tareas programadas recurrentes'

    def handle(self, *args, **options):
        tareas = TareaProgramada.objects.filter(activa=True)

        for tarea in tareas:
            if tarea.debe_generar_instancia_hoy():
                tarea.generar_instancia()
                self.stdout.write(
                    self.style.SUCCESS(f'Instancia creada para: {tarea}')
                )

Testing

# inventario/tests.py
import pytest
from model_bakery import baker
from inventario.models import Casa

@pytest.mark.django_db
class TestCasaModel:
    def test_crear_casa(self):
        casa = baker.make(Casa, clave='TEST001')
        assert casa.clave == 'TEST001'

    def test_clave_unique(self):
        baker.make(Casa, clave='TEST001')
        with pytest.raises(Exception):
            baker.make(Casa, clave='TEST001')

Best Practices Implementadas

Fat models, thin views: Lógica en modelos y servicios ✅ DRY: Utilidades compartidas en services/Separation of concerns: Cada app tiene su responsabilidad ✅ Type hints: Donde es posible (Python 3.8+) ✅ Logging: Para debugging y auditoría ✅ Transactions: Para operaciones críticas ✅ Indexing: En campos de búsqueda frecuente ✅ Select/Prefetch related: Para optimizar queries


Para ver cómo los modelos se relacionan en la base de datos, consulta Base de Datos.