Compare commits

...
Author SHA1 Message Date
Kara Engelhardt ad1c9fe2cc Add pdf tests 2026-09-17 17:42:51 +02:00
Kara Engelhardt 21c979b657 PDF: Fix background page being rotated multiple times
transfer_rotation_to_content (or rather add_transformation/replace_content) do not properly duplicate the contentstream they're modifying. this means that if you pass a pdf-level copy of the same bg_page multiple times, the content will be rotated multiple times but the rotation-property ('/Rotate') will only be changed for the first page

pypdfs docs explicitly say that you should not to add_transformation + merge, but instead use merge_transformed_page, which this now does for everything
2026-09-16 13:34:52 +02:00
Kara Engelhardt 6ce41dd7c2 pdf: calculate sizebox based on cropbox and mediabox 2026-09-16 11:11:55 +02:00
Richard Schreiber 96b936af47 PDF: use artbox before trimbox before mediabox from background-PDF as size for canvas 2026-09-16 10:19:17 +02:00
Raphael Michel d3cbfcd4da Invoice export: Add transmission-specific data (Z#23239732) (#6522) 2026-09-15 16:44:00 +02:00
10 changed files with 947 additions and 16 deletions
+27 -4
View File
@@ -54,6 +54,7 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable
from ..exporter import BaseExporter, MultiSheetListExporter
from ..invoicing.transmission import get_transmission_types
from ..services.export import ExportError
from ..services.invoices import invoice_pdf_task
from ..signals import (
@@ -197,7 +198,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
def iterate_sheet(self, form_data, sheet):
_ = gettext
if sheet == 'invoices':
yield [
headers = [
_('Invoice number'),
_('Date'),
_('Order code'),
@@ -230,8 +231,18 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Total value (without taxes)'),
_('Payment matching IDs'),
_('Payment providers'),
_('Transmission type'),
_('Transmission status'),
_('Transmission date'),
]
transmission_types = get_transmission_types()
for tt in transmission_types:
for c in tt.describe_info_columns():
headers.append(str(tt.verbose_name) + ': ' + str(c))
yield headers
p_providers = OrderPayment.objects.filter(
order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -242,7 +253,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
'm'
).order_by()
base_qs = self.invoices_queryset(form_data)\
base_qs = self.invoices_queryset(form_data)
qs = base_qs.select_related(
'order', 'refers'
@@ -280,7 +291,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
if mid:
pmis.append(mid)
pmi = '\n'.join(pmis)
yield [
line = [
i.full_invoice_no,
date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code,
@@ -315,8 +326,20 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
', '.join([
str(self.providers.get(p, p)) for p in sorted(set((i.payment_providers or '').split(',')))
if p and p != 'free'
])
]),
i.transmission_type_instance.verbose_name,
i.get_transmission_status_display(),
date_format(i.transmission_date, "SHORT_DATETIME_FORMAT") if i.transmission_date else "",
]
for tt in transmission_types:
if tt.identifier == i.transmission_type:
described = dict(tt.describe_info(i.invoice_to_transmission_info, i.invoice_to_country, i.invoice_to_is_business))
for c in tt.describe_info_columns():
line.append(described.get(c, ""))
else:
for c in tt.describe_info_columns():
line.append("")
yield line
elif sheet == 'lines':
yield [
_('Invoice number'),
@@ -107,6 +107,9 @@ class TransmissionType:
def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
return transmission_info
def describe_info_columns(self):
return [f.label for f in self.invoice_address_form_fields.values()]
def describe_info(self, transmission_info: dict, country: Country, is_business: bool):
form_data = self.transmission_info_to_form_data(transmission_info)
data = []
+27 -12
View File
@@ -801,6 +801,18 @@ def generate_compressed_addon_list(op, order, event, only_checked_in=False):
return addonlist
def get_sizebox(page: pypdf.PageObject):
mediabox = page.mediabox
cropbox = page.cropbox
return pypdf.generic.RectangleObject((
max(mediabox[0], cropbox[0]),
max(mediabox[1], cropbox[1]),
min(mediabox[2], cropbox[2]),
min(mediabox[3], cropbox[3]),
))
class Renderer:
def __init__(self, event, layout, background_file):
@@ -1153,11 +1165,10 @@ class Renderer:
elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o)
if self.bg_pdf:
page_size = (
self.bg_pdf.pages[0].mediabox[2] - self.bg_pdf.pages[0].mediabox[0],
self.bg_pdf.pages[0].mediabox[3] - self.bg_pdf.pages[0].mediabox[1]
)
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
first_page = self.bg_pdf.pages[0]
sizebox = get_sizebox(first_page)
page_size = (sizebox.width, sizebox.height)
if first_page.rotation in (90, 270):
# swap dimensions due to pdf being rotated
page_size = page_size[::-1]
canvas.setPageSize(page_size)
@@ -1312,14 +1323,18 @@ def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
def _merge_with_correct_page_media_box(output: pypdf.PdfWriter, fg_page: pypdf.PageObject, bg_page: pypdf.PageObject):
if bg_page.rotation != 0:
bg_page.transfer_rotation_to_content()
media_box = bg_page.mediabox
"""
Adds fg_page to output, merging bg_page behind it.
If bg_page has a non-zero mergebox/cropbox or is rotated via /Rotate, a transformation is applied to fix this."""
trsf = pypdf.Transformation()
if media_box.bottom != 0:
trsf = trsf.translate(0, -media_box.bottom)
if media_box.left != 0:
trsf = trsf.translate(-media_box.left, 0)
if bg_page.rotation != 0:
trsf = trsf.rotate(-bg_page.rotation)
mb = get_sizebox(bg_page)
pt1 = trsf.apply_on(mb.lower_left)
pt2 = trsf.apply_on(mb.upper_right)
trsf = trsf.translate(-min(pt1[0], pt2[0]), -min(pt1[1], pt2[1]))
fg_page = output.add_page(fg_page)
fg_page.merge_transformed_page(bg_page, trsf, over=False, expand=False)
@@ -0,0 +1,99 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/PageMode /UseNone
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Count 1
/Kids [ 3 0 R ]
/Type /Pages
>>
endobj
3 0 obj
<<
/Contents 4 0 R
/MediaBox [ 0 0 595.2756 841.8898 ]
/Parent 2 0 R
/Resources <<
/Font 5 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/CropBox [ 10 10 307.84 220 ]
>>
endobj
4 0 obj
<<
/Length 117
>>
stream
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
endstream
endobj
5 0 obj
<<
/F1 6 0 R
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
7 0 obj
<<
/Author (anonymous)
/CreationDate (D\07220260917173038\05302\04700\047)
/Creator (anonymous)
/Keywords ()
/ModDate (D\07220260917173038\05302\04700\047)
/Producer (ReportLab PDF Library \055 \050opensource\051)
/Subject (unspecified)
/Title (untitled)
/Trapped /False
>>
endobj
xref
0 8
0000000000 65535 f
0000000015 00000 n
0000000083 00000 n
0000000142 00000 n
0000000371 00000 n
0000000539 00000 n
0000000570 00000 n
0000000677 00000 n
trailer
<<
/Size 8
/Root 1 0 R
/Info 7 0 R
/ID [ <a3fd776687419b9853cf896bf5d6dc96> <a3fd776687419b9853cf896bf5d6dc96> ]
>>
startxref
966
%%EOF
@@ -0,0 +1,213 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Producer (pypdf)
>>
endobj
2 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 11 0 R ]
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
4 0 obj
<<
/Contents 5 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 6 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
5 0 obj
[ 9 0 R 10 0 R ]
endobj
6 0 obj
<<
/F1 7 0 R
/F1-0 8 0 R
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
9 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
10 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
11 0 obj
<<
/Contents 12 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 13 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
12 0 obj
[ 15 0 R 16 0 R ]
endobj
13 0 obj
<<
/F1 14 0 R
/F1-0 8 0 R
>>
endobj
14 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
15 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
16 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
xref
0 17
0000000000 65535 f
0000000015 00000 n
0000000054 00000 n
0000000120 00000 n
0000000169 00000 n
0000000361 00000 n
0000000393 00000 n
0000000436 00000 n
0000000543 00000 n
0000000650 00000 n
0000000871 00000 n
0000001029 00000 n
0000001224 00000 n
0000001258 00000 n
0000001303 00000 n
0000001411 00000 n
0000001633 00000 n
trailer
<<
/Size 17
/Root 3 0 R
/Info 1 0 R
>>
startxref
1791
%%EOF
@@ -0,0 +1,98 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/PageMode /UseNone
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Count 1
/Kids [ 3 0 R ]
/Type /Pages
>>
endobj
3 0 obj
<<
/Contents 4 0 R
/MediaBox [ 10 10 307.84 220 ]
/Parent 2 0 R
/Resources <<
/Font 5 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Length 117
>>
stream
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
endstream
endobj
5 0 obj
<<
/F1 6 0 R
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
7 0 obj
<<
/Author (anonymous)
/CreationDate (D\07220260917173038\05302\04700\047)
/Creator (anonymous)
/Keywords ()
/ModDate (D\07220260917173038\05302\04700\047)
/Producer (ReportLab PDF Library \055 \050opensource\051)
/Subject (unspecified)
/Title (untitled)
/Trapped /False
>>
endobj
xref
0 8
0000000000 65535 f
0000000015 00000 n
0000000083 00000 n
0000000142 00000 n
0000000336 00000 n
0000000504 00000 n
0000000535 00000 n
0000000642 00000 n
trailer
<<
/Size 8
/Root 1 0 R
/Info 7 0 R
/ID [ <a3fd776687419b9853cf896bf5d6dc96> <a3fd776687419b9853cf896bf5d6dc96> ]
>>
startxref
931
%%EOF
@@ -0,0 +1,213 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Producer (pypdf)
>>
endobj
2 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 11 0 R ]
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
4 0 obj
<<
/Contents 5 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 6 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
5 0 obj
[ 9 0 R 10 0 R ]
endobj
6 0 obj
<<
/F1 7 0 R
/F1-0 8 0 R
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
9 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
10 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
11 0 obj
<<
/Contents 12 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 13 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
12 0 obj
[ 15 0 R 16 0 R ]
endobj
13 0 obj
<<
/F1 14 0 R
/F1-0 8 0 R
>>
endobj
14 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
15 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
16 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
xref
0 17
0000000000 65535 f
0000000015 00000 n
0000000054 00000 n
0000000120 00000 n
0000000169 00000 n
0000000361 00000 n
0000000393 00000 n
0000000436 00000 n
0000000543 00000 n
0000000650 00000 n
0000000871 00000 n
0000001029 00000 n
0000001224 00000 n
0000001258 00000 n
0000001303 00000 n
0000001411 00000 n
0000001633 00000 n
trailer
<<
/Size 17
/Root 3 0 R
/Info 1 0 R
>>
startxref
1791
%%EOF
Binary file not shown.
@@ -0,0 +1,216 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<<
/Producer (pypdf)
>>
endobj
2 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 11 0 R ]
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
4 0 obj
<<
/Contents 5 0 R
/MediaBox [ 0 0 210 297.84 ]
/Resources <<
/Font 6 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
/ExtGState <<
/GS0 8 0 R
>>
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
5 0 obj
[ 9 0 R 10 0 R ]
endobj
6 0 obj
<<
/F1 7 0 R
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/Type /ExtGState
/OP false
/op false
/OPM 1
>>
endobj
9 0 obj
<<
/Length 287
>>
stream
q
0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm
0.0 0.0 297.84 210 re
W
n
/GS0 gs
0.925811 0.881331 0.538643 rg
0 0.23622 297.638 209.764 re
f*
0.476631 0.756542 0.260899 rg
0 0.23622 297.638 209.764 re
292.638 205 m
5 205 l
5 5.23622 l
292.638 5.23622 l
h
f*
Q
endstream
endobj
10 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
11 0 obj
<<
/Contents 12 0 R
/MediaBox [ 0 0 210 297.84 ]
/Resources <<
/Font 13 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
/ExtGState <<
/GS0 8 0 R
>>
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
12 0 obj
[ 15 0 R 16 0 R ]
endobj
13 0 obj
<<
/F1 14 0 R
>>
endobj
14 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
15 0 obj
<<
/Length 287
>>
stream
q
0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm
0.0 0.0 297.84 210 re
W
n
/GS0 gs
0.925811 0.881331 0.538643 rg
0 0.23622 297.638 209.764 re
f*
0.476631 0.756542 0.260899 rg
0 0.23622 297.638 209.764 re
292.638 205 m
5 205 l
5 5.23622 l
292.638 5.23622 l
h
f*
Q
endstream
endobj
16 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
xref
0 17
0000000000 65535 f
0000000015 00000 n
0000000054 00000 n
0000000120 00000 n
0000000169 00000 n
0000000389 00000 n
0000000421 00000 n
0000000452 00000 n
0000000559 00000 n
0000000624 00000 n
0000000962 00000 n
0000001120 00000 n
0000001343 00000 n
0000001377 00000 n
0000001410 00000 n
0000001518 00000 n
0000001857 00000 n
trailer
<<
/Size 17
/Root 3 0 R
/Info 1 0 R
>>
startxref
2015
%%EOF
+51
View File
@@ -32,13 +32,18 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import tempfile
from datetime import timedelta
from decimal import Decimal
from io import BytesIO
from pathlib import Path
import pypdfium2
import pytest
from django.core.files import File
from django.utils.timezone import now
from django_scopes import scope
from PIL import ImageChops
from pypdf import PdfReader
from pretix.base.models import (
@@ -118,3 +123,49 @@ def test_generate_pdf_multi(env):
assert ftype == 'application/pdf'
pdf = PdfReader(BytesIO(buf))
assert len(pdf.pages) == 1
def test_asset(name):
return Path(__file__).parent / "assets" / name
def compare_pdfs(inp_a: Path | bytes, inp_b: Path | bytes):
pdf_a = pypdfium2.PdfDocument(inp_a)
pdf_b = pypdfium2.PdfDocument(inp_b)
assert len(pdf_a) == len(pdf_b)
for i, (expected_page, output_page) in enumerate(zip(pdf_a, pdf_b)):
expected_render = expected_page.render()
output_render = output_page.render()
assert expected_render.height == output_render.height
assert expected_render.width == output_render.width
diff = ImageChops.difference(expected_render.to_pil(), output_render.to_pil())
if diff.getbbox():
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as f:
diff.save(f)
assert not diff.getbbox(), f"Page {i} differs. Diff was written to {f.name}"
@pytest.mark.django_db
@pytest.mark.parametrize("case", [
"bg-rotated",
"bg-mediabox-offset",
"bg-cropbox"
])
def test_generate_pdf_weird_bgs(env, case):
event, order, shirt = env
asset_folder = test_asset(case)
with open(asset_folder / "bg.pdf", 'rb') as fi:
event.badge_layouts.create(name="Default", default=True, background=File(fi, name="test.pdf"))
e = BadgeExporter(event, organizer=event.organizer)
fname, ftype, buf = e.render({
'items': [shirt.pk],
'rendering': 'one',
'include_pending': True
})
assert ftype == 'application/pdf'
assert buf
compare_pdfs(buf, asset_folder / "expected.pdf")