Top.Mail.Ru

Генерация чека с PDF и QR-кодом в Django

Генерация чека с PDF и QR-кодом в Django

 

В этом проекте мы реализуем мини-систему кассового аппарата с использованием Django и Django REST Framework. Она позволяет пользователю отправить список товаров по API и получить в ответ:

  • чек в формате PDF;

  • сгенерированный QR-код, ведущий к этому чеку;

  • ссылку на скачивание PDF.

Это может быть полезно для интернет-магазинов, офлайн-касс, киосков самообслуживания или даже курсов по Python 😄.


Создание проекта

Создаём новый проект в PyCharm и называем его cash_project.

Открываем терминал и устанавливаем необходимые библиотеки:

pip install Django              # сам фреймворк
pip install djangorestframework # для API-запросов
pip install pdfkit              # для генерации PDF
pip install qrcode              # для создания QR-кодов


Создание проекта и приложения

django-admin startproject config
cd config python manage.py startapp aap


Настройка settings.py

Добавляем в конец INSTALLED_APPS:

'rest_framework',
'aap',

Добавляем настройки для медиафайлов:

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')


Настройка маршрутов (urls.py)

from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from aap.views import CashMachineView, serve_pdf

urlpatterns = [
    path('admin/', admin.site.urls),
    path('cash_machine/', CashMachineView.as_view(), name='cash_machine'),  # маршрут для генерации PDF и QR-кода
    path('media/<str:filename>', serve_pdf, name='serve_pdf'),              # маршрут получения PDF чеков
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


Модель Item (models.py)

from django.db import models

class Item(models.Model):
    title = models.CharField(max_length=255, verbose_name="Наименование товара")
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Стоимость")

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = "Товар"
        verbose_name_plural = "Товары"

Модель предназначена для хранения информации о товарах.


Сериализаторы (serializers.py)

from rest_framework import serializers
from .models import Item

class ItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = Item
        fields = ['id', 'title', 'price']

class ReceiptSerializer(serializers.Serializer):
    items = serializers.ListField(
        child=serializers.IntegerField(),
        allow_empty=False
    )


Основная логика (views.py)

import os
import qrcode
import pdfkit
from django.conf import settings
from django.http import JsonResponse, FileResponse
from django.template.loader import render_to_string
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Item
from .serializers import ReceiptSerializer
from datetime import datetime
from pdfkit.configuration import Configuration


class CashMachineView(APIView):
def post(self, request):
serializer = ReceiptSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

item_ids = serializer.validated_data['items']
items = Item.objects.filter(id__in=item_ids)

# Проверяем что все товары найдены
found_ids = {item.id for item in items}
not_found = [i for i in item_ids if i not in found_ids]

if not_found:
return Response(
{"error": f"Items not found: {not_found}"},
status=status.HTTP_404_NOT_FOUND
)

# Подсчет количества и суммы
item_counts = {}
for item_id in item_ids:
item_counts[item_id] = item_counts.get(item_id, 0) + 1

receipt_items = []
total = 0
for item in items:
quantity = item_counts[item.id]
item_total = item.price * quantity
receipt_items.append({
'title': item.title,
'quantity': quantity,
'price': float(item.price),
'total': float(item_total)
})
total += item_total

# Подготовка данных для чека
context = {
'items': receipt_items,
'total': float(total),
'date': datetime.now().strftime("%d.%m.%Y %H:%M"),
'company_name': 'ООО "КОМПАНИЯ К"',
'cashier': 'АДМИ',
'receipt_number': datetime.now().strftime("%Y%m%d%H%M%S"),
}

# Генерация HTML
html_content = render_to_string('receipt_template.html', context)

# Создаем папку media если нет
os.makedirs(settings.MEDIA_ROOT, exist_ok=True)

pdf_filename = f"receipt_{context['receipt_number']}.pdf"
pdf_path = os.path.join(settings.MEDIA_ROOT, pdf_filename)

try:
# Конфигурация для Windows
config = Configuration(
wkhtmltopdf=settings.WKHTMLTOPDF_PATH
)

options = {
'encoding': 'UTF-8',
'quiet': '',
'enable-local-file-access': '',
'margin-top': '0mm',
'margin-right': '0mm',
'margin-bottom': '0mm',
'margin-left': '0mm',
'page-size': 'A7',
'disable-smart-shrinking': '',
}

pdfkit.from_string(
html_content,
pdf_path,
configuration=config,
options=options
)
except Exception as e:
return Response(
{
"error": "PDF generation failed",
"details": str(e),
"solution": "Check wkhtmltopdf installation at " + settings.WKHTMLTOPDF_PATH
},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)

# Генерация QR-кода
try:

qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(f"http://192.168.31.141:8080/{settings.MEDIA_URL}{pdf_filename}")
qr.make(fit=True)
qr_img = qr.make_image(fill_color="black", back_color="white")

qr_filename = f"qr_{context['receipt_number']}.png"
qr_path = os.path.join(settings.MEDIA_ROOT, qr_filename)
qr_img.save(qr_path)
except Exception as e:
# Удаляем PDF если не удалось создать QR-код
if os.path.exists(pdf_path):
os.remove(pdf_path)
return Response(
{"error": f"QR code generation failed: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)

return Response({
'qr_code_url': f"http://192.168.31.141:8080/{settings.MEDIA_URL}{qr_filename}",
'pdf_url': f"http://192.168.31.141:8080/{settings.MEDIA_URL}{pdf_filename}",
'receipt_number': context['receipt_number']
})


def serve_pdf(request, filename):
file_path = os.path.join(settings.MEDIA_ROOT, filename)
if os.path.exists(file_path):
return FileResponse(
open(file_path, 'rb'),
content_type='application/png',
as_attachment=False
)
return JsonResponse(
{'error': 'File not found'},
status=status.HTTP_404_NOT_FOUND
)

Регистрация модели в админке (admin.py)

from django.contrib import admin
from .models import Item

admin.site.register(Item)

Шаблон receipt_template.html

Создаем шаблон receipt_template.html в папке templates

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Кассовый чек</title>
<style>
body {
font-family: Arial, sans-serif;
width: 80mm;
margin: 0 auto;
padding: 10px;
font-size: 14px;
}
.header {
text-align: center;
margin-bottom: 10px;
}
.company-name {
font-weight: bold;
margin-bottom: 5px;
}
.welcome {
margin-bottom: 10px;
}
.item {
margin-bottom: 5px;
border-bottom: 1px dashed #ccc;
padding-bottom: 5px;
}
.item-title {
font-weight: bold;
}
.item-quantity {
display: inline-block;
width: 20px;
}
.item-price {
float: right;
}
.total {
font-weight: bold;
text-align: right;
margin-top: 10px;
border-top: 1px solid #000;
padding-top: 5px;
}
.payment {
margin-top: 5px;
text-align: right;
}
.footer {
margin-top: 15px;
text-align: center;
font-size: 12px;
}
</style>
</head>
<body>
<div class="header">
<div class="company-name">{{ company_name }}</div>
<div class="welcome">Добро пожаловать</div>
<div>ККМ 00075411 #3969</div>
<div>ИНН 1087746942040</div>
<div>ЭКЛЗ 3851495566</div>
<div>{{ date }} СИС.</div>
</div>

<div class="cashier">{{ cashier }}</div>

<div class="items">
{% for item in items %}
<div class="item">
<div class="item-title">{{ item.title }}</div>
<div>
<span class="item-quantity">{{ item.quantity }}</span> x {{ item.price }} =
<span class="item-price">{{ item.total }}</span>
</div>
</div>
{% endfor %}
</div>

<div class="total">ИТОГ {{ total }}</div>
<div class="payment">НАЛИЧНЫМИ ={{ total }}</div>

<div class="footer">
***************<br>
{{ receipt_number }}# 05970
</div>
</body>
</html>

Выполняем миграции:

python manage.py makemigrations
python manage.py migrate

Создаём суперпользователя:

python manage.py createsuperuser

Запускаем сервер:

python manage.py runserver

Переходим по адресу: http://127.0.0.1:8000/admin/ — авторизуемся и добавляем товары.

Открываем http://127.0.0.1:8000/cash_machine/ в Postman или другом REST-клиенте, делаем POST-запрос с телом:

{
  "items": [1, 2, 3]
}

Автор:

15

Читайте также

0 комментариев

Оставьте комментарий

Комментарии

Бот: Здравствуйте! Я чат-бот веб-разработчика. Чем могу помочь в создании или доработке вашего сайта?