docs: add full documentation for all functions in tienda/views.py
Agent-Logs-Url: https://github.com/dsaub/proyecto-final/sessions/49df17c8-213a-4e23-adfe-465a0104f6a3 Co-authored-by: dsaub <54474838+dsaub@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
47ea00c822
commit
ed92991872
@@ -0,0 +1,24 @@
|
|||||||
|
# Documentación del Proyecto
|
||||||
|
|
||||||
|
Bienvenido a la documentación del proyecto **Tienda**. Aquí encontrarás la referencia detallada de los módulos y funciones que componen la aplicación.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Módulos documentados
|
||||||
|
|
||||||
|
### [`views/`](./views/INDEX.md)
|
||||||
|
|
||||||
|
Documentación completa de todas las funciones definidas en `tienda/views.py`, organizadas por área funcional:
|
||||||
|
|
||||||
|
- Funciones auxiliares y helpers
|
||||||
|
- Carrito de compra
|
||||||
|
- Reservas de stock
|
||||||
|
- Pedidos y checkout
|
||||||
|
- Pago con Stripe y PayPal
|
||||||
|
- Panel de vendedor
|
||||||
|
- Portal de usuario
|
||||||
|
- Direcciones de envío
|
||||||
|
- Búsqueda
|
||||||
|
- Catálogo público
|
||||||
|
- Autenticación y cuentas
|
||||||
|
- Páginas estáticas y utilidades
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# Documentación de `tienda/views.py`
|
||||||
|
|
||||||
|
Este directorio contiene la documentación de cada función definida en `tienda/views.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Funciones auxiliares y helpers
|
||||||
|
|
||||||
|
### Helpers privados de localización y validación
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`_normalize_location_text`](./_normalize_location_text.md) | Normaliza texto de localidad (quita acentos, símbolos, pasa a minúsculas). |
|
||||||
|
| [`_is_almeria_postal_code`](./_is_almeria_postal_code.md) | Valida que un código postal pertenezca a Almería (`04xxx`). |
|
||||||
|
| [`_is_almeria_city`](./_is_almeria_city.md) | Comprueba si un municipio pertenece a la provincia de Almería. |
|
||||||
|
| [`_address_form_context`](./_address_form_context.md) | Construye el contexto de template para formularios de dirección. |
|
||||||
|
|
||||||
|
### Helpers privados de petición
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`_get_client_ip`](./_get_client_ip.md) | Obtiene la IP real del cliente (soporta proxy con `X-Forwarded-For`). |
|
||||||
|
|
||||||
|
### Helpers públicos de precio
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`get_price_with_vat_decimal`](./get_price_with_vat_decimal.md) | Calcula el precio con IVA aplicado, redondeado a 2 decimales. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛒 Carrito de compra
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`get_or_create_cart`](./get_or_create_cart.md) | Obtiene o crea el carrito del usuario/sesión actual. |
|
||||||
|
| [`add_to_cart`](./add_to_cart.md) | Agrega un producto al carrito. |
|
||||||
|
| [`view_cart`](./view_cart.md) | Muestra el contenido del carrito. |
|
||||||
|
| [`update_cart_item`](./update_cart_item.md) | Actualiza la cantidad de un ítem del carrito. |
|
||||||
|
| [`remove_from_cart`](./remove_from_cart.md) | Elimina un ítem del carrito. |
|
||||||
|
| [`clear_cart`](./clear_cart.md) | Vacía el carrito completo. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Reservas de stock
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`_get_or_create_session_key`](./_get_or_create_session_key.md) | Garantiza que la sesión tiene una clave activa. |
|
||||||
|
| [`_get_reservation_owner_filters`](./_get_reservation_owner_filters.md) | Filtros ORM para identificar al propietario de una reserva. |
|
||||||
|
| [`_release_expired_stock_reservations`](./_release_expired_stock_reservations.md) | Marca como expiradas las reservas caducadas. |
|
||||||
|
| [`_clear_stock_reservation_session`](./_clear_stock_reservation_session.md) | Elimina los datos de reserva de la sesión HTTP. |
|
||||||
|
| [`_cancel_active_stock_reservations_for_request`](./_cancel_active_stock_reservations_for_request.md) | Cancela las reservas activas del usuario/sesión actual. |
|
||||||
|
| [`_get_reserved_quantities_by_product`](./_get_reserved_quantities_by_product.md) | Calcula la cantidad reservada activamente por producto. |
|
||||||
|
| [`_get_active_reservation_ids_for_request`](./_get_active_reservation_ids_for_request.md) | Devuelve los IDs de reservas activas del usuario/sesión. |
|
||||||
|
| [`_get_available_stock_by_product`](./_get_available_stock_by_product.md) | Calcula el stock disponible real (total menos reservado). |
|
||||||
|
| [`_get_cart_stock_issues`](./_get_cart_stock_issues.md) | Detecta conflictos de stock en los ítems del carrito. |
|
||||||
|
| [`_build_stock_issue_message`](./_build_stock_issue_message.md) | Construye el mensaje de error para un conflicto de stock. |
|
||||||
|
| [`_create_stock_reservation_for_cart`](./_create_stock_reservation_for_cart.md) | Crea atómicamente una reserva de stock para el carrito. |
|
||||||
|
| [`_get_session_stock_reservation`](./_get_session_stock_reservation.md) | Recupera la reserva de stock activa desde la sesión. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💳 Pedidos y checkout
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`_get_selected_shipping_address`](./_get_selected_shipping_address.md) | Obtiene y valida la dirección de envío seleccionada. |
|
||||||
|
| [`create_order_from_cart`](./create_order_from_cart.md) | Crea el pedido, descuenta stock y vacía el carrito. |
|
||||||
|
| [`checkout`](./checkout.md) | Página de checkout con resumen de carrito y direcciones. |
|
||||||
|
| [`checkout_success`](./checkout_success.md) | Página de confirmación de pago exitoso (Stripe). |
|
||||||
|
| [`checkout_cancel`](./checkout_cancel.md) | Página de cancelación de pago. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💳 Pago con Stripe
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`stripe_config`](./stripe_config.md) | Expone la clave pública de Stripe al frontend. |
|
||||||
|
| [`create_checkout_session`](./create_checkout_session.md) | Crea una sesión de pago en Stripe Checkout. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🅿️ Pago con PayPal
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`create_paypal_payment`](./create_paypal_payment.md) | Crea un pago en PayPal y devuelve la URL de aprobación. |
|
||||||
|
| [`paypal_execute`](./paypal_execute.md) | Confirma y ejecuta el pago de PayPal tras la aprobación. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏪 Panel de vendedor
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`mis_productos`](./mis_productos.md) | Lista de productos del vendedor autenticado. |
|
||||||
|
| [`pedidos_vendedor`](./pedidos_vendedor.md) | Lista de pedidos asignados al vendedor. |
|
||||||
|
| [`cambiar_estado_pedido`](./cambiar_estado_pedido.md) | Cambia el estado de un ítem de pedido. |
|
||||||
|
| [`enviar_mensaje_pedido`](./enviar_mensaje_pedido.md) | Envía un mensaje al comprador sobre un pedido. |
|
||||||
|
| [`crear_producto`](./crear_producto.md) | Formulario para crear un nuevo producto. |
|
||||||
|
| [`editar_producto`](./editar_producto.md) | Formulario para editar un producto existente. |
|
||||||
|
| [`borrar_producto`](./borrar_producto.md) | Elimina un producto del vendedor. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 Portal de usuario
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`portal_usuario`](./portal_usuario.md) | Dashboard del portal de usuario. |
|
||||||
|
| [`mis_compras`](./mis_compras.md) | Historial de compras del usuario. |
|
||||||
|
| [`mis_recibos`](./mis_recibos.md) | Lista de recibos / pedidos pagados. |
|
||||||
|
| [`editar_perfil`](./editar_perfil.md) | Edita el perfil del usuario. |
|
||||||
|
| [`cambiar_contrasena`](./cambiar_contrasena.md) | Cambia la contraseña del usuario. |
|
||||||
|
| [`mensajes_comprador`](./mensajes_comprador.md) | Mensajes de vendedores al comprador. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📍 Direcciones de envío
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`direcciones_usuario`](./direcciones_usuario.md) | Lista las direcciones del usuario. |
|
||||||
|
| [`crear_direccion`](./crear_direccion.md) | Crea una nueva dirección de entrega. |
|
||||||
|
| [`editar_direccion`](./editar_direccion.md) | Edita una dirección existente. |
|
||||||
|
| [`eliminar_direccion`](./eliminar_direccion.md) | Elimina una dirección. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Búsqueda
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`search`](./search.md) | Búsqueda de productos por nombre y descripción. |
|
||||||
|
| [`search_suggestions`](./search_suggestions.md) | API AJAX de sugerencias para el autocompletado. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏷️ Catálogo público
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`home`](./home.md) | Página de inicio con productos destacados. |
|
||||||
|
| [`index`](./index.md) | Catálogo paginado de productos. |
|
||||||
|
| [`producto`](./producto.md) | Detalle de un producto (con caché Redis). |
|
||||||
|
| [`categoria`](./categoria.md) | Catálogo filtrado por categoría. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Autenticación y cuentas
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`login`](./login.md) | Inicio de sesión. |
|
||||||
|
| [`register`](./register.md) | Registro de nuevo usuario. |
|
||||||
|
| [`logout`](./logout.md) | Cierre de sesión. |
|
||||||
|
| [`verify`](./verify.md) | Verificación de cuenta por email. |
|
||||||
|
| [`reset_password`](./reset_password.md) | Solicitud de restablecimiento de contraseña. |
|
||||||
|
| [`reset_password_phase2`](./reset_password_phase2.md) | Segunda fase: establecer la nueva contraseña. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Páginas estáticas y utilidades
|
||||||
|
|
||||||
|
| Función | Descripción |
|
||||||
|
|---------|-------------|
|
||||||
|
| [`rgpd`](./rgpd.md) | Página de política de privacidad / RGPD. |
|
||||||
|
| [`send_test_email`](./send_test_email.md) | Vista de prueba para envío de correo (solo desarrollo). |
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# `_address_form_context`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Construye el diccionario de contexto que se pasa a los templates del formulario de dirección (`tienda/editar_direccion.html`). Centraliza los datos necesarios para renderizar el formulario, evitando repetición de código en las vistas de creación y edición de direcciones.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _address_form_context(direccion=None) -> dict:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-------------|-----------------------------|-------------------------------------------------------------------------------|
|
||||||
|
| `direccion` | `ShippingAddress` o `None` | Instancia de dirección a editar, o `None` para un formulario en blanco. También acepta `request.POST` para repoblar el formulario tras un error de validación. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Diccionario con las siguientes claves:
|
||||||
|
|
||||||
|
| Clave | Tipo | Descripción |
|
||||||
|
|---------------------------|--------|--------------------------------------------------|
|
||||||
|
| `direccion` | objeto | La dirección pasada como argumento. |
|
||||||
|
| `almeria_municipalities` | lista | Lista de municipios de Almería para el selector. |
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Utilizada en [`crear_direccion`](./crear_direccion.md) y [`editar_direccion`](./editar_direccion.md) para preparar el contexto del template.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# `_build_stock_issue_message`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Construye el mensaje de error legible por el usuario cuando hay un conflicto de stock para un ítem del carrito.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _build_stock_issue_message(issue: dict) -> str:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|---------|--------|----------------------------------------------------------------------------------|
|
||||||
|
| `issue` | `dict` | Diccionario con las claves `product_name`, `available` y `requested`. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Cadena con el mensaje de error, por ejemplo:
|
||||||
|
|
||||||
|
> `No hay stock suficiente de 'Camiseta azul'. Disponible: 3, solicitado: 5.`
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada en [`_create_stock_reservation_for_cart`](./_create_stock_reservation_for_cart.md), [`create_order_from_cart`](./create_order_from_cart.md), [`create_checkout_session`](./create_checkout_session.md) y [`create_paypal_payment`](./create_paypal_payment.md).
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# `_cancel_active_stock_reservations_for_request`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Cancela todas las reservas de stock activas y no caducadas asociadas al usuario o sesión actual. Antes de cancelar, llama a [`_release_expired_stock_reservations`](./_release_expired_stock_reservations.md) para que el estado de las reservas sea consistente.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _cancel_active_stock_reservations_for_request(request: HttpRequest) -> None:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
No devuelve ningún valor.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada cuando el usuario modifica el carrito (agrega, actualiza o elimina un ítem, o lo vacía por completo), garantizando que cualquier reserva de stock previa quede liberada antes de la nueva operación. Usada en [`add_to_cart`](./add_to_cart.md), [`update_cart_item`](./update_cart_item.md), [`remove_from_cart`](./remove_from_cart.md), [`clear_cart`](./clear_cart.md) y [`checkout_cancel`](./checkout_cancel.md).
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# `_clear_stock_reservation_session`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Elimina de la sesión HTTP las claves relacionadas con la reserva de stock actual:
|
||||||
|
- `STOCK_RESERVATION_SESSION_KEY` → ID de la reserva activa.
|
||||||
|
- `STOCK_RESERVATION_PAYMENT_SESSION_KEY` → Método de pago de la reserva.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _clear_stock_reservation_session(request: HttpRequest) -> None:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
No devuelve ningún valor.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada tras completar o cancelar el proceso de pago para limpiar el estado de sesión. Usada en [`add_to_cart`](./add_to_cart.md), [`update_cart_item`](./update_cart_item.md), [`remove_from_cart`](./remove_from_cart.md), [`clear_cart`](./clear_cart.md), [`checkout_success`](./checkout_success.md), [`checkout_cancel`](./checkout_cancel.md) y [`paypal_execute`](./paypal_execute.md).
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# `_create_stock_reservation_for_cart`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Crea atómicamente una reserva de stock para todos los ítems del carrito, bloqueando el inventario durante el proceso de pago.
|
||||||
|
|
||||||
|
El proceso es:
|
||||||
|
|
||||||
|
1. Libera reservas expiradas.
|
||||||
|
2. Dentro de una transacción atómica con bloqueo (`SELECT FOR UPDATE`), cancela la reserva activa previa del usuario.
|
||||||
|
3. Verifica la disponibilidad de stock para cada producto.
|
||||||
|
4. Si todo está disponible, crea un registro `StockReservation` con los `StockReservationItem` asociados, con una caducidad de `STOCK_RESERVATION_MINUTES` minutos.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _create_stock_reservation_for_cart(
|
||||||
|
request: HttpRequest,
|
||||||
|
cart_items,
|
||||||
|
payment_method: str
|
||||||
|
) -> tuple[StockReservation | None, list[str]]:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|------------------|-----------------------|-----------------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `cart_items` | lista de `CartItem` | Ítems del carrito a reservar. |
|
||||||
|
| `payment_method` | `str` | Método de pago (`stripe` o `paypal`). |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Tupla `(reserva, errores)`:
|
||||||
|
|
||||||
|
- Si la reserva se crea con éxito: `(StockReservation, [])`.
|
||||||
|
- Si hay problemas de stock o el carrito está vacío: `(None, [mensaje_error])`.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada desde [`create_checkout_session`](./create_checkout_session.md) y [`create_paypal_payment`](./create_paypal_payment.md) justo antes de redirigir al usuario a la pasarela de pago.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> La operación se ejecuta dentro de una transacción con bloqueos de fila para evitar condiciones de carrera en entornos con alta concurrencia.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# `_get_active_reservation_ids_for_request`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Devuelve los IDs de todas las reservas de stock activas y no caducadas que pertenecen al usuario o sesión actuales. Antes de la consulta, libera automáticamente las reservas expiradas.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_active_reservation_ids_for_request(request: HttpRequest) -> list[int]:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Lista de enteros con los IDs de reservas activas. Puede estar vacía.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada desde [`view_cart`](./view_cart.md) y [`checkout`](./checkout.md) para excluir la propia reserva del usuario al calcular el stock disponible, evitando así falsos avisos de stock insuficiente durante el proceso de pago.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# `_get_available_stock_by_product`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Calcula el stock disponible real de cada producto, descontando las cantidades reservadas por otras reservas activas.
|
||||||
|
|
||||||
|
El stock disponible se calcula como:
|
||||||
|
|
||||||
|
```
|
||||||
|
disponible = max(stock_total - cantidades_reservadas, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Antes de calcular, libera las reservas expiradas llamando a [`_release_expired_stock_reservations`](./_release_expired_stock_reservations.md).
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_available_stock_by_product(
|
||||||
|
product_ids,
|
||||||
|
exclude_reservation_ids=None
|
||||||
|
) -> dict:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|---------------------------|------------------|------------------------------------------------------------------------------|
|
||||||
|
| `product_ids` | lista de `int` | IDs de los productos a consultar. |
|
||||||
|
| `exclude_reservation_ids` | lista de `int` o `None` | Reservas a excluir del cómputo (habitualmente, la reserva activa propia del usuario). |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Diccionario `{product_id: stock_disponible}`.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada desde [`add_to_cart`](./add_to_cart.md), [`update_cart_item`](./update_cart_item.md) y [`_get_cart_stock_issues`](./_get_cart_stock_issues.md).
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# `_get_cart_stock_issues`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Comprueba si alguno de los ítems del carrito supera el stock disponible y devuelve la lista de conflictos encontrados.
|
||||||
|
|
||||||
|
Para cada ítem, compara la cantidad solicitada con el stock disponible obtenido mediante [`_get_available_stock_by_product`](./_get_available_stock_by_product.md). Si la cantidad supera el disponible, se añade un objeto de issue a la lista de resultado.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_cart_stock_issues(
|
||||||
|
cart_items,
|
||||||
|
exclude_reservation_ids=None
|
||||||
|
) -> list[dict]:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|---------------------------|------------------|-----------------------------------------------------|
|
||||||
|
| `cart_items` | lista de `CartItem` | Ítems del carrito a verificar. |
|
||||||
|
| `exclude_reservation_ids` | lista de `int` o `None` | Reservas a excluir del cómputo de stock reservado. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Lista de diccionarios con la estructura:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"product_name": str, # Nombre del producto con conflicto
|
||||||
|
"requested": int, # Cantidad solicitada
|
||||||
|
"available": int, # Cantidad disponible
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lista vacía si no hay problemas de stock.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada en [`view_cart`](./view_cart.md) y [`checkout`](./checkout.md) para mostrar avisos al usuario, y en [`create_checkout_session`](./create_checkout_session.md) y [`create_paypal_payment`](./create_paypal_payment.md) para bloquear el pago si hay problemas.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# `_get_client_ip`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Obtiene la dirección IP real del cliente a partir de la petición HTTP.
|
||||||
|
|
||||||
|
Cuando la aplicación se encuentra detrás de un proxy inverso (como Nginx), la IP real del cliente viaja en la cabecera `X-Forwarded-For`. Esta función la extrae y devuelve la primera IP de la cadena. Si dicha cabecera no está presente, devuelve el valor de `REMOTE_ADDR`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_client_ip(request: HttpRequest) -> str:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|--------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Cadena de texto con la dirección IP del cliente. Devuelve una cadena vacía si no hay ninguna disponible.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada desde [`login`](./login.md), [`register`](./register.md) y [`logout`](./logout.md) para incluir la IP del cliente en los registros de auditoría.
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> La cabecera `X-Forwarded-For` puede ser falsificada por un cliente malicioso. Confiar en ella para control de acceso o límites de tasa sin validación adicional conlleva riesgos de seguridad.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# `_get_or_create_session_key`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Garantiza que la sesión HTTP tiene una clave (`session_key`) activa. Si la sesión aún no tiene clave asignada, la crea explícitamente. Devuelve la clave de sesión resultante.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_or_create_session_key(request: HttpRequest) -> str:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Cadena con la clave de sesión activa.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Utilizada en [`_get_reservation_owner_filters`](./_get_reservation_owner_filters.md) y [`_create_stock_reservation_for_cart`](./_create_stock_reservation_for_cart.md) para identificar al usuario anónimo en las reservas de stock.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# `_get_reservation_owner_filters`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Construye el diccionario de filtros para identificar al propietario de una reserva de stock, adaptándose según si el usuario está autenticado o no.
|
||||||
|
|
||||||
|
- Usuario autenticado → filtra por `user`.
|
||||||
|
- Usuario anónimo → filtra por `session_key`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_reservation_owner_filters(request: HttpRequest) -> dict:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Diccionario listo para usar como `**kwargs` en consultas ORM de `StockReservation`.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada desde [`_create_stock_reservation_for_cart`](./_create_stock_reservation_for_cart.md), [`_cancel_active_stock_reservations_for_request`](./_cancel_active_stock_reservations_for_request.md), [`_get_session_stock_reservation`](./_get_session_stock_reservation.md) y [`_get_active_reservation_ids_for_request`](./_get_active_reservation_ids_for_request.md).
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# `_get_reserved_quantities_by_product`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Calcula la cantidad total de stock reservada activamente para un conjunto de productos, excluyendo opcionalmente ciertas reservas.
|
||||||
|
|
||||||
|
Consulta `StockReservationItem` para encontrar todas las reservas activas y no caducadas, agrupa por producto y suma las cantidades.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_reserved_quantities_by_product(
|
||||||
|
product_ids,
|
||||||
|
exclude_reservation_ids=None
|
||||||
|
) -> dict:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|---------------------------|------------------|------------------------------------------------------------------|
|
||||||
|
| `product_ids` | lista de `int` | IDs de los productos a consultar. |
|
||||||
|
| `exclude_reservation_ids` | lista de `int` o `None` | IDs de reservas a excluir del cómputo (por ejemplo, la reserva propia del usuario que está finalizando el pago). |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Diccionario `{product_id: total_reserved}`. Si `product_ids` está vacío devuelve `{}`.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Utilizada en [`_get_available_stock_by_product`](./_get_available_stock_by_product.md) y [`_create_stock_reservation_for_cart`](./_create_stock_reservation_for_cart.md).
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# `_get_selected_shipping_address`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Obtiene la dirección de envío seleccionada por el usuario durante el proceso de pago, validando que pertenezca al usuario autenticado.
|
||||||
|
|
||||||
|
Intenta leer `shipping_address_id` en el siguiente orden:
|
||||||
|
1. Parámetro `POST` del formulario.
|
||||||
|
2. Cuerpo JSON de la petición (para peticiones AJAX/API).
|
||||||
|
|
||||||
|
Si el ID no se encuentra o no corresponde al usuario actual, devuelve `None`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_selected_shipping_address(request: HttpRequest) -> ShippingAddress | None:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Instancia de `ShippingAddress` perteneciente al usuario, o `None` si no se encontró o no es válida.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada desde [`create_checkout_session`](./create_checkout_session.md) y [`create_paypal_payment`](./create_paypal_payment.md).
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> La validación `user=request.user` es crítica para la seguridad. Nunca omitas este filtro, ya que evita que un usuario use la dirección de otro.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# `_get_session_stock_reservation`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Recupera la reserva de stock almacenada en la sesión del usuario, validando que siga activa, no haya caducado y corresponda al método de pago indicado.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _get_session_stock_reservation(
|
||||||
|
request: HttpRequest,
|
||||||
|
payment_method: str
|
||||||
|
) -> StockReservation | None:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|------------------|---------------|----------------------------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `payment_method` | `str` | Método de pago esperado (`stripe` o `paypal`). |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
La instancia `StockReservation` activa si existe y es válida, o `None` si la sesión no tiene reserva, el método de pago no coincide o la reserva ha caducado.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada desde [`checkout_success`](./checkout_success.md) y [`paypal_execute`](./paypal_execute.md) para recuperar la reserva antes de confirmar el pedido.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# `_is_almeria_city`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Comprueba si el nombre de un municipio o localidad pertenece a la provincia de Almería.
|
||||||
|
|
||||||
|
La comprobación se realiza normalizando el texto de entrada con [`_normalize_location_text`](./_normalize_location_text.md) y verificando si el resultado está presente en el conjunto `ALMERIA_MUNICIPALITIES`, que se construye a partir de `ALMERIA_MUNICIPALITIES_DISPLAY` definido en `tienda/vars.py`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _is_almeria_city(city: str) -> bool:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|--------|-------|----------------------------------------------|
|
||||||
|
| `city` | `str` | Nombre del municipio o localidad a comprobar. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
`True` si la ciudad pertenece a Almería, `False` en caso contrario.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada desde [`crear_direccion`](./crear_direccion.md) y [`editar_direccion`](./editar_direccion.md) para validar que la dirección de envío esté dentro de la zona de cobertura.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> El conjunto `ALMERIA_MUNICIPALITIES` incluye variantes sin el artículo inicial (`la`, `los`) para mayor flexibilidad en la comparación.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# `_is_almeria_postal_code`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Valida que un código postal pertenezca a la provincia de Almería.
|
||||||
|
|
||||||
|
Un código postal de Almería tiene exactamente 5 dígitos y comienza con el prefijo `04` (definido en `tienda/vars.py` como `ALMERIA_POSTAL_CODE_PREFIX`).
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _is_almeria_postal_code(postal_code: str) -> bool:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|---------------|-------|----------------------------------------|
|
||||||
|
| `postal_code` | `str` | Código postal a validar. Se elimina el espacio inicial/final antes de la comprobación. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
`True` si el código postal es válido para Almería, `False` en caso contrario.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada desde [`crear_direccion`](./crear_direccion.md) y [`editar_direccion`](./editar_direccion.md) para rechazar direcciones de envío fuera de la provincia.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Solo se aceptan envíos dentro de la provincia de Almería. Cualquier código postal que no comience por `04` será rechazado.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# `_normalize_location_text`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Normaliza una cadena de texto que representa un nombre de localidad para facilitar comparaciones insensibles a mayúsculas, acentos y caracteres especiales.
|
||||||
|
|
||||||
|
El proceso de normalización aplica los siguientes pasos en orden:
|
||||||
|
|
||||||
|
1. Descompone el texto Unicode (NFD) para separar los caracteres base de sus diacríticos.
|
||||||
|
2. Elimina los diacríticos (tildes, diéresis, cedillas, etc.).
|
||||||
|
3. Elimina cualquier símbolo que no sea alfanumérico, espacio o guión.
|
||||||
|
4. Convierte guiones en espacios, pasa a minúsculas y colapsa espacios múltiples.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _normalize_location_text(value: str) -> str:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|---------|-------|---------------------------------------|
|
||||||
|
| `value` | `str` | Texto de localidad a normalizar. Puede ser `None` o cadena vacía. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Cadena de texto normalizada (minúsculas, sin acentos, sin símbolos especiales).
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Esta función es utilizada al construir el conjunto `ALMERIA_MUNICIPALITIES` al inicio del módulo, y también es llamada por [`_is_almeria_city`](./_is_almeria_city.md) para normalizar la ciudad antes de comprobar si pertenece a la provincia.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Al ser una función privada (prefijo `_`) no debe importarse ni llamarse desde fuera del módulo `views.py`.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# `_release_expired_stock_reservations`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar privada
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Marca como expiradas (`STATUS_EXPIRED`) todas las reservas de stock activas cuya fecha de caducidad (`expires_at`) haya pasado. Se ejecuta como paso previo a cualquier operación que lea o modifique reservas, garantizando que el inventario virtual refleje la realidad.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _release_expired_stock_reservations() -> None:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
No devuelve ningún valor.
|
||||||
|
|
||||||
|
## Uso interno
|
||||||
|
|
||||||
|
Llamada al inicio de [`_cancel_active_stock_reservations_for_request`](./_cancel_active_stock_reservations_for_request.md), [`_get_reserved_quantities_by_product`](./_get_reserved_quantities_by_product.md) (indirectamente), [`_get_active_reservation_ids_for_request`](./_get_active_reservation_ids_for_request.md), [`_get_available_stock_by_product`](./_get_available_stock_by_product.md) y [`create_order_from_cart`](./create_order_from_cart.md).
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Esta función realiza una actualización masiva en la base de datos (`bulk update`). No envía señales individuales de Django por reserva.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# `add_to_cart`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/carrito/agregar/<product_id>/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Agrega un producto al carrito del usuario (autenticado o anónimo). Antes de añadir, cancela cualquier reserva de stock activa para que el inventario se recalcule limpiamente.
|
||||||
|
|
||||||
|
- Si el producto ya existe en el carrito, incrementa la cantidad.
|
||||||
|
- Valida que la cantidad resultante no supere el stock disponible.
|
||||||
|
- Si la petición incluye la cabecera `X-Requested-With: XMLHttpRequest`, devuelve una respuesta JSON en lugar de redirigir.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def add_to_cart(request: HttpRequest, product_id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|--------------|---------------|----------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `product_id` | `int` | ID del producto a agregar. |
|
||||||
|
|
||||||
|
## Parámetros POST
|
||||||
|
|
||||||
|
| Campo | Tipo | Por defecto | Descripción |
|
||||||
|
|------------|-------|-------------|--------------------------|
|
||||||
|
| `quantity` | `int` | `1` | Cantidad a agregar. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|---------------------------------|--------------------------|
|
||||||
|
| Éxito (no AJAX) | `view_cart` |
|
||||||
|
| Stock insuficiente / cantidad inválida | `producto` (detalle del producto) |
|
||||||
|
| Producto no encontrado | `index` |
|
||||||
|
|
||||||
|
## Respuesta AJAX
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"cart_count": 3,
|
||||||
|
"message": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# `borrar_producto`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/venta/borrar-producto/<id>/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Método HTTP:** Solo `POST`
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Elimina un producto del vendedor autenticado. Solo acepta peticiones POST. Verifica que el producto pertenezca al usuario actual; de lo contrario lanza un error 404.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def borrar_producto(request: HttpRequest, id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `id` | `int` | ID del producto a eliminar. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
Siempre redirige a `mis_productos`.
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> La eliminación es permanente. El producto y sus imágenes asociadas se borran de la base de datos. Asegúrate de que no haya pedidos activos vinculados al producto antes de eliminarlo.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# `cambiar_contrasena`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/cambiar-contrasena/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Método HTTP:** Solo `POST`
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Cambia la contraseña del usuario autenticado tras verificar la contraseña actual.
|
||||||
|
|
||||||
|
- Verifica que la contraseña actual sea correcta.
|
||||||
|
- Comprueba que la nueva contraseña y su confirmación coincidan.
|
||||||
|
- Valida que la nueva contraseña tenga al menos 8 caracteres.
|
||||||
|
- Establece la nueva contraseña y vuelve a iniciar sesión para mantener la sesión activa.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def cambiar_contrasena(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|--------------------|--------------------------------------|
|
||||||
|
| `current_password` | Contraseña actual del usuario. |
|
||||||
|
| `new_password` | Nueva contraseña (mínimo 8 caracteres). |
|
||||||
|
| `confirm_password` | Confirmación de la nueva contraseña. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|--------|------------------|
|
||||||
|
| Éxito | `portal_usuario` |
|
||||||
|
| Error | Template `tienda/editar_perfil.html` |
|
||||||
|
| GET | `editar_perfil` |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Tras cambiar la contraseña se llama a `auth_login` para re-autenticar al usuario y evitar que la sesión quede invalidada.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# `cambiar_estado_pedido`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/venta/pedidos/estado/<item_id>/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Método HTTP:** Solo `POST`
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Permite al vendedor cambiar el estado de un ítem de pedido que le pertenece. Solo acepta peticiones POST; cualquier otro método devuelve un error.
|
||||||
|
|
||||||
|
Valida que el nuevo estado sea uno de los valores definidos en `OrderItem.STATUS_CHOICES`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def cambiar_estado_pedido(request: HttpRequest, item_id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `item_id` | `int` | ID del `OrderItem` a actualizar. |
|
||||||
|
|
||||||
|
## Parámetros POST
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|----------|------------------------------------------------|
|
||||||
|
| `estado` | Nuevo estado del pedido (valor de `STATUS_CHOICES`). |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
Siempre redirige a `pedidos_vendedor`.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# `categoria`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/categoria/<id>/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra el catálogo paginado de productos filtrados por categoría. Reutiliza el template `tienda/index.html` para mostrar los resultados.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def categoria(request: HttpRequest, id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|--------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `id` | `int` | Identificador de la categoría. |
|
||||||
|
|
||||||
|
## Parámetros GET
|
||||||
|
|
||||||
|
| Parámetro | Tipo | Por defecto | Descripción |
|
||||||
|
|-----------|-------|-------------|-----------------------------|
|
||||||
|
| `page` | `int` | `1` | Número de página a mostrar. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|--------------|-----------|------------------------------------------------------|
|
||||||
|
| `products` | QuerySet | Productos de la categoría en la página actual. |
|
||||||
|
| `categories` | QuerySet | Todas las categorías para el menú de navegación. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/index.html`
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# `checkout`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/checkout/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Renderiza la página de checkout con el resumen del carrito, las direcciones de envío del usuario y los posibles problemas de stock. Desde esta página el usuario puede iniciar el pago con Stripe o PayPal.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def checkout(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|-----------------------|-----------------|-----------------------------------------------------------------|
|
||||||
|
| `cart` | `Cart` | Carrito actual del usuario. |
|
||||||
|
| `cart_items` | lista | Ítems del carrito con productos precargados. |
|
||||||
|
| `addresses` | QuerySet | Direcciones de envío registradas por el usuario. |
|
||||||
|
| `stock_issues` | lista de `dict` | Conflictos de stock (vacía si no hay problemas). |
|
||||||
|
| `reservation_minutes` | `int` | Minutos de validez de la reserva de stock. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/checkout.html`
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# `checkout_cancel`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/pago-cancelado/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Página que se muestra cuando el usuario cancela el proceso de pago en Stripe o PayPal. Cancela las reservas de stock activas y limpia los datos de reserva de la sesión, devolviendo el inventario bloqueado.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def checkout_cancel(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/checkout_cancel.html`
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# `checkout_success`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/pago-exitoso/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Página de confirmación tras un pago exitoso con Stripe. Crea el pedido a partir del carrito usando la sesión Stripe y la reserva de stock almacenadas en la sesión HTTP.
|
||||||
|
|
||||||
|
Si la creación del pedido falla (por ejemplo, porque la reserva caducó), muestra un error y redirige al checkout.
|
||||||
|
|
||||||
|
Tras confirmar el pedido, limpia las claves de sesión relacionadas con el pago.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def checkout_success(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|----------|---------|--------------------------|
|
||||||
|
| `order` | `Order` | Pedido recién creado. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/checkout_success.html`
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|-------------------|------------|
|
||||||
|
| Error en el pedido | `checkout` |
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# `clear_cart`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/carrito/vaciar/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Elimina todos los ítems del carrito de una sola vez. Cancela las reservas de stock activas antes de vaciar el carrito.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def clear_cart(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
Siempre redirige a `view_cart`.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# `crear_direccion`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/direcciones/nueva/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra el formulario para crear una nueva dirección de entrega y procesa su envío.
|
||||||
|
|
||||||
|
- **GET** → Renderiza el formulario vacío con la lista de municipios de Almería.
|
||||||
|
- **POST** → Valida y crea la dirección:
|
||||||
|
- Comprueba que todos los campos obligatorios estén rellenos.
|
||||||
|
- Valida que la ciudad pertenezca a la provincia de Almería.
|
||||||
|
- Valida que el código postal sea de Almería (`04xxx`).
|
||||||
|
- Guarda la dirección asociada al usuario con el país fijo `España`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def crear_direccion(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
| Campo | Obligatorio | Descripción |
|
||||||
|
|-------------------|-------------|-----------------------------------------------|
|
||||||
|
| `full_name` | Sí | Nombre completo del destinatario. |
|
||||||
|
| `address_line_1` | Sí | Línea principal de la dirección. |
|
||||||
|
| `address_line_2` | No | Línea secundaria (piso, puerta, etc.). |
|
||||||
|
| `city` | Sí | Municipio (debe pertenecer a Almería). |
|
||||||
|
| `postal_code` | Sí | Código postal (`04xxx`). |
|
||||||
|
| `phone` | Sí | Teléfono de contacto. |
|
||||||
|
| `is_default` | No | Marcar como dirección predeterminada (`on`). |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|--------|-----------------------|
|
||||||
|
| Éxito | `direcciones_usuario` |
|
||||||
|
| Error | Mismo formulario |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/editar_direccion.html`
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Solo se aceptan direcciones dentro de la provincia de Almería. El campo `country` siempre se fija a `España` y no es editable por el usuario.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# `crear_producto`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/venta/crear-producto/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra el formulario para crear un nuevo producto y procesa su envío.
|
||||||
|
|
||||||
|
- **GET** → Renderiza el formulario vacío con las categorías disponibles.
|
||||||
|
- **POST** → Valida los datos, crea el producto y sus imágenes asociadas:
|
||||||
|
- Valida campos obligatorios, precio (≥ 0) y stock (entero ≥ 0).
|
||||||
|
- Crea la imagen principal si se proporciona.
|
||||||
|
- Crea el producto asociado al usuario autenticado como `creator`.
|
||||||
|
- Agrega imágenes secundarias si se proporcionan.
|
||||||
|
- Redirige al panel de vendedor con un mensaje de éxito.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def crear_producto(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
| Campo | Tipo | Obligatorio | Descripción |
|
||||||
|
|---------------------|----------|-------------|----------------------------------------|
|
||||||
|
| `name` | texto | Sí | Nombre del producto. |
|
||||||
|
| `briefdesc` | texto | No | Descripción breve. |
|
||||||
|
| `description` | texto | Sí | Descripción completa. |
|
||||||
|
| `price` | decimal | Sí | Precio base sin IVA (≥ 0). |
|
||||||
|
| `stock` | entero | Sí | Unidades disponibles (≥ 0). |
|
||||||
|
| `category` | `int` | Sí | ID de la categoría. |
|
||||||
|
| `primary_image` | archivo | No | Imagen principal del producto. |
|
||||||
|
| `secondary_images` | archivos | No | Imágenes secundarias (lista). |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|---------------|-----------------|
|
||||||
|
| Éxito | `mis_productos` |
|
||||||
|
| Error | Mismo formulario |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/crear_producto.html`
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# `create_checkout_session`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/crear-sesion-pago/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Método HTTP:** Solo `POST`
|
||||||
|
**Decoradores:** `@login_required`, `@csrf_exempt`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Crea una sesión de pago en Stripe Checkout y devuelve el ID de sesión al frontend. Es llamada por JavaScript para iniciar el flujo de pago con tarjeta.
|
||||||
|
|
||||||
|
El proceso es:
|
||||||
|
|
||||||
|
1. Obtiene y valida la dirección de envío seleccionada.
|
||||||
|
2. Verifica que el carrito no esté vacío y que no haya problemas de stock.
|
||||||
|
3. Crea una reserva de stock atómica para bloquear el inventario.
|
||||||
|
4. Construye los `line_items` con precios IVA incluido.
|
||||||
|
5. Crea la sesión en Stripe con URLs de retorno y cancelación.
|
||||||
|
6. Guarda el ID de sesión, la dirección y la reserva en la sesión HTTP.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_checkout_session(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Cuerpo de la petición (JSON o form-data)
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|------------------------|-------------------------------------------|
|
||||||
|
| `shipping_address_id` | ID de la dirección de envío seleccionada. |
|
||||||
|
|
||||||
|
## Respuesta exitosa
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "sessionId": "cs_live_..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Respuestas de error
|
||||||
|
|
||||||
|
| Código | Descripción |
|
||||||
|
|--------|--------------------------------------------------|
|
||||||
|
| 400 | Carrito vacío, stock insuficiente, sin dirección. |
|
||||||
|
| 405 | Método no permitido. |
|
||||||
|
| 500 | Error interno al crear la sesión en Stripe. |
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> La reserva de stock creada expira en `STOCK_RESERVATION_MINUTES` minutos. Si el usuario no completa el pago en ese tiempo, el stock queda liberado automáticamente.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# `create_order_from_cart`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Crea un pedido (`Order`) a partir del contenido actual del carrito, descuenta el stock de cada producto y vacía el carrito. Es el núcleo de la lógica de confirmación de compra.
|
||||||
|
|
||||||
|
El proceso completo se ejecuta dentro de una transacción atómica con bloqueos de fila:
|
||||||
|
|
||||||
|
1. Verifica que el carrito no está vacío.
|
||||||
|
2. Calcula el precio de cada ítem con IVA.
|
||||||
|
3. Libera reservas expiradas.
|
||||||
|
4. Si se proporcionó una reserva de stock, la bloquea y valida que siga activa.
|
||||||
|
5. Valida que el stock disponible sea suficiente para cada producto.
|
||||||
|
6. Crea el registro `Order` y los `OrderItem` asociados.
|
||||||
|
7. Descuenta el stock de cada producto.
|
||||||
|
8. Elimina los ítems del carrito.
|
||||||
|
9. Marca la reserva como completada.
|
||||||
|
10. Si el usuario está autenticado, lanza la tarea Celery `process_purchase` de forma asíncrona.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_order_from_cart(
|
||||||
|
request,
|
||||||
|
payment_method: str,
|
||||||
|
payment_reference: str = "",
|
||||||
|
shipping_address=None,
|
||||||
|
stock_reservation=None
|
||||||
|
) -> tuple[Order | None, str]:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|---------------------|-----------------------------|-----------------------------------------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `payment_method` | `str` | Constante de pago (`Order.PAYMENT_STRIPE` o `Order.PAYMENT_PAYPAL`). |
|
||||||
|
| `payment_reference` | `str` | Referencia de la transacción en la pasarela (ID de sesión Stripe, ID de pago PayPal, etc.). |
|
||||||
|
| `shipping_address` | `ShippingAddress` o `None` | Dirección de envío seleccionada. |
|
||||||
|
| `stock_reservation` | `StockReservation` o `None` | Reserva de stock a completar junto con el pedido. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Tupla `(order, error_message)`:
|
||||||
|
|
||||||
|
- Pedido creado correctamente: `(Order, "")`.
|
||||||
|
- Error (carrito vacío, stock insuficiente, reserva caducada, etc.): `(None, "mensaje de error")`.
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
Llamada desde [`checkout_success`](./checkout_success.md) y [`paypal_execute`](./paypal_execute.md).
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Toda la operación se ejecuta bajo `transaction.atomic()` con `SELECT FOR UPDATE` para garantizar la integridad del stock en entornos concurrentes.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# `create_paypal_payment`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/paypal/crear/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Método HTTP:** Solo `POST`
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Crea un pago en PayPal y devuelve la URL de aprobación al frontend para redirigir al usuario. Sigue un flujo similar a [`create_checkout_session`](./create_checkout_session.md) pero usando la SDK de PayPal REST.
|
||||||
|
|
||||||
|
El proceso es:
|
||||||
|
|
||||||
|
1. Obtiene y valida la dirección de envío seleccionada.
|
||||||
|
2. Verifica el carrito y el stock.
|
||||||
|
3. Crea una reserva de stock atómica.
|
||||||
|
4. Configura PayPal SDK con las credenciales del entorno.
|
||||||
|
5. Construye la lista de items con precios IVA incluido.
|
||||||
|
6. Crea el pago en PayPal y guarda el `payment_id` en la sesión.
|
||||||
|
7. Devuelve la URL de aprobación de PayPal.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_paypal_payment(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Cuerpo de la petición (JSON o form-data)
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|------------------------|-------------------------------------------|
|
||||||
|
| `shipping_address_id` | ID de la dirección de envío seleccionada. |
|
||||||
|
|
||||||
|
## Respuesta exitosa
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "redirect": "https://www.paypal.com/checkoutnow?token=..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Respuestas de error
|
||||||
|
|
||||||
|
| Código | Descripción |
|
||||||
|
|--------|----------------------------------------------------|
|
||||||
|
| 400 | Carrito vacío, stock insuficiente, sin dirección, error de PayPal. |
|
||||||
|
| 405 | Método no permitido. |
|
||||||
|
| 500 | SDK de PayPal no instalado u otro error interno. |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Requiere que `paypalrestsdk` esté instalado y las variables `PAYPAL_MODE`, `PAYPAL_CLIENT_ID` y `PAYPAL_CLIENT_SECRET` estén configuradas en `settings.py`.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# `direcciones_usuario`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/direcciones/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Lista todas las direcciones de envío registradas por el usuario autenticado.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def direcciones_usuario(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|---------------|-----------|------------------------------------------|
|
||||||
|
| `direcciones` | QuerySet | Direcciones de envío del usuario. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/direcciones.html`
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# `editar_direccion`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/direcciones/editar/<id>/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra el formulario para editar una dirección de entrega existente y procesa sus cambios. Solo el propietario de la dirección puede editarla; intentar acceder a una dirección ajena lanza un error 404.
|
||||||
|
|
||||||
|
Aplica las mismas validaciones de zona de envío que [`crear_direccion`](./crear_direccion.md).
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def editar_direccion(request: HttpRequest, id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|----------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `id` | `int` | ID de la dirección a editar. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
Idénticos a [`crear_direccion`](./crear_direccion.md).
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|--------|-----------------------|
|
||||||
|
| Éxito | `direcciones_usuario` |
|
||||||
|
| Error | Mismo formulario |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/editar_direccion.html`
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# `editar_perfil`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/editar-perfil/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Permite al usuario editar su información de perfil (nombre, apellido y correo electrónico).
|
||||||
|
|
||||||
|
- **GET** → Renderiza el formulario con los datos actuales.
|
||||||
|
- **POST** → Valida y guarda los cambios.
|
||||||
|
- Verifica que el nuevo email no esté ya en uso por otro usuario.
|
||||||
|
- Actualiza `first_name`, `last_name` y `email` del usuario.
|
||||||
|
- Redirige al portal de usuario.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def editar_perfil(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|--------------|-------------------------|
|
||||||
|
| `first_name` | Nombre del usuario. |
|
||||||
|
| `last_name` | Apellido del usuario. |
|
||||||
|
| `email` | Correo electrónico. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|--------|------------------|
|
||||||
|
| Éxito | `portal_usuario` |
|
||||||
|
| Error | Mismo template |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/editar_perfil.html`
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# `editar_producto`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/venta/editar-producto/<id>/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra el formulario de edición de un producto y procesa sus cambios. Solo el `creator` del producto puede editarlo; un intento de acceder a un producto ajeno lanza un error 404.
|
||||||
|
|
||||||
|
- **GET** → Renderiza el formulario con los datos actuales del producto.
|
||||||
|
- **POST** → Valida y aplica los cambios:
|
||||||
|
- Campos obligatorios, precio (≥ 0) y stock (entero ≥ 0).
|
||||||
|
- Si se sube una nueva imagen principal, se crea y sustituye la anterior.
|
||||||
|
- Si se suben imágenes secundarias, se reemplazan todas las existentes.
|
||||||
|
- Guarda el producto actualizado y redirige al panel de vendedor.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def editar_producto(request: HttpRequest, id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `id` | `int` | ID del producto a editar. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
Idénticos a [`crear_producto`](./crear_producto.md).
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|---------|-----------------|
|
||||||
|
| Éxito | `mis_productos` |
|
||||||
|
| Error | Mismo formulario |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/editar_producto.html`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> La caché Redis del producto (`product_{id}`) se invalida automáticamente al guardar el producto para que los cambios sean visibles de inmediato.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# `eliminar_direccion`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/direcciones/eliminar/<id>/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Método HTTP:** Solo `POST`
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Elimina una dirección de entrega del usuario. Solo acepta peticiones POST. Verifica que la dirección pertenezca al usuario actual; de lo contrario lanza un error 404.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def eliminar_direccion(request: HttpRequest, id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `id` | `int` | ID de la dirección a eliminar. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
Siempre redirige a `direcciones_usuario`.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# `enviar_mensaje_pedido`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/venta/pedidos/mensaje/<item_id>/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Método HTTP:** Solo `POST`
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Permite al vendedor enviar un mensaje al comprador sobre un ítem de pedido específico. El mensaje se almacena como un registro `OrderMessage` vinculado al `OrderItem` y al remitente.
|
||||||
|
|
||||||
|
Rechaza mensajes vacíos y cualquier método que no sea POST.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def enviar_mensaje_pedido(request: HttpRequest, item_id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `item_id` | `int` | ID del `OrderItem` al que va dirigido el mensaje. |
|
||||||
|
|
||||||
|
## Parámetros POST
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|-----------|--------------------------------|
|
||||||
|
| `mensaje` | Texto del mensaje (requerido). |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
Siempre redirige a `pedidos_vendedor`.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# `get_or_create_cart`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Obtiene o crea el carrito de compra asociado a la sesión o al usuario actual.
|
||||||
|
|
||||||
|
- Si el usuario está autenticado, el carrito se asocia a su instancia `User`.
|
||||||
|
- Si el usuario es anónimo, se utiliza la clave de sesión (`session_key`). Si la sesión no tiene clave, se crea automáticamente.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_or_create_cart(request) -> Cart:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
Instancia de `Cart` existente o recién creada.
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
Es la función de entrada estándar para acceder al carrito desde cualquier vista. Llamada en [`add_to_cart`](./add_to_cart.md), [`view_cart`](./view_cart.md), [`update_cart_item`](./update_cart_item.md), [`remove_from_cart`](./remove_from_cart.md), [`clear_cart`](./clear_cart.md), [`create_order_from_cart`](./create_order_from_cart.md), [`checkout`](./checkout.md) y las vistas de pago.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# `get_price_with_vat_decimal`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**Tipo:** Función auxiliar pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Calcula el precio de un producto con el IVA incluido y lo devuelve redondeado a dos decimales usando la regla de redondeo `ROUND_HALF_UP`.
|
||||||
|
|
||||||
|
La tasa de IVA se toma de `VAT_RATE` definida en `tienda/vars.py`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_price_with_vat_decimal(price) -> Decimal:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|---------|-----------------------------|-------------------------------------|
|
||||||
|
| `price` | `float`, `int` o `Decimal` | Precio base del producto sin IVA. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
`Decimal` con el precio final (base + IVA) redondeado a dos decimales.
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Suponiendo VAT_RATE = 0.21
|
||||||
|
get_price_with_vat_decimal(10.00) # → Decimal('12.10')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
Llamada desde [`create_order_from_cart`](./create_order_from_cart.md), [`create_checkout_session`](./create_checkout_session.md) y [`create_paypal_payment`](./create_paypal_payment.md) para calcular el importe correcto a cobrar al cliente.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Se utiliza `Decimal` para evitar errores de precisión en operaciones de punto flotante propios de los cálculos monetarios.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# `home`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Renderiza la página de inicio de la tienda. Muestra una selección de productos destacados (los 8 más recientes por ID), todas las categorías disponibles y estadísticas generales como el total de productos y el número de vendedores activos.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def home(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|--------------------|-------------|----------------------------------------------------------|
|
||||||
|
| `featured_products` | QuerySet | Últimos 8 productos ordenados por ID descendente. |
|
||||||
|
| `categories` | QuerySet | Todas las categorías disponibles. |
|
||||||
|
| `total_products` | `int` | Número total de productos en la tienda. |
|
||||||
|
| `total_sellers` | `int` | Número de usuarios que tienen al menos un producto creado. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/home.html`
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# `index`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/productos/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra el catálogo paginado de productos. El tamaño de página se controla con la constante `PAGE_SIZE` definida en `tienda/vars.py`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def index(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Parámetros GET
|
||||||
|
|
||||||
|
| Parámetro | Tipo | Por defecto | Descripción |
|
||||||
|
|-----------|-------|-------------|----------------------|
|
||||||
|
| `page` | `int` | `1` | Número de página a mostrar. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|--------------|-----------|---------------------------------------------------|
|
||||||
|
| `products` | QuerySet | Productos de la página actual (slice de la BD). |
|
||||||
|
| `categories` | QuerySet | Todas las categorías para el menú de navegación. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/index.html`
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# `login`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/login/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Gestiona el inicio de sesión de un usuario.
|
||||||
|
|
||||||
|
- **GET** → Renderiza el formulario de login.
|
||||||
|
- **POST** → Autentica al usuario con email y contraseña.
|
||||||
|
- Si el correo no existe: muestra error y registra el intento fallido.
|
||||||
|
- Si la cuenta no está verificada (`registration_status == "CR"`): muestra error sin autenticar.
|
||||||
|
- Si las credenciales son correctas: inicia la sesión, configura su duración, envía un correo de bienvenida asíncrono (tarea Celery) y redirige al catálogo.
|
||||||
|
- Si la contraseña es incorrecta: muestra error y registra el intento fallido.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def login(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|-------------|-----------------------------------------------------------------------|
|
||||||
|
| `email` | Correo electrónico del usuario. |
|
||||||
|
| `password` | Contraseña del usuario. |
|
||||||
|
| `remember` | Si está presente, la sesión dura 14 días; de lo contrario, expira al cerrar el navegador. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|-----------------------------|-----------------|
|
||||||
|
| Login correcto | `index` |
|
||||||
|
| Login fallido / cuenta no verificada | Mismo template `tienda/login.html` |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/login.html`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Los intentos de inicio de sesión (correctos y fallidos) se registran en el logger de auditoría `tienda.audit` con la IP del cliente.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# `logout`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/logout/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Cierra la sesión del usuario autenticado actual. Registra el evento de cierre de sesión en el logger de auditoría y redirige al catálogo con un mensaje de confirmación.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def logout(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
Siempre redirige a `index` tras el cierre de sesión.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> El evento se registra con `user_id`, `email` e IP del cliente en el logger `tienda.audit`, independientemente de si había un usuario autenticado o no.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# `mensajes_comprador`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/mensajes/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra los mensajes enviados por los vendedores al comprador, agrupados por ítem de pedido. Permite al comprador ver el estado de sus pedidos y las comunicaciones de los vendedores.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def mensajes_comprador(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|---------------|-----------|-----------------------------------------------------------------------------|
|
||||||
|
| `order_items` | QuerySet | `OrderItem` de pedidos del comprador, con mensajes, productos y vendedores precargados, ordenados por fecha descendente. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/mensajes_comprador.html`
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# `mis_compras`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/mis-compras/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra el historial completo de pedidos realizados por el usuario autenticado, ordenados de más reciente a más antiguo.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def mis_compras(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|----------------|-----------|-------------------------------------------------|
|
||||||
|
| `orders` | QuerySet | Todos los pedidos del usuario con sus ítems precargados. |
|
||||||
|
| `total_orders` | `int` | Número total de pedidos. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/mis_compras.html`
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# `mis_productos`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/venta/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra el panel de vendedor con la lista de todos los productos creados por el usuario autenticado. Accesible desde el botón "Panel Vendedor" en la cabecera.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def mis_productos(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|-------------------|-----------|------------------------------------------------------|
|
||||||
|
| `productos` | QuerySet | Productos del usuario con categoría e imagen principal precargados. |
|
||||||
|
| `total_productos` | `int` | Número total de productos del vendedor. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/mis_productos.html`
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# `mis_recibos`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/mis-recibos/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra los recibos del usuario, es decir, los pedidos con estado `PAID`. Útil para consultar el historial de pagos completados.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def mis_recibos(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|------------------|-----------|-----------------------------------------------------|
|
||||||
|
| `receipts` | QuerySet | Pedidos pagados del usuario, con ítems precargados. |
|
||||||
|
| `total_receipts` | `int` | Número total de recibos. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/mis_recibos.html`
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# `paypal_execute`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/paypal/ejecutar/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Procesa la confirmación de un pago de PayPal después de que el usuario lo aprueba en el portal de PayPal. PayPal redirige al usuario a esta URL con los parámetros `PayerID` y el `token` de la transacción.
|
||||||
|
|
||||||
|
El proceso es:
|
||||||
|
|
||||||
|
1. Obtiene `payment_id` de la sesión y `PayerID` de los parámetros GET.
|
||||||
|
2. Configura y ejecuta el pago en PayPal.
|
||||||
|
3. Si el pago es exitoso, llama a [`create_order_from_cart`](./create_order_from_cart.md) para crear el pedido.
|
||||||
|
4. Limpia las claves de sesión relacionadas con el pago.
|
||||||
|
5. Renderiza la página de éxito con el pedido.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def paypal_execute(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Parámetros GET (de PayPal)
|
||||||
|
|
||||||
|
| Parámetro | Descripción |
|
||||||
|
|------------|----------------------------------------------|
|
||||||
|
| `PayerID` | ID del pagador proporcionado por PayPal. |
|
||||||
|
|
||||||
|
## Template (en caso de éxito)
|
||||||
|
|
||||||
|
`tienda/checkout_success.html`
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|-------------------|------------|
|
||||||
|
| Datos incompletos / error en el pedido | `checkout` |
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# `pedidos_vendedor`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/venta/pedidos/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra todos los pedidos en los que el usuario autenticado figura como vendedor (es decir, `OrderItem` donde `seller` es el usuario). Incluye la información del comprador, la dirección de envío y los mensajes de cada pedido.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def pedidos_vendedor(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|----------------|-----------|---------------------------------------------------------|
|
||||||
|
| `pedidos` | QuerySet | `OrderItem` del vendedor ordenados por fecha descendente, con relaciones precargadas. |
|
||||||
|
| `total_pedidos`| `int` | Número total de pedidos del vendedor. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/pedidos_vendedor.html`
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# `portal_usuario`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/usuario/`
|
||||||
|
**Tipo:** Vista privada (requiere autenticación)
|
||||||
|
**Decorador:** `@login_required`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Dashboard del portal de usuario. Muestra un resumen de la actividad del usuario: total de pedidos, total de direcciones guardadas, pedidos recientes y mensajes recientes de vendedores no leídos.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def portal_usuario(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|-------------------|-----------|----------------------------------------------------------------------|
|
||||||
|
| `total_orders` | `int` | Número total de pedidos realizados por el usuario. |
|
||||||
|
| `total_addresses` | `int` | Número de direcciones de envío registradas. |
|
||||||
|
| `recent_orders` | QuerySet | Últimos 5 pedidos del usuario ordenados por fecha descendente. |
|
||||||
|
| `recent_messages` | QuerySet | Últimos 5 mensajes de vendedores no enviados por el propio usuario. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/portal_usuario.html`
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# `producto`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/producto/<id>/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra la página de detalle de un producto. Implementa una capa de caché en Redis para mejorar el rendimiento: el producto se almacena durante 5 minutos con la clave `product_{id}`. Si el producto no está en caché, se obtiene de la base de datos junto con sus relaciones (`category`, `primary_image`, `creator`, `secondary_images`) y se guarda en caché.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def producto(request: HttpRequest, id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|--------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `id` | `int` | Identificador del producto. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|-----------|-----------|-----------------------|
|
||||||
|
| `product` | `Product` | Instancia del producto. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/producto.html`
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> La caché se invalida automáticamente cuando el producto es editado o eliminado (ver [`editar_producto`](./editar_producto.md) y [`borrar_producto`](./borrar_producto.md)). La clave de caché tiene el formato `:1:product_{id}` en Redis (prefijo de la base de datos 1).
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# `register`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/register/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Gestiona el registro de nuevos usuarios.
|
||||||
|
|
||||||
|
- **GET** → Redirige al catálogo si el usuario ya está autenticado; de lo contrario muestra el formulario de registro.
|
||||||
|
- **POST** → Valida los datos e intenta crear la cuenta:
|
||||||
|
- Comprueba que las contraseñas coincidan y tengan al menos 8 caracteres.
|
||||||
|
- Comprueba que el email no esté ya registrado.
|
||||||
|
- Genera un nombre de usuario único a partir del prefijo del email.
|
||||||
|
- Crea el usuario y envía un correo de confirmación asíncrono (tarea Celery).
|
||||||
|
- Redirige al catálogo con un mensaje de éxito.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def register(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|--------------------|------------------------------------|
|
||||||
|
| `name` | Nombre del usuario. |
|
||||||
|
| `email` | Correo electrónico. |
|
||||||
|
| `password` | Contraseña (mínimo 8 caracteres). |
|
||||||
|
| `password_confirm` | Confirmación de la contraseña. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino |
|
||||||
|
|---------------------------------|----------|
|
||||||
|
| Usuario ya autenticado | `index` |
|
||||||
|
| Registro exitoso | `index` |
|
||||||
|
| Error de validación | Mismo template `tienda/register.html` |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/register.html`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> El usuario se crea con `registration_status = "CR"` (pendiente de verificación). El acceso está bloqueado hasta que se verifique el correo electrónico.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# `remove_from_cart`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/carrito/eliminar/<item_id>/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Elimina un ítem específico del carrito. Cancela las reservas de stock activas antes de eliminar el ítem, para que el stock quede libre inmediatamente.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def remove_from_cart(request: HttpRequest, item_id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|---------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `item_id` | `int` | ID del `CartItem` a eliminar. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
Siempre redirige a `view_cart`.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# `reset_password`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/reset-password/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Gestiona la solicitud de restablecimiento de contraseña.
|
||||||
|
|
||||||
|
- **GET** → Renderiza el formulario para introducir el correo electrónico. Si el usuario ya está autenticado, redirige al catálogo.
|
||||||
|
- **POST** → Lanza la tarea Celery `enviar_correo_recuperacion` para enviar un correo con el enlace de recuperación si existe una cuenta con ese email. Siempre muestra el mismo mensaje informativo para no revelar si el email existe.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> En el código fuente hay dos definiciones de `reset_password`. La segunda (línea 1636) sobreescribe a la primera (línea 1626). Solo la segunda definición está activa en tiempo de ejecución.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def reset_password(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|---------|-----------------------------------------|
|
||||||
|
| `email` | Correo electrónico de la cuenta. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/reset_password.html`
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Para evitar la enumeración de usuarios, el mensaje de respuesta siempre es el mismo independientemente de si el email existe o no.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# `reset_password_phase2`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/reset-password/<code>/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Segunda fase del flujo de restablecimiento de contraseña. El usuario llega aquí desde el enlace enviado a su correo electrónico.
|
||||||
|
|
||||||
|
- **GET** → Renderiza el formulario para introducir la nueva contraseña.
|
||||||
|
- **POST** → Valida las contraseñas, actualiza la contraseña del usuario y redirige al catálogo.
|
||||||
|
- Cualquier otro método → lanza `Http404`.
|
||||||
|
|
||||||
|
Si el código no existe o no corresponde al modo `RESET_PASSWORD`, lanza `Http404`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def reset_password_phase2(request: HttpRequest, code: str):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `code` | `str` | Código de verificación del email de recuperación. |
|
||||||
|
|
||||||
|
## Campos del formulario POST
|
||||||
|
|
||||||
|
| Campo | Descripción |
|
||||||
|
|-------------------|------------------------------------------|
|
||||||
|
| `password` | Nueva contraseña. |
|
||||||
|
| `verify_password` | Confirmación de la nueva contraseña. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|----------|-------|-------------------------------------------|
|
||||||
|
| `code` | `str` | Código necesario para el envío del formulario. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino / Respuesta |
|
||||||
|
|------------------------------|----------------------------|
|
||||||
|
| Contraseñas no coinciden | Mismo formulario con error |
|
||||||
|
| Cambio exitoso | `index` |
|
||||||
|
| Código inválido o modo incorrecto | `Http404` |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/reset_password_phase2.html`
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# `rgpd`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/rgpd/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Renderiza la página de política de privacidad / RGPD (Reglamento General de Protección de Datos) de la tienda.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def rgpd(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/rgpd.html`
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# `search`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/buscar/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Busca productos por nombre, descripción breve o descripción completa y devuelve los resultados. Si no se proporciona ningún término de búsqueda, se devuelve una lista vacía.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def search(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Parámetros GET
|
||||||
|
|
||||||
|
| Parámetro | Tipo | Descripción |
|
||||||
|
|-----------|--------|------------------------------|
|
||||||
|
| `q` | `str` | Término de búsqueda. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|--------------|-----------|----------------------------------------------------|
|
||||||
|
| `products` | QuerySet o lista | Resultados de la búsqueda. |
|
||||||
|
| `query` | `str` | Término de búsqueda utilizado. |
|
||||||
|
| `categories` | QuerySet | Todas las categorías para el menú de navegación. |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/search.html`
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# `search_suggestions`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/sugerencias/`
|
||||||
|
**Tipo:** Vista pública (API AJAX)
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Endpoint JSON que devuelve hasta 8 sugerencias de productos para el autocompletado de la barra de búsqueda. Solo responde si el término de búsqueda tiene al menos 2 caracteres.
|
||||||
|
|
||||||
|
Busca coincidencias en el nombre del producto y en la descripción breve.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def search_suggestions(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Parámetros GET
|
||||||
|
|
||||||
|
| Parámetro | Tipo | Descripción |
|
||||||
|
|-----------|-------|------------------------------------------|
|
||||||
|
| `q` | `str` | Texto a buscar (mínimo 2 caracteres). |
|
||||||
|
|
||||||
|
## Respuesta
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"suggestions": [
|
||||||
|
{ "name": "Camiseta azul", "id": 42, "price": 19.99 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Si la consulta tiene menos de 2 caracteres, `suggestions` es una lista vacía.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# `send_test_email`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** No registrada en producción (uso interno de desarrollo)
|
||||||
|
**Tipo:** Vista de utilidad / desarrollo
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Vista de prueba que envía un correo de prueba a una dirección codificada en el código fuente. Devuelve un mensaje HTTP indicando si el correo se envió correctamente.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def send_test_email(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Retorno
|
||||||
|
|
||||||
|
`HttpResponse` con el texto `"Mira tu bandeja"` si el correo se envió, o el mensaje de error si falló.
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> Esta vista es solo para desarrollo. La dirección de destino está codificada en el código fuente y no debe estar disponible en producción.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# `stripe_config`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/stripe-config/`
|
||||||
|
**Tipo:** Vista pública (CSRF exento)
|
||||||
|
**Método HTTP:** `GET`
|
||||||
|
**Decorador:** `@csrf_exempt`
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Endpoint de configuración de Stripe que expone la clave pública (`publishable key`) al frontend JavaScript. Es llamado por el cliente antes de iniciar una sesión de Stripe Checkout para obtener la clave necesaria para inicializar `stripe.js`.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def stripe_config(request):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Respuesta
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"publicKey": "pk_live_..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> Esta vista está marcada con `@csrf_exempt`. Asegúrate de que solo expone la clave **pública** de Stripe, nunca la clave secreta.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# `update_cart_item`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/carrito/actualizar/<item_id>/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Actualiza la cantidad de un ítem del carrito.
|
||||||
|
|
||||||
|
- Si la nueva cantidad es mayor que cero, actualiza el registro.
|
||||||
|
- Si la nueva cantidad es cero o menor, elimina el ítem del carrito.
|
||||||
|
- Valida que la cantidad no supere el stock disponible.
|
||||||
|
- Cancela las reservas de stock activas antes de modificar el carrito.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def update_cart_item(request: HttpRequest, item_id: int):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `item_id` | `int` | ID del `CartItem` a actualizar. |
|
||||||
|
|
||||||
|
## Parámetros POST
|
||||||
|
|
||||||
|
| Campo | Tipo | Por defecto | Descripción |
|
||||||
|
|------------|-------|-------------|-----------------------|
|
||||||
|
| `quantity` | `int` | `1` | Nueva cantidad. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
Siempre redirige a `view_cart`.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# `verify`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/verificar/<code>/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Verifica la cuenta de un usuario a partir de un código de verificación enviado por correo electrónico. Cuando el código es válido y corresponde al modo `VERIFY_ACCOUNT`, actualiza el estado de registro del usuario a `ACTIVE` y elimina el código usado.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def verify(request: HttpRequest, code: str):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
| `code` | `str` | Código de verificación único del email. |
|
||||||
|
|
||||||
|
## Redirecciones
|
||||||
|
|
||||||
|
| Caso | Destino / Respuesta |
|
||||||
|
|-----------------------------|-------------------------------------------------------------|
|
||||||
|
| Verificación exitosa | `index` |
|
||||||
|
| Código no encontrado | `HttpResponse` con mensaje de error HTML |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> El código de verificación se elimina de la base de datos tras ser utilizado, por lo que no puede usarse dos veces.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# `view_cart`
|
||||||
|
|
||||||
|
**Archivo:** `tienda/views.py`
|
||||||
|
**URL:** `/tienda/carrito/`
|
||||||
|
**Tipo:** Vista pública
|
||||||
|
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Muestra el contenido actual del carrito de compra junto con los posibles problemas de stock. Excluye del cálculo de stock las reservas activas del propio usuario para no mostrar falsos avisos durante el checkout.
|
||||||
|
|
||||||
|
## Firma
|
||||||
|
|
||||||
|
```python
|
||||||
|
def view_cart(request: HttpRequest):
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parámetros
|
||||||
|
|
||||||
|
| Nombre | Tipo | Descripción |
|
||||||
|
|-----------|---------------|-----------------------|
|
||||||
|
| `request` | `HttpRequest` | Petición HTTP de Django. |
|
||||||
|
|
||||||
|
## Contexto del template
|
||||||
|
|
||||||
|
| Variable | Tipo | Descripción |
|
||||||
|
|----------------|-----------------|----------------------------------------------------------------|
|
||||||
|
| `cart` | `Cart` | Carrito actual del usuario. |
|
||||||
|
| `cart_items` | lista | Ítems del carrito con sus productos relacionados precargados. |
|
||||||
|
| `stock_issues` | lista de `dict` | Lista de conflictos de stock (vacía si no hay problemas). |
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
`tienda/cart.html`
|
||||||
Reference in New Issue
Block a user