feat: Add user purchase and receipt management

- Implemented 'Mis Compras' and 'Mis Recibos' pages for users to view their orders and payment receipts.
- Enhanced address validation in 'editar_direccion.html' to ensure cities and postal codes belong to Almería.
- Added shipping address display in seller order details on 'pedidos_vendedor.html'.
- Updated user portal to include links to purchases and receipts.
- Introduced email verification functionality during user registration.
- Refactored email sending utility for better error handling and logging.
- Improved session management for checkout processes with selected shipping addresses.
This commit is contained in:
2026-03-10 13:08:10 +01:00
parent 01024bb97e
commit 162b63cae9
51 changed files with 1082 additions and 385 deletions
+26 -21
View File
@@ -1,4 +1,5 @@
{% load static %}
{% load cache %}
{% load compress %}
<!DOCTYPE html>
<html lang="es">
@@ -69,6 +70,7 @@
{% block head %}{% endblock %}
</head>
<body>
{% cache 500 sidebar request.user.username %}
<nav class="navbar navbar-expand-md header">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'home' %}">
@@ -102,7 +104,7 @@
{% if user.is_authenticated %}
<a href="{% url 'mis_productos' %}" class="nav-link btn btn-outline-secondary btn-sm">Panel Vendedor</a>
<span class="nav-text d-none d-md-inline text-white">{{ user.first_name|default:user.username }}</span>
<a href="{% url 'portal_usuario' %}" class="nav-link btn btn-outline-light btn-sm">{{ user.first_name|default:user.username }}</a>
<a href="{% url 'logout' %}" class="nav-link btn btn-primary btn-sm">Cerrar Sesión</a>
{% else %}
<a href="{% url 'login' %}" class="nav-link btn btn-primary btn-sm">Iniciar Sesión</a>
@@ -112,6 +114,7 @@
</div>
</div>
</nav>
{% endcache %}
<div class="container-fluid">
<!-- Mensajes -->
@@ -131,30 +134,31 @@
<!-- Contenido-->
{% block content %}{% endblock %}
{% cache 500 footer %}
<!-- Footer-->
<div id="footer" class="row pt-2 pb-2 mt-5">
<div class="col-md-12 grid">
<p class="text-center">Enlace 1</p>
<p class="text-center">Enlace 2</p>
<p class="text-center">Enlace 3</p>
<p class="text-center">Enlace 4</p>
<p class="text-center">Enlace 5</p>
<p class="text-center">Enlace 6</p>
<p class="text-center">Enlace 7</p>
<p class="text-center">Enlace 8</p>
<p class="text-center">Enlace 9</p>
<p class="text-center">Enlace 10</p>
<p class="text-center">Enlace 11</p>
<p class="text-center">Enlace 12</p>
<p class="text-center">Enlace 13</p>
<p class="text-center">Enlace 14</p>
<p class="text-center">Enlace 15</p>
<p class="text-center">Enlace 16</p>
</div>
<div class="col-md-12 grid">
<p class="text-center">Enlace 1</p>
<p class="text-center">Enlace 2</p>
<p class="text-center">Enlace 3</p>
<p class="text-center">Enlace 4</p>
<p class="text-center">Enlace 5</p>
<p class="text-center">Enlace 6</p>
<p class="text-center">Enlace 7</p>
<p class="text-center">Enlace 8</p>
<p class="text-center">Enlace 9</p>
<p class="text-center">Enlace 10</p>
<p class="text-center">Enlace 11</p>
<p class="text-center">Enlace 12</p>
<p class="text-center">Enlace 13</p>
<p class="text-center">Enlace 14</p>
<p class="text-center">Enlace 15</p>
<p class="text-center">Enlace 16</p>
</div>
</div>
{% endcache %}
</div>
{% cache 500 scripts %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script>
@@ -243,5 +247,6 @@
}
});
</script>
{% endcache %}
</body>
</html>
+38 -4
View File
@@ -49,6 +49,30 @@
</div>
{% if cart_items %}
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title mb-3">1) Selecciona la dirección de envío</h5>
{% if addresses %}
<div class="mb-3">
<label for="shipping-address" class="form-label">Dirección</label>
<select id="shipping-address" class="form-select" required>
<option value="">Selecciona una dirección...</option>
{% for address in addresses %}
<option value="{{ address.id }}" {% if address.is_default %}selected{% endif %}>
{{ address.full_name }} - {{ address.address_line_1 }}, {{ address.postal_code }} {{ address.city }}
</option>
{% endfor %}
</select>
</div>
{% else %}
<div class="alert alert-warning mb-0 d-flex justify-content-between align-items-center flex-wrap gap-2">
<span>No tienes direcciones de envío creadas.</span>
<a href="{% url 'crear_direccion' %}" class="btn btn-primary btn-sm">Crear dirección</a>
</div>
{% endif %}
</div>
</div>
<div class="table-responsive mb-4">
<table class="table table-striped align-middle">
<thead>
@@ -87,20 +111,22 @@
</div>
<div class="payment-section">
<h3>Selecciona tu método de pago</h3>
<h3>2) Selecciona tu método de pago</h3>
<div class="payment-methods">
<button
id="checkout-button"
class="btn btn-primary payment-btn"
data-config-url="/tienda/config/"
data-session-url="/tienda/create-checkout-session/">
data-session-url="/tienda/create-checkout-session/"
{% if not addresses %}disabled{% endif %}>
💳 Pagar con Stripe
</button>
<button
id="paypal-button"
class="btn btn-warning payment-btn"
data-payment-url="{% url 'create_paypal_payment' %}">
data-payment-url="{% url 'create_paypal_payment' %}"
{% if not addresses %}disabled{% endif %}>
🅿️ Pagar con PayPal
</button>
</div>
@@ -115,6 +141,13 @@
// Manejo del botón de PayPal
document.getElementById('paypal-button').addEventListener('click', async function(e) {
e.preventDefault();
const shippingAddressSelect = document.getElementById('shipping-address');
const selectedShippingAddress = shippingAddressSelect ? shippingAddressSelect.value : '';
if (!selectedShippingAddress) {
alert('Selecciona una dirección de envío para continuar.');
return;
}
const button = this;
const originalText = button.innerHTML;
@@ -138,7 +171,8 @@
headers: {
'X-CSRFToken': csrfToken || '',
'Content-Type': 'application/json',
}
},
body: JSON.stringify({ shipping_address_id: selectedShippingAddress })
});
console.log('Response status:', response.status);
+74 -4
View File
@@ -35,17 +35,27 @@
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="city" class="form-label">Ciudad *</label>
<input type="text" class="form-control" id="city" name="city" value="{{ direccion.city|default:'' }}" required>
<label for="city" class="form-label">Ciudad/Pueblo (Almería) *</label>
<input type="text" class="form-control" id="city" name="city" value="{{ direccion.city|default:'' }}" list="almeria-towns" autocomplete="off" required>
<datalist id="almeria-towns">
{% for town in almeria_municipalities %}
<option value="{{ town }}"></option>
{% endfor %}
</datalist>
<div class="form-text">Selecciona o escribe un municipio de la provincia de Almería.</div>
<div class="invalid-feedback" id="city-validation-message">
El pueblo/ciudad debe pertenecer a la provincia de Almería.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="postal_code" class="form-label">Código Postal *</label>
<input type="text" class="form-control" id="postal_code" name="postal_code" value="{{ direccion.postal_code|default:'' }}" required>
<input type="text" class="form-control" id="postal_code" name="postal_code" value="{{ direccion.postal_code|default:'' }}" pattern="04[0-9]{3}" maxlength="5" placeholder="04XXX" required>
<div class="form-text">Solo aceptamos códigos postales de Almería (04xxx).</div>
</div>
</div>
<div class="mb-3">
<label for="country" class="form-label">País *</label>
<input type="text" class="form-control" id="country" name="country" value="{{ direccion.country|default:'España' }}" required>
<input type="text" class="form-control" id="country" name="country" value="España" readonly>
</div>
<div class="mb-3">
<label for="phone" class="form-label">Teléfono *</label>
@@ -67,4 +77,64 @@
</div>
</div>
<script>
(function () {
const cityInput = document.getElementById('city');
const cityValidationMessage = document.getElementById('city-validation-message');
const form = cityInput ? cityInput.form : null;
if (!cityInput || !form) {
return;
}
const almeriaTowns = new Set([
{% for town in almeria_municipalities %}
"{{ town|escapejs }}",
{% endfor %}
].map(normalizeTown));
function normalizeTown(value) {
return (value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-zA-Z0-9\s-]/g, '')
.replace(/-/g, ' ')
.toLowerCase()
.trim()
.replace(/\s+/g, ' ')
.replace(/^(la|los)\s+/, '');
}
function validateTown() {
const normalized = normalizeTown(cityInput.value);
if (!normalized) {
cityInput.setCustomValidity('');
cityInput.classList.remove('is-invalid');
return;
}
const isValid = almeriaTowns.has(normalized);
if (isValid) {
cityInput.setCustomValidity('');
cityInput.classList.remove('is-invalid');
} else {
cityInput.setCustomValidity('El pueblo/ciudad debe pertenecer a la provincia de Almería.');
cityInput.classList.add('is-invalid');
}
cityValidationMessage.textContent = cityInput.validationMessage || 'El pueblo/ciudad debe pertenecer a la provincia de Almería.';
}
cityInput.addEventListener('input', validateTown);
cityInput.addEventListener('blur', validateTown);
form.addEventListener('submit', function () {
validateTown();
});
validateTown();
})();
</script>
{% endblock %}
+63
View File
@@ -0,0 +1,63 @@
{% extends "tienda/base.html" %}
{% load static %}
{% block content %}
<div class="row mt-4">
<div class="col-12">
<h2>Mis Compras</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'portal_usuario' %}">Portal de Usuario</a></li>
<li class="breadcrumb-item active">Compras</li>
</ol>
</nav>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="btn-group" role="group">
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
<a href="{% url 'mis_compras' %}" class="btn btn-primary">Compras</a>
<a href="{% url 'mis_recibos' %}" class="btn btn-outline-primary">Recibos</a>
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<p class="text-muted">Total de compras: <strong>{{ total_orders }}</strong></p>
{% if orders %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Pedido #</th>
<th>Fecha</th>
<th>Total</th>
<th>Estado</th>
<th>Método</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>{{ order.id }}</td>
<td>{{ order.created_at|date:"d/m/Y H:i" }}</td>
<td>{{ order.total }}€</td>
<td><span class="badge bg-success">{{ order.get_status_display }}</span></td>
<td>{{ order.get_payment_method_display }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info mb-0">
Aún no has realizado compras.
</div>
{% endif %}
</div>
</div>
{% endblock %}
+63
View File
@@ -0,0 +1,63 @@
{% extends "tienda/base.html" %}
{% load static %}
{% block content %}
<div class="row mt-4">
<div class="col-12">
<h2>Mis Recibos</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'portal_usuario' %}">Portal de Usuario</a></li>
<li class="breadcrumb-item active">Recibos</li>
</ol>
</nav>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="btn-group" role="group">
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
<a href="{% url 'mis_compras' %}" class="btn btn-outline-primary">Compras</a>
<a href="{% url 'mis_recibos' %}" class="btn btn-primary">Recibos</a>
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<p class="text-muted">Total de recibos: <strong>{{ total_receipts }}</strong></p>
{% if receipts %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Recibo #</th>
<th>Fecha</th>
<th>Total</th>
<th>Método</th>
<th>Referencia</th>
</tr>
</thead>
<tbody>
{% for receipt in receipts %}
<tr>
<td>{{ receipt.id }}</td>
<td>{{ receipt.created_at|date:"d/m/Y H:i" }}</td>
<td>{{ receipt.total }}€</td>
<td>{{ receipt.get_payment_method_display }}</td>
<td>{{ receipt.payment_reference|default:"-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info mb-0">
No tienes recibos disponibles todavía.
</div>
{% endif %}
</div>
</div>
{% endblock %}
@@ -43,6 +43,22 @@
<li><strong>Precio total:</strong> {{ item.total_price }}€</li>
<li><strong>Fecha:</strong> {{ item.created_at|date:"d/m/Y H:i" }}</li>
</ul>
<h6 class="mt-3">Dirección de envío</h6>
{% if item.order.shipping_address %}
<ul class="list-unstyled">
<li><strong>Destinatario:</strong> {{ item.order.shipping_address.full_name }}</li>
<li><strong>Dirección:</strong> {{ item.order.shipping_address.address_line_1 }}</li>
{% if item.order.shipping_address.address_line_2 %}
<li><strong>Detalle:</strong> {{ item.order.shipping_address.address_line_2 }}</li>
{% endif %}
<li><strong>Ciudad:</strong> {{ item.order.shipping_address.city }}</li>
<li><strong>Código Postal:</strong> {{ item.order.shipping_address.postal_code }}</li>
<li><strong>País:</strong> {{ item.order.shipping_address.country }}</li>
<li><strong>Teléfono:</strong> {{ item.order.shipping_address.phone }}</li>
</ul>
{% else %}
<p class="text-muted mb-0">Dirección no disponible.</p>
{% endif %}
</div>
<div class="col-md-6">
<h6>Cambiar Estado</h6>
@@ -14,6 +14,8 @@
<div class="col-12">
<div class="btn-group" role="group">
<a href="{% url 'portal_usuario' %}" class="btn btn-primary">Inicio</a>
<a href="{% url 'mis_compras' %}" class="btn btn-outline-primary">Compras</a>
<a href="{% url 'mis_recibos' %}" class="btn btn-outline-primary">Recibos</a>
<a href="{% url 'editar_perfil' %}" class="btn btn-outline-primary">Mi Perfil</a>
<a href="{% url 'direcciones_usuario' %}" class="btn btn-outline-primary">Direcciones</a>
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
@@ -29,6 +31,7 @@
<h5 class="card-title">📦 Mis Pedidos</h5>
<p class="display-4">{{ total_orders }}</p>
<p class="text-muted">pedidos realizados</p>
<a href="{% url 'mis_compras' %}" class="btn btn-sm btn-primary">Ver compras</a>
</div>
</div>
</div>
@@ -54,6 +57,18 @@
</div>
</div>
<div class="row mt-2">
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-body text-center">
<h5 class="card-title">🧾 Recibos</h5>
<p class="text-muted">consulta tus recibos de pago</p>
<a href="{% url 'mis_recibos' %}" class="btn btn-sm btn-primary">Ver recibos</a>
</div>
</div>
</div>
</div>
<!-- Pedidos recientes -->
<div class="row mt-4">
<div class="col-12">