manager: timesheet to PDF (#61070)
parent
1f255d7ab9
commit
8ebbbfdfb7
|
@ -4,3 +4,4 @@ local_settings.py
|
|||
/dist
|
||||
/chrono.egg-info
|
||||
/chrono/manager/static/css/style.css
|
||||
/chrono/manager/static/css/timesheet.css
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ Depends: ${misc:Depends},
|
|||
python3-psycopg2,
|
||||
python3-django-mellon,
|
||||
python3-dateutil,
|
||||
python3-weasyprint,
|
||||
uwsgi,
|
||||
uwsgi-plugin-python3
|
||||
Recommends: nginx,
|
||||
|
|
1
setup.py
1
setup.py
|
@ -167,6 +167,7 @@ setup(
|
|||
'python-dateutil',
|
||||
'requests',
|
||||
'workalendar',
|
||||
'weasyprint<0.43',
|
||||
],
|
||||
zip_safe=False,
|
||||
cmdclass={
|
||||
|
|
|
@ -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.']
|
||||
|
|
Loading…
Reference in New Issue