Как мне получить user по order.owner? def invoice_...
作成日: 2025年6月16日
作成日: 2025年6月16日
Как мне получить user по order.owner?
def invoice_pdf(request, id):
# Если у пользователя статус 2, проверяем, что заказ с таким id ему принадлежит
if request.user.profile.status == 2:
order = get_object_or_404(Order, pk=id)
if order.owner != request.user:
# Выдаем 404, будто заказа нет
raise Http404("Заказ не найден")
text# 2) Рендерим HTML в строку context = { 'order_id': order.id, 'order_created': order.created, 'owner': '', }
В Django, если в модели Order
вы завели поле
pythonowner = models.ForeignKey(User, on_delete=models.CASCADE)
то при выборке заказа order = Order.objects.get(pk=id)
атрибут order.owner
уже будет экземпляром User
. Соответственно, в вашем методе достаточно сделать:
pythonfrom django.contrib.auth.models import User from django.shortcuts import get_object_or_404 from django.http import Http404 def invoice_pdf(request, id): # 1) Проверяем, что заказ принадлежит пользователю со статусом 2 if request.user.profile.status == 2: order = get_object_or_404(Order, pk=id) if order.owner != request.user: raise Http404("Заказ не найден") else: # на случай других статусов — всё равно нужна выборка заказа order = get_object_or_404(Order, pk=id) # 2) Формируем контекст, передавая самого пользователя context = { 'order_id': order.id, 'order_created': order.created, 'owner': order.owner, # <-- вот здесь User } # …рендер и отдача PDF…
Если же в Order
вы храните не саму связку на User
, а, скажем, на Profile
:
pythonowner = models.ForeignKey(Profile, on_delete=models.CASCADE)
то достать связанный User
можно через обратное поле:
pythonuser = order.owner.user context['owner'] = user
Ещё вариант — если по какой-то причине в order.owner
у вас лежит просто числовой ID пользователя, то:
pythonfrom django.contrib.auth.models import User user = User.objects.get(pk=order.owner) context['owner'] = user
Но рекомендую именно хранить owner = ForeignKey(User, ...)
— тогда order.owner
сразу даёт вам нужный объект.
@status_required(0, 2)
def invoice_pdf(request, id):
# Если у пользователя статус 2, проверяем, что заказ с таким id ему принадлежит
if request.user.profile.status == 2:
order = get_object_or_404(Order, pk=id)
if order.owner != request.user:
# Выдаем 404, будто заказа нет
raise Http404("Заказ не найден")
text# 2) Рендерим HTML в строку context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, }
мне нужно получить все артикулы которые принадлежать к id(order.id)
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name='order_items', on_delete=models.CASCADE)
seller = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
article_number = models.CharField(max_length=255, null=True, blank=True)
name = models.CharField(max_length=255, null=True, blank=True)
delivery = models.IntegerField(null=True, blank=True)
price = models.DecimalField(max_digits=15, decimal_places=2, null=True, blank=True)
price_partner = models.DecimalField(max_digits=15, decimal_places=2, null=True, blank=True)
manufacturer = models.CharField(max_length=255, null=True, blank=True)
quantity = models.PositiveIntegerField(default=1)
quantity_received = models.PositiveIntegerField(default=0)
status = models.PositiveSmallIntegerField(default=0)
# count = models.PositiveIntegerField(default=1)
textdef __str__(self): return str(self.id)
Чтобы получить все article_number
, связанные с данным заказом, можно воспользоваться связью related_name='order_items'
на вашей модели OrderItem
. Вот несколько вариантов:
python# Получаем QuerySet заказанных позиций items_qs = OrderItem.objects.filter(order=order) # Извлекаем список артикулов article_numbers = list(items_qs.values_list('article_number', flat=True))
python# Если у вас в модели Order определено related_name='order_items': items_qs = order.order_items.all() # А затем list comprehension article_numbers = [item.article_number for item in items_qs]
python@status_required(0, 2) def invoice_pdf(request, id): # …ваша валидация и получение order… order = get_object_or_404(Order, pk=id) if request.user.profile.status == 2 and order.owner != request.user: raise Http404("Заказ не найден") # Получаем все артикулы по заказу article_numbers = order.order_items.values_list('article_number', flat=True) context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'article_numbers': article_numbers, # QuerySet или список строк } # …рендеринг PDF…
Если вам нужно не просто список строк, а полноценные объекты OrderItem
, используйте order.order_items.all()
, а потом в шаблоне перебирайте их и выводите item.article_number
.
@status_required(0, 2)
def invoice_pdf(request, id):
# Если у пользователя статус 2, проверяем, что заказ с таким id ему принадлежит
if request.user.profile.status == 2:
order = get_object_or_404(Order, pk=id)
if order.owner != request.user:
# Выдаем 404, будто заказа нет
raise Http404("Заказ не найден")
text# Получаем все артикулы по заказу articles = order.order_items.values_list('article_number', flat=True) # 2) Рендерим HTML в строку context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': articles, } html_string = render_to_string('cart/contract/invoice.html', context) base_url = settings.BASE_DIR # 3) Генерируем PDF pdf_file = HTML(string=html_string, base_url=base_url).write_pdf(stylesheets=[ CSS(string=''' @page { size: 960px 1200px; ; margin: 0 } html, body { margin: 0; padding: 0;} ''') ]) # 4) Возвращаем пользователю с заголовками response = HttpResponse(pdf_file, content_type='application/pdf') # response['Content-Disposition'] = f'inline; filename="invoice_{invoice.number}.pdf"' response['Content-Dispositioe'] = f'inline; filename="invoice.pdf"' print('-----') print(articles) print('-----') return response
Не понимаю почему не выводятся в pdf
{% for x in articles %}
{{ x }}
{% endfor %}
Выводится только 5137AA191 как строока
Проблема здесь, скорее всего, не в PDF-библиотеке, а в двух вещах:
order
может не инициализироваться, если у пользователя статус 0 (в вашем коде вы получаете order
только внутри if status == 2
).{{ x }}
без обёртки — если строк выводится несколько, они сольются без разделителей и визуально может показаться, что «выводится только один».order
Вынесите вызов get_object_or_404
перед проверкой статуса, а внутри if
оставьте только логику валидации:
python@status_required(0, 2) def invoice_pdf(request, id): # 1) Всегда загружаем заказ order = get_object_or_404(Order, pk=id) # 2) Если статус == 2, проверяем, что заказ принадлежит текущему пользователю if request.user.profile.status == 2 and order.owner != request.user: raise Http404("Заказ не найден") # 3) Получаем все артикулы заказа articles = list( order.order_items .filter(article_number__isnull=False) # опционально — отфильтровать пустые .values_list('article_number', flat=True) ) # 4) Формируем контекст context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': articles, } # 5) Рендерим HTML html_string = render_to_string('cart/contract/invoice.html', context) base_url = settings.BASE_DIR # 6) Генерируем PDF pdf_file = HTML(string=html_string, base_url=base_url).write_pdf(stylesheets=[ CSS(string=''' @page { size: 960px 1200px; margin: 0 } html, body { margin: 0; padding: 0; } ''') ]) # 7) Отдаём PDF с правильным заголовком response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = 'inline; filename="invoice.pdf"' return response
Обратите внимание на:
order = get_object_or_404
до любых проверок.articles
к обычному list
— явно выгружаем все строки в память.Content-Disposition
.Чтобы каждый артикул был на новой строке (или в списке), оберните вывод в блоковый элемент:
django{# cart/contract/invoice.html #} <h1>Счёт №{{ order_id }}</h1> <p>Дата: {{ order_created }}</p> <p>Клиент: {{ client.get_full_name }}</p> <h2>Артикулы:</h2> <ul> {% for art in articles %} <li>{{ art }}</li> {% empty %} <li>Артикулы отсутствуют</li> {% endfor %} </ul>
Без <li>…</li>
или <p>…</p>
несколько значений сливаются в одну строку:
django{% for x in articles %} {{ x }} {# <- без тега у вас выводится "5137AA1911234ABC" #} {% endfor %}
Поэтому используйте:
<li>{{ x }}</li>
внутри <ul>
для списка<p>{{ x }}</p>
для параграфов.С учётом этих двух правок — PDF будет содержать ВСЕ ваши артикулы, каждый на отдельной строке.
Нет, у меня пользователь со статусом 2
Проблема не в статусе пользователя, а в том, как вы выводите массив articles
в шаблоне — без блочных тегов все строки сливаются в одну (и кажется, что выводится только первый элемент).
Убедитесь, что в контекст передаётся именно список
pythonarticles = list( order.order_items .filter(article_number__isnull=False) .values_list('article_number', flat=True) )
Это гарантирует, что в шаблоне articles
— обычный list
, а не ленивый QuerySet
.
Используйте блочные HTML-теги, чтобы каждый артикул оказался на своей строке или в своём пункте списка. Например, так:
django{# cart/contract/invoice.html #} <h2>Артикулы заказа №{{ order_id }}:</h2> <ul> {% for art in articles %} <li>{{ art }}</li> {% empty %} <li>Артикулы отсутствуют</li> {% endfor %} </ul>
— либо, если не хотите список:
django<h2>Артикулы:</h2> {% for art in articles %} <p>{{ art }}</p> {% empty %} <p>Артикулы отсутствуют</p> {% endfor %}
Полный пример функции с учётом вынесения get_object_or_404
и приведения к списку:
python@status_required(0, 2) def invoice_pdf(request, id): # 1) Всегда загружаем заказ order = get_object_or_404(Order, pk=id) # 2) Проверяем принадлежность, если статус == 2 if request.user.profile.status == 2 and order.owner != request.user: raise Http404("Заказ не найден") # 3) Получаем список артикулов articles = list( order.order_items .filter(article_number__isnull=False) .values_list('article_number', flat=True) ) # 4) Формируем контекст context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': articles, } # 5) Рендерим HTML и генерируем PDF html_string = render_to_string('cart/contract/invoice.html', context) pdf_file = HTML(string=html_string, base_url=settings.BASE_DIR).write_pdf(stylesheets=[ CSS(string=''' @page { size: 960px 1200px; margin: 0 } html, body { margin: 0; padding: 0; } ''') ]) # 6) Отдаём PDF response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = 'inline; filename="invoice.pdf"' return response
После этих правок в вашем PDF каждый артикул будет выводиться отдельным <li>
(или <p>
) и вы точно увидите все значения, а не только первую строку.
articles = order.order_items.values_list('article_number', flat=True)
Это даёт мне строку а нужно обекты
def invoice_pdf(request, id):
# Если у пользователя статус 2, проверяем, что заказ с таким id ему принадлежит
if request.user.profile.status == 2:
order = get_object_or_404(Order, pk=id)
if order.owner != request.user:
# Выдаем 404, будто заказа нет
raise Http404("Заказ не найден")
text# Получаем все артикулы по заказу articles = order.order_items.values_list('article_number', flat=True) # 2) Рендерим HTML в строку context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': articles, } html_string = render_to_string('cart/contract/invoice.html', context) base_url = settings.BASE_DIR # 3) Генерируем PDF pdf_file = HTML(string=html_string, base_url=base_url).write_pdf(stylesheets=[ CSS(string=''' @page { size: 960px 1200px; ; margin: 0 } html, body { margin: 0; padding: 0;} ''') ]) # 4) Возвращаем пользователю с заголовками response = HttpResponse(pdf_file, content_type='application/pdf') # response['Content-Disposition'] = f'inline; filename="invoice_{invoice.number}.pdf"' response['Content-Dispositioe'] = f'inline; filename="invoice.pdf"' print('-----') for x in articles: print(x.name, ' <<name') print('-----') return response
Чтобы получить объекты OrderItem
, а не только строки article_number
, вместо values_list
берите сам QuerySet моделей. Например:
python@status_required(0, 2) def invoice_pdf(request, id): # Всегда загружаем заказ order = get_object_or_404(Order, pk=id) # Проверяем принадлежность, если статус == 2 if request.user.profile.status == 2 and order.owner != request.user: raise Http404("Заказ не найден") # Получаем все объекты OrderItem для этого заказа articles = order.order_items.all() # QuerySet of OrderItem # Если нужно отфильтровать по наличию article_number: # articles = order.order_items.filter(article_number__isnull=False) context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': articles, } html_string = render_to_string('cart/contract/invoice.html', context) pdf_file = HTML(string=html_string, base_url=settings.BASE_DIR).write_pdf(stylesheets=[ CSS(string=''' @page { size: 960px 1200px; margin: 0 } html, body { margin: 0; padding: 0; } ''') ]) response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = 'inline; filename="invoice.pdf"' return response
В шаблоне работать с объектами так:
django<h2>Позиции заказа №{{ order_id }}:</h2> <table> <thead> <tr> <th>Артикул</th> <th>Название</th> <th>Количество</th> <th>Цена</th> {# доп. поля по необходимости #} </tr> </thead> <tbody> {% for item in articles %} <tr> <td>{{ item.article_number }}</td> <td>{{ item.name }}</td> <td>{{ item.quantity }}</td> <td>{{ item.price }}</td> </tr> {% empty %} <tr><td colspan="4">Позиции отсутствуют</td></tr> {% endfor %} </tbody> </table>
Если нужно обращаться к другим связанным полям (например, seller
или order
), можно заранее сделать select_related('seller')
:
pythonarticles = order.order_items.select_related('seller').all()
чтобы при выводе item.seller
не было дополнительных запросов.
И в отладке в Python-коде:
pythonfor item in articles: print(item.article_number, item.name)
будут работать, потому что item
— это экземпляр OrderItem
, а не строка.
Как я могу запустить цикл в шаблоне примерно вот так?
{% for x in range(5) %}
<tr>
<td class="border-td ft06">something</td>
<td class="border-td ft06">something</td>
<td class="border-td">something</td>
<td class="border-td ft06 vert_bottom">something</td>
<td class="border-td ft06 vert_bottom">something</td>
<td class="border-td ft06 vert_bottom">something</td>
<td class="border-td ft06">something</td>
<td class="border-td ft06">something</td>
</tr>
{% endfor %}
В Django-шаблонах нет встроенного конструктора range
, поэтому вариант
django{% for x in range(5) %} … {% endfor %}
не сработает. Самый простой способ — передать в контекст ровно тот список или итератор, по которому вы хотите пройтись.
range
из вьюхиpython@status_required(0, 2) def invoice_pdf(request, id): # …ваша валидация заказа… order = get_object_or_404(Order, pk=id) if request.user.profile.status == 2 and order.owner != request.user: raise Http404("Заказ не найден") # Количество строк, которые вам нужно «пройти» rows_count = 5 context = { 'order': order, 'articles': order.order_items.all(), 'rows': range(rows_count), # <— вот он, передаём range } html_string = render_to_string('cart/contract/invoice.html', context) # …генерация и возврат PDF…
В шаблоне:
django{% for i in rows %} <tr> <td class="border-td ft06">something</td> <td class="border-td ft06">something</td> <td class="border-td">something</td> <td class="border-td ft06 vert_bottom">something</td> <td class="border-td ft06 vert_bottom">something</td> <td class="border-td ft06 vert_bottom">something</td> <td class="border-td ft06">something</td> <td class="border-td ft06">something</td> </tr> {% endfor %}
Здесь переменная i
будет принимать значения 0,1,2,3,4
, но вы её даже не используете (в теле цикла просто «печатаете» фиксированную разметку).
Если вы заранее не знаете, сколько строк нужно вывести, а, скажем, хотите добавить «дополнительные» пустые строки до фиксированной длины (например, вывести на странице всегда 10 строк, заполнив недостающие «пустыми»), то можно вычислять это тоже во вьюхе:
pythonMAX_ROWS = 10 existing = order.order_items.count() empty_rows = max(0, MAX_ROWS - existing) context = { 'articles': order.order_items.all(), 'empty_rows': range(empty_rows), }
В шаблоне:
django{# сначала ваши реальные позиции #} {% for item in articles %} <tr>…</tr> {% endfor %} {# затем «пустые» строки #} {% for _ in empty_rows %} <tr> <td class="border-td ft06"> </td> <td class="border-td ft06"> </td> <td class="border-td"> </td> <td class="border-td ft06 vert_bottom"> </td> <td class="border-td ft06 vert_bottom"> </td> <td class="border-td ft06 vert_bottom"> </td> <td class="border-td ft06"> </td> <td class="border-td ft06"> </td> </tr> {% endfor %}
Можно написать свой простой тег:
bash# myapp/templatetags/rangetags.py from django import template register = template.Library() @register.filter def times(value): """Возвращает range(int(value)).""" try: return range(int(value)) except (ValueError, TypeError): return []
И в шаблоне:
django{% load rangetags %} {% for _ in 5|times %} <tr>…</tr> {% endfor %}
Но это уже чуть более «продвинутое» решение.
Итог: самый простой путь — сформировать range(5)
(или любую другую коллекцию) во вьюхе и передать его в контекст. В шаблоне вы спокойно пишете {% for i in rows %}…{% endfor %}
.
вот смотри есть такая функция
def invoice_pdf(request, id):
# Если у пользователя статус 2, проверяем, что заказ с таким id ему принадлежит
if request.user.profile.status == 2:
order = get_object_or_404(Order, pk=id)
if order.owner != request.user:
# Выдаем 404, будто заказа нет
raise Http404("Заказ не найден")
text# Получаем все артикулы по заказу articles = order.order_items.all() total_price_for_user = order.order_items.annotate(total_price=F('quantity') * F('price')) queryset = get_order_items_with_weight(Order, OrderItem, ItemWeight, id) order_items = queryset[0] total_weight = queryset[1] result_total = [] for x in total_price_for_user: result_total.append(x.total_price) # 2) Рендерим HTML в строку context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': order_items, 'total_weight': total_weight, 'result_total': sum(result_total), 'rows': range(11), } html_string = render_to_string('cart/contract/invoice.html', context) base_url = settings.BASE_DIR # 3) Генерируем PDF pdf_file = HTML(string=html_string, base_url=base_url).write_pdf(stylesheets=[ CSS(string=''' @page { size: 960px 1200px; ; margin: 0 } html, body { margin: 0; padding: 0;} ''') ]) # 4) Возвращаем пользователю с заголовками response = HttpResponse(pdf_file, content_type='application/pdf') # response['Content-Disposition'] = f'inline; filename="invoice_{invoice.number}.pdf"' response['Content-Dispositioe'] = f'inline; filename="invoice.pdf"' return response
Проблема в том что если в таблице будет энное количество товаров, тогда оно не очень корректно переходит на следующую страницу при генерации pdf файла. Как это можно исправить?
Чтобы таблица корректно разбивается на страницы и шапка (header) повторяется, а строки не обрезаются посередине, нужно добавить соответствующие CSS-правила для печати. Для WeasyPrint (HTML→PDF) можно использовать примерно такой подход:
Повторение заголовка таблицы
Добавьте в CSS:
cssthead { display: table-header-group; } tfoot { display: table-footer-group; }
Это укажет движку PDF повторять <thead>
на каждой новой странице, если таблица «перетекает» через границу страницы.
Контроль разрыва внутри строк
Чтобы строка не разрывалась посередине:
csstr { page-break-inside: avoid; break-inside: avoid; }
Это постарается не разрывать отдельные <tr>
между страницами. Учтите, однако, что при очень больших строках (с высоким рядом ячеек или вложенными блоками) могут понадобиться дополнительные правки (например, разбить длинный контент внутри ячейки на несколько элементов).
Разрешить разбиение таблицы между группами строк
Можно дополнительно указать для <tbody>
:
csstbody { display: table-row-group; }
Обычно это по умолчанию, но если вы оборачиваете таблицу в контейнеры с display, убедитесь, что поведение table-layout не нарушается.
Пример включения CSS в ваш код
В месте, где вы генерируете PDF, расширьте стили:
pythonpdf_file = HTML(string=html_string, base_url=base_url).write_pdf(stylesheets=[ CSS(string=''' @page { size: 960px 1200px; margin: 0 } html, body { margin: 0; padding: 0; } table { border-collapse: collapse; width: 100%; } thead { display: table-header-group; } tfoot { display: table-footer-group; } tr { page-break-inside: avoid; break-inside: avoid; } td, th { /* ваши отступы/границы */ padding: 4px; border: 1px solid #000; } ''') ])
Если у вас уже есть CSS-файл, можно вынести эти правила туда и подключить его через CSS(filename=...)
.
Проверка структуры HTML
<table><thead>…</thead><tbody>…</tbody></table>
. Если вы не используете <thead>
, добавьте его для шапки.<tr>
или <td>
в лишние блоки, которые могут менять модель отображения таблицы, иначе движок может не распознать группу строк как единое целое.Разбиение очень больших таблиц
Если таблица слишком длинная и содержит группы строк, которые нужно разбить «по секциям», можно программно разделять: например, после N строк закрывать текущую таблицу и начинать новую, чтобы явно вставлять разделитель страниц. Но чаще достаточно CSS-подхода.
Диагностика
<td>
нет элементов с фиксированным высочай (например, больших картинок), которые тянут строку.<tr>
именно внутри <tbody>
.Пример шаблона
django<table> <thead> <tr> <th>Артикул</th> <th>Название</th> <th>Кол-во</th> <th>Цена</th> {# и т.д. #} </tr> </thead> <tbody> {% for item in articles %} <tr> <td>{{ item.article_number }}</td> <td>{{ item.name }}</td> <td>{{ item.quantity }}</td> <td>{{ item.price }}</td> </tr> {% endfor %} </tbody> </table>
И вышеописанные CSS-принципы позволят корректно разбить эту табличку по страницам и повторять шапку.
Итого: добавьте CSS-правила thead { display: table-header-group; }
, tr { page-break-inside: avoid; }
и убедитесь в правильной структуре <table>
. Это обычно решает проблему «некорректного» перехода таблицы на следующую страницу при генерации PDF.
Но у меня нет tbody. Вот мой html котоорый ненерируется в pdf файл. Проблема не только в таблице но и в контенте.
{% load static %}
{% load cart_extras %}
textH1, H2, H3, H4, H5, H6 {font-family: Arial, Helvetica, sans-serif;} @page { size: 960px 1200px; /* или landscape 1200*/ margin: 0; } body { margin: 35px; margin-left: 35px; } .border-td { border: 0.7px solid black; } .vert_bottom { vertical-align: bottom; } .between-tags { line-height: 12px; } tr { page-break-inside: avoid; break-inside: avoid; } </style> </head> <body> <div id="page1"> <div class="d-flex"> <p class="ft011">В отсутствии между сторонами заключенного договора поставки настоящий Счет является письменным <br> предложением (офертой) Поставщика заключить Договор. Оплата Покупателем настоящего счета признается<br> акцептом.</p> <img style="height: 50px; position: relative; top: -10px; left: 30px;" src="static/i/contract/code.png" alt="background image"/> </div> <br> <p class="ft011"><b>Цена, указанная в счете, действительна для Покупателя в течение 2 (двух) рабочих дней</b> с даты его выставления Поставщиком (срок оферты).<br/> В случае несвоевременной оплаты счета Поставщик в праве отказаться от заключения договора или предложит заключить договор на иных условиях.<br/> Товар должен быть передан Покупателю в течение 90 (девяноста) рабочих дней со дня поступления денежных средств на расчетный счет продавца.</p> <br> <p class="ft011">В случае покупки товара, входящего в национальную систему прослеживаемости, до оплаты счета Покупатель обязуется направить по ЭДО приглашение<br/> Поставщику. Поставщик обязуется принять указанное приглашение. Поставщик вправе без применения к нему каких – либо мер ответственности не передавать<br/> товар, входящий в национальную систему прослеживаемости, Покупателю, если приглашение по ЭДО не получено от Покупателя, вне зависимости от<br/> исполнения Покупателем обязательства по оплате указанного товара. Покупатель обязуется подписать документы, подтверждающие передачу прав на товар в<br/> системе Электронного документооборота.</p> <br> <div class="table-warning" style="border: 0.7px solid black;"> <center><b>Внимание!</b></center> <div class="mx-2"> <i class="ft011" style="text-decoration: underline;">При получении товара при себе необходимо иметь:</i> <p class="ft011"> • для представителя организации вне зависимости от наличия печати - паспорт и доверенность на получение товара, оформленную в соответствии со ст.<br> 185-187 ГКРФ и ФЗ N 402-ФЗ от 06.12.2011г. (рекомендованные формы М-2 и М-2а, либо доверенность в нотариальной или простой письменной форме)<br> • для руководителя организации - печать организации, (если печать организация не использует, то копия устава) заверенные копии приказа (или решения<br> о назначении), и паспорт<br> • для индивидуального предпринимателя - копию свидетельства о постановке на учет в качестве ИП, или лист записи о регистрации в качестве ИП,<br> паспорт<br> • для представителя индивидуального предпринимателя - паспорт, доверенность (рекомендованные формы М-2 и М2а, либо доверенность в нотариальной<br> форме)<br> • в случае покупки лицензии - два экземпляра подписанного уполномоченным лицом сублицензионного договора<br> <b>При оформлении доверенности на получение товара в строке «На получение от» указывать ООО "ЗЕ СОЛЮШЕНС"</b><br> Стороны установили, что Поставщик в счете на оплату может использовать факсимильное воспроизведение подписей должностных лиц и печати<br> Поставщика.<br> Стороны признают равную юридическую силу собственноручной подписи и факсимильной подписи Поставщика. Указанное условие не распространяется на<br> иные документы (УПД, счета-фактуры).<br> </p> </div> </div> <br/> <div> <p class="ft15" style="font-family: Arial, sans-serif; letter-spacing: 0; font-variant-numeric: tabular-nums; font-weight: 700;">Поставщик: ООО "ЗЕ СОЛЮШЕНС" ИНН: 9717173620</p> <p class="ft08">г Москва, ул 1-я Мытищинская, д 3 стр 1</p> </div> <br/> <div class="mb-2"> <p class="ft08"><b>Образец заполнения платежного поручения</b></p> <table class="border-td" style="border-collapse: collapse; width: 100%;"> <tr> <td class="border-td ft06">Получатель: ИНН 9717173620, КПП 771701001,<br>ООО "ЗЕ СОЛЮШЕНС"</td> <td class="border-td">Сч.№</td> <td class="border-td ft06 vert_bottom">40702810301100034793</td> </tr> <tr> <td class="border-td ft06">Банк получателя<br/>АО АЛЬФА-БАНК, Москва</td> <td rowspan="2" class="border-td ft06">БИК<br/>Сч.№</td> <td class="border-td ft06 vert_bottom">044525593<br/>30101810200000000593</td> </tr> </table> </div> <div> <p class="ft09 text-center"><b>Для автоматического зачисления денежных средств на баланс Покупателя необходимо указать номер Счета на<br/> оплату в графе «Назначение платежа». Отсутствие номера Счета на оплату в Платежном поручении может<br/> привести к увеличению срока обработки Заказа. После оплаты счета просим уведомить об этом Поставщика.</b></p> </div> <br> <center> <H3><b>Счет на оплату № {{ order_id }} от {{ order_created|date:"d.m.Y" }}</b></H3> </center> <br> <div> <!-- <H4 class="between-tags"><b>Покупатель: ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "АВТОПАРТС" ИНН: 7717750560</b></H4> <p class="ft08"> 129515, Г.МОСКВА, ВН.ТЕР.Г. МУНИЦИПАЛЬНЫЙ ОКРУГ ОСТАНКИНСКИЙ, УЛ АКАДЕМИКА КОРОЛЕВА, Д. 5, ПОМЕЩ. 14П .<br/> Телефон: +7(963)-975-7010 </p> </div>
-->
</html>text<div> <H4 class="between-tags"><b>Покупатель: {{ client.profile.name }}</b></H4> <p class="ft08"> {{ client.profile.value_with_postal_code }}.<br/> </p> </div> <br/> <div> <p class="ft08">Счет действителен в течение 2 рабочих дней. Оплату счета Вы можете проверить на сайте zparts.ru</p> <table class="border-td" style="border-collapse: collapse; width: 100%;"> <tr> <th class="border-td">Артикул</th> <th class="border-td">Бренд</th> <th class="border-td">Наименование</th> <th class="border-td">дней</th> <th class="border-td">Шт</th> <th class="border-td">Вес</th> <th class="border-td">Цена</th> <th class="border-td">Стоимость</th> </tr> {% for x in articles %} <tr> <td class="border-td ft06">{{ x.article_number }}</td> <td class="border-td">{{ x.manufacturer }}</td> <td class="border-td ft06 vert_bottom">{{ x.name }}</td> <td class="border-td ft06 vert_bottom">{{ x.delivery }}</td> <td class="border-td ft06 vert_bottom">{{ x.quantity }}</td> {% if x.average_weight %} <td class="border-td ft06">{{ x.weight|default:x.average_weight }}*</td> {% else %} <td class="border-td ft06">{{ x.weight }}</td> {% endif %} <td class="border-td ft06 vert_bottom">{{ x.price }}</td> <td class="border-td ft06">{{ x.price|multiple:x.quantity|format_price }}</td> </tr> {% endfor %} {% for x in rows %} <tr> <td class="border-td ft06">something</td> <td class="border-td ft06">something</td> <td class="border-td">something</td> <td class="border-td ft06 vert_bottom">something</td> <td class="border-td ft06 vert_bottom">something</td> <td class="border-td ft06 vert_bottom">something</td> <td class="border-td ft06">something</td> <td class="border-td ft06">something</td> </tr> {% endfor %} </table> <p class="ft06 text-end"><b class="ft06 me-5">Всего: {{ total_weight }}кг.</b><b>Итого: {{ result_total }} руб.</b></p> </div> <!-- <p class="ft011" style="text-decoration: underline;">Всего наименований 2, на сумму 201 599,00 руб.</p> --> <br/> <div class="d-flex justify-content-center"> <p class="ft011 me-5"><b> Директор Филиал<br> Центральный ОООО<br> "ЗЕ СОЛЮШЕНС" </b></p> <p class="me-5">___________________________</p><br> <p class="ft011 me-5 mt-3"><b>Ципельников М.В</b></p> <img src="static/i/contract/shtamp.png" alt=""> </div> </div> <br> <br> <br> <div id="page2" style="position: relative; top: 30px;"> <div class="table-warning" style="border: 0.7px solid black;"> <center><p class="ft06"><b>Присоединяйтесь к Электронному документообороту</b></p></center> <p class="ft04 mx-2"> Предлагаем вам получать счета, счета-фактуры, акты и другие документы от ООО «ЗЕ СОЛЮШЕНС» в<br/> электронном виде, присоединившись к Электронному документообороту (ЭДО).<br/><br/> 1. Войдите в вашу систему ЭДО.<br/> 2. Найдите в системе участника ЭДО – ООО «» (Оператор ЭДО «Контур.Диадок» (АО «ПФ «СКБ<br/> Контур»), ID участника ЭДО: -, ИНН: 9717173620, КПП:<br/> 783450001) и примите направленное вам приглашение.<br/><br/> 3. Обменивайтесь документами, выбрав в системе Центральный филиал ООО «ЗЕ СОЛЮШЕНС» (ID участника<br/> ЭДО: -, ИНН: 9717173620, КПП: 771701001).<br/><br/> </p> </div> <br> </div> </body>
Ваша проблема на самом деле состоит из двух частей:
page-break-inside: avoid;
или повтора шапки, а также некорректно делит длинный контент между страницами.base_url = settings.BASE_DIR
ему ничего не даёт.WeasyPrint разбирает HTML-поток, и чтобы он правильно повторял <thead>
на каждой странице и не резал ряды на части, ваша таблица должна иметь <thead>
и <tbody>
:
django<table class="border-td" style="border-collapse: collapse; width: 100%;"> <thead> <tr> <th class="border-td">Артикул</th> <th class="border-td">Бренд</th> <th class="border-td">Наименование</th> <th class="border-td">дней</th> <th class="border-td">Шт</th> <th class="border-td">Вес</th> <th class="border-td">Цена</th> <th class="border-td">Стоимость</th> </tr> </thead> <tbody> {% for x in articles %} <tr> <td class="border-td ft06">{{ x.article_number }}</td> <!-- ... остальные ячейки ... --> </tr> {% endfor %} {% for _ in rows %} <tr> <td class="border-td ft06"> </td> <!-- пустые строки, чтобы выровнять по сетке --> </tr> {% endfor %} </tbody> </table>
И в вашем встроенном CSS добавить (или убедиться, что уже есть):
cssthead { display: table-header-group; /* повторять шапку на новых страницах */ } tbody { display: table-row-group; } tr { page-break-inside: avoid; break-inside: avoid; }
МыasyPrint зачастую отказывается подтягивать удалённые стили по HTTPS (CDN-ссылки Bootstrap), поэтому либо:
<style>
вашего шаблона,base_url
Вместо
pythonbase_url = settings.BASE_DIR
лучше передать WeasyPrint абсолютный URL к корню вашего статического контента. Например:
pythonfrom django.contrib.staticfiles.storage import staticfiles_storage # ... base_url = request.build_absolute_uri('/') # или # base_url = staticfiles_storage.url('') # если настроен STATICFILES_STORAGE
или даже
pythonbase_url = settings.STATIC_ROOT # если это абсолютный путь на диске
— так картинка <img src="{% static 'i/contract/code.png' %}">
будет найдена.
В вашем примере опечатка:
pythonresponse['Content-Dispositioe'] = 'inline; filename="invoice.pdf"'
должно быть
pythonresponse['Content-Disposition'] = 'inline; filename="invoice.pdf"'
python@status_required(0, 2) def invoice_pdf(request, id): order = get_object_or_404(Order, pk=id) if request.user.profile.status == 2 and order.owner != request.user: raise Http404("Заказ не найден") articles = order.order_items.select_related('seller').all() total_price = order.order_items.aggregate( total=Sum(F('quantity') * F('price')) )['total'] or 0 queryset = get_order_items_with_weight(Order, OrderItem, ItemWeight, id) order_items, total_weight = queryset context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': order_items, 'total_weight': total_weight, 'result_total': total_price, 'rows': range(11), } html_string = render_to_string('cart/contract/invoice.html', context) # Абсолютный путь к статике base_url = request.build_absolute_uri('/') css = CSS(string=''' @page { size: 960px 1200px; margin: 0 } html,body { margin: 35px; padding: 0; } table { border-collapse: collapse; width: 100%; } thead { display: table-header-group; } tbody { display: table-row-group; } tr { page-break-inside: avoid; break-inside: avoid; } td,th { border: 0.7px solid black; padding: 4px; } /* остальные ваши .ftXX правила… */ ''') pdf_file = HTML(string=html_string, base_url=base_url).write_pdf(stylesheets=[css]) response = HttpResponse(pdf_file, content_type='application/pdf') response['Content-Disposition'] = 'inline; filename="invoice.pdf"' return response
<thead>
/<tbody>
и CSS-правилами для печати.base_url
, которое указывает на корень сайта/статических файлов.После этих правок и ваша табличка, и весь остальной контент (абзацы, картинки, разделители страниц) в WeasyPrint будут отображаться и разбиваться на страницы корректно.
А как сделать чтобы был чуть чуть padding сверху на новой странице?
Чтобы добавить отступ сверху на каждой новой странице (или хотя бы на второй и последующих), есть два простых подхода.
@page
Самый «чистый» способ — задать больший верхний отступ во всей странице через правило @page
. Например, сейчас у вас:
css@page { size: 960px 1200px; margin: 0; } body { margin: 35px; margin-left: 35px; }
Можно переписать на:
css@page { size: 960px 1200px; margin-top: 50px; /* <-- увеличенный верхний */ margin-right: 35px; margin-bottom: 35px; margin-left: 35px; } body { margin: 0; /* обнуляем, т.к. все через @page */ }
Теперь на всех страницах сверху будет по 50px свободного пространства.
page2
)Если вам нужно, чтобы первая страница была «плотной» сверху, а небольшой отступ появлялся только при переходе на второй и последующие, можно:
page-break-before
.#page2
добавить margin-top
.Например, в вашем шаблоне:
html<div id="page1"> …весь контент первой страницы… </div> <div id="page2" style=" page-break-before: always; margin-top: 20px; /* <-- вот этот отступ сверху на новой странице */ "> …контент второй страницы… </div>
WeasyPrint всегда вставит разрыв перед #page2
, а затем сдвинет его содержимое вниз на 20px.
@page { margin-top: XXpx; }
.page-break-before: always;
и добавляйте margin-top
на самом блоке, с которого начинается новая страница.У меня есть вот такая функция которую я использую.
@status_required(0, 2)
def invoice_pdf(request, id):
# Если у пользователя статус 2, проверяем, что заказ с таким id ему принадлежит
if request.user.profile.status == 2:
order = get_object_or_404(Order, pk=id)
if order.owner != request.user:
# Выдаем 404, будто заказа нет
raise Http404("Заказ не найден")
else:
order = get_object_or_404(Order, pk=id)
text# Получаем все артикулы по заказу articles = order.order_items.all() total_price_for_user = order.order_items.annotate(total_price=F('quantity') * F('price')) queryset = get_order_items_with_weight(Order, OrderItem, ItemWeight, id) order_items = queryset[0] total_weight = queryset[1] result_total = [] for x in total_price_for_user: result_total.append(x.total_price) # 2) Рендерим HTML в строку context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': order_items, 'total_weight': total_weight, 'result_total': sum(result_total), } html_string = render_to_string('cart/contract/invoice.html', context) base_url = settings.BASE_DIR # 3) Генерируем PDF css = CSS(string=''' @page { size: 960px 1200px; margin: 0 } html,body { margin: 35px; padding: 0; } table { border-collapse: collapse; width: 100%; } thead { display: table-header-group; } tbody { display: table-row-group; } tr { page-break-inside: avoid; break-inside: avoid; } td,th { border: 0.7px solid black; padding: 4px; } /* остальные ваши .ftXX правила… */ ''') pdf_file = HTML(string=html_string, base_url=base_url).write_pdf(stylesheets=[css]) # 4) Возвращаем пользователю с заголовками response = HttpResponse(pdf_file, content_type='application/pdf') # response['Content-Disposition'] = f'inline; filename="invoice_{invoice.number}.pdf"' response['Content-Disposition'] = 'inline; filename="invoice.pdf"' return response
Сейчас у меня есть и вот такая функция тоже
@login_required
def order_confirm(request, id):
if request.method == 'POST':
print('-------- ORDER CONFIRM --------')
ids = json.loads(request.body.decode('utf-8'))
ids = list(map(int, ids))
# user_email = Order.objects.get(id=id)
order = Order.objects.get(id=id)
text######################33 existing_ids = OrderItem.objects.filter(order__id=id, id__in=ids, seller=request.user).update(status=1) not_existing_ids = OrderItem.objects.filter(order__id=id, seller=request.user).exclude(id__in=ids).update(status=2) ###################### user_order_confirm_rating(Order, OrderItem, UserRating, id, request.user.id, existing_ids, not_existing_ids) cos = check_orderitems_status(OrderItem, Order, id) # id - id of Ord if cos == 1: OrderStatusHistory.objects.create(user=request.user, order=order, status=1) elif cos == 2: OrderStatusHistory.objects.create(user=request.user, order=order, status=2) elif cos == 3: OrderStatusHistory.objects.create(user=request.user, order=order, status=3) # check_orderitems_status(OrderItem, Order, OrderStatusHistory, id) # id - id of Ord items_status_1 = OrderItem.objects.filter(order__id=id, seller=request.user, status=1) items_status_2 = OrderItem.objects.filter(order__id=id, seller=request.user, status=2) # my_settings = My_settings.objects.get(name='settings') requisites = get_setting('requisites') print('-----------------------') print(requisites, ' <---- requesites') print('-----------------------') # order_items = OrderItem.objects.filter(order__id=id, seller=request.user) order_items = OrderItem.objects.filter(order__id=id) # total = order_items.annotate(total_price=F('quantity') * F('price')) total = order_items.annotate( effective_price=Case( When(price_partner__isnull=False, then=F('price_partner')), When(price_partner__isnull=True, then=F('price')), output_field=DecimalField() ), total_price=F('quantity') * F('effective_price') ) total_price_for_user = order_items.annotate(total_price=F('quantity') * F('price')) # Sending for user result_total = [] for x in total_price_for_user: result_total.append(x.total_price) if items_status_1.count() == 0: text = 'Поставка артикулов не была подтверждена поставщиком, но вы можете выбрать требуемые артикулы у других' elif items_status_2.count() > 0: text = 'Поставка следующих артикулов не была подтверждена поставщиком, но вы можете выбрать требуемые артикулы у других' else: text = None context = { 'items_status_1': items_status_1.count(), 'items': order_items, 'order': order, 'result_total': sum(result_total), 'requisites': requisites, 'text': text, 'SITE_URL': settings.SITE_URL} check = get_order_items_status(OrderItem, id) if check: msg_to_user_confirmed = render_to_string('emails/confirmed_by_manufacturer.html', context) confirm_email.delay(text, order.owner.email, msg_to_user_confirmed) return JsonResponse(status=200, data={'success': 1, 'url_redirect': f'/cart/order/{id}/confirm/'})
Мне в order_confirm нужно использовать invoice_pdf для отправки поо email сгенерированного pdf файла. Как так сделать я не знаю, такое возможно?
Вам не надо пытаться «вызывать» сами представления друг из друга, а лучше вынести логику генерации PDF в утилитарную функцию, которую сможете дергать и из invoice_pdf
, и из order_confirm
.
Например, в utils.py
вашего приложения:
python# cart/utils.py from django.template.loader import render_to_string from weasyprint import HTML, CSS from django.conf import settings def generate_invoice_pdf(order, context_extra=None): """ Генерирует PDF по заказу и возвращает байты файла. `context_extra` — опциональный dict, который будет слит с базовым контекстом. """ # Собираем базовый контекст base_context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': order.order_items.all(), # ... другие данные: вес, суммы, и т.п. } if context_extra: base_context.update(context_extra) html_string = render_to_string('cart/contract/invoice.html', base_context) # Путь к статике (чтобы картинки и CSS подтянулись) base_url = settings.STATIC_ROOT # или request.build_absolute_uri('/') если у вас настроен css = CSS(string=''' @page { size: 960px 1200px; margin: 35px; } html, body { margin: 0; padding: 0; } table { border-collapse: collapse; width: 100%; } thead { display: table-header-group; } tbody { display: table-row-group; } tr { page-break-inside: avoid; break-inside: avoid; } td,th { border: 0.7px solid black; padding: 4px; } /* ... ваш остальной CSS ... */ ''') return HTML(string=html_string, base_url=base_url).write_pdf(stylesheets=[css])
invoice_pdf
, и в order_confirm
invoice_pdf
:pythonfrom .utils import generate_invoice_pdf @status_required(0, 2) def invoice_pdf(request, id): order = get_object_or_404(Order, pk=id) # ... проверка статуса/принадлежности ... pdf_bytes = generate_invoice_pdf(order) return HttpResponse(pdf_bytes, content_type='application/pdf', headers={ 'Content-Disposition': 'inline; filename="invoice.pdf"' })
order_confirm
, чтобы отправить PDF на почту:pythonfrom django.core.mail import EmailMessage from .utils import generate_invoice_pdf @login_required def order_confirm(request, id): # ... ваша логика подтверждения ... # после того, как вы обновили статусы, высчитали суммы и т.п. order = Order.objects.get(id=id) # генерируем PDF pdf_bytes = generate_invoice_pdf(order) # готовим письмо subject = f'Ваш счет по заказу №{order.id}' body = render_to_string('emails/confirmed_by_manufacturer.html', { 'items_status_1': items_status_1.count(), 'items': order_items, 'order': order, 'result_total': sum(result_total), 'requisites': requisites, 'text': text, 'SITE_URL': settings.SITE_URL }) email = EmailMessage( subject=subject, body=body, from_email=settings.DEFAULT_FROM_EMAIL, to=[order.owner.email], ) # прикрепляем PDF email.attach(f'invoice_{order.id}.pdf', pdf_bytes, 'application/pdf') # отправляем (delay, если хотите через Celery) email.send() return JsonResponse({'success': 1, 'url_redirect': f'/cart/order/{id}/confirm/'})
SMTP vs Celery
Если вы уже используете confirm_email.delay(...)
— вы можете передавать туда PDF как bytes
или сохранить его временно в файловую систему/Redis, и в задаче присоединять при отправке.
base_url
для картинок и CSS
В примере я использовал settings.STATIC_ROOT
. Если ваши шаблоны ссылаются на {% static '...' %}
, то убедитесь, что WeasyPrint может прочитать эти файлы. Альтернативно, можно смапить base_url = request.build_absolute_uri('/')
.
Рефакторинг контекста
Если контекст письма и контекст для PDF пересекаются, тоже можно вынести общую часть в утилитную функцию и дополнять по месту.
Такой подход позволяет единообразно генерировать PDF и легко прикреплять его к любому письму, без дублирования кода.
Не совсем понял как передать pdf файл в order_confirm
@login_required
def order_confirm(request, id):
if request.method == 'POST':
print('-------- ORDER CONFIRM --------')
ids = json.loads(request.body.decode('utf-8'))
ids = list(map(int, ids))
# user_email = Order.objects.get(id=id)
order = Order.objects.get(id=id)
text######################33 existing_ids = OrderItem.objects.filter(order__id=id, id__in=ids, seller=request.user).update(status=1) not_existing_ids = OrderItem.objects.filter(order__id=id, seller=request.user).exclude(id__in=ids).update(status=2) ###################### user_order_confirm_rating(Order, OrderItem, UserRating, id, request.user.id, existing_ids, not_existing_ids) cos = check_orderitems_status(OrderItem, Order, id) # id - id of Ord if cos == 1: OrderStatusHistory.objects.create(user=request.user, order=order, status=1) elif cos == 2: OrderStatusHistory.objects.create(user=request.user, order=order, status=2) elif cos == 3: OrderStatusHistory.objects.create(user=request.user, order=order, status=3) # check_orderitems_status(OrderItem, Order, OrderStatusHistory, id) # id - id of Ord items_status_1 = OrderItem.objects.filter(order__id=id, seller=request.user, status=1) items_status_2 = OrderItem.objects.filter(order__id=id, seller=request.user, status=2) # my_settings = My_settings.objects.get(name='settings') requisites = get_setting('requisites') print('-----------------------') print(requisites, ' <---- requesites') print('-----------------------') # order_items = OrderItem.objects.filter(order__id=id, seller=request.user) order_items = OrderItem.objects.filter(order__id=id) # total = order_items.annotate(total_price=F('quantity') * F('price')) total = order_items.annotate( effective_price=Case( When(price_partner__isnull=False, then=F('price_partner')), When(price_partner__isnull=True, then=F('price')), output_field=DecimalField() ), total_price=F('quantity') * F('effective_price') ) total_price_for_user = order_items.annotate(total_price=F('quantity') * F('price')) # Sending for user result_total = [] for x in total_price_for_user: result_total.append(x.total_price) if items_status_1.count() == 0: text = 'Поставка артикулов не была подтверждена поставщиком, но вы можете выбрать требуемые артикулы у других' elif items_status_2.count() > 0: text = 'Поставка следующих артикулов не была подтверждена поставщиком, но вы можете выбрать требуемые артикулы у других' else: text = None context = { 'items_status_1': items_status_1.count(), 'items': order_items, 'order': order, 'result_total': sum(result_total), 'requisites': requisites, 'text': text, 'SITE_URL': settings.SITE_URL} check = get_order_items_status(OrderItem, id) if check: msg_to_user_confirmed = render_to_string('emails/confirmed_by_manufacturer.html', context) confirm_email.delay(text, order.owner.email, msg_to_user_confirmed) return JsonResponse(status=200, data={'success': 1, 'url_redirect': f'/cart/order/{id}/confirm/'})
И вот мо task confirm_email
@shared_task
def confirm_email(text, user_email, msg_to_user_confirmed):
send_mail(
"Подтвержедние заказа поставщиком",
text,
"[email protected]",
[user_email],
html_message=msg_to_user_confirmed,
fail_silently=False,
)
Как мне заюзать generate_invoice_pdf чтобы отправить файл на почту?
Чтобы прикрепить сгенерированный PDF к письму в order_confirm
, нужно:
generate_invoice_pdf
)order_confirm
вызвать эту функцию, получить bytes
— и прикрепить их к письму через EmailMessage.attach()
confirm_email
, либо отправить прямо в order_confirm
.Вынесите логику из invoice_pdf
в cart/utils.py
:
python# cart/utils.py from django.template.loader import render_to_string from weasyprint import HTML, CSS from django.conf import settings def generate_invoice_pdf(order, extra_context=None): ctx = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': order.order_items.all(), # сюда можно добавить вес/итоговую сумму и т.п. } if extra_context: ctx.update(extra_context) html = render_to_string('cart/contract/invoice.html', ctx) # если ваши статики лежат в STATIC_ROOT: base_url = settings.STATIC_ROOT css = CSS(string=''' @page { size: 960px 1200px; margin: 35px; } table { border-collapse: collapse; width: 100%; } thead { display: table-header-group; } tr { page-break-inside: avoid; } td,th { border: 0.7px solid black; padding: 4px; } ''') return HTML(string=html, base_url=base_url).write_pdf(stylesheets=[css])
invoice_pdf
Теперь invoice_pdf
упростится:
pythonfrom .utils import generate_invoice_pdf @status_required(0, 2) def invoice_pdf(request, id): order = get_object_or_404(Order, pk=id) if request.user.profile.status == 2 and order.owner != request.user: raise Http404 pdf_bytes = generate_invoice_pdf(order) return HttpResponse(pdf_bytes, content_type='application/pdf', headers={'Content-Disposition': 'inline; filename="invoice.pdf"'})
order_confirm
В вашем order_confirm
импортируйте generate_invoice_pdf
и Django-класс EmailMessage
:
pythonfrom django.core.mail import EmailMessage from .utils import generate_invoice_pdf
И после того, как вы отработали статусы и собрали контекст для письма, добавьте:
python# …ваша текущая логика формирования context и text… # генерируем PDF pdf_bytes = generate_invoice_pdf(order, extra_context={ 'total_weight': total_weight, 'result_total': sum(result_total), # если нужно, передайте сюда любой дополнительный контекст }) # готовим письмо subject = f'Счет №{order.id} по вашему заказу' html_body = render_to_string('emails/confirmed_by_manufacturer.html', context) email = EmailMessage( subject=subject, body=text or '', # plain-text часть from_email=settings.DEFAULT_FROM_EMAIL, to=[order.owner.email], ) email.content_subtype = 'html' # чтобы html_body показался в теле письма email.body = html_body # прикрепляем PDF email.attach(f'invoice_{order.id}.pdf', pdf_bytes, 'application/pdf') # отправляем синхронно или через Celery email.send(fail_silently=False)
Если вы хотите делать это через Celery-задачу confirm_email
, расширьте её сигнатуру:
python# tasks.py @shared_task def confirm_email(text, user_email, html_body, pdf_bytes, filename): email = EmailMessage( subject="Подтверждение заказа поставщиком", body=html_body, from_email="[email protected]", to=[user_email], ) email.content_subtype = 'html' email.attach(filename, pdf_bytes, 'application/pdf') email.send(fail_silently=False)
И в order_confirm
вызывайте:
pythonconfirm_email.delay( text, order.owner.email, msg_to_user_confirmed, pdf_bytes, f'invoice_{order.id}.pdf' )
generate_invoice_pdf
возвращает bytes
PDF.order_confirm
вы вызываете эту функцию, получаете pdf_bytes
и через EmailMessage.attach(...)
прикрепляете его к письму.Я использую send_mail для оотправки письма. Поэтому как мне в ней отправить pdf_bytes?
@shared_task
def confirm_email(text, user_email, msg_to_user_confirmed, pdf_bytes, filename):
send_mail(
"Подтвержедние заказа поставщиком",
text,
"[email protected]",
[user_email],
html_message=msg_to_user_confirmed,
fail_silently=False,
)
Функция send_mail
из django.core.mail
не умеет прикреплять файлы, поэтому вам нужно перейти на более «низкоуровневый» класс EmailMessage
(или его подкласс EmailMultiAlternatives
), который позволяет добавлять вложения.
Вот пример, как можно переписать вашу задачу confirm_email
, чтобы вместе с HTML-сообщением отправлять и PDF:
pythonfrom django.core.mail import EmailMessage from django.conf import settings @shared_task def confirm_email(text, user_email, html_message, pdf_bytes, filename): # Создаём EmailMessage email = EmailMessage( subject="Подтверждение заказа поставщиком", body=text, # если нужен plain-text, иначе можно оставить пустым from_email=settings.DEFAULT_FROM_EMAIL, to=[user_email], ) # Указываем, что тело — HTML email.content_subtype = 'html' # Заменяем тело на html_message email.body = html_message # Прикрепляем PDF email.attach( filename, # например: "invoice_123.pdf" pdf_bytes, # байты вашего PDF 'application/pdf' ) # Отправляем email.send(fail_silently=False)
EmailMessage
вместо send_mail
.EmailMessage
, где можно задать subject
, body
, from_email
, to
.email.content_subtype = 'html'
говорим, что body
— HTML.email.attach(name, content, mimetype)
— прикрепляет файл.email.send()
окончательно шлёт письмо.order_confirm
В order_confirm
вы по-прежнему собираете pdf_bytes = generate_invoice_pdf(...)
и вызываете:
python# после формирования context, text и pdf_bytes msg_to_user_confirmed = render_to_string('emails/confirmed_by_manufacturer.html', context) confirm_email.delay( text, order.owner.email, msg_to_user_confirmed, pdf_bytes, f'invoice_{order.id}.pdf' )
Таким образом, в задаче confirm_email
у вас уже есть и HTML-шаблон письма, и байты PDF, и имя файла — и вы спокойно шлёте их в одном письме.
А как сделать чтобы этот файл на почте был просто ссылкой?
Чтобы вместо прикреплённого файла в письме был некий «прямой» линк на PDF, вам нужно:
generate_invoice_pdf
).MEDIA_ROOT
/MEDIA_URL
(через Django default_storage
),<a href="cid:…">
или вложения просто вставить <a href="{{ pdf_url }}">Ссылка на счёт в PDF</a>
.MEDIA_ROOT
pythonfrom django.core.files.storage import default_storage from django.core.files.base import ContentFile from django.urls import reverse from django.conf import settings def save_invoice_pdf_and_get_url(order): # 1) Сгенерировать PDF pdf_bytes = generate_invoice_pdf(order) # 2) Выложить в MEDIA_ROOT, например: filename = f'invoices/invoice_{order.id}.pdf' # ContentFile позволяет сохранить байты как файл file_path = default_storage.save(filename, ContentFile(pdf_bytes)) # 3) Построить URL # Если MEDIA_URL настроен как '/media/', то: return settings.MEDIA_URL + file_path
Убедитесь, что в
settings.py
вы прописали:pythonMEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/'
и в
urls.py
раздаётеMEDIA_URL
в режиме отладки:pythonfrom django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ... ваши urlpatterns ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
order_confirm
pythonfrom .utils import save_invoice_pdf_and_get_url @login_required def order_confirm(request, id): # ... ваша логика статусов, сбор данных и контекста ... # сохраняем PDF и получаем ссылку pdf_url = save_invoice_pdf_and_get_url(order) # рендерим html-письмо, передавая ссылку context['pdf_url'] = request.build_absolute_uri(pdf_url) html_message = render_to_string('emails/confirmed_by_manufacturer.html', context) # отправляем через send_mail, вставляя ссылку в html_message send_mail( subject="Подтверждение заказа поставщиком", message=text, # plain-text часть письма from_email="[email protected]", recipient_list=[order.owner.email], html_message=html_message, fail_silently=False, ) return JsonResponse({...})
django{# emails/confirmed_by_manufacturer.html #} <p>Здравствуйте!</p> {% if text %} <p>{{ text }}</p> {% endif %} <p>Ваш счёт доступен по ссылке:</p> <p><a href="{{ pdf_url }}">Открыть счёт в формате PDF</a></p> <p>Спасибо за работу с нами!</p>
Если вместо default_storage
вы пользуетесь, например, Amazon S3 (через django-storages
), то в save_invoice_pdf_and_get_url
примерно так:
python# django-storages настроен, default_storage — S3Boto3Storage def save_invoice_pdf_and_get_url(order): pdf_bytes = generate_invoice_pdf(order) filename = f'invoices/invoice_{order.id}.pdf' default_storage.save(filename, ContentFile(pdf_bytes)) # Получаем публичный (или подписанный) URL return default_storage.url(filename)
В остальном — точно так же передаёте pdf_url
в шаблон и вставляете ссылку.
Таким образом вы не вкладываете сам PDF, а даёте пользователю прямой линк, по которому он может его скачать или открыть.
Так значит не сделать его ссылкой не сохранняя файла а просто прикрепляя?
К сожалению, в стандарте SMTP/HTML-письма нет механизма, который позволил бы в теле письма сделать <a href="…">
и ссылаться на само вложение (attachment) — клиенты просто не умеют так: они показывают вложение под письмом, но не «хостят» его по URL.
Есть два «костыля»:
Data-URI
Можно прямо в теге <a>
засунуть Base64-версию PDF:
html<a href="data:application/pdf;base64,{{ pdf_bytes|b64encode }}" download="invoice.pdf"> Скачать счёт </a>
Но:
Встроить HTML attachment как cid:
Можно прикрепить PDF как inline-attachment с Content-ID, а в письме вставить <a href="cid:invoice.pdf">ссылка</a>
. Но:
Единственно надёжный способ дать пользователю ссылку на скачивание PDF — сохранить его в каком-то веб-доступном хранилище (локальный MEDIA_ROOT
или S3/облако) и в тело письма вставить обычный <a href="https://…/invoice_123.pdf">Скачать счёт</a>
. Тогда:
Если загрузка «на диск/в S3» для вас неприемлема, придётся мириться с тем, что вложение отобразится под письмом, а внутри текста будет просто текстовый призыв типа «смотри вложение».
Почему на какоом то девайсе у моего знакомого некоторые данные в даблице сьезжают из ячеек, и как это исправить?
def generate_invoice_pdf(order, context_extra=None):
"""
Генерирует PDF по заказу и возвращает байты файла.
context_extra
— опциональный dict, который будет слит с базовым контекстом.
"""
# articles = order.order_items.all()
texttotal_price_for_user = order.order_items.annotate(total_price=F('quantity') * F('price')) # queryset = get_order_items_with_weight(Order, OrderItem, ItemWeight, id) queryset = get_order_items_with_weight(Order, OrderItem, ItemWeight, order.id) order_items = queryset[0] total_weight = queryset[1] result_total = [] for x in total_price_for_user: result_total.append(x.total_price) # Собираем базовый контекст base_context = { 'order_id': order.id, 'order_created': order.created, 'client': order.owner, 'articles': order_items, 'total_weight': total_weight, 'result_total': sum(result_total), } if context_extra: base_context.update(context_extra) html_string = render_to_string('cart/contract/invoice.html', base_context) # Путь к статике (чтобы картинки и CSS подтянулись) base_url = settings.BASE_DIR css = CSS(string=''' @page { size: 960px 1200px; margin: 35px; } html, body { margin: 0; padding: 0; } table { border-collapse: collapse; width: 100%; } thead { display: table-header-group; } tbody { display: table-row-group; } tr { page-break-inside: avoid; break-inside: avoid; } td,th { border: 0.7px solid black; padding: 4px; } ''') return HTML(string=html_string, base_url=base_url).write_pdf(stylesheets=[css])
Сдвиг ячеек в PDF-таблице на разных «девайсах» (то есть в разных PDF-ридерах) обычно вызван тем, что:
Чтобы придать вашей таблице более жёсткую, однозначную разметку, попробуйте добавить в ваш CSS пару правил:
csstable { border-collapse: collapse; width: 100%; table-layout: fixed; /* фиксируем схему расчёта ширин колонок */ } th, td { /* задаём в процентах или конкретных единицах ширины колонок: */ /* можно выставить в шаблоне через <colgroup> или прямо здесь */ /* Например: */ /* width: 12.5%; */ word-wrap: break-word; /* перенос слов, чтобы контент не вылазил */ white-space: normal; /* сбрасываем возможный nowrap */ vertical-align: top; /* привязываем содержимое к верху ячейки */ padding: 4px; /* ваш стандартный внутренний отступ */ box-sizing: border-box; /* чтобы padding не «ломал» ширину */ border: 0.7px solid black; }
Добавьте table-layout: fixed;
— тогда браузер/WeasyPrint рассчитает ширины колонок один раз по заданным width
(в <col>
или в th, td
), и уже не будет «растягивать» их по содержимому.
Либо объявите <colgroup>
в вашем шаблоне, чтобы жестко распределить ширины:
html<table> <colgroup> <col style="width: 12%;" /> <col style="width: 12%;" /> <col style="width: 24%;" /> <!-- и т.д., пока в сумме не будет 100% --> </colgroup> <thead>…</thead> <tbody>…</tbody> </table>
Перенос слов (word-wrap: break-word;
) предотвратит «выдавливание» содержимого за пределы ячейки, а white-space: normal;
снимет любые случайные nowrap
.
vertical-align: top;
гарантирует, что если ячейка станет выше, её содержимое всегда начнёт «сверху», а не в центре или внизу.
box-sizing: border-box;
учтёт padding
и border
внутри общей ширины ячейки, чтобы итоговая сетка не «поплыла» из-за разных вычислений.
pythoncss = CSS(string=''' @page { size: 960px 1200px; margin: 35px; } html, body { margin: 0; padding: 0; } table { border-collapse: collapse; width: 100%; table-layout: fixed; } thead { display: table-header-group; } tbody { display: table-row-group; } tr { page-break-inside: avoid; break-inside: avoid; } th, td { box-sizing: border-box; padding: 4px; border: 0.7px solid black; word-wrap: break-word; white-space: normal; vertical-align: top; } ''')
Добавив эти правила, вы добьётесь одинакового, предсказуемого при любом рендере расположения ячеек и предотвратите их «ползучесть» в разных PDF-ридерах.
Смотри, я сгенерировал из html файла pdf файл. На моём девайсе таблица выглядит ровно, и на всех других этот pdf файл выглядит тоже ровно а вот у моего друга таблица в этом pdf файле выглядит криво. Почему?
Поведение, когда один и тот же PDF «плывёт» в разных вьюверах, — не редкость. Причины обычно кроются в том, что PDF не содержит всего необходимого для абсолютно одинакового рендеринга:
Разные шрифты на клиенте.
Вы используете font-family: TimesNewRomanPSMT;
и Arial/Bootstrap-шрифты, но WeasyPrint по умолчанию встраивает далеко не все. Если на машине друга нет точно такого же шрифта, система подставит наиболее близкий, и размеры букв/интервалов поменяются — табличные ячейки "поедут".
Разная реализация PDF-движка.
Одни мобильные или десктоп-ридеры могут чуть иначе интерпретировать метрику линий, box-sizing
и table-layout
. МыasyPrint в момент генерации считает одно, а потом ваша ОС может переинтерпретировать.
Отсутствие жёстко заданных ширин.
Если вы даёте таблице width: 100%
, но не фиксируете колонки, разные вьюверы могут по-разному делить остаток между ними.
Разный DPI экрана/печати.
Некоторые PDF-ридеры по умолчанию масштабируют страницу под свой DPI, а другие — под экран. При автоподгонке метрики «слетают».
Встраивайте шрифты
Добавьте в ваш CSS:
css@font-face { font-family: "TimesNewRomanPSMT"; src: url("file:///absolute/path/to/TimesNewRomanPSMT.ttf") format("truetype"); }
— или пользуйтесь бесплатными шрифтами, которые WeasyPrint точно умеет встраивать (например, Liberation Serif/Sans).
Жёстко фиксируйте ширины колонок
Вставьте в таблицу <colgroup>
:
html<table> <colgroup> <col style="width:12%;" /> <col style="width:12%;" /> <col style="width:24%;" /> <!-- … в сумме 100% --> </colgroup> <thead>…</thead> <tbody>…</tbody> </table>
И в CSS:
csstable { table-layout: fixed; } th, td { box-sizing: border-box; word-wrap: break-word; white-space: normal; vertical-align: top; }
Встройте всё CSS внутрь PDF
Не полагайтесь на внешние Bootstrap-ссылки. Скопируйте нужные правила в <style>
и уберите <link rel="stylesheet" href="…">
. Тогда движок WeasyPrint и любой ридер будут работать с точно одинаковым набором стилей.
Установите единый box-sizing
и padding
css*, *::before, *::after { box-sizing: border-box; } td, th { padding: 4px; border: 0.7px solid #000; }
Проверка в разных PDF-ридерах
После исправлений протестируйте результат в том же приложении, где у вашего друга проблемы (Adobe Reader, встроенный iOS Preview, Android-ридер и т.п.). Это поможет понять, что осталось «ломаться» на его стороне.
Итог: у вашего друга другой PDF-ридер и нет встроенных шрифтов, поэтому метрика таблицы смещается. Чтобы навсегда устранить эти артефакты, нужно:
<colgroup>
и table-layout: fixed
.Сделав это, вы добьётесь единообразного рендера на любом устройстве и в любом PDF-ридере.
Как мне использовать стандартные шрифты без скачивания для weasyprint и какие они?
WeasyPrint по умолчанию встраивает в PDF так называемые «PDF Base Fonts» — 14 шрифтов, которые уже присутствуют в любом PDF-ридере, и которые не требуют подгрузки файлов (.ttf
) с диска или из сети. Если вы будете ссылаться на них в CSS, WeasyPrint их подхватит и вольёт прямо в PDF, а у вашего друга таблица точно не «поплывёт» из-за отсутствия нужного шрифта.
Просто укажите в font-family
одно из этих имён (или CSS-generic family, который WeasyPrint автоматически свяжет с PDF Base Fonts):
css/* Пример: всю страницу — шрифт семейства «serif» (т.е. Times-Roman) */ body { font-family: serif; } /* Или прямо: */ h1, h2, h3 { font-family: "Times-Roman", serif; } /* Для текста без засечек: */ p.sans { font-family: sans-serif; /* это эквивалент Helvetica */ } /* Или явно: */ .small-code { font-family: "Courier", monospace; }
pythoncss = CSS(string=''' @page { size: 960px 1200px; margin: 35px; } html, body { margin: 0; padding: 0; /* используем базовый serif-шрифт PDF */ font-family: serif; font-size: 12px; } /* Если нужно где-то sans-serif: */ .ft06 { font-family: sans-serif; font-size: 15px; } /* Таблица */ table { border-collapse: collapse; width: 100%; table-layout: fixed; } thead { display: table-header-group; } tr { page-break-inside: avoid; } th, td { box-sizing: border-box; padding: 4px; border: 0.7px solid black; word-wrap: break-word; white-space: normal; vertical-align: top; /* при желании: моноширинный в особых колонках */ /* font-family: "Courier", monospace; */ } ''')
.ttf
файлы на сервере или прописывать @font-face
.font-family: serif;
или "Times-Roman", serif
для обычного текста.sans-serif
или "Helvetica", sans-serif
.monospace
или "Courier", monospace
.С такими шрифтами ваш PDF всегда будет выглядеть абсолютно одинаково на любом устройстве.