invoicing: start pdf invoice generation (#75729) #50

Merged
lguerin merged 1 commits from wip/75729-invoice-pdf into main 2023-04-21 14:09:16 +02:00
10 changed files with 508 additions and 8 deletions

1
debian/control vendored
View File

@ -19,6 +19,7 @@ Depends: python3-distutils,
python3-gadjo,
python3-publik-django-templatetags,
python3-requests,
weasyprint,
${misc:Depends},
${python3:Depends},
Recommends: python3-django-mellon

View File

@ -358,7 +358,9 @@ class AbstractInvoice(models.Model):
class DraftInvoice(AbstractInvoice):
pass
@property
def formatted_number(self):
return '%s-%s' % (_('TEMPORARY'), self.pk)
class Counter(models.Model):

View File

@ -0,0 +1,160 @@
{% load static i18n %}
<!doctype html><html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{{ invoice.formatted_number }}</title>
<meta name="author" content="Entr'ouvert">
<style>
html {
color: #14213d;
font-family: Source Sans Pro;
font-size: 11pt;
line-height: 1.6;
}
body {
margin: 0;
}
h1 {
color: #1ee494;
font-family: Pacifico;
font-size: 40pt;
margin: 0;
}
aside {
display: flex;
margin: 2em 0 4em;
}
aside address {
font-style: normal;
white-space: pre-line;
}
aside address#from {
color: #a9a;
flex: 1;
}
aside address#to {
text-align: right;
}
dl {
position: absolute;
right: 0;
text-align: right;
top: 0;
}
dt, dd {
display: inline;
margin: 0;
}
dt {
color: #a9a;
}
dt::before {
content: '';
display: block;
}
dt::after {
content: ':';
}
table {
border-collapse: collapse;
width: 100%;
}
th {
border-bottom: .2mm solid #a9a;
color: #a9a;
font-size: 10pt;
font-weight: 400;
padding-bottom: .25cm;
text-transform: uppercase;
}
td {
padding-top: 7mm;
}
td:last-of-type {
color: #1ee494;
font-weight: bold;
text-align: right;
}
th, td {
text-align: center;
}
th:first-of-type, td:first-of-type {
text-align: left;
}
th:last-of-type, td:last-of-type {
text-align: right;
}
table#total {
background: #f6f6f6;
border-color: #f6f6f6;
border-style: solid;
border-width: 2cm 3cm;
bottom: 0;
font-size: 20pt;
margin: 0 -3cm;
position: absolute;
width: 18cm;
}
div.break {
break-before: page;
}
</style>
</head>
<body>
<h1>{% trans "Invoice" %}</h1>
<aside>
<address id="from">
{{ regie.label }}
</address>
<address id="to">
{{ invoice.payer_first_name}} {{ invoice.payer_last_name}}
</address>
</aside>
<dl id="informations">
<dt>{% trans "Invoice number" %}</dt>
<dd>{{ invoice.formatted_number }}</dd>
<dt>{% trans "Date" %}</dt>
<dd>{{ invoice.created_at|date }}</dd>
</dl>
<table id="lines">
<thead>
<tr>
<th>{% trans "Description" %}</th>
<th>{% trans "Beneficiary" %}</th>
<th>{% trans "Price" %}</th>
<th>{% trans "Quantity" %}</th>
<th>{% trans "Subtotal" %}</th>
</tr>
</thead>
<tbody>
{% for line in lines %}
<tr>
<td>{{ line.label }}</td>
<td>{{ line.user_first_name}} {{ line.user_last_name}}</td>
<td>{{ line.unit_amount }}</td>
<td>{{ line.quantity }}</td>
<td>{{ line.total_amount }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table id="total">
<thead>
<tr>
<th>{% trans "Payment deadline" %}</th>
<th>{% trans "Total due" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ invoice.date_payment_deadline }}</td>
<td>{{ invoice.total_amount }}</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -29,6 +29,7 @@
{% url 'lingo-manager-invoicing-pool-detail' regie_pk=regie.pk pk=invoice.pool.campaign_id pool_pk=invoice.pool.pk as pool_url %}
<li class="invoice untoggled" data-invoice-id="{{ invoice.pk }}" data-invoice-lines-url="{% url 'lingo-manager-invoicing-regie-invoice-line-list' regie_pk=regie.pk invoice_pk=invoice.pk %}">
{% blocktrans with invoice_number=invoice.formatted_number payer_id=invoice.payer_external_id payer_name=invoice.payer_name payer_demat=invoice.payer_demat|yesno payer_direct_debit=invoice.payer_direct_debit|yesno amount=invoice.total_amount number=invoice.formatted_number %}Invoice <a href="{{ pool_url }}?number={{ number }}">{{ invoice_number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer_id }}">{{ payer_name }} ({{ payer_id }})</a>, amount {{ amount }}€, demat: {{ payer_demat }}, direct debit: {{ payer_direct_debit }}{% endblocktrans %}
- <a href="{% url 'lingo-manager-invoicing-regie-invoice-pdf' regie_pk=regie.pk invoice_pk=invoice.pk %}">{% trans "download" %}</a>
<span class="togglable"></span>
</li>
{% endfor %}

View File

@ -49,10 +49,11 @@
{% for invoice in object_list %}
<li class="invoice untoggled" data-invoice-id="{{ invoice.pk }}" data-invoice-lines-url="{% url 'lingo-manager-invoicing-invoice-line-list' regie_pk=regie.pk pk=object.pk pool_pk=pool.pk invoice_pk=invoice.pk %}">
{% if pool.draft %}
{% blocktrans with number=invoice.pk payer_id=invoice.payer_external_id payer_name=invoice.payer_name payer_demat=invoice.payer_demat|yesno payer_direct_debit=invoice.payer_direct_debit|yesno amount=invoice.total_amount prefix=_('TEMPORARY')%}Invoice <a href="{{ journal_url }}?invoice_id={{ number }}">{{ prefix }}-{{ number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer_id }}">{{ payer_name }} ({{ payer_id }})</a>, amount {{ amount }}€, demat: {{ payer_demat }}, direct debit: {{ payer_direct_debit }}{% endblocktrans %}
{% blocktrans with invoice_number=invoice.formatted_number payer_id=invoice.payer_external_id payer_name=invoice.payer_name payer_demat=invoice.payer_demat|yesno payer_direct_debit=invoice.payer_direct_debit|yesno amount=invoice.total_amount number=invoice.pk %}Invoice <a href="{{ journal_url }}?invoice_id={{ number }}">{{ invoice_number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer_id }}">{{ payer_name }} ({{ payer_id }})</a>, amount {{ amount }}€, demat: {{ payer_demat }}, direct debit: {{ payer_direct_debit }}{% endblocktrans %}
{% else %}
{% blocktrans with invoice_number=invoice.formatted_number payer_id=invoice.payer_external_id payer_name=invoice.payer_name payer_demat=invoice.payer_demat|yesno payer_direct_debit=invoice.payer_direct_debit|yesno amount=invoice.total_amount number=invoice.formatted_number %}Invoice <a href="{{ journal_url }}?invoice_number={{ number }}">{{ invoice_number }}</a> addressed to <a href="{{ journal_url }}?payer_external_id={{ payer_id }}">{{ payer_name }} ({{ payer_id }})</a>, amount {{ amount }}€, demat: {{ payer_demat }}, direct debit: {{ payer_direct_debit }}{% endblocktrans %}
{% endif %}
- <a href="{% url 'lingo-manager-invoicing-invoice-pdf' regie_pk=regie.pk pk=invoice.pool.campaign_id pool_pk=invoice.pool.pk invoice_pk=invoice.pk %}">{% trans "download" %}</a>
<span class="togglable"></span>
</li>
{% endfor %}

View File

@ -98,6 +98,11 @@ urlpatterns = [
views.pool_delete,
name='lingo-manager-invoicing-pool-delete',
),
path(
'regie/<int:regie_pk>/campaign/<int:pk>/pool/<int:pool_pk>/invoice/<int:invoice_pk>/pdf/',
views.invoice_pdf,
name='lingo-manager-invoicing-invoice-pdf',
),
path(
'ajax/regie/<int:regie_pk>/campaign/<int:pk>/pool/<int:pool_pk>/invoice/<int:invoice_pk>/lines/',
views.invoice_line_list,
@ -118,6 +123,11 @@ urlpatterns = [
views.regie_invoice_list,
name='lingo-manager-invoicing-regie-invoice-list',
),
path(
'regie/<int:regie_pk>/invoice/<int:invoice_pk>/pdf/',
views.regie_invoice_pdf,
name='lingo-manager-invoicing-regie-invoice-pdf',
),
path(
'ajax/regie/<int:regie_pk>/invoice/<int:invoice_pk>/lines/',
views.regie_invoice_line_list,

View File

@ -24,6 +24,7 @@ from django.db.models import CharField, Count, IntegerField, JSONField, OuterRef
from django.db.models.functions import Coalesce
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.template.loader import get_template
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
@ -36,6 +37,7 @@ from django.views.generic import (
TemplateView,
UpdateView,
)
from weasyprint import HTML
from lingo.agendas.chrono import ChronoError, unlock_events_check
from lingo.agendas.models import Agenda
@ -616,6 +618,50 @@ class PoolDeleteView(DeleteView):
pool_delete = PoolDeleteView.as_view()
class PDFMixin:
def html(self):
template = get_template(self.template_name)
context = {
'regie': self.regie,
'invoice': self.object,
'lines': self.object.lines.all().order_by('user_external_id', 'event_date', 'pk'),
}
return template.render(context)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
result = self.html()
if 'html' in request.GET:
return HttpResponse(result)
html = HTML(string=result)
pdf = html.write_pdf()
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % self.object.formatted_number
return response
class InvoicePDFView(PDFMixin, DetailView):
template_name = 'lingo/invoicing/invoice.html'
pk_url_kwarg = 'invoice_pk'
model = Invoice
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
self.pool = get_object_or_404(
Pool, pk=kwargs['pool_pk'], campaign=kwargs['pk'], campaign__regie=self.regie
)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
invoice_model = Invoice
if self.pool.draft:
invoice_model = DraftInvoice
return invoice_model.objects.filter(pool=self.pool)
invoice_pdf = InvoicePDFView.as_view()
class InvoiceLineListView(ListView):
template_name = 'lingo/invoicing/manager_invoice_lines.html'
@ -791,6 +837,22 @@ class RegieInvoiceListView(ListView):
regie_invoice_list = RegieInvoiceListView.as_view()
class RegieInvoicePDFView(PDFMixin, DetailView):
template_name = 'lingo/invoicing/invoice.html'
pk_url_kwarg = 'invoice_pk'
model = Invoice
def dispatch(self, request, *args, **kwargs):
self.regie = get_object_or_404(Regie, pk=kwargs['regie_pk'])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return super().get_queryset().filter(regie=self.regie)
regie_invoice_pdf = RegieInvoicePDFView.as_view()
class RegieInvoiceLineListView(ListView):
template_name = 'lingo/invoicing/manager_invoice_lines.html'

View File

@ -166,6 +166,7 @@ setup(
'eopayment>=1.60',
'djangorestframework>=3.3, <3.13',
'django-filter',
'weasyprint',
],
zip_safe=False,
cmdclass={

View File

@ -3,6 +3,8 @@ import datetime
from unittest import mock
import pytest
from django.utils.formats import date_format
from django.utils.timezone import localtime
from lingo.agendas.chrono import ChronoError
from lingo.agendas.models import Agenda
@ -1211,13 +1213,13 @@ def test_detail_pool_invoices(app, admin_user, draft):
if draft:
assert (
resp.pyquery('li[data-invoice-id="%s"]' % invoice1.pk).text()
== 'Invoice TEMPORARY-%s addressed to First1 Name1 (payer:1), amount 6.00€, demat: yes, direct debit: no'
== 'Invoice TEMPORARY-%s addressed to First1 Name1 (payer:1), amount 6.00€, demat: yes, direct debit: no - download'
% invoice1.pk
)
else:
assert resp.pyquery(
'li[data-invoice-id="%s"]' % invoice1.pk
).text() == 'Invoice F%02s-%s-0000001 addressed to First1 Name1 (payer:1), amount 6.00€, demat: yes, direct debit: no' % (
).text() == 'Invoice F%02d-%s-0000001 addressed to First1 Name1 (payer:1), amount 6.00€, demat: yes, direct debit: no - download' % (
regie.pk,
invoice1.created_at.strftime('%y-%m'),
)
@ -1245,13 +1247,13 @@ def test_detail_pool_invoices(app, admin_user, draft):
if draft:
assert (
resp.pyquery('li[data-invoice-id="%s"]' % invoice2.pk).text()
== 'Invoice TEMPORARY-%s addressed to First2 Name2 (payer:2), amount 1.00€, demat: no, direct debit: yes'
== 'Invoice TEMPORARY-%s addressed to First2 Name2 (payer:2), amount 1.00€, demat: no, direct debit: yes - download'
% invoice2.pk
)
else:
assert resp.pyquery(
'li[data-invoice-id="%s"]' % invoice2.pk
).text() == 'Invoice F%02d-%s-0000002 addressed to First2 Name2 (payer:2), amount 1.00€, demat: no, direct debit: yes' % (
).text() == 'Invoice F%02d-%s-0000002 addressed to First2 Name2 (payer:2), amount 1.00€, demat: no, direct debit: yes - download' % (
regie.pk,
invoice2.created_at.strftime('%y-%m'),
)
@ -1359,6 +1361,164 @@ def test_detail_pool_invoices(app, admin_user, draft):
assert len(resp.pyquery('li.invoice')) == 2
@pytest.mark.parametrize('draft', [True, False])
def test_invoice_pdf(app, admin_user, draft):
invoice_model = DraftInvoice if draft else Invoice
line_model = DraftInvoiceLine if draft else InvoiceLine
regie = Regie.objects.create(label='Foo')
campaign = Campaign.objects.create(
regie=regie,
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
date_publication=datetime.date(2022, 10, 1),
date_payment_deadline=datetime.date(2022, 10, 31),
date_issue=datetime.date(2022, 10, 31),
date_debit=datetime.date(2022, 11, 15),
)
pool = Pool.objects.create(
campaign=campaign,
draft=draft,
status='completed',
)
invoice = invoice_model.objects.create(
date_publication=campaign.date_publication,
date_payment_deadline=campaign.date_payment_deadline,
date_issue=campaign.date_issue,
regie=regie,
pool=pool,
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
payer_demat=True,
payer_direct_debit=False,
)
if not draft:
invoice.set_number()
invoice.save()
line_model.objects.create(
event_date=datetime.date(2022, 9, 1),
invoice=invoice,
quantity=1,
unit_amount=1,
total_amount=1,
status='success',
pool=pool,
label='Label 11',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
line_model.objects.create(
event_date=datetime.date(2022, 9, 2),
invoice=invoice,
quantity=1,
unit_amount=2,
total_amount=2,
status='success',
pool=pool,
label='Label 12',
user_external_id='user:2',
user_first_name='User2',
user_last_name='Name2',
)
line_model.objects.create(
event_date=datetime.date(2022, 9, 3),
invoice=invoice,
quantity=1,
unit_amount=3,
total_amount=3,
status='success',
pool=pool,
label='Label 13',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
app = login(app)
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
% (regie.pk, campaign.pk, pool.pk, invoice.pk)
)
assert resp.pyquery('address#from').text() == 'Foo'
assert resp.pyquery('address#to').text() == 'First1 Name1'
if draft:
assert resp.pyquery('dl#informations').text() == 'Invoice number\nTEMPORARY-%s\nDate\n%s' % (
invoice.pk,
date_format(localtime(invoice.created_at), 'DATE_FORMAT'),
)
else:
assert resp.pyquery('dl#informations').text() == 'Invoice number\nF%02d-%s-0000001\nDate\n%s' % (
regie.pk,
invoice.created_at.strftime('%y-%m'),
date_format(localtime(invoice.created_at), 'DATE_FORMAT'),
)
assert len(resp.pyquery('table#lines tbody tr')) == 3
assert (
resp.pyquery('table#lines tbody tr:nth-child(1)').text() == 'Label 11\nUser1 Name1\n1.00\n1.0\n1.00'
)
assert (
resp.pyquery('table#lines tbody tr:nth-child(2)').text() == 'Label 13\nUser1 Name1\n3.00\n1.0\n3.00'
)
assert (
resp.pyquery('table#lines tbody tr:nth-child(3)').text() == 'Label 12\nUser2 Name2\n2.00\n1.0\n2.00'
)
assert resp.pyquery('table#total tbody').text() == 'Oct. 31, 2022\n6.00'
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
% (0, campaign.pk, pool.pk, invoice.pk),
status=404,
)
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
% (regie.pk, 0, pool.pk, invoice.pk),
status=404,
)
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
% (regie.pk, campaign.pk, 0, invoice.pk),
status=404,
)
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
% (regie.pk, campaign.pk, pool.pk, 0),
status=404,
)
other_regie = Regie.objects.create(label='Foo')
other_campaign = Campaign.objects.create(
regie=other_regie,
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
date_publication=datetime.date(2022, 10, 1),
date_payment_deadline=datetime.date(2022, 10, 31),
date_issue=datetime.date(2022, 10, 31),
date_debit=datetime.date(2022, 11, 15),
)
other_pool = Pool.objects.create(
campaign=other_campaign,
draft=draft,
status='completed',
)
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
% (other_regie.pk, campaign.pk, pool.pk, invoice.pk),
status=404,
)
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
% (regie.pk, other_campaign.pk, pool.pk, invoice.pk),
status=404,
)
resp = app.get(
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
% (regie.pk, campaign.pk, other_pool.pk, invoice.pk),
status=404,
)
def test_journal_pool(app, admin_user):
regie = Regie.objects.create(label='Foo')
campaign = Campaign.objects.create(

View File

@ -5,6 +5,8 @@ from urllib.parse import urlparse
import pytest
from django.contrib.auth.models import Group
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.timezone import localtime
from webtest import Upload
from lingo.agendas.models import Agenda
@ -628,7 +630,7 @@ def test_regie_invoices(app, admin_user):
assert '#%s' % orphan_line.pk not in resp
assert resp.pyquery(
'li[data-invoice-id="%s"]' % invoice1.pk
).text() == 'Invoice F%02s-%s-0000001 addressed to First1 Name1 (payer:1), amount 6.00€, demat: yes, direct debit: no' % (
).text() == 'Invoice F%02d-%s-0000001 addressed to First1 Name1 (payer:1), amount 6.00€, demat: yes, direct debit: no - download' % (
regie.pk,
invoice1.created_at.strftime('%y-%m'),
)
@ -653,7 +655,7 @@ def test_regie_invoices(app, admin_user):
)
assert resp.pyquery(
'li[data-invoice-id="%s"]' % invoice2.pk
).text() == 'Invoice F%02d-%s-0000002 addressed to First2 Name2 (payer:2), amount 1.00€, demat: no, direct debit: yes' % (
).text() == 'Invoice F%02d-%s-0000002 addressed to First2 Name2 (payer:2), amount 1.00€, demat: no, direct debit: yes - download' % (
regie.pk,
invoice2.created_at.strftime('%y-%m'),
)
@ -750,3 +752,103 @@ def test_regie_invoices(app, admin_user):
params={'user_last_name': 'name1'},
)
assert len(resp.pyquery('li.invoice')) == 2
def test_regie_invoice_pdf(app, admin_user):
regie = Regie.objects.create(label='Foo')
campaign = Campaign.objects.create(
regie=regie,
date_start=datetime.date(2022, 9, 1),
date_end=datetime.date(2022, 10, 1),
date_publication=datetime.date(2022, 10, 1),
date_payment_deadline=datetime.date(2022, 10, 31),
date_issue=datetime.date(2022, 10, 31),
date_debit=datetime.date(2022, 11, 15),
)
pool = Pool.objects.create(
campaign=campaign,
draft=False,
status='completed',
)
invoice = Invoice.objects.create(
date_publication=campaign.date_publication,
date_payment_deadline=campaign.date_payment_deadline,
date_issue=campaign.date_issue,
regie=regie,
pool=pool,
payer_external_id='payer:1',
payer_first_name='First1',
payer_last_name='Name1',
payer_demat=True,
payer_direct_debit=False,
)
invoice.set_number()
invoice.save()
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 1),
invoice=invoice,
quantity=1,
unit_amount=1,
total_amount=1,
status='success',
pool=pool,
label='Label 11',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 2),
invoice=invoice,
quantity=1,
unit_amount=2,
total_amount=2,
status='success',
pool=pool,
label='Label 12',
user_external_id='user:2',
user_first_name='User2',
user_last_name='Name2',
)
InvoiceLine.objects.create(
event_date=datetime.date(2022, 9, 3),
invoice=invoice,
quantity=1,
unit_amount=3,
total_amount=3,
status='success',
pool=pool,
label='Label 13',
user_external_id='user:1',
user_first_name='User1',
user_last_name='Name1',
)
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/invoice/%s/pdf/?html' % (regie.pk, invoice.pk))
assert resp.pyquery('address#from').text() == 'Foo'
assert resp.pyquery('address#to').text() == 'First1 Name1'
assert resp.pyquery('dl#informations').text() == 'Invoice number\nF%02d-%s-0000001\nDate\n%s' % (
regie.pk,
invoice.created_at.strftime('%y-%m'),
date_format(localtime(invoice.created_at), 'DATE_FORMAT'),
)
assert len(resp.pyquery('table#lines tbody tr')) == 3
assert (
resp.pyquery('table#lines tbody tr:nth-child(1)').text() == 'Label 11\nUser1 Name1\n1.00\n1.0\n1.00'
)
assert (
resp.pyquery('table#lines tbody tr:nth-child(2)').text() == 'Label 13\nUser1 Name1\n3.00\n1.0\n3.00'
)
assert (
resp.pyquery('table#lines tbody tr:nth-child(3)').text() == 'Label 12\nUser2 Name2\n2.00\n1.0\n2.00'
)
assert resp.pyquery('table#total tbody').text() == 'Oct. 31, 2022\n6.00'
resp = app.get('/manage/invoicing/regie/%s//invoice/%s/pdf/?html' % (0, invoice.pk), status=404)
resp = app.get('/manage/invoicing/regie/%s//invoice/%s/pdf/?html' % (regie.pk, 0), status=404)
other_regie = Regie.objects.create(label='Foo')
resp = app.get(
'/manage/invoicing/regie/%s/invoice/%s/pdf/?html' % (other_regie.pk, invoice.pk), status=404
)