invoicing: use common PDF document model for invoice and payment (#80894)
gitea/lingo/pipeline/head Build started... Details

This commit is contained in:
Frédéric Péters 2023-09-26 14:55:42 +02:00
parent d49258e16b
commit 54a169159f
5 changed files with 303 additions and 402 deletions

View File

@ -666,6 +666,7 @@ class AbstractInvoice(models.Model):
template = get_template('lingo/invoicing/invoice.html')
context = {
'regie': self.regie,
'object': self,
'invoice': self,
'lines': self.get_grouped_and_ordered_lines(),
'appearance_settings': AppearanceSettings.singleton(),
@ -1176,8 +1177,10 @@ class Payment(models.Model):
template = get_template('lingo/invoicing/payment.html')
context = {
'regie': self.regie,
'object': self,
'payment': self,
'invoice_payments': self.get_invoice_payments(),
'appearance_settings': AppearanceSettings.singleton(),
}
return template.render(context)

View File

@ -1,243 +1,81 @@
{% load static i18n lingo %}
<!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 {
padding: 0;
margin: 0;
color: #14213d;
font-family: "DejaVu Sans", sans-serif;
font-size: 9pt;
line-height: 1.2;
}
body {
margin: 0;
}
@media screen {
body {
margin: auto;
width: 21cm;
position: relative;
}
}
h1 {
color: #df5a13;
margin: 0.4em 0;
font-size: 12pt;
}
h2 {
margin: 0.4em 0;
font-size: 10pt;
}
header {
/* top of page, addresses, can be folded */
position: absolute;
top: 0;
left: 0;
width: 100%;
}
#logo {
max-height: 4cm;
max-width: 4cm;
}
main {
/* page content, after the fold */
padding-top: 9cm;
}
header aside {
position: absolute;
top: 0;
left: 0;
width: 30%;
}
header > div {
margin-left: 30%;
}
address {
font-style: normal;
}
address#to {
position: absolute;
box-sizing: border-box;
padding: 0.5cm;
top: 3cm; /* in addition to page margin (2cm) */
left: 8cm;
width: 10cm;
height: 5cm;
background: #fafafa;
}
#extra-info {
}
{% extends "lingo/invoicing/print_document_base.html" %}
{% load i18n lingo %}
dl#informations {
margin-left: 30%; /* same as header/aside width */
margin-bottom: 1cm;
}
dt, dd {
display: inline;
margin: 0;
}
dt {
color: #a9a;
}
dt::before {
content: '';
display: block;
}
table {
border-collapse: collapse;
width: 100%;
}
tr.new-event {
border-top: .2mm solid;
}
th {
border-bottom: .2mm solid #14213d;
color: #a9a;
font-size: 9pt;
font-weight: 400;
padding-bottom: .25cm;
text-transform: uppercase;
}
td.total-amount {
font-weight: bold;
}
tr.grand-total {
background: #eee;
border-top: 0.5cm solid white;
}
tr.grand-total td {
padding-top: 0.5cm;
padding-bottom: 0.5cm;
}
td.grand-total-intro {
text-align: right;
}
td.details {
vertical-align: top;
}
th.amount, td.amount, th.quantity, td.quantity {
padding-right: 0.2cm;
text-align: right;
}
div.break {
break-before: page;
}
#regie-custom-text {
margin: auto;
margin-top: 2cm;
max-width: 55em;
}
@page {
margin: 1cm;
@bottom-right{
content: "{% trans "Page" %} " counter(page) "/" counter(pages);
}
}
</style>
</head>
<body>
<header>
<aside>
{% if appearance_settings.logo %}
<img id="logo" src="data:{{appearance_settings.logo.content_type}};base64,{{ appearance_settings.logo_base64_encoded }}">
{% endif %}
{% if appearance_settings.address %}
<address id="from">
{{ appearance_settings.address|safe }}
</address>
{% endif %}
{% if appearance_settings.extra_info %}
<div id="extra-info">
{{ appearance_settings.extra_info|safe }}
</div>
{% endif %}
</aside>
<div>
<h1 id="invoice-label">{{ invoice.label }}</h1>
<h2 id="regie-label">{{ regie.label }}</h2>
<address id="to">
{{ invoice.payer_first_name}} {{ invoice.payer_last_name}}
<br />
{{ invoice.payer_address|linebreaksbr }}
</address>
</div>
</header>
<main>
<dl id="informations">
<dt>{% trans "Invoice number:" %}</dt>
<dd>{{ invoice.formatted_number }}</dd>
<dt>{% trans "Date:" %}</dt>
<dd>{{ invoice.created_at|date }}</dd>
</dl>
{% for line in lines %}
{% ifchanged line.user_external_id %}
{% if not forloop.first %}
</tbody>
</table>
{% endif %}
<h2 class="user-name">{{ line.user_name }}</h2>
<table id="lines">
<thead>
<tr>
<th class="description">{% trans "Description" %}</th>
<th class="details">{% trans "Details" %}</th>
<th class="amount unit-amount">{% trans "UA" context 'unit amount' %}</th>
<th class="quantity">{% trans "QTY" context 'quantity' %}</th>
<th class="amount total-amount">{% trans "TA" context 'total amount' %}</th>
</tr>
</thead>
<tbody>
{% endifchanged %}
<tr {% if not line.details %}class="new-event"{% else %}{% ifchanged line.label %}class="new-event"{% endifchanged %}{% endif %}>
<td class="description">
{% if line.details %}
{% ifchanged line.user_external_id line.label %}
{{ line.label }}
<br />
{% if line.agenda %}
<i>{{ line.agenda.label }}</i>
<br />
{% endif %}
{% endifchanged %}
{% else %}
{{ line.event_date|date:"d/m/Y" }} - {{ line.label }}
{% block document-label %}{{ invoice.label }}{% endblock %}
{% block informations %}
<dt>{% trans "Invoice number:" %}</dt>
<dd>{{ invoice.formatted_number }}</dd>
<dt>{% trans "Date:" %}</dt>
<dd>{{ invoice.created_at|date }}</dd>
{% endblock %}
{% block content %}
{% for line in lines %}
{% ifchanged line.user_external_id %}
{% if not forloop.first %}
</tbody>
</table>
{% endif %}
<h2 class="user-name">{{ line.user_name }}</h2>
<table id="lines">
<thead>
<tr>
<th class="description">{% trans "Description" %}</th>
<th class="details">{% trans "Details" %}</th>
<th class="amount unit-amount">{% trans "UA" context 'unit amount' %}</th>
<th class="quantity">{% trans "QTY" context 'quantity' %}</th>
<th class="amount total-amount">{% trans "TA" context 'total amount' %}</th>
</tr>
</thead>
<tbody>
{% endifchanged %}
<tr {% if not line.details %}class="new-event"{% else %}{% ifchanged line.label %}class="new-event"{% endifchanged %}{% endif %}>
<td class="description">
{% if line.details %}
{% ifchanged line.user_external_id line.label %}
{{ line.label }}
<br />
{% if line.agenda %}
<i>{{ line.agenda.label }}</i>
<br />
{% if line.agenda %}
<i>{{ line.agenda.label }}</i>
<br />
{% endif %}
{% endif %}
{% if line.details.check_type_label %}
{{ line.details.check_type_label }}
{% elif line.details.status == 'absence' %}
{% trans "Absence" %}
{% endif %}
</td>
<td class="details">
{% if line.details.dates and not line.agenda.partial_bookings %}
{% for d in line.details.dates %}{{ d|parse_date|date:"Dd" }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% endif %}
</td>
{% if line.details.check_type_label or line.details.status != 'absence' %}
<td class="amount unit-amount">{% blocktrans with amount=line.unit_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="quantity">{{ line.quantity|floatformat }}</td>
<td class="amount total-amount">{% blocktrans with amount=line.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
{% endifchanged %}
{% else %}
{{ line.event_date|date:"d/m/Y" }} - {{ line.label }}
<br />
{% if line.agenda %}
<i>{{ line.agenda.label }}</i>
<br />
{% endif %}
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr class="grand-total">
<td class="grand-total-intro" colspan="4">
{% trans "Total amount to be paid before:" %} <strong id="payment-deadline">{{ invoice.date_payment_deadline }}</strong>
</td>
<td class="amount total-amount">{% blocktrans with amount=invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
</tfoot>
{% endif %}
{% if line.details.check_type_label %}
{{ line.details.check_type_label }}
{% elif line.details.status == 'absence' %}
{% trans "Absence" %}
{% endif %}
</td>
<td class="details">
{% if line.details.dates and not line.agenda.partial_bookings %}
{% for d in line.details.dates %}{{ d|parse_date|date:"Dd" }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% endif %}
</td>
{% if line.details.check_type_label or line.details.status != 'absence' %}
<td class="amount unit-amount">{% blocktrans with amount=line.unit_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="quantity">{{ line.quantity|floatformat }}</td>
<td class="amount total-amount">{% blocktrans with amount=line.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr class="grand-total">
<td class="grand-total-intro" colspan="4">
{% trans "Total amount to be paid before:" %} <strong id="payment-deadline">{{ invoice.date_payment_deadline }}</strong>
</td>
<td class="amount total-amount">{% blocktrans with amount=invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
</tfoot>
</table>
{% if regie.invoice_custom_text %}
@ -245,6 +83,4 @@
{{ regie.invoice_custom_text|safe }}
</div>
{% endif %}
</main>
</body>
</html>
{% endblock %}

View File

@ -1,160 +1,41 @@
{% load static i18n lingo %}
<!doctype html><html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{{ payment.formatted_number }}</title>
<meta name="author" content="Entr'ouvert">
<style>
html {
color: #14213d;
font-family: Source Sans Pro;
font-size: 8pt;
line-height: 1.2;
}
body {
margin: 0;
}
{% extends "lingo/invoicing/print_document_base.html" %}
{% load i18n lingo %}
h1 {
color: #df5a13;
font-family: Pacifico;
margin: 0.4em 0;
font-size: 12pt;
}
h2 {
margin: 2em 0;
font-size: 10pt;
}
{% block document-label %}{% trans "Payment receipt" %}{% endblock %}
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;
}
{% block informations %}
<dt>{% trans "Payment number:" %}</dt>
<dd>{{ payment.formatted_number }}</dd>
<dt>{% trans "Date:" %}</dt>
<dd>{{ payment.created_at|date }}</dd>
</dl>
{% endblock %}
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: 9pt;
font-weight: 400;
padding-bottom: .25cm;
text-transform: uppercase;
}
td {
padding-top: 7mm;
}
th.amount, td.amount {
text-align: right;
}
table#total {
background: #f6f6f6;
border-color: #f6f6f6;
border-style: solid;
border-width: 2cm 3cm;
bottom: 0;
right: 0;
font-size: 10pt;
margin: 0 -3cm;
position: absolute;
width: 12cm;
text-align: right;
}
div.break {
break-before: page;
}
@page {
@bottom-right{
content: "{% trans "Page" %} " counter(page) "/" counter(pages);
}
}
</style>
</head>
<body>
<h1>{% trans "Payment receipt" %}</h1>
<aside>
<address id="from">
{{ regie.label }}
</address>
<address id="to">
{{ payment.payer_first_name}} {{ payment.payer_last_name}}
<br />
{{ payment.payer_address|linebreaksbr }}
</address>
</aside>
<dl id="informations">
<dt>{% trans "Payment number" %}</dt>
<dd>{{ payment.formatted_number }}</dd>
<dt>{% trans "Date" %}</dt>
<dd>{{ payment.created_at|date }}</dd>
</dl>
<table id="lines">
<thead>
{% block content %}
<table id="lines">
<thead>
<tr>
<th class="invoice">{% trans "Invoice number" %}</th>
<th class="object">{% trans "Invoice object" %}</th>
<th class="amount">{% trans "Amount charged" %}</th>
<th class="amount">{% trans "Amount assigned" %}</th>
</tr>
</thead>
<tbody>
{% for invoice_payment in invoice_payments %}
<tr>
<th class="invoice">{% trans "Invoice number" %}</th>
<th class="object">{% trans "Invoice object" %}</th>
<th class="amount">{% trans "Amount charged" %}</th>
<th class="amount">{% trans "Amount assigned" %}</th>
<td class="invoice">{{ invoice_payment.invoice.formatted_number }}</td>
<td class="object">{{ invoice_payment.invoice.label }}</td>
<td class="amount">{% blocktrans with amount=invoice_payment.invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="amount">{% blocktrans with amount=invoice_payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
</thead>
<tbody>
{% for invoice_payment in invoice_payments %}
<tr>
<td class="invoice">{{ invoice_payment.invoice.formatted_number }}</td>
<td class="object">{{ invoice_payment.invoice.label }}</td>
<td class="amount">{% blocktrans with amount=invoice_payment.invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
<td class="amount">{% blocktrans with amount=invoice_payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table id="total">
<thead>
<tr>
<th>{% trans "Payment type" %}</th>
<th>{% trans "Total" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ payment.payment_type }}</td>
<td>{% blocktrans with amount=payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
</tbody>
</table>
</body>
</html>
{% endfor %}
</tbody>
<tfoot>
<tr class="grand-total">
<td class="grand-total-intro" colspan="3">{% trans "Payment type:" %} {{ payment.payment_type }}</td>
<td class="amount total-amount">{% blocktrans with amount=payment.amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
</tr>
</tfoot>
</table>
{% endblock %}

View File

@ -0,0 +1,182 @@
{% load static i18n lingo %}<!DOCTYPE html><html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{% block title %}{{ object.formatted_number }}{% endblock %}</title>
<meta name="author" content="Entr'ouvert">
<style>
{% block css %}
html {
padding: 0;
margin: 0;
color: #14213d;
font-family: "DejaVu Sans", sans-serif;
font-size: 9pt;
line-height: 1.2;
}
body {
margin: 0;
}
@media screen {
body {
margin: auto;
width: 21cm;
position: relative;
}
}
h1 {
color: #df5a13;
margin: 0.4em 0;
font-size: 12pt;
}
h2 {
margin: 0.4em 0;
font-size: 10pt;
}
header {
/* top of page, addresses, can be folded */
position: absolute;
top: 0;
left: 0;
width: 100%;
}
#logo {
max-height: 4cm;
max-width: 4cm;
}
main {
/* page content, after the fold */
padding-top: 9cm;
}
header aside {
position: absolute;
top: 0;
left: 0;
width: 30%;
}
header > div {
margin-left: 30%;
}
address {
font-style: normal;
}
address#to {
position: absolute;
box-sizing: border-box;
padding: 0.5cm;
top: 3cm; /* in addition to page margin (2cm) */
left: 8cm;
width: 10cm;
height: 5cm;
background: #fafafa;
}
#extra-info {
}
dl#informations {
margin-left: 30%; /* same as header/aside width */
margin-bottom: 1cm;
}
dt, dd {
display: inline;
margin: 0;
}
dt {
color: #a9a;
}
dt::before {
content: '';
display: block;
}
table {
border-collapse: collapse;
width: 100%;
}
tr.new-event {
border-top: .2mm solid;
}
th {
border-bottom: .2mm solid #14213d;
color: #a9a;
font-size: 9pt;
font-weight: 400;
padding-bottom: .25cm;
text-transform: uppercase;
}
td.total-amount {
font-weight: bold;
}
tr.grand-total {
background: #eee;
border-top: 0.5cm solid white;
}
tr.grand-total td {
padding-top: 0.5cm;
padding-bottom: 0.5cm;
}
td.grand-total-intro {
text-align: right;
}
td.details {
vertical-align: top;
}
th.amount, td.amount, th.quantity, td.quantity {
padding-right: 0.2cm;
text-align: right;
}
div.break {
break-before: page;
}
#regie-custom-text {
margin: auto;
margin-top: 2cm;
max-width: 55em;
}
@page {
margin: 1cm;
@bottom-right{
content: "{% trans "Page" %} " counter(page) "/" counter(pages);
}
}
{% endblock %}
</style>
</head>
<body>
<header>
<aside>
{% if appearance_settings.logo %}
<img id="logo" src="data:{{appearance_settings.logo.content_type}};base64,{{ appearance_settings.logo_base64_encoded }}">
{% endif %}
{% if appearance_settings.address %}
<address id="from">
{{ appearance_settings.address|safe }}
</address>
{% endif %}
{% if appearance_settings.extra_info %}
<div id="extra-info">
{{ appearance_settings.extra_info|safe }}
</div>
{% endif %}
</aside>
<div>
<h1 id="document-label">{% block document-label %}{% endblock %}</h1>
<h2 id="regie-label">{{ regie.label }}</h2>
<address id="to">
{% block address_to %}
{{ object.payer_first_name}} {{ object.payer_last_name}}
<br />
{{ object.payer_address|linebreaksbr }}
{% endblock %}
</address>
</div>
</header>
<main>
<dl id="informations">
{% block informations %}
{% endblock %}
</dl>
{% block content %}
{% endblock %}
</main>
</body>
</html>

View File

@ -1261,7 +1261,7 @@ def test_regie_invoice_pdf(app, admin_user):
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/invoice/%s/pdf/?html' % (regie.pk, invoice.pk))
assert resp.pyquery('#invoice-label').text() == 'Invoice from 01/09/2022 to 30/09/2022'
assert resp.pyquery('#document-label').text() == 'Invoice from 01/09/2022 to 30/09/2022'
assert resp.pyquery('#regie-label').text() == 'Foo'
assert resp.pyquery('address#to').text() == 'First1 Name1\n41 rue des kangourous\n99999 Kangourou Ville'
assert resp.pyquery('dl#informations').text() == 'Invoice number:\nF%02d-%s-0000001\nDate:\n%s' % (
@ -1804,17 +1804,16 @@ def test_regie_payment_pdf(app, admin_user):
app = login(app)
resp = app.get('/manage/invoicing/regie/%s/payment/%s/pdf/?html' % (regie.pk, payment.pk))
assert resp.pyquery('h1').text() == 'Payment receipt'
assert resp.pyquery('address#from').text() == 'Foo'
assert resp.pyquery('#document-label').text() == 'Payment receipt'
assert resp.pyquery('#regie-label').text() == 'Foo'
assert resp.pyquery('address#to').text() == 'First1 Name1\n41 rue des kangourous\n99999 Kangourou Ville'
assert resp.pyquery('dl#informations').text() == 'Payment number\nR%02d-%s-0000001\nDate\n%s' % (
assert resp.pyquery('dl#informations').text() == 'Payment number:\nR%02d-%s-0000001\nDate:\n%s' % (
regie.pk,
payment.created_at.strftime('%y-%m'),
date_format(localtime(payment.created_at), 'DATE_FORMAT'),
)
assert [PyQuery(tr).text() for tr in resp.pyquery.find('thead tr')] == [
'Invoice number\nInvoice object\nAmount charged\nAmount assigned',
'Payment type\nTotal',
'Invoice number\nInvoice object\nAmount charged\nAmount assigned'
]
assert [PyQuery(tr).text() for tr in resp.pyquery.find('tbody tr')] == [
'F%02d-%s-0000001\n40.00€\n5.00€'
@ -1827,9 +1826,9 @@ def test_regie_payment_pdf(app, admin_user):
regie.pk,
invoice2.created_at.strftime('%y-%m'),
),
'Check\n55.00€',
]
assert resp.pyquery('table#total tbody').text() == 'Check\n55.00€'
assert resp.pyquery('.grand-total-intro').text() == 'Payment type: Check'
assert resp.pyquery('tfoot .total-amount').text() == '55.00€'
resp = app.get('/manage/invoicing/regie/%s//payment/%s/pdf/?html' % (0, payment.pk), status=404)
resp = app.get('/manage/invoicing/regie/%s//payment/%s/pdf/?html' % (regie.pk, 0), status=404)