Как мне получить user по order.owner? def invoice_...

Created on: June 16, 2025

Answered using O4 Mini by Chat01

Question

Как мне получить 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': '', }

Thought

Clarifying order.owner in Django

The user’s question seems to be about how to retrieve a user from the order.owner in Django. Based on the code, order.owner is likely a ForeignKey to the User model, meaning accessing order.owner will return the user instance. So, if they want specific user fields, like the username, they should use order.owner.username. If they were getting an ID, they can use order.owner_id for the ID directly.

Thought for a few seconds

Answer

В Django, если в модели Order вы завели поле

python
owner = models.ForeignKey(User, on_delete=models.CASCADE)

то при выборке заказа order = Order.objects.get(pk=id) атрибут order.owner уже будет экземпляром User. Соответственно, в вашем методе достаточно сделать:

python
from 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:

python
owner = models.ForeignKey(Profile, on_delete=models.CASCADE)

то достать связанный User можно через обратное поле:

python
user = order.owner.user context['owner'] = user

Ещё вариант — если по какой-то причине в order.owner у вас лежит просто числовой ID пользователя, то:

python
from django.contrib.auth.models import User user = User.objects.get(pk=order.owner) context['owner'] = user

Но рекомендую именно хранить owner = ForeignKey(User, ...) — тогда order.owner сразу даёт вам нужный объект.

Question

@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)

text
def __str__(self): return str(self.id)

Thought

Thought for a couple of seconds

Answer

Чтобы получить все article_number, связанные с данным заказом, можно воспользоваться связью related_name='order_items' на вашей модели OrderItem. Вот несколько вариантов:

  1. Через QuerySet
python
# Получаем QuerySet заказанных позиций items_qs = OrderItem.objects.filter(order=order) # Извлекаем список артикулов article_numbers = list(items_qs.values_list('article_number', flat=True))
  1. Через обратную связь из объекта заказа
python
# Если у вас в модели Order определено related_name='order_items': items_qs = order.order_items.all() # А затем list comprehension article_numbers = [item.article_number for item in items_qs]
  1. Встраиваем прямо в контекст вашей функции
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.

Question

@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 как строока

Thought

Troubleshooting PDF generation

It seems the issue has to do with how the articles are being fetched in the Django template. The query returns a string like '5137AA191', and there may be a bug in how orders are handled. If request.user.status != 2, the order isn't defined, causing this issue. The .values_list might be returning a LazyList of tuples, or you could just have one article in the database rather than several. Prefetch might not be the problem here. I’ll check the logic around article fetching and order definitions.

Troubleshooting PDF article print issue

It looks like the issue is related to how articles are being printed. The queryset returns a list like <QuerySet ['5137AA191', ...]>, but the template might be

Thought for 5 seconds

Answer

Проблема здесь, скорее всего, не в PDF-библиотеке, а в двух вещах:

  1. Переменная order может не инициализироваться, если у пользователя статус 0 (в вашем коде вы получаете order только внутри if status == 2).
  2. HTML-разметка в шаблоне, где вы просто пишете {{ x }} без обёртки — если строк выводится несколько, они сольются без разделителей и визуально может показаться, что «выводится только один».

1. Всегда получать 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.

2. Правильная разметка в шаблоне

Чтобы каждый артикул был на новой строке (или в списке), оберните вывод в блоковый элемент:

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 будет содержать ВСЕ ваши артикулы, каждый на отдельной строке.

Question

Нет, у меня пользователь со статусом 2

Thought

Investigating template issue

It looks like the user has a template issue where only the first article displays. They mentioned seeing "5137AA191," and it seems like there's a misunderstanding in handling article data. The issue might be related to how the article numbers are returned—right now, it seems that the values returned by order_items.values_list(flat=True) are treated as a queryset of strings. I think the user might need to ensure that it gets properly formatted or processed for display, particularly for the PDF output.

Addressing template issue

It looks like the

Thought for a few seconds

Answer

Проблема не в статусе пользователя, а в том, как вы выводите массив articles в шаблоне — без блочных тегов все строки сливаются в одну (и кажется, что выводится только первый элемент).

  1. Убедитесь, что в контекст передаётся именно список

    python
    articles = list( order.order_items .filter(article_number__isnull=False) .values_list('article_number', flat=True) )

    Это гарантирует, что в шаблоне articles — обычный list, а не ленивый QuerySet.

  2. Используйте блочные 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 %}
  3. Полный пример функции с учётом вынесения 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>) и вы точно увидите все значения, а не только первую строку.

Question

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

Thought

Explaining how to access objects

It looks like the user wants to fetch complete objects instead of just strings with values_list('article_number', flat=True). Since the article_number is a string, x.name won’t work. To get the full OrderItem objects, they should use order.order_items.all() instead. Then, when rendering in the template, they can access both the article_number and item.name. If performance is a concern, they could consider using select_related or prefetch_related. Also, always ensure that the order is correctly defined.

Thought for a few seconds

Answer

Чтобы получить объекты 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'):

python
articles = order.order_items.select_related('seller').all()

чтобы при выводе item.seller не было дополнительных запросов.

И в отладке в Python-коде:

python
for item in articles: print(item.article_number, item.name)

будут работать, потому что item — это экземпляр OrderItem, а не строка.

Question

Как я могу запустить цикл в шаблоне примерно вот так?
{% 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 %}

Thought

Explaining looping in Django template

It seems the user is asking how to make for loops

Thought for a couple of seconds

Answer

В Django-шаблонах нет встроенного конструктора range, поэтому вариант

django
{% for x in range(5) %} {% endfor %}

не сработает. Самый простой способ — передать в контекст ровно тот список или итератор, по которому вы хотите пройтись.

1. Передать 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("Заказ не найден") # Количество строк, которые вам нужно &laquo;пройти&raquo; 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, но вы её даже не используете (в теле цикла просто «печатаете» фиксированную разметку).


2. Динамическое число строк

Если вы заранее не знаете, сколько строк нужно вывести, а, скажем, хотите добавить «дополнительные» пустые строки до фиксированной длины (например, вывести на странице всегда 10 строк, заполнив недостающие «пустыми»), то можно вычислять это тоже во вьюхе:

python
MAX_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 %} {# затем &laquo;пустые&raquo; строки #} {% for _ in empty_rows %} <tr> <td class="border-td ft06">&nbsp;</td> <td class="border-td ft06">&nbsp;</td> <td class="border-td">&nbsp;</td> <td class="border-td ft06 vert_bottom">&nbsp;</td> <td class="border-td ft06 vert_bottom">&nbsp;</td> <td class="border-td ft06 vert_bottom">&nbsp;</td> <td class="border-td ft06">&nbsp;</td> <td class="border-td ft06">&nbsp;</td> </tr> {% endfor %}

3. Если хочется «чисто в шаблоне»

Можно написать свой простой тег:

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 %}.

Question

вот смотри есть такая функция
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 файла. Как это можно исправить?

Thought

Fixing PDF table page breaks

It sounds like the user wants to improve page breaks in a table for a generated PDF. For using WeasyPrint, I can suggest applying page-break-inside: avoid or break-inside: avoid to prevent content from being split within rows. Additionally, using CSS for table headers like thead { display: table-header-group; } ensures they repeat on each

Thought for a couple of seconds

Answer

Чтобы таблица корректно разбивается на страницы и шапка (header) повторяется, а строки не обрезаются посередине, нужно добавить соответствующие CSS-правила для печати. Для WeasyPrint (HTML→PDF) можно использовать примерно такой подход:

  1. Повторение заголовка таблицы
    Добавьте в CSS:

    css
    thead { display: table-header-group; } tfoot { display: table-footer-group; }

    Это укажет движку PDF повторять <thead> на каждой новой странице, если таблица «перетекает» через границу страницы.

  2. Контроль разрыва внутри строк
    Чтобы строка не разрывалась посередине:

    css
    tr { page-break-inside: avoid; break-inside: avoid; }

    Это постарается не разрывать отдельные <tr> между страницами. Учтите, однако, что при очень больших строках (с высоким рядом ячеек или вложенными блоками) могут понадобиться дополнительные правки (например, разбить длинный контент внутри ячейки на несколько элементов).

  3. Разрешить разбиение таблицы между группами строк
    Можно дополнительно указать для <tbody>:

    css
    tbody { display: table-row-group; }

    Обычно это по умолчанию, но если вы оборачиваете таблицу в контейнеры с display, убедитесь, что поведение table-layout не нарушается.

  4. Пример включения CSS в ваш код
    В месте, где вы генерируете PDF, расширьте стили:

    python
    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; } 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=...).

  5. Проверка структуры HTML

    • Убедитесь, что таблица оформлена правильно: <table><thead>…</thead><tbody>…</tbody></table>. Если вы не используете <thead>, добавьте его для шапки.
    • Не оборачивайте <tr> или <td> в лишние блоки, которые могут менять модель отображения таблицы, иначе движок может не распознать группу строк как единое целое.
  6. Разбиение очень больших таблиц
    Если таблица слишком длинная и содержит группы строк, которые нужно разбить «по секциям», можно программно разделять: например, после N строк закрывать текущую таблицу и начинать новую, чтобы явно вставлять разделитель страниц. Но чаще достаточно CSS-подхода.

  7. Диагностика

    • Сгенерируйте HTML и откройте в браузере с функцией «Печать в PDF» — проверьте, как разбивается таблица. Если в браузере при печати все работает, то аналогичные CSS-правила должны работать и в WeasyPrint.
    • Если строки всё равно обрываются некорректно, убедитесь, что внутри <td> нет элементов с фиксированным высочай (например, больших картинок), которые тянут строку.
    • Можно логировать HTML, чтобы убедиться, что в цикле шаблона вы формируете <tr> именно внутри <tbody>.
  8. Пример шаблона

    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.

Question

Но у меня нет tbody. Вот мой html котоорый ненерируется в pdf файл. Проблема не только в таблице но и в контенте.

{% load static %}
{% load cart_extras %}

<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous"> <style> p {margin: 0; padding: 0;} .ft00{font-size:10px;font-family:TimesNewRomanPSMT;color:#000000;} .ft00{font-size:10px;font-family:TimesNewRomanPSMT;color:#000000;} .ft01{font-size:10px;font-family:TimesNewRomanPSMT;color:#000000;} .ft02{font-size:10px;line-height:16px;font-family:TimesNewRomanPSMT;color:#000000;} .ft03{font-size:10px;font-family:TimesNewRomanPSMT;color:#000000;} .ft04{font-size:13px;font-family:TimesNewRomanPSMT;color:#000000;} .ft05{font-size:13px;font-family:TimesNewRomanPSMT;color:#000000;} .ft06{font-size:15px;font-family:TimesNewRomanPSMT;color:#000000;} .ft07{font-size:21px;font-family:TimesNewRomanPSMT;color:#000000;} .ft08{font-size:12px;line-height:12px; font-family:TimesNewRomanPSMT;color:#000000;} .ft09{font-size:12px;font-family:TimesNewRomanPSMT;color:#000000;} .ft010{font-size:13px;font-family:TimesNewRomanPSMT;color:#000000;} .ft011{font-size:10px;line-height:12px;font-family:TimesNewRomanPSMT;color:#000000;} .ft012{font-size:10px;line-height:10px;font-family:TimesNewRomanPSMT;color:#000000;} .ft013{font-size:10px;line-height:18px;font-family:TimesNewRomanPSMT;color:#000000;}
text
H1, 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>При оформлении доверенности на получение товара в строке &laquo;На получение от&raquo; указывать ООО "ЗЕ СОЛЮШЕНС"</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/> оплату в графе &laquo;Назначение платежа&raquo;. Отсутствие номера Счета на оплату в Платежном поручении может<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>

-->

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"> Предлагаем вам получать счета, счета-фактуры, акты и другие документы от ООО &laquo;ЗЕ СОЛЮШЕНС&raquo; в<br/> электронном виде, присоединившись к Электронному документообороту (ЭДО).<br/><br/> 1. Войдите в вашу систему ЭДО.<br/> 2. Найдите в системе участника ЭДО – ООО &laquo;&raquo; (Оператор ЭДО &laquo;Контур.Диадок&raquo; (АО &laquo;ПФ &laquo;СКБ<br/> Контур&raquo;), ID участника ЭДО: -, ИНН: 9717173620, КПП:<br/> 783450001) и примите направленное вам приглашение.<br/><br/> 3. Обменивайтесь документами, выбрав в системе Центральный филиал ООО &laquo;ЗЕ СОЛЮШЕНС&raquo; (ID участника<br/> ЭДО: -, ИНН: 9717173620, КПП: 771701001).<br/><br/> </p> </div> <br> </div> </body>
</html>

Thought

Fixing HTML structure and static paths

The user has HTML missing a <tbody>, and that’s impacting their content display in PDFs. I need to tell them the HTML for the table should include both <thead> and <tbody>. The fragmented structure must be fixed. Also, they should ensure static files use the correct absolute path, and that they're using {% static %} for static tags, with paths properly set using STATIC_ROOT. Their Content-Disposition header has a typo, which affects browser behavior but won't impact PDF generation. Images need proper URLs too.

Thought for a few seconds

Answer

Ваша проблема на самом деле состоит из двух частей:

  1. WeasyPrint не «видит» вашу табличную разметку как настоящую таблицу и потому не применяет CSS-правила page-break-inside: avoid; или повтора шапки, а также некорректно делит длинный контент между страницами.
  2. Часть вашего CSS и ассеты (Bootstrap, картинки) подключаются через внешние URL или через «static/i/...», но WeasyPrint по умолчанию не грузит внешние ресурсы, а относительный base_url = settings.BASE_DIR ему ничего не даёт.

1. Приведите таблицу к семантически корректному виду

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">&nbsp;</td> <!-- пустые строки, чтобы выровнять по сетке --> </tr> {% endfor %} </tbody> </table>

И в вашем встроенном CSS добавить (или убедиться, что уже есть):

css
thead { display: table-header-group; /* повторять шапку на новых страницах */ } tbody { display: table-row-group; } tr { page-break-inside: avoid; break-inside: avoid; }

2. Подключение стилей и ассетов

2.1 Встроенный CSS

МыasyPrint зачастую отказывается подтягивать удалённые стили по HTTPS (CDN-ссылки Bootstrap), поэтому либо:

  • Скопируйте нужные вам правила из Bootstrap целиком в <style> вашего шаблона,
  • либо скачайте CSS-файл локально и подключите его через абсолютный путь (см.ниже).

2.2 Корректный base_url

Вместо

python
base_url = settings.BASE_DIR

лучше передать WeasyPrint абсолютный URL к корню вашего статического контента. Например:

python
from django.contrib.staticfiles.storage import staticfiles_storage # ... base_url = request.build_absolute_uri('/') # или # base_url = staticfiles_storage.url('') # если настроен STATICFILES_STORAGE

или даже

python
base_url = settings.STATIC_ROOT # если это абсолютный путь на диске

— так картинка <img src="{% static 'i/contract/code.png' %}"> будет найдена.


3. Правка заголовка HTTP

В вашем примере опечатка:

python
response['Content-Dispositioe'] = 'inline; filename="invoice.pdf"'

должно быть

python
response['Content-Disposition'] = 'inline; filename="invoice.pdf"'

4. Пример итоговой функции

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
  1. Таблица теперь правильно размечена <thead>/<tbody> и CSS-правилами для печати.
  2. Стили встроены прямо в PDF, без зависимости от CDN.
  3. Ассеты (картинки) будут искаться относительно base_url, которое указывает на корень сайта/статических файлов.
  4. Заголовок исправлен, чтобы PDF открывался «inline» без ошибок.

После этих правок и ваша табличка, и весь остальной контент (абзацы, картинки, разделители страниц) в WeasyPrint будут отображаться и разбиваться на страницы корректно.

Question

А как сделать чтобы был чуть чуть padding сверху на новой странице?

Thought

Adding top margin in PDF generation

To add padding at the top of a new page in a PDF generated with CSS, you can use the @page rule with margin-top. For example, @page { margin: 35px 35px 0; } will affect the outer margins, or you can set it specifically for the first page using @page:first { margin-top: 35px; }. Another option is to adjust the body margin, or use margin-top before elements like .page-break for content break placement. Adding @top-center in @page can also work with specific tools like WeasyPrint.

Thought for 5 seconds

Answer

Чтобы добавить отступ сверху на каждой новой странице (или хотя бы на второй и последующих), есть два простых подхода.


1. Глобальный отступ через @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 свободного пространства.


2. Локальный отступ для второго блока (page2)

Если вам нужно, чтобы первая страница была «плотной» сверху, а небольшой отступ появлялся только при переходе на второй и последующие, можно:

  1. Явно разрывать страницу в нужном месте через CSS-свойство page-break-before.
  2. Для блока #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 на самом блоке, с которого начинается новая страница.

Question

У меня есть вот такая функция которую я использую.
@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 файла. Как так сделать я не знаю, такое возможно?

Thought

Thought for a couple of seconds

Answer

Вам не надо пытаться «вызывать» сами представления друг из друга, а лучше вынести логику генерации PDF в утилитарную функцию, которую сможете дергать и из invoice_pdf, и из order_confirm.

1. Вынесите генерацию PDF в отдельную функцию

Например, в 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])

2. Используйте эту функцию и в invoice_pdf, и в order_confirm

В invoice_pdf:

python
from .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 на почту:

python
from 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/'})

Несколько замечаний

  1. SMTP vs Celery
    Если вы уже используете confirm_email.delay(...) — вы можете передавать туда PDF как bytes или сохранить его временно в файловую систему/Redis, и в задаче присоединять при отправке.

  2. base_url для картинок и CSS
    В примере я использовал settings.STATIC_ROOT. Если ваши шаблоны ссылаются на {% static '...' %}, то убедитесь, что WeasyPrint может прочитать эти файлы. Альтернативно, можно смапить base_url = request.build_absolute_uri('/').

  3. Рефакторинг контекста
    Если контекст письма и контекст для PDF пересекаются, тоже можно вынести общую часть в утилитную функцию и дополнять по месту.

Такой подход позволяет единообразно генерировать PDF и легко прикреплять его к любому письму, без дублирования кода.

Question

Не совсем понял как передать 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 чтобы отправить файл на почту?

Thought

Thought for a couple of seconds

Answer

Чтобы прикрепить сгенерированный PDF к письму в order_confirm, нужно:

  1. Вынести генерацию PDF в утилитарную функцию (например, generate_invoice_pdf)
  2. Из order_confirm вызвать эту функцию, получить bytes — и прикрепить их к письму через EmailMessage.attach()
  3. Либо передать PDF в вашу Celery-задачу confirm_email, либо отправить прямо в order_confirm.

1. Утилитная функция генерации PDF

Вынесите логику из 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])

2. Использование в invoice_pdf

Теперь invoice_pdf упростится:

python
from .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"'})

3. Прикрепляем PDF в order_confirm

В вашем order_confirm импортируйте generate_invoice_pdf и Django-класс EmailMessage:

python
from 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 вызывайте:

python
confirm_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(...) прикрепляете его к письму.
  • Отправляете письмо синхронно или через Celery-задачу — в обоих случаях PDF будет отправлен как вложение.

Question

Я использую 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,
)

Thought

Thought for a second

Answer

Функция send_mail из django.core.mail не умеет прикреплять файлы, поэтому вам нужно перейти на более «низкоуровневый» класс EmailMessage (или его подкласс EmailMultiAlternatives), который позволяет добавлять вложения.

Вот пример, как можно переписать вашу задачу confirm_email, чтобы вместе с HTML-сообщением отправлять и PDF:

python
from 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)

Что изменилось

  1. Импортируем EmailMessage вместо send_mail.
  2. Создаём объект EmailMessage, где можно задать subject, body, from_email, to.
  3. У email.content_subtype = 'html' говорим, что body — HTML.
  4. email.attach(name, content, mimetype) — прикрепляет файл.
  5. 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, и имя файла — и вы спокойно шлёте их в одном письме.

Question

А как сделать чтобы этот файл на почте был просто ссылкой?

Thought

Thought for a couple of seconds

Answer

Чтобы вместо прикреплённого файла в письме был некий «прямой» линк на PDF, вам нужно:

  1. Сгенерировать PDF в байтах (как вы уже делаете через generate_invoice_pdf).
  2. Сохранить эти байты куда-нибудь, откуда по HTTP будет доступен URL. Чаще всего это либо:
    • ваша MEDIA_ROOT/MEDIA_URL (через Django default_storage),
    • либо объект в облачном хранилище (S3, Google Cloud Storage и т.п.), где вы можете получить публичную или подписанную ссылку.
  3. В теле письма вместо <a href="cid:…"> или вложения просто вставить <a href="{{ pdf_url }}">Ссылка на счёт в PDF</a>.

Пример: сохраняем в MEDIA_ROOT

1. Генерация и сохранение файла

python
from 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 вы прописали:

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

и в urls.py раздаёте MEDIA_URL в режиме отладки:

python
from 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)

2. Используем в order_confirm

python
from .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({...})

3. Шаблон письма с ссылкой

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, а даёте пользователю прямой линк, по которому он может его скачать или открыть.

Question

Так значит не сделать его ссылкой не сохранняя файла а просто прикрепляя?

Thought

Providing guidance on email links

The user is asking if it's possible to make a file a link in an email without saving it first. Unfortunately, it's not possible to hyperlink directly to an attached file in most email clients. One alternative could be embedding the file as a base64 Data URI in the link, but that may not work reliably across all clients. The best approach is to host the file online and

Thought for a few seconds

Answer

К сожалению, в стандарте SMTP/HTML-письма нет механизма, который позволил бы в теле письма сделать <a href="…"> и ссылаться на само вложение (attachment) — клиенты просто не умеют так: они показывают вложение под письмом, но не «хостят» его по URL.

Есть два «костыля»:

  1. Data-URI
    Можно прямо в теге <a> засунуть Base64-версию PDF:

    html
    <a href="data:application/pdf;base64,{{ pdf_bytes|b64encode }}" download="invoice.pdf"> Скачать счёт </a>

    Но:

    • Письмо получится очень «тяжёлым»;
    • Большинство почтовых клиентов не поддерживают такой приём (или режут большие data: URIs).
  2. Встроить HTML attachment как cid:
    Можно прикрепить PDF как inline-attachment с Content-ID, а в письме вставить <a href="cid:invoice.pdf">ссылка</a>. Но:

    • Тоже не поддерживается большинством почтовых клиентов;
    • CID-ссылки работают только для картинок (встраиваемых в тело).

Итог

Единственно надёжный способ дать пользователю ссылку на скачивание PDF — сохранить его в каком-то веб-доступном хранилище (локальный MEDIA_ROOT или S3/облако) и в тело письма вставить обычный <a href="https://…/invoice_123.pdf">Скачать счёт</a>. Тогда:

  • Письмо остаётся лёгким,
  • Ссылка работает в любом клиенте,
  • Вам не нужно мучиться с нестандартным поведением.

Если загрузка «на диск/в S3» для вас неприемлема, придётся мириться с тем, что вложение отобразится под письмом, а внутри текста будет просто текстовый призыв типа «смотри вложение».

Question

Почему на какоом то девайсе у моего знакомого некоторые данные в даблице сьезжают из ячеек, и как это исправить?

def generate_invoice_pdf(order, context_extra=None):
"""
Генерирует PDF по заказу и возвращает байты файла.
context_extra — опциональный dict, который будет слит с базовым контекстом.
"""
# articles = order.order_items.all()

text
total_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])

Thought

Thought for a couple of seconds

Answer

Сдвиг ячеек в PDF-таблице на разных «девайсах» (то есть в разных PDF-ридерах) обычно вызван тем, что:

  1. Вы не задали фиксированный алгоритм расчёта ширин колонок — по умолчанию браузер/WeasyPrint в каждом «движке» может немного по-своему перераспределять оставшееся пространство.
  2. Контент в ячейках «переливается» и меняет размеры строк, табличная сетка «плывёт».
  3. Разные рендереры по-разному интерпретируют отступы (padding) и границы (border).

Чтобы придать вашей таблице более жёсткую, однозначную разметку, попробуйте добавить в ваш CSS пару правил:

css
table { 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 не &laquo;ломал&raquo; ширину */ border: 0.7px solid black; }

Как это внедрить

  1. Добавьте table-layout: fixed; — тогда браузер/WeasyPrint рассчитает ширины колонок один раз по заданным width<col> или в th, td), и уже не будет «растягивать» их по содержимому.

  2. Либо объявите <colgroup> в вашем шаблоне, чтобы жестко распределить ширины:

    html
    <table> <colgroup> <col style="width: 12%;" /> <col style="width: 12%;" /> <col style="width: 24%;" /> <!-- и т.д., пока в сумме не будет 100% --> </colgroup> <thead></thead> <tbody></tbody> </table>
  3. Перенос слов (word-wrap: break-word;) предотвратит «выдавливание» содержимого за пределы ячейки, а white-space: normal; снимет любые случайные nowrap.

  4. vertical-align: top; гарантирует, что если ячейка станет выше, её содержимое всегда начнёт «сверху», а не в центре или внизу.

  5. box-sizing: border-box; учтёт padding и border внутри общей ширины ячейки, чтобы итоговая сетка не «поплыла» из-за разных вычислений.


Пример итогового CSS в вашей функции

python
css = 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-ридерах.

Question

Смотри, я сгенерировал из html файла pdf файл. На моём девайсе таблица выглядит ровно, и на всех других этот pdf файл выглядит тоже ровно а вот у моего друга таблица в этом pdf файле выглядит криво. Почему?

Thought

Thought for a couple of seconds

Answer

Поведение, когда один и тот же PDF «плывёт» в разных вьюверах, — не редкость. Причины обычно кроются в том, что PDF не содержит всего необходимого для абсолютно одинакового рендеринга:

  1. Разные шрифты на клиенте.
    Вы используете font-family: TimesNewRomanPSMT; и Arial/Bootstrap-шрифты, но WeasyPrint по умолчанию встраивает далеко не все. Если на машине друга нет точно такого же шрифта, система подставит наиболее близкий, и размеры букв/интервалов поменяются — табличные ячейки "поедут".

  2. Разная реализация PDF-движка.
    Одни мобильные или десктоп-ридеры могут чуть иначе интерпретировать метрику линий, box-sizing и table-layout. МыasyPrint в момент генерации считает одно, а потом ваша ОС может переинтерпретировать.

  3. Отсутствие жёстко заданных ширин.
    Если вы даёте таблице width: 100%, но не фиксируете колонки, разные вьюверы могут по-разному делить остаток между ними.

  4. Разный DPI экрана/печати.
    Некоторые PDF-ридеры по умолчанию масштабируют страницу под свой DPI, а другие — под экран. При автоподгонке метрики «слетают».


Как увести рендер к детерминированности

  1. Встраивайте шрифты
    Добавьте в ваш CSS:

    css
    @font-face { font-family: "TimesNewRomanPSMT"; src: url("file:///absolute/path/to/TimesNewRomanPSMT.ttf") format("truetype"); }

    — или пользуйтесь бесплатными шрифтами, которые WeasyPrint точно умеет встраивать (например, Liberation Serif/Sans).

  2. Жёстко фиксируйте ширины колонок
    Вставьте в таблицу <colgroup>:

    html
    <table> <colgroup> <col style="width:12%;" /> <col style="width:12%;" /> <col style="width:24%;" /> <!-- … в сумме 100% --> </colgroup> <thead></thead> <tbody></tbody> </table>

    И в CSS:

    css
    table { table-layout: fixed; } th, td { box-sizing: border-box; word-wrap: break-word; white-space: normal; vertical-align: top; }
  3. Встройте всё CSS внутрь PDF
    Не полагайтесь на внешние Bootstrap-ссылки. Скопируйте нужные правила в <style> и уберите <link rel="stylesheet" href="…">. Тогда движок WeasyPrint и любой ридер будут работать с точно одинаковым набором стилей.

  4. Установите единый box-sizing и padding

    css
    *, *::before, *::after { box-sizing: border-box; } td, th { padding: 4px; border: 0.7px solid #000; }
  5. Проверка в разных PDF-ридерах
    После исправлений протестируйте результат в том же приложении, где у вашего друга проблемы (Adobe Reader, встроенный iOS Preview, Android-ридер и т.п.). Это поможет понять, что осталось «ломаться» на его стороне.


Итог: у вашего друга другой PDF-ридер и нет встроенных шрифтов, поэтому метрика таблицы смещается. Чтобы навсегда устранить эти артефакты, нужно:

  • Встроить шрифты (или перейти на стандартные, которые WeasyPrint точно встраивает).
  • Чётко зафиксировать ширины колонок через <colgroup> и table-layout: fixed.
  • Встроить все CSS-правила локально, без внешних подключений.

Сделав это, вы добьётесь единообразного рендера на любом устройстве и в любом PDF-ридере.

Question

Как мне использовать стандартные шрифты без скачивания для weasyprint и какие они?

Thought

Thought for a couple of seconds

Answer

WeasyPrint по умолчанию встраивает в PDF так называемые «PDF Base Fonts» — 14 шрифтов, которые уже присутствуют в любом PDF-ридере, и которые не требуют подгрузки файлов (.ttf) с диска или из сети. Если вы будете ссылаться на них в CSS, WeasyPrint их подхватит и вольёт прямо в PDF, а у вашего друга таблица точно не «поплывёт» из-за отсутствия нужного шрифта.


1. Какие это шрифты

1.1. Serif-семейство (Times-style)

  • Times-Roman
  • Times-Bold
  • Times-Italic
  • Times-BoldItalic

1.2. Sans-serif-семейство (Helvetica-style)

  • Helvetica
  • Helvetica-Bold
  • Helvetica-Oblique
  • Helvetica-BoldOblique

1.3. Monospace

  • Courier
  • Courier-Bold
  • Courier-Oblique
  • Courier-BoldOblique

1.4. Специальные

  • Symbol
  • ZapfDingbats

2. Как их подключить в вашем CSS

Просто укажите в font-family одно из этих имён (или CSS-generic family, который WeasyPrint автоматически свяжет с PDF Base Fonts):

css
/* Пример: всю страницу — шрифт семейства &laquo;serif&raquo; (т.е. 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; }

2.1. Полный пример вставки в вашу функцию

python
css = 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; */ } ''')

2.2. Зачем это помогает

  • Гарантированная встраиваемость. PDF Base Fonts уже «залиты» в любой PDF-ридер: Adobe Reader, Preview на macOS, встроенный ридер на Android/iOS, сторонние библиотеки и т.п.
  • Фиксированная метрика. Поскольку это стандарт, у всех рендереров одни и те же метрики, и таблицы не “плывут” из-за того, что у вас на машине свой Times New Roman, а у друга — Liberation.
  • Никаких внешних зависимостей. Вам не нужно прятать .ttf файлы на сервере или прописывать @font-face.

3. Резюме

  1. В CSS используйте font-family: serif; или "Times-Roman", serif для обычного текста.
  2. Для беззасечек — sans-serif или "Helvetica", sans-serif.
  3. Для моноширинного — monospace или "Courier", monospace.
  4. Не подключайте внешние TTF/OTF-шрифты — PDF Base Fonts уже встроены.

С такими шрифтами ваш PDF всегда будет выглядеть абсолютно одинаково на любом устройстве.

Share this Q&A