DEV Community

Cover image for Desarrollo de Ecommerce con Django (parte 5)
Gabriel Villacis
Gabriel Villacis

Posted on

Desarrollo de Ecommerce con Django (parte 5)

A continuación se presenta un tutorial completo y detallado que te guiará paso a paso para implementar la funcionalidad de checkout en tu ecommerce. En este ejemplo aprenderás a:

  • Definir los modelos Pedido y ItemPedido para almacenar cada pedido realizado y sus elementos, utilizando el modelo Producto existente y el modelo de usuario de Django.
  • Programar la vista y la URL para procesar el checkout mediante una solicitud AJAX con Axios, enviando los datos del carrito (almacenado en localStorage) al servidor.
  • Adaptar el script del carrito para que, además de permitir agregar y eliminar productos, envíe el pedido al hacer clic en el botón de checkout (identificado por un id único).
  • Configurar el panel de administración para visualizar el detalle de cada pedido y permitir filtrar por usuario, fechas e incluso por producto.

Con estos pasos, lograrás integrar el proceso de checkout de forma asíncrona, manteniendo la experiencia del usuario fluida y además tendrás herramientas en el admin para gestionar los pedidos.


1. Creación de los Modelos de Pedido e ItemPedido

1.1. Modelo Pedido

El modelo Pedido almacenará la información general del pedido, asociándolo al usuario (si está autenticado), registrando la fecha de creación y el total del pedido.

# store/models/pedido.py
from django.db import models
from django.conf import settings

class Pedido(models.Model):
    usuario = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True, blank=True,
        verbose_name="Usuario"
    )
    fecha_creacion = models.DateTimeField(auto_now_add=True, verbose_name="Fecha de Creación")
    total = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Total del Pedido")

    def __str__(self):
        return f"Pedido {self.id}"

    class Meta:
        db_table = 'st_pedidos'
        verbose_name = "Pedido"
        verbose_name_plural = "Pedidos"
Enter fullscreen mode Exit fullscreen mode

1.2. Modelo ItemPedido

El modelo ItemPedido representará cada producto incluido en el pedido.

# store/models/itempedido.py
from django.db import models
from store.models import Producto, Pedido

class ItemPedido(models.Model):
    pedido = models.ForeignKey(
        Pedido, related_name='items', 
        on_delete=models.CASCADE, verbose_name="Pedido"
    )
    producto = models.ForeignKey(
        Producto, on_delete=models.PROTECT, verbose_name="Producto"
    )
    precio = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Precio Unitario")
    cantidad = models.PositiveIntegerField(verbose_name="Cantidad")

    def total_item(self):
        return self.precio * self.cantidad

    def __str__(self):
        return f"{self.producto.nombre}"

    class Meta:
        db_table = 'st_items_pedido'
        verbose_name = "Item de Pedido"
        verbose_name_plural = "Items de Pedido"
Enter fullscreen mode Exit fullscreen mode

Recomendación:

Si en el futuro deseas actualizar el precio del producto o conservar el precio histórico, este modelo almacena el precio en el momento del pedido.


2. Programación de la Vista y URL para el Checkout

2.1. Vista de Checkout

La vista de checkout recibirá una solicitud POST vía AJAX con un JSON que contenga los datos del carrito. Se espera que cada ítem incluya, al menos, el product_id (para identificar el producto), la cantidad y, opcionalmente, el precio y el nombre (aunque se obtendrá el precio real del producto en el servidor). La vista calculará el total, creará un Pedido y, para cada ítem, un ItemPedido.

En store/views.py agrega la siguiente función:

import json
from decimal import Decimal
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
from store.models import Producto
from .models.pedido import Pedido
from .models.itempedido import ItemPedido

@require_POST
@login_required  # Solo usuarios autenticados pueden realizar pedidos
def ajax_checkout(request):
    """
    Se espera recibir un JSON en el cuerpo de la solicitud con la estructura:
    {
        "items": [
            {"product_id": "1", "nombre": "Producto A", "precio": 9.99, "cantidad": 2},
            {"product_id": "3", "nombre": "Producto B", "precio": 19.99, "cantidad": 1},
            ...
        ]
    }
    """
    try:
        data = json.loads(request.body)
        items = data.get('items', [])
        if not items:
            return JsonResponse({'success': False, 'message': 'El carrito está vacío.'})

        total_pedido = Decimal('0.00')
        for item in items:
            precio = Decimal(str(item.get('precio', '0.00')))
            cantidad = int(item.get('cantidad', 0))
            total_pedido += precio * cantidad

        pedido = Pedido.objects.create(
            usuario=request.user,
            total=total_pedido
        )

        for item in items:
            product_id = item.get('product_id')
            cantidad = int(item.get('cantidad', 0))
            # Se obtiene el producto desde el modelo Producto
            producto = Producto.objects.get(pk=product_id)
            # Se usa el precio actual del producto; alternativamente, se puede usar el enviado
            precio = producto.precio  
            ItemPedido.objects.create(
                pedido=pedido,
                producto=producto,
                precio=precio,
                cantidad=cantidad
            )

        return JsonResponse({
            'success': True,
            'message': 'Pedido procesado exitosamente.',
            'pedido_id': pedido.id
        })
    except Exception as e:
        return JsonResponse({'success': False, 'message': str(e)})
Enter fullscreen mode Exit fullscreen mode

2.2. Configuración de la URL para Checkout

En store/urls.py añade la ruta correspondiente:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='home'),
    path('catalog/', views.catalog, name='catalog'),
    path('cart/', views.cart, name='cart'),
    path('contact/', views.contact, name='contact'),
    path('signup/', views.signup_view, name='signup'),
    path('signin/', views.signin_view, name='signin'),

    # Endpoints para solicitudes AJAX (registro, inicio, logout)
    path('ajax/signup/', views.ajax_signup, name='ajax_signup'),
    path('ajax/signin/', views.ajax_signin, name='ajax_signin'),
    path('logout/', views.ajax_logout, name='logout'),

    # Ruta para el checkout
    path('checkout/', views.ajax_checkout, name='checkout'),
]
Enter fullscreen mode Exit fullscreen mode

3. Adaptación y Administración del Carrito y Checkout en el Front End

3.1. Actualización de las Plantillas y el Script

a) Inyección de Datos del Producto en la Plantilla

Para que el script pueda almacenar correctamente el carrito, es necesario que cada tarjeta de producto incluya el product_id. Por ejemplo, en la plantilla del catálogo (o en la de inicio) modifica la tarjeta de producto para incluir un atributo data-product-id:

<!-- Ejemplo de una tarjeta de producto en catalog.html -->
<div class="product-card" data-product-id="{{ producto.id }}">
    <img src="{{ producto.imagen.url }}" alt="{{ producto.nombre }}">
    <h3>{{ producto.nombre }}</h3>
    <p>${{ producto.precio|floatformat:2 }}</p>
    <input type="number" min="1" value="1">
    <button class="btn" onclick="agregarAlCarrito(this)">Añadir al carrito</button>
</div>
Enter fullscreen mode Exit fullscreen mode

b) Adaptación del Script JavaScript (static/js/script.js)

Actualiza el script para que al agregar un producto al carrito se almacene también el product_id, y para que el botón de checkout tenga un id (por ejemplo, btn-checkout) para asociarle el evento solo cuando esté presente en la página del carrito.

// Guardar y obtener el carrito en localStorage
const guardarCarrito = (carrito) => {
    localStorage.setItem('carrito', JSON.stringify(carrito));
}

const obtenerCarrito = () => {
    return JSON.parse(localStorage.getItem('carrito')) || [];
}

// Agregar un producto al carrito
const agregarAlCarrito = (boton) => {
    let divProductCard = boton.closest('.product-card');
    let productId = divProductCard.getAttribute('data-product-id');
    let nombreProducto = divProductCard.querySelector('h3').textContent;
    let precioProducto = parseFloat(divProductCard.querySelector('p').textContent.replace('$', ''));
    let cantidad = parseInt(divProductCard.querySelector('input').value);

    let carrito = obtenerCarrito();
    // Crear objeto con los datos necesarios
    let producto = { product_id: productId, nombre: nombreProducto, precio: precioProducto, cantidad: cantidad };
    let existente = carrito.find(item => item.product_id === productId);
    if (existente) {
        existente.cantidad += cantidad;
    } else {
        carrito.push(producto);
    }
    guardarCarrito(carrito);
    alert(`Se añadió el producto al carrito: ${nombreProducto}`);
}

// Renderizar el carrito en la página del carrito
const renderizarCarrito = () => {
    let tablaCarrito = document.getElementById('body-cart');
    if (!tablaCarrito) return;
    tablaCarrito.innerHTML = '';
    let carrito = obtenerCarrito();
    let total = 0;

    carrito.forEach((item, index) => {
        let fila = document.createElement('tr');
        fila.innerHTML = `
            <td>${item.nombre}</td>
            <td>$${item.precio.toFixed(2)}</td>
            <td>${item.cantidad}</td>
            <td>$${(item.precio * item.cantidad).toFixed(2)}</td>
            <td><button onclick="eliminarDelCarrito(${index})">Eliminar</button></td>
        `;
        total += (item.precio * item.cantidad);
        tablaCarrito.appendChild(fila);
    });
    document.querySelector('#foot-cart td:last-child').textContent = `$${total.toFixed(2)}`;
}

// Eliminar un elemento del carrito
const eliminarDelCarrito = (index) => {
    let carrito = obtenerCarrito();
    carrito.splice(index, 1);
    guardarCarrito(carrito);
    renderizarCarrito();
}

// Función para procesar el checkout del pedido
const procesarCheckout = () => {
    let carrito = obtenerCarrito();
    if (carrito.length === 0) {
        alert("El carrito está vacío.");
        return;
    }

    // Enviar los datos del carrito en formato JSON al servidor
    axios.post(ajaxCheckoutUrl, {
        items: carrito
    }, {
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
            'X-CSRFToken': csrfToken
        }
    })
    .then(response => {
        if (response.data.success) {
            alert(response.data.message);
            // Limpiar el carrito
            localStorage.removeItem('carrito');
            window.location.href = homeUrl;
        } else {
            alert("Error al procesar el pedido: " + response.data.message);
        }
    })
    .catch(error => {
        console.error('Error en checkout:', error);
        alert("Ocurrió un error en el proceso de compra.");
    });
}

// Asignar eventos basados en la existencia de elementos en el DOM
document.addEventListener('DOMContentLoaded', function() {
    // Si estamos en la página del carrito, renderizamos el contenido
    if (document.getElementById('body-cart')) {
        renderizarCarrito();
    }
    // Si existe el botón de checkout (por id "btn-checkout"), asociar el evento
    let btnCheckout = document.getElementById('btn-checkout');
    if (btnCheckout) {
        btnCheckout.addEventListener('click', procesarCheckout);
    }
});
Enter fullscreen mode Exit fullscreen mode

Variables Globales:

Estas variables (por ejemplo, ajaxCheckoutUrl, csrfToken, homeUrl) se definirán en la plantilla base, como se muestra a continuación.

c) Actualización de la Plantilla del Carrito (cart.html)

Asegúrate de que en la plantilla del carrito se incluya el id en el botón de checkout y los elementos para renderizar el carrito:

<!-- Fragmento de cart.html -->
...
<tbody id="body-cart"></tbody>
<tfoot id="foot-cart">
    <tr>
        <td colspan="3">Total</td>
        <td></td>
    </tr>
</tfoot>
...
<div class="cart-actions">
    <button id="btn-checkout" class="btn">Finalizar Compra</button>
</div>
...
Enter fullscreen mode Exit fullscreen mode

4. Actualización de la Plantilla Base

En store/templates/base.html se incluirán es script y se definirá la variable global checkoutUrlque usará el script.


    <!-- Incluir Axios desde un CDN -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <!-- Definir variables globales para el script -->
    <script>
        var ajaxSignupUrl = "{% url 'ajax_signup' %}";
        var ajaxSigninUrl = "{% url 'ajax_signin' %}";
        var signinUrl = "{% url 'signin' %}";
        var homeUrl = "{% url 'home' %}";
        var logoutUrl = "{% url 'logout' %}";
        var csrfToken = "{{ csrf_token }}";
        var ajaxCheckoutUrl = "{% url 'checkout' %}";

    </script>
    <!-- Incluir el JavaScript de autenticación -->
    <script src="{% static 'js/auth.js' %}"></script>
    <!-- Incluir el JavaScript del carrito y checkout -->
    <script src="{% static 'js/script.js' %}"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

5. Administración de Pedidos en el Panel de Django

Para facilitar la gestión de pedidos, configuraremos el panel de administración para que muestre el detalle de cada pedido y permita filtrar por usuario, fecha y producto.

En store/admin.py agrega lo siguiente:

from django.contrib import admin
from django.db.models import Count
from store.models import Pedido, ItemPedido

class ItemPedidoInline(admin.TabularInline):
    model = ItemPedido
    extra = 0  # No muestra formularios vacíos adicionales para agregar nuevos items inline

@admin.register(Pedido)
class PedidoAdmin(admin.ModelAdmin):
    inlines = [ItemPedidoInline]
    list_display = ('id', 'usuario', 'fecha_creacion', 'total', 'cantidad_items')
    list_filter = ('usuario', 'fecha_creacion')
    search_fields = ('items__producto__nombre',)

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.annotate(num_items=Count('items'))

    def cantidad_items(self, obj):
        return obj.num_items
    cantidad_items.short_description = 'Cantidad de Items'
Enter fullscreen mode Exit fullscreen mode

Detalles:

  • Se utiliza un TabularInline para mostrar los ítems asociados a cada pedido.
  • Con list_filter se pueden filtrar los pedidos por usuario y fecha.
  • Con search_fields se permite buscar pedidos por el nombre del producto relacionado (accediendo a través de la relación de ítems).

- extra=0 en ItemPedidoInline indica que en el panel de administración no se mostrarán formularios en blanco adicionales para agregar nuevos registros inline. Es decir, solo se mostrarán los items que ya existen, sin crear formularios vacíos extra.


Conclusión

En este tutorial se ha implementado de manera integral la funcionalidad de checkout en un ecommerce. Se han realizado los siguientes pasos:

  1. Modelado de Datos:

    Se crearon los modelos Pedido e ItemPedido para almacenar los pedidos y los elementos de cada pedido, relacionándolos con el usuario y con el modelo Producto.

  2. Procesamiento del Checkout:

    Se desarrolló una vista que recibe los datos del carrito (en formato JSON) vía AJAX, calcula el total, crea un pedido y sus respectivos ítems, y retorna una respuesta JSON.

  3. Integración con el Front End:

    El script de carrito se adaptó para almacenar el product_id junto con los demás datos, y se configuró para renderizar el carrito solo en la página correspondiente. Además, se agregó un botón de checkout con id específico para asociarle la función de procesar el pedido.

  4. Administración en Django:

    Se configuró el panel de administración para gestionar los pedidos, mostrando los detalles y permitiendo filtrar por usuario, fecha y producto.

Con estos pasos, tu ecommerce contará con un proceso de checkout asíncrono y administrable, integrando de forma fluida la parte del cliente (carrito en localStorage) y la persistencia en el servidor. ¡Sigue avanzando y personalizando tu proyecto para adaptarlo a tus necesidades!

Top comments (1)

Collapse
 
programmerraja profile image
Boopathi

Nice tutorial! This detailed guide on implementing checkout in Django E-commerce with AJAX is very useful for developers working on similar projects.