domingo, 14 de enero de 2024

24.- Exportando los pedidos a archivos CSV. Personalización del panel de Administración y generación y envio de PDFs.

 A veces, es posible que desees exportar la información contenida en un modelo a un archivo para poder importarla en otro sistema. Uno de los formatos más ampliamente utilizados para exportar/importar datos es el de Valores Separados por Comas (CSV). Un archivo CSV es un archivo de texto plano que consta de varios registros. Normalmente, hay un registro por línea y algún carácter delimitador, generalmente una coma literal, que separa los campos del registro. Vamos a personalizar el sitio de administración para poder exportar pedidos a archivos CSV.


Añadiendo acciones personalizadas al sitio de administración 


Django ofrece una amplia gama de opciones para personalizar el sitio de administración. Vas a modificar la vista de lista de objetos para incluir una acción de administración personalizada. Puedes implementar acciones de administración personalizadas para permitir que los usuarios apliquen acciones a varios elementos a la vez en la vista de lista de cambios. 

Una acción de administración funciona de la siguiente manera: un usuario selecciona objetos de la página de lista de objetos de administración con casillas de verificación, luego selecciona una acción para realizar en todos los elementos seleccionados y ejecuta las acciones. La siguiente imagen muestra dónde se encuentran las acciones en el sitio de administración:


The drop-down menu for Django administration actions



Puedes crear una acción personalizada escribiendo una función regular que reciba los siguientes parámetros:

• El ModelAdmin actual que se está mostrando
• El objeto de solicitud actual como una instancia de HttpRequest
• Un QuerySet para los objetos seleccionados por el usuario

Esta función se ejecutará cuando se active la acción desde el sitio de administración.

Vamos a crear una acción personalizada de administración para descargar una lista de pedidos como un archivo CSV.

Edita el archivo admin.py de la aplicación de pedidos y agrega el siguiente código antes de la clase OrderAdmin:

PracticaDjango/Orders/admin.py

# Para exportar los pedidos como archivos csv
import csv
import datetime
from django.http import HttpResponse

def export_to_csv(modeladmin, request, queryset):
    opts = modeladmin.model._meta
    content_disposition = f'attachment; filename={opts.verbose_name}.csv'
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = content_disposition
    writer = csv.writer(response)
    fields = [field for field in opts.get_fields() if not \
              field.many_to_many and not field.one_to_many]
    # Escribe la primera línea con información de la cabecera
    writer.writerow([field.verbose_name for field in fields])
    # Escribe las filas con los datos
    for obj in queryset:
        data_row = []
        for field in fields:
            value = getattr(obj, field.name)
            if isinstance(value, datetime.datetime):
                value = value.strftime('%d/%m/%Y')
            data_row.append(value)
        writer.writerow(data_row)
    return response
export_to_csv.short_description = 'Exportar a CSV'


En este código, realizas las siguientes tareas:

1. Creas una instancia de HttpResponse, especificando el tipo de contenido text/csv, para indicar al navegador que la respuesta debe tratarse como un archivo CSV. También agregas una cabecera Content-Disposition para indicar que la respuesta HTTP contiene un archivo adjunto.

2. Creas un objeto escritor CSV que escribirá en el objeto de respuesta.

3. Obtienes dinámicamente los campos del modelo utilizando el método get_fields() de las opciones _meta del modelo. Excluyes las relaciones muchos a muchos y uno a muchos.

4. Escribe una fila de encabezado que incluye los nombres de los campos.

5. Iteras sobre el conjunto de consultas dado y escribes una fila para cada objeto devuelto por el conjunto de consultas. Te ocupas del formato de objetos datetime porque el valor de salida para CSV debe ser una cadena o string.

6. Personalizas el nombre de visualización para la acción en el elemento desplegable de acciones del sitio de administración estableciendo un atributo short_description en la función.

Has creado una acción de administración genérica que se puede agregar a cualquier clase ModelAdmin.

Finalmente, añade la nueva acción de administración export_to_csv a la clase OrderAdmin, de la siguiente manera. El nuevo código está resaltado en azul:

PracticaDjango/Orders/admin.py

#...
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email',
                    'address', 'postal_code', 'city', 'paid', order_payment,
                    'created', 'updated']
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]
    actions = [export_to_csv]

Abre la siguiente dirección en tu navegador http://127.0.0.1:8000/admin/Orders/order/. Si has introducido pedidos, deberías ver algo como esto:

Using the custom Export to CSV administration action

Selecciona algunos pedidos y haz clic en "Exportar a CSV" en el menú que pone "acción" en la parte superior, y haz clic en el botón "Ir". Tu navegador descargará el archivo CSV que hemos generado con el nombre de order.csv. Abre este archivo usando un editor de texto. Verás algo parecido a esto:

archivo css

Como puedes ver, crear acciones personalizadas en el panel de administración es bastante sencillo. Puedes obtener más información sobre la generación de archivos CSV con Django en https://docs.djangoproject.com/en/5.0/howto/outputting-csv/.

A continuación, personalizaremos aún más el panel de administración mediante la creación de una vista personalizada.


Ampliando el sitio de administración con vistas personalizadas

En ocasiones, es posible que desees personalizar el sitio de administración más allá de lo que es posible mediante la configuración de ModelAdmin, la creación de acciones personalizadas de administración y la anulación de plantillas de administración. Puede que desees implementar funcionalidades adicionales que no estén disponibles en las vistas o plantillas de administración existentes. Si este es el caso, necesitas crear una vista de administración personalizada. Con una vista personalizada, puedes construir cualquier funcionalidad que desees; solo asegúrate de que solo los usuarios con privilegios de staff puedan acceder a tu vista y de que mantengas el aspecto y la sensación de administración al hacer que tu plantilla extienda una plantilla de administración.

Vamos a crear una vista personalizada para mostrar información sobre un pedido. Edita el archivo views.py de la aplicación de pedidos y agrega el siguiente código resaltado en azul:

PracticaDjango/Orders/views.py

#...
# Para crear una vista personalizada en el panel de administración.
from django.shortcuts import get_object_or_404
from django.contrib.admin.views.decorators import staff_member_required
from .models import Order

def order_create(request):
    #...

@staff_member_required
def admin_order_detail(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    return render(request, 'admin/Orders/order/detail.html', {'order': order})

El decorador `staff_member_required` verifica que tanto los campos `is_active` como `is_staff` del usuario que solicita la página estén configurados como Verdaderos. En esta vista, obtienes el objeto Order con el ID proporcionado y renderizas una plantilla para mostrar la orden.

A continuación, edita el archivo urls.py de la aplicación de órdenes y agrega el siguiente patrón de URL resaltado en azul:

PracticaDjango/Orders/urls.py

from django.urls import path
from . import views

app_name = 'orders'

urlpatterns = [
path('create/', views.order_create, name='order_create'),
path('admin/Order/<int:order_id>/', views.admin_order_detail,
name='admin_order_detail'),
]
Crea la siguiente estructura de directorios dentro de las plantillas (templates) de la aplicación de pedidos:

admin/

    Order/

        order/

            detail.html

Edita la plantilla "detail.html" que hemos creado y añade el siguiente código:

PracticaDjango/Orders/templates/admin/Orders/order/detail.html

<html>

<body>
    <h1>Unikgame</h1>
    <p>
        Factura no. {{ order.id }}<br>
        <span class="secondary">
            {{ order.created|date:"M d, Y" }}
        </span>
    </p>
    <h3>Facturar a:</h3>
    <p>
        {{ order.first_name }} {{ order.last_name }}<br>
        {{ order.email }}<br>
        {{ order.address }}<br>
        {{ order.postal_code }}, {{ order.city }}
    </p>
    <h3>Elementos Comprados</h3>
    <table>
        <thead>
            <tr>
                <th>Producto</th>
                <th>Precio</th>
                <th>Cantidad</th>
                <th>Coste</th>
            </tr>
        </thead>
        <tbody>
            {% for item in order.items.all %}
            <tr class="row{% cycle '1' '2' %}">
                <td>{{ item.product.name }}</td>
                <td class="num">{{ item.price }} €</td>
                <td class="num">{{ item.quantity }}</td>
                <td class="num">{{ item.get_cost }} €</td>
            </tr>
            {% endfor %}
            <tr class="total">
                <td colspan="3">Total</td>
                <td class="num">{{ order.get_total_cost }} €</td>
            </tr>
        </tbody>
    </table>
    <span class="{% if order.paid %}paid{% else %}pending{% endif %}">
        {% if order.paid %}Pagado{% else %}Pendiente de Pago{% endif %}
    </span>
</body>

</html>

Asegúrate de que ninguna etiqueta de plantilla esté dividida en varias líneas.

Este es el modelo para mostrar los detalles de un pedido en el sitio de administración. Este modelo extiende el modelo admin/base_site.html del sitio de administración de Django, que contiene la estructura principal de HTML y estilos CSS. Utilizas los bloques definidos en el modelo padre para incluir tu propio contenido. Muestras información sobre el pedido y los artículos comprados.

Cuando deseas extender un modelo de administración, necesitas conocer su estructura e identificar los bloques existentes. Puedes encontrar todos los modelos de administración en https://github.com/django/django/tree/5.0/django/contrib/admin/templates/admin.

También puedes anular un modelo de administración si es necesario. Para hacerlo, copia un modelo en tu directorio de plantillas (templates/), manteniendo la misma ruta y nombre de archivo. El sitio de administración de Django utilizará tu modelo personalizado en lugar del predeterminado.

Finalmente, agreguemos un enlace a cada objeto Order en la página de visualización de la lista del sitio de administración. Edita el archivo admin.py de la aplicación de pedidos y agrega el siguiente código, encima de la clase OrderAdmin:

PracticaBlog/PracticaDjango/Orders/admin.py

#...
from django.urls import reverse

#...

def order_detail(obj):
    url = reverse('orders:admin_order_detail', args=[obj.id])
    return mark_safe(f'<a href="{url}">View</a>')


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    #...

Esta es una función que toma un objeto Order como argumento y devuelve un enlace HTML para la URL admin_order_detail. Django escapa la salida HTML de forma predeterminada. Debes utilizar la función mark_safe para evitar el autoescape.

Luego, edita la clase OrderAdmin para mostrar el enlace de la siguiente manera. El código nuevo está resaltado en azul:

PracticaBlog/PracticaDjango/Orders/admin.py

#...
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email',
                    'address', 'postal_code', 'city', 'paid', order_payment,
                    'created', 'updated', order_detail]
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]
    actions = [export_to_csv]
Para finalizar ejecuta el servidor de desarrollo con :
env()$ python manage.py runserver

Abre la dirección http://127.0.0.1:8000/admin/Orders/order/ en tu navegador. Cada fila de datos incluye un link view a la derecha del todo, como se puede apreciar en al siguiente imagen:


nueva opción view en las vistas.

Después de haber visto esto vamos a continuación a ver como podemos generar facturas dinámicas en PDF.


Generando facturas en PDF dinámicamente


Ahora que tienes un sistema completo de pago y compra, puedes generar una factura en PDF para cada pedido. Existen varias bibliotecas de Python para generar archivos PDF. Una biblioteca popular para generar PDFs con código Python es ReportLab. Puedes encontrar información sobre cómo generar archivos PDF con ReportLab en https://docs.djangoproject.com/en/5.0/howto/outputting-pdf/.

En la mayoría de los casos, deberás agregar estilos y formato personalizados a tus archivos PDF. Encontrarás más sencillo renderizar una plantilla HTML y convertirla en un archivo PDF, manteniendo a Python alejado de la capa de presentación. Vas a seguir este enfoque y utilizar un módulo para generar archivos PDF con Django. Utilizaremos WeasyPrint, que es una biblioteca de Python que puede generar archivos PDF a partir de plantillas HTML.


Instalando WeasyPrint

Primero, instala las dependencias de WeasyPrint para tu sistema operativo desde https://doc.courtbouillon.org/weasyprint/stable/first_steps.html. Luego, instala WeasyPrint a través de pip utilizando el siguiente comando:

pip install weasyprint


Creando la plantilla para el PDF.


Tenemos que crear una plantilla HTML que servirá de base para el PDF. Crearemos la plantilla, la renderizaremos usando Django y la pasaremos a WeasyPrint para generar el PDF. Crearemos una nueva plantilla dentro de la aplicación de pedidos en templates/Orders/order/ y la llamaremos pdf.html. Añádele el siguiente código:

PracticaDjango/Orders/templates/Orders/order/pdf.html

<html>

<body>
    <h1>Unikgame</h1>
    <p>
        Factura no. {{ order.id }}<br>
        <span class="secondary">
            {{ order.created|date:"M d, Y" }}
        </span>
    </p>
    <h3>Facturar a:</h3>
    <p>
        {{ order.first_name }} {{ order.last_name }}<br>
        {{ order.email }}<br>
        {{ order.address }}<br>
        {{ order.postal_code }}, {{ order.city }}
    </p>
    <h3>Elementos Comprados</h3>
    <table>
        <thead>
            <tr>
                <th>Producto</th>
                <th>Precio</th>
                <th>Cantidad</th>
                <th>Coste</th>
            </tr>
        </thead>
        <tbody>
            {% for item in order.items.all %}
            <tr class="row{% cycle '1' '2' %}">
                <td>{{ item.product.name }}</td>
                <td class="num">${{ item.price }}</td>
                <td class="num">{{ item.quantity }}</td>
                <td class="num">${{ item.get_cost }}</td>
            </tr>
            {% endfor %}
            <tr class="total">
                <td colspan="3">Total</td>
                <td class="num">{{ order.get_total_cost }} €</td>
            </tr>
        </tbody>
    </table>
    <span class="{% if order.paid %}pagado{% else %}pending{% endif %}">
        {% if order.paid %}Paid{% else %}Pendiente de Pago{% endif %}
    </span>
</body>

</html>    


Esta es la plantilla de la factura que usaremos de base para crear el PDF. En ella mostraremos todos los detalles de la factura, así como un mensaje de si está pagada o no.


Renderizando archivos PDF.


Vamos a crear la vista para renderizar los archivos PDF para los pedidos existentes y usaremos para ello el panel de administración de Django. Edita el archivo views.py de la aplicación de pedidos y añade el siguiente código.

PracticaBlog/PracticaDjango/Orders/views.py

#...
# Para renderizar el archivo PDF de las facturas.
from django.conf import settings
from django.http import HttpResponse
from django.template.loader import render_to_string
import weasyprint

# Create your views here.


@staff_member_required
def admin_order_pdf(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    html = render_to_string('Orders/order/pdf.html', {'order': order})
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename=order_{order.id}.pdf'
    weasyprint.HTML(string=html).write_pdf(response, stylesheets=[
        weasyprint.CSS(settings.STATIC_ROOT / 'Proyecto_web_app/css/pdf.css')])
    return response


Esta es la vista para generar una factura en formato PDF para un pedido. Se utiliza el decorador staff_member_required para asegurarse de que solo los usuarios del personal tengan acceso a esta vista. Se obtiene el objeto Order con el ID proporcionado y se utiliza la función render_to_string() proporcionada por Django para renderizar Orders/order/pdf.html. El HTML resultante se guarda en la variable html. Luego, se genera un nuevo objeto HttpResponse especificando el tipo de contenido application/pdf e incluyendo la cabecera Content-Disposition para especificar el nombre de archivo. Se utiliza WeasyPrint para generar un archivo PDF a partir del código HTML renderizado y se escribe el archivo en el objeto HttpResponse.

Se utiliza el archivo de estilo CSS pdf.css ubicado en archivos estáticos para agregar estilos CSS al archivo PDF generado. Luego, se carga desde la ruta local utilizando la configuración STATIC_ROOT. Finalmente, se devuelve la respuesta generada.

Dado que necesitas usar la configuración de STATIC_ROOT, debes agregarla a tu proyecto. Esta es la ruta del proyecto donde residen los archivos estáticos. Edita el archivo settings.py del proyecto PracticaDjango y agrega la siguiente configuración:

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'static'

Después ejecuta el siguiente comando:

python manage.py collectstatic

La salida del comando debería ser parecida a esto:

 142 static files copied to '/home/chema/PycharmProjects/PracticaBlog/PracticaDjango/static'.


El comando collectstatic copia todos los archivos estáticos de tus aplicaciones al directorio definido en la configuración STATIC_ROOT. Esto permite que cada aplicación proporcione sus propios archivos estáticos utilizando un directorio static/ que los contenga. También puedes proporcionar fuentes adicionales de archivos estáticos en la configuración STATICFILES_DIRS. Todos los directorios especificados en la lista STATICFILES_DIRS también se copiarán al directorio STATIC_ROOT cuando se ejecute collectstatic. Cada vez que ejecutas collectstatic nuevamente, se te preguntará si deseas sobrescribir los archivos estáticos existentes.

Edita el archivo urls.py dentro del directorio de la aplicación Orders y agrega el siguiente patrón de URL resaltado en azul:

PracticaBlog/PracticaDjango/Orders/urls.py

from django.urls import path
from . import views

app_name = 'orders'

urlpatterns = [
    path('create/', views.order_create, name='order_create'),
    path('admin/Order/<int:order_id>/', views.admin_order_detail,
         name='admin_order_detail'),
    path('admin/Order/<int:order_id>/pdf/',
         views.admin_order_pdf, name='admin_order_pdf'),
]

Ahora puedes editar la página de visualización de la lista de administración para el modelo Order para agregar un enlace al archivo PDF para cada resultado. Edita el archivo admin.py dentro de la aplicación Orders y agrega el siguiente código encima de la clase OrderAdmin:


PracticaBlog/PracticaDjango/Orders/admin.py

def order_pdf(obj):
    url = reverse('orders:admin_order_pdf', args=[obj.id])
    return mark_safe(f'<a href="{url}">PDF</a>')
order_pdf.short_description = 'Factura'

Si especificas un atributo short_description para tu función, Django lo utilizará como nombre de la columna.

Añade order_pdf al atributo list_display de la clase OrderAdmin, de la siguiente manera:

PracticaBlog/PracticaDjango/Orders/admin.py

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email',
                    'address', 'postal_code', 'city', 'paid', order_payment,
                    'created', 'updated', order_detail, order_pdf]
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]
    actions = [export_to_csv]

Asegúrate de que el servidor de desarrollo esté en ejecución. Abre http://127.0.0.1:8000/admin/Orders/order/ en tu navegador. Ahora, cada fila debería incluir un enlace PDF, similar a esto:enlace a pdfs



Haz clic en el enlace PDF para cualquier pedido. Deberías ver un archivo PDF generado, similar al siguiente:

fra generada en pdf



Enviando el archivo PDF de la factura por email.


Lo que vamos a hacer ahora es preparar el programa para que cuando se realice un pago exitosamente se envíe un email al cliente con la factura que hemos generado en PDF. Lo haremos a través de una tarea asincrónica. 

Crea un nuevo archivo en la aplicación de pago y llámalo task.py. Añade el siguiente código:

PracticaBlog/PracticaDjango/Payment/tasks.py

from io import BytesIO
from celery import shared_task
import weasyprint
from django.template.loader import render_to_string
from django.core.mail import EmailMessage
from django.conf import settings
from Orders.models import Order


@shared_task
def payment_completed(order_id):
    """
    Tarea que envia una notificación por email cuando el 
    pedido ha sido pagado con exito.
    """
    order = Order.objects.get(id=order_id)
    # crea la factura por email
    subject = f'Unikgame - Factura no. {order.id}'
    message = 'Te enviamos la factura de tu compra en el archivo adjunto.'
    email = EmailMessage(subject,
                         message,
                         'admin@unikgame.com',
                         [order.email])
    # genera el PDF
    html = render_to_string('Orders/order/pdf.html', {'order': order})
    out = BytesIO()
    stylesheets = [weasyprint.CSS(
        settings.STATIC_ROOT / 'Proyecto_web_app/css/pdf.css')]
    weasyprint.HTML(string=html).write_pdf(out,
                                           stylesheets=stylesheets)
    # adjunta el archivo PDF a la notificación
    email.attach(f'order_{order.id}.pdf',
                 out.getvalue(),
                 'application/pdf')
    # send e-mail
    email.send()
Definimos la tarea payment_completed utilizando el decorador @shared_task. En esta tarea, utilizas la clase EmailMessage proporcionada por Django para crear un objeto de correo electrónico. Luego, renderizas la plantilla en la variable html. Generas el archivo PDF a partir de la plantilla renderizada y lo envías a una instancia de BytesIO, que es un búfer de bytes en memoria. Después, adjuntas el archivo PDF generado al objeto EmailMessage utilizando el método attach(), incluyendo el contenido del búfer de salida. Finalmente, envías el correo electrónico. 

Recuerda configurar tus ajustes del Protocolo Simple de Transferencia de Correo (SMTP) en el archivo settings.py del proyecto para enviar correos electrónicos. Si no deseas configurar los ajustes de correo electrónico, puedes indicarle a Django que escriba los correos electrónicos en la consola agregando la siguiente configuración al archivo settings.py

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'


Añadamos la tarea `payment_completed` al punto final del webhook que maneja los eventos de finalización de pagos. Edita el archivo `webhooks.py` de la aplicación de pagos y modifícalo para que se vea de la siguiente manera:

PracticaBlog/PracticaDjango/Payment/tasks.py

import stripe
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from Orders.models import Order
from .tasks import payment_completed

#...
            order.paid = True
            # store Stripe payment ID
            order.stripe_id = session.payment_intent
            order.save()
            # lanza la tarea de forma asincrona para enviar un correo al usuario
            payment_completed.delay(order.id)
            
    return HttpResponse(status=200)


La tarea `payment_completed` se manda a la cola al llamar a su método 'delay()'. La tarea se agregará a la cola y se ejecutará de forma asíncrona por un trabajador de Celery tan pronto como sea posible.

Ahora puedes completar un nuevo proceso de pago para recibir la factura en formato PDF en tu correo electrónico. Si estás utilizando 'console.EmailBackend' como tu servicio de correo electrónico, en la consola donde estás ejecutando Celery, podrás ver la siguiente salida:

Message-ID: <170525972355.5337.6997908321665427625@machine>

--===============2428656861460200554==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit

Te enviamos la factura de tu compra en el archivo adjunto.
--===============2428656861460200554==
Content-Type: application/pdf
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="order_34.pdf"

JVBERi0xLjcKJfCflqQKNSAwIG9iago8PC9GaWx0ZXIgL0ZsYXRlRGVjb2RlL0xlbmd0aCA5Nzc+

Y con esto finalizamos este capítulo. 

Puedes encontrar el código del mismo en este enlace de GITHUB.



No hay comentarios:

Publicar un comentario