manager: timesheet to PDF (#61070)

This commit is contained in:
Lauréline Guérin 2022-02-18 11:39:17 +01:00
parent 1f255d7ab9
commit 8ebbbfdfb7
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
10 changed files with 153 additions and 32 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ local_settings.py
/dist
/chrono.egg-info
/chrono/manager/static/css/style.css
/chrono/manager/static/css/timesheet.css

View File

@ -376,6 +376,14 @@ class EventsTimesheetForm(forms.Form):
required=False,
help_text=_('Comma separated list of keys defined in extra_data.'),
)
orientation = forms.ChoiceField(
label=_('PDF orientation'),
choices=[
('portrait', _('Portrait')),
('landscape', _('Landscape')),
],
initial='portrait',
)
def __init__(self, *args, **kwargs):
self.agenda = kwargs.pop('agenda')

View File

@ -0,0 +1,38 @@
@charset "UTF-8";
@page {
margin: 0.5cm;
margin-bottom: 1cm;
@bottom-right {
font-size: 10pt;
content: counter(page) " / " counter(pages);
height: 1cm;
text-align: right;
width: 2cm;
}
}
body {
font-family: sans-serif;
font-size: 8pt;
}
table.timesheet {
border-collapse: collapse;
th {
padding: 0.5em 0.5ex;
border: 0.5px solid black;
&.date {
width: 30px;
text-align: center;
}
}
td {
border: 0.5px solid black;
padding: 0.5em 0.5ex;
&.date {
text-align: center;
padding: 0px;
max-width: 2em;
}
}
}

View File

@ -1,5 +1,5 @@
{% extends "chrono/manager_agenda_view.html" %}
{% load i18n chrono %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
@ -12,44 +12,18 @@
<div class="section">
<h3>{% trans "Timesheet configuration" %}</h3>
<div>
<form>
<form id="timesheet">
{{ form.as_p }}
<button class="submit-button">{% trans "See timesheet" %}</button>
{% if request.GET and form.is_valid %}
<button class="submit-button" name="pdf">{% trans "Get PDF file" %}</button>
{% endif %}
</form>
{% if request.GET and form.is_valid %}
<h4>{% blocktrans with start=form.cleaned_data.date_start end=form.cleaned_data.date_end %}Timesheet from {{ start }} to {{ end }}{% endblocktrans %}</h4>
{% with slots=form.get_slots %}
{% with events_num=slots.events|length %}
<table class="main timesheet">
<thead>
<tr>
<th>{% trans "First name" %}</th>
<th>{% trans "Last name" %}</th>
{% for k in slots.extra_data %}<th>{{ k }}</th>{% endfor %}
{% if events_num > 1 %}<th>{% trans "Activity" %}</th>{% endif %}
{% for date in slots.dates %}<th class="date">{{ date|date:"D d/m" }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for user in slots.users %}{% for event in user.events %}
<tr>
{% if forloop.first %}
<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_first_name }}</td>
<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_last_name }}</td>
{% for k in slots.extra_data %}<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.extra_data|get:k }}</td>{% endfor %}
{% endif %}
{% if events_num > 1 %}<td>{{ event.event }}</td>{% endif %}
{% for date in slots.dates %}
{% with booked=event.dates|get:date %}<td class="date">{% if booked is True %}☐{% elif booked is None %}-{% endif %}</td>{% endwith %}
{% endfor %}
</tr>
{% endfor %}{% endfor %}
</tbody>
</table>
{% endwith %}
{% endwith %}
{% include 'chrono/manager_events_timesheet_fragment.html' %}
{% endif %}
</div>
</div>

View File

@ -0,0 +1,32 @@
{% load i18n chrono %}
{% with slots=form.get_slots %}
{% with events_num=slots.events|length %}
<table class="main timesheet">
<thead>
<tr>
<th>{% trans "First name" %}</th>
<th>{% trans "Last name" %}</th>
{% for k in slots.extra_data %}<th>{{ k }}</th>{% endfor %}
{% if events_num > 1 %}<th>{% trans "Activity" %}</th>{% endif %}
{% for date in slots.dates %}<th class="date">{{ date|date:"D d/m" }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for user in slots.users %}{% for event in user.events %}
<tr>
{% if forloop.first %}
<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_first_name }}</td>
<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_last_name }}</td>
{% for k in slots.extra_data %}<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.extra_data|get:k }}</td>{% endfor %}
{% endif %}
{% if events_num > 1 %}<td>{{ event.event }}</td>{% endif %}
{% for date in slots.dates %}
{% with booked=event.dates|get:date %}<td class="date">{% if booked is True %}☐{% elif booked is None %}-{% endif %}</td>{% endwith %}
{% endfor %}
</tr>
{% endfor %}{% endfor %}
</tbody>
</table>
{% endwith %}
{% endwith %}

View File

@ -0,0 +1,22 @@
{% load static i18n %}
<html>
<head>
<base href="{{ base_uri }}" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{% blocktrans with start=form.cleaned_data.date_start end=form.cleaned_data.date_end %}Timesheet from {{ start }} to {{ end }}{% endblocktrans %}</title>
<meta name="author" content="Entr'ouvert" />
<link href="{% static 'css/timesheet.css' %}" media="print" rel="stylesheet" />
<style media="print">
@page {
size: {{ form.cleaned_data.orientation|default:"portrait" }};
}
</style>
</head>
<body>
<div id="main">
{% if request.GET and form.is_valid %}
{% include 'chrono/manager_events_timesheet_fragment.html' %}
{% endif %}
</div>
</body>
</html>

View File

@ -54,6 +54,7 @@ from django.views.generic import (
UpdateView,
View,
)
from weasyprint import HTML
from chrono.agendas.models import (
AbsenceReason,
@ -1972,6 +1973,27 @@ class EventsTimesheetView(ViewableAgendaMixin, DetailView):
context['form'] = form
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
if 'pdf' in request.GET and context['form'].is_valid():
return self.pdf(request, context)
return self.render_to_response(context)
def pdf(self, request, context):
context['base_uri'] = request.build_absolute_uri('/')
html = HTML(
string=render_to_string('chrono/manager_events_timesheet_pdf.html', context, request=request)
)
pdf = html.write_pdf()
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="timesheet_{}_{}_{}.pdf"'.format(
self.agenda.slug,
context['form'].cleaned_data['date_start'].strftime('%Y-%m-%d'),
context['form'].cleaned_data['date_end'].strftime('%Y-%m-%d'),
)
return response
events_timesheet = EventsTimesheetView.as_view()

1
debian/control vendored
View File

@ -25,6 +25,7 @@ Depends: ${misc:Depends},
python3-psycopg2,
python3-django-mellon,
python3-dateutil,
python3-weasyprint,
uwsgi,
uwsgi-plugin-python3
Recommends: nginx,

View File

@ -167,6 +167,7 @@ setup(
'python-dateutil',
'requests',
'workalendar',
'weasyprint<0.43',
],
zip_safe=False,
cmdclass={

View File

@ -2565,3 +2565,25 @@ def test_events_timesheet_extra_data(app, admin_user):
assert slots['extra_data'] == ['foo', 'baz']
assert slots['users'][0]['extra_data']['foo'] == 'baz'
assert slots['users'][0]['extra_data']['baz'] == ''
def test_events_timesheet_pdf(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')
login(app)
resp = app.get(
'/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&orientation=portrait'
% agenda.pk
)
assert resp.headers['Content-Type'] == 'application/pdf'
assert (
resp.headers['Content-Disposition']
== 'attachment; filename="timesheet_events_2022-02-01_2022-02-28.pdf"'
)
# form invalid
resp = app.get(
'/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28'
% agenda.pk
)
assert resp.context['form'].errors['orientation'] == ['This field is required.']