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" 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" 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',

Feb 21, 2025 - 02:12
 0
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"

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"

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)})

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'),
]

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:


 class="product-card" data-product-id="{{ producto.id }}">
     src="{{ producto.imagen.url }}" alt="{{ producto.nombre }}">
    

{{ producto.nombre }}

${{ producto.precio|floatformat:2 }} type="number" min="1" value="1"> class="btn" onclick="agregarAlCarrito(this)">Añadir al carrito

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 = `
            ${item.nombre}
            $${item.precio.toFixed(2)}
            ${item.cantidad}
            $${(item.precio * item.cantidad).toFixed(2)}
            
        `;
        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);
    }
});

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:


...
 id="body-cart">
 id="foot-cart">
    
         colspan="3">Total
        
    

...
 class="cart-actions">
     id="btn-checkout" class="btn">Finalizar Compra
...