eo_conges: add new app to report worker holidays (#72676)
This commit is contained in:
parent
0bc94be4ca
commit
7f25841bce
|
@ -16,6 +16,7 @@ Depends: ${misc:Depends}, ${python3:Depends},
|
|||
python3-gadjo,
|
||||
python3-django-mellon,
|
||||
weasyprint,
|
||||
python3-caldav,
|
||||
Description: Logiciel de compta/facturation interne
|
||||
|
||||
Package: barbacompta
|
||||
|
|
|
@ -21,9 +21,12 @@ from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
|
|||
from django.contrib.auth.admin import GroupAdmin, UserAdmin
|
||||
from django.contrib.auth.models import Group
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import path
|
||||
from django.utils.http import urlencode
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
from .eo_conges.views import workers_monthly_report, workers_monthly_report_redirect
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
|
@ -46,6 +49,17 @@ class EOGestionAdminSite(admin.AdminSite):
|
|||
return HttpResponseRedirect(url)
|
||||
return super().logout(request, extra_context=extra_context)
|
||||
|
||||
def get_urls(self):
|
||||
urls = [
|
||||
path('conges/', self.admin_view(workers_monthly_report_redirect)),
|
||||
path(
|
||||
'conges/<int:year>/<int:month>/',
|
||||
self.admin_view(workers_monthly_report),
|
||||
name='conges-workers-monthly-report',
|
||||
),
|
||||
]
|
||||
return urls + super().get_urls()
|
||||
|
||||
|
||||
site = EOGestionAdminSite()
|
||||
site.enable_nav_sidebar = False
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.worker span {
|
||||
display: block;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "admin/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "css/conges.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<a href="{{ view.get_previous_month_url }}">Mois précédent</span></a> -
|
||||
<a href="{{ view.get_next_month_url }}">Mois suivant</span></a>
|
||||
|
||||
{% if fatal_error %}
|
||||
<p>{{ fatal_error }}</p>
|
||||
{% else %}
|
||||
{% if errors %}
|
||||
<h2>Attention, des erreurs ont été rencontrées :</h2>
|
||||
{% for error in errors %}
|
||||
<p>{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<h2>Rapport :</h2>
|
||||
{% for worker in workers %}
|
||||
<p class='worker'>
|
||||
<span>{{ worker.display_name }} (matricule {{ worker.number }})</span>
|
||||
<span>Tickets restaurant : 19 tickets de 8 euros</span>
|
||||
{% if worker.is_parisian %}<span>Carte transport Paris 50%</span>{% endif %}
|
||||
<span class='holidays'>Congés : {% firstof worker.holidays|join:', ' '0' %}.</span>
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,271 @@
|
|||
import dataclasses
|
||||
import datetime
|
||||
|
||||
import caldav
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.text import slugify
|
||||
from django.views.generic.base import RedirectView, TemplateView
|
||||
from django.views.generic.dates import MonthMixin, YearMixin
|
||||
from lxml.etree import XMLSyntaxError
|
||||
|
||||
|
||||
class ReportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Holiday:
|
||||
start: datetime.date
|
||||
end: datetime.date
|
||||
half: bool = False
|
||||
|
||||
def __str__(self):
|
||||
if self.half:
|
||||
return '0,5j le %s' % date_format(self.start, 'd/m')
|
||||
elif self.start == self.end:
|
||||
return 'le %s' % date_format(self.start, 'd/m')
|
||||
elif (self.end - self.start).days == 1:
|
||||
return 'les %s et %s' % (date_format(self.start, 'd'), date_format(self.end, 'd/m'))
|
||||
|
||||
return 'du %s au %s' % (date_format(self.start, 'd'), date_format(self.end, 'd/m'))
|
||||
|
||||
|
||||
class WorkersMonthlyReportRedirect(RedirectView):
|
||||
pattern_name = 'admin:conges-workers-monthly-report'
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
today = datetime.date.today()
|
||||
kwargs['year'] = today.year
|
||||
kwargs['month'] = today.month
|
||||
return super().get_redirect_url(*args, **kwargs)
|
||||
|
||||
|
||||
workers_monthly_report_redirect = WorkersMonthlyReportRedirect.as_view()
|
||||
|
||||
|
||||
class WorkersMonthlyReport(MonthMixin, YearMixin, TemplateView):
|
||||
template_name = 'eo_conges/workers_monthly_report.html'
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
try:
|
||||
self.date = datetime.date(self.get_year(), self.get_month(), 1)
|
||||
except ValueError:
|
||||
raise Http404()
|
||||
|
||||
self.last_date = (self.date + datetime.timedelta(days=40)).replace(day=1) - datetime.timedelta(days=1)
|
||||
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def get_previous_month_url(self):
|
||||
previous_month = self.date - datetime.timedelta(days=1)
|
||||
return reverse(
|
||||
'admin:conges-workers-monthly-report',
|
||||
kwargs={'year': previous_month.year, 'month': previous_month.month},
|
||||
)
|
||||
|
||||
def get_next_month_url(self):
|
||||
next_month = self.last_date + datetime.timedelta(days=1)
|
||||
return reverse(
|
||||
'admin:conges-workers-monthly-report', kwargs={'year': next_month.year, 'month': next_month.month}
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['errors'] = self.errors = []
|
||||
try:
|
||||
context['workers'] = self.build_workers_data()
|
||||
except ReportError as e:
|
||||
context['fatal_error'] = e
|
||||
|
||||
return context
|
||||
|
||||
def get_events(self):
|
||||
config = settings.WORKERS_CALENDAR_CONFIG
|
||||
client = caldav.DAVClient(config['url'], username=config['username'], password=config['password'])
|
||||
try:
|
||||
calendar = client.principal().calendar(name=config['calendar'])
|
||||
except caldav.error.DAVError as e:
|
||||
raise ReportError('Erreur lors de la récupération du calendrier : %s' % e)
|
||||
|
||||
try:
|
||||
events = calendar.search(event=True, start=self.date, end=self.last_date)
|
||||
except caldav.error.DAVError as e:
|
||||
raise ReportError('Erreur lors de la récupération du calendrier : %s' % e)
|
||||
except XMLSyntaxError:
|
||||
try:
|
||||
events = calendar.search(event=True, start=self.date, end=self.last_date)
|
||||
except XMLSyntaxError:
|
||||
raise ReportError('Erreur lors de la récupération du calendrier : %s' % e)
|
||||
|
||||
return [x.vobject_instance.vevent for x in events]
|
||||
|
||||
def build_workers_data(self):
|
||||
events = self.get_events()
|
||||
holidays_events = [x for x in events if self.is_holiday(x)]
|
||||
day_off_events = [x for x in events if self.is_day_off(x)]
|
||||
|
||||
workers_data = [
|
||||
self.get_data_for_worker(worker, day_off_events, holidays_events)
|
||||
for worker in settings.WORKERS_CALENDAR_CONFIG['workers']
|
||||
]
|
||||
workers_data = [x for x in workers_data if not x['ignore']]
|
||||
|
||||
events_no_match = [
|
||||
x.summary.value for x in holidays_events + day_off_events if not hasattr(x, 'match')
|
||||
]
|
||||
if events_no_match:
|
||||
self.errors.append(
|
||||
'Aucun·e coopérateurice trouvé·e pour les évènements : %s.' % ', '.join(events_no_match)
|
||||
)
|
||||
|
||||
return sorted(workers_data, key=lambda x: x['number'])
|
||||
|
||||
def is_day_off(self, event):
|
||||
if not hasattr(event, 'categories'):
|
||||
return False
|
||||
|
||||
return bool(settings.WORKERS_CALENDAR_CONFIG['day_off_category'] in event.categories.value)
|
||||
|
||||
def is_holiday(self, event):
|
||||
if not hasattr(event, 'categories'):
|
||||
return False
|
||||
|
||||
return bool(settings.WORKERS_CALENDAR_CONFIG['holiday_category'] in event.categories.value)
|
||||
|
||||
def get_data_for_worker(self, worker, day_off_events, holidays_events):
|
||||
name = worker['display_name']
|
||||
first_name, last_name = name.split()
|
||||
worker_possible_names = [
|
||||
worker['username'], # jdupont
|
||||
first_name, # Jean
|
||||
first_name + last_name[0], # JeanD
|
||||
first_name[0] + last_name[0], # JD
|
||||
]
|
||||
worker_possible_names.extend(worker.get('aliases', [])) # ponpon
|
||||
worker_possible_names = [slugify(x) for x in worker_possible_names]
|
||||
|
||||
errors = []
|
||||
day_off = None
|
||||
days_off = [x for x in day_off_events if self.is_event_for_worker(x, worker_possible_names)]
|
||||
if len(days_off) > 1:
|
||||
errors.append(
|
||||
'%s a plusieurs jours off : %s.' % (name, ', '.join(x.summary.value for x in days_off))
|
||||
)
|
||||
elif not days_off:
|
||||
errors.append("%s n'a pas de jour off." % name)
|
||||
else:
|
||||
day_off = days_off[0].getrruleset()[0].weekday()
|
||||
|
||||
def get_date_end(event):
|
||||
if hasattr(event, 'rrule'):
|
||||
return event.getrruleset()[-1].date() + datetime.timedelta(days=1)
|
||||
|
||||
return event.dtend.value
|
||||
|
||||
holidays = [
|
||||
(x.dtstart.value, get_date_end(x))
|
||||
for x in holidays_events
|
||||
if self.is_event_for_worker(x, worker_possible_names)
|
||||
]
|
||||
holidays = self.normalize_holidays(holidays, day_off)
|
||||
|
||||
ignore = worker.get('ignore', False)
|
||||
if not ignore:
|
||||
self.errors.extend(errors)
|
||||
|
||||
return {
|
||||
'number': worker['number'],
|
||||
'ignore': ignore,
|
||||
'is_parisian': worker.get('is_parisian', False),
|
||||
'display_name': name,
|
||||
'holidays': holidays,
|
||||
}
|
||||
|
||||
def is_event_for_worker(self, event, worker_possible_names):
|
||||
splitted_summary = event.summary.value.split()
|
||||
|
||||
worker_match = any(slugify(x) in worker_possible_names for x in splitted_summary)
|
||||
if worker_match:
|
||||
if hasattr(event, 'match'):
|
||||
self.errors.append(
|
||||
'L\'évènement %s correspond à plusieurs coopérateurices : %s et %s.'
|
||||
% (event.summary.value, worker_possible_names[0], event.match)
|
||||
)
|
||||
event.__dict__['match'] = worker_possible_names[0]
|
||||
|
||||
return worker_match
|
||||
|
||||
def normalize_holidays(self, worker_holidays, day_off=None):
|
||||
def make_holiday(start, end):
|
||||
if type(start) != type(end):
|
||||
start = start.date() if hasattr(start, 'date') else start
|
||||
end = end.date() if hasattr(end, 'date') else end
|
||||
|
||||
half = False
|
||||
if isinstance(start, datetime.datetime):
|
||||
if start.date() == end.date():
|
||||
if start.time() > datetime.time(12, 00):
|
||||
half = True
|
||||
elif end.time() <= datetime.time(15, 00):
|
||||
half = True
|
||||
start = start.date()
|
||||
end = end.date()
|
||||
else:
|
||||
end -= datetime.timedelta(days=1)
|
||||
|
||||
start = max(start, self.date)
|
||||
end = min(end, self.last_date)
|
||||
|
||||
return Holiday(start, end, half)
|
||||
|
||||
def split_holidays_on_day_off(holiday):
|
||||
start, end = holiday.start, holiday.end
|
||||
if start == end:
|
||||
if start.weekday() != day_off:
|
||||
yield holiday
|
||||
return
|
||||
|
||||
if start.weekday() == day_off:
|
||||
start += datetime.timedelta(days=1)
|
||||
|
||||
interval_start = start
|
||||
for i in range((end - start).days + 1):
|
||||
interval_end = start + datetime.timedelta(days=i)
|
||||
if interval_end.weekday() == day_off:
|
||||
yield Holiday(interval_start, interval_end - datetime.timedelta(days=1))
|
||||
interval_start = interval_end + datetime.timedelta(days=1)
|
||||
|
||||
if interval_start <= interval_end:
|
||||
yield Holiday(interval_start, interval_end)
|
||||
|
||||
def strip_weekend(holiday):
|
||||
if holiday.start.weekday() in (5, 6):
|
||||
holiday.start += datetime.timedelta(days=7 - holiday.start.weekday())
|
||||
if holiday.end.weekday() in (5, 6):
|
||||
holiday.end -= datetime.timedelta(days=holiday.end.weekday() - 4)
|
||||
if holiday.start <= holiday.end:
|
||||
return holiday
|
||||
|
||||
worker_holidays = [make_holiday(*dates) for dates in worker_holidays]
|
||||
|
||||
if day_off is not None:
|
||||
worker_holidays = [
|
||||
sub_holiday
|
||||
for holiday in worker_holidays
|
||||
for sub_holiday in split_holidays_on_day_off(holiday)
|
||||
]
|
||||
|
||||
worker_holidays = [
|
||||
holiday_no_weekend
|
||||
for holiday in worker_holidays
|
||||
if (holiday_no_weekend := strip_weekend(holiday))
|
||||
]
|
||||
|
||||
return worker_holidays
|
||||
|
||||
|
||||
workers_monthly_report = WorkersMonthlyReport.as_view()
|
|
@ -168,6 +168,7 @@ INSTALLED_APPS = (
|
|||
"django.contrib.admin",
|
||||
"eo_gestion.eo_facture",
|
||||
"eo_gestion.eo_banque",
|
||||
'eo_gestion.eo_conges',
|
||||
'eo_gestion.chorus',
|
||||
'taggit',
|
||||
'adminsortable2',
|
||||
|
@ -187,6 +188,25 @@ REQUESTS_TIMEOUT = 32
|
|||
|
||||
TAGGIT_CASE_INSENSITIVE = True
|
||||
|
||||
WORKERS_CALENDAR_CONFIG = {
|
||||
'url': 'https://calendar.entrouvert.org/groupdav.php/',
|
||||
'username': 'user',
|
||||
'password': 'pass',
|
||||
'calendar': 'Calendrier Groupe Entrouvert',
|
||||
'day_off_category': 'Journées off',
|
||||
'holiday_category': 'Congés',
|
||||
'workers': [
|
||||
# {
|
||||
# 'username': 'lmichel',
|
||||
# 'display_name': 'Louise Michel',
|
||||
# 'number': 42,
|
||||
# 'aliases': ['enjolras'],
|
||||
# 'is_parisian': True,
|
||||
# 'ignore': False,
|
||||
# },
|
||||
],
|
||||
}
|
||||
|
||||
local_settings_file = os.environ.get("BARBACOMPTA_SETTINGS_FILE", "local_settings.py")
|
||||
if os.path.exists(local_settings_file):
|
||||
with open(local_settings_file) as fd:
|
||||
|
|
1
setup.py
1
setup.py
|
@ -26,6 +26,7 @@ install_requires = [
|
|||
'gadjo',
|
||||
'django-mellon',
|
||||
'python-dateutil',
|
||||
'caldav',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,337 @@
|
|||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import vobject
|
||||
from django.utils.html import escape
|
||||
|
||||
from eo_gestion.eo_conges.views import ReportError
|
||||
|
||||
from .utils import login
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def events():
|
||||
with mock.patch('caldav.davclient.DAVClient.principal') as mocked_principal:
|
||||
mocked_principal.side_effet = FakePrincipal()
|
||||
yield
|
||||
|
||||
|
||||
def get_event(summary, start, end, category='Congés'):
|
||||
event = vobject.newFromBehavior('vevent')
|
||||
event = event.transformToNative()
|
||||
event.add('summary').value = summary
|
||||
event.add('dtstart').value = start
|
||||
event.add('dtend').value = end
|
||||
event.add('categories').value = [category]
|
||||
return event
|
||||
|
||||
|
||||
def test_conges_redirect(app, settings):
|
||||
login(app)
|
||||
resp = app.get('/conges/')
|
||||
assert resp.location == '/conges/2019/1/'
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
]
|
||||
events = mocked_get_events.return_value = []
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert [x.text for x in resp.pyquery('.worker span')] == [
|
||||
'Jean Dupont (matricule 42)',
|
||||
'Tickets restaurant : 19 tickets de 8 euros',
|
||||
'Congés : 0.',
|
||||
]
|
||||
|
||||
assert '/conges/2022/12/' in resp.text
|
||||
assert '/conges/2023/2/' in resp.text
|
||||
|
||||
events.append(
|
||||
get_event('Jean', datetime.datetime(2023, 1, 2, 8, 00), datetime.datetime(2023, 1, 2, 18, 00))
|
||||
)
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert [x.text for x in resp.pyquery('.worker span')] == [
|
||||
'Jean Dupont (matricule 42)',
|
||||
'Tickets restaurant : 19 tickets de 8 euros',
|
||||
'Congés : le 02/01.',
|
||||
]
|
||||
|
||||
events += [
|
||||
# one day, using dates
|
||||
get_event('Jean', datetime.date(2023, 1, 4), datetime.date(2023, 1, 5)),
|
||||
# two days range, using mixed date/datetimes
|
||||
get_event('Jean', datetime.datetime(2023, 1, 10, 8, 00), datetime.datetime(2023, 1, 11)),
|
||||
# two days range, using dates
|
||||
get_event('Jean', datetime.date(2023, 1, 12), datetime.date(2023, 1, 14)),
|
||||
# 4 days range, using dates
|
||||
get_event('Jean', datetime.date(2023, 1, 16), datetime.date(2023, 1, 20)),
|
||||
# only morning
|
||||
get_event('Jean', datetime.datetime(2023, 1, 23, 8, 00), datetime.datetime(2023, 1, 23, 14, 00)),
|
||||
# only afternoon
|
||||
get_event('Jean', datetime.datetime(2023, 1, 24, 14, 00), datetime.datetime(2023, 1, 24, 18, 00)),
|
||||
]
|
||||
|
||||
# 6 days range, using rrule
|
||||
event = get_event('Jean', datetime.date(2023, 1, 25), datetime.date(2023, 1, 26))
|
||||
event.add('rrule').value = 'FREQ=DAILY;UNTIL=20230131'
|
||||
events.append(event)
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert resp.pyquery('.worker span.holidays').text() == (
|
||||
'Congés : le 02/01, le 04/01, les 10 et 11/01, les 12 et 13/01, du 16 au 19/01, 0,5j le 23/01, '
|
||||
'0,5j le 24/01, du 25 au 31/01.'
|
||||
)
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_fatal_errors(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
]
|
||||
mocked_get_events.side_effect = ReportError('Error xxx')
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert 'Error xxx' in resp.text
|
||||
|
||||
app.get('/conges/2023/42/', status=404)
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_multiple_match_error(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
{'username': 'jdurand', 'display_name': 'Jean Durand', 'number': 43},
|
||||
]
|
||||
|
||||
mocked_get_events.return_value = [
|
||||
get_event('Jean', datetime.date(2023, 1, 6), datetime.date(2023, 1, 10)),
|
||||
]
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert (
|
||||
escape("L'évènement Jean correspond à plusieurs coopérateurices : jdurand et jdupont.") in resp.text
|
||||
)
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_no_match_error(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
]
|
||||
|
||||
mocked_get_events.return_value = [
|
||||
get_event('Jean', datetime.date(2023, 1, 6), datetime.date(2023, 1, 7)),
|
||||
get_event('xxx', datetime.date(2023, 1, 8), datetime.date(2023, 1, 9)),
|
||||
get_event('yyy', datetime.date(2023, 1, 8), datetime.date(2023, 1, 9)),
|
||||
]
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert escape("Aucun·e coopérateurice trouvé·e pour les évènements : xxx, yyy") in resp.text
|
||||
assert 'le 06/01' in resp.text
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_day_off_error(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
]
|
||||
mocked_get_events.return_value = []
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert escape("Jean Dupont n'a pas de jour off.") in resp.text
|
||||
|
||||
ev1 = get_event('Jean off', datetime.date(2023, 1, 6), datetime.date(2023, 1, 7), category='Journées off')
|
||||
ev1.add('rrule').value = 'FREQ=WEEKLY;BYDAY=MO'
|
||||
ev2 = get_event(
|
||||
'Jean off 2', datetime.date(2023, 1, 8), datetime.date(2023, 1, 9), category='Journées off'
|
||||
)
|
||||
ev2.add('rrule').value = 'FREQ=WEEKLY;BYDAY=WE'
|
||||
mocked_get_events.return_value = [ev1, ev2]
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert 'Jean Dupont a plusieurs jours off : Jean off, Jean off 2.' in resp.text
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_is_parisian(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
]
|
||||
mocked_get_events.return_value = []
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert 'Carte transport Paris 50%' not in resp.text
|
||||
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'][0]['is_parisian'] = True
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert 'Carte transport Paris 50%' in resp.text
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_ignored_workers(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
{'username': 'lmichel', 'display_name': 'Louise Michel', 'number': 0, 'ignore': True},
|
||||
]
|
||||
mocked_get_events.return_value = [
|
||||
get_event('congés jdupont', datetime.date(2023, 1, 2), datetime.date(2023, 1, 3)),
|
||||
get_event('congés lmichel', datetime.date(2023, 1, 3), datetime.date(2023, 1, 4)),
|
||||
]
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert 'Jean' in resp.text
|
||||
assert 'Louise' not in resp.text
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_worker_possible_names(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
{'username': 'jmartin', 'display_name': 'Jean Martin', 'number': 43, 'aliases': ['Jeannot']},
|
||||
]
|
||||
|
||||
mocked_get_events.return_value = [
|
||||
get_event('congés jdupont', datetime.date(2023, 1, 2), datetime.date(2023, 1, 3)),
|
||||
get_event('Jeannot en vacances', datetime.date(2023, 1, 3), datetime.date(2023, 1, 4)),
|
||||
get_event('congés JD', datetime.date(2023, 1, 5), datetime.date(2023, 1, 6)),
|
||||
get_event('JeanM', datetime.date(2023, 1, 6), datetime.date(2023, 1, 7)),
|
||||
]
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert [x.text for x in resp.pyquery('.worker span.holidays')] == [
|
||||
'Congés : le 02/01, le 05/01.',
|
||||
'Congés : le 03/01, le 06/01.',
|
||||
]
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_strip_weekend(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
]
|
||||
|
||||
mocked_get_events.return_value = [
|
||||
# starts friday, ends monday
|
||||
get_event('Jean', datetime.date(2023, 1, 6), datetime.date(2023, 1, 10)),
|
||||
# start saturday, ends sunday next week
|
||||
get_event('Jean', datetime.date(2023, 1, 14), datetime.date(2023, 1, 23)),
|
||||
# two days of weekend
|
||||
get_event('Jean', datetime.date(2023, 1, 28), datetime.date(2023, 1, 30)),
|
||||
]
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert resp.pyquery('.worker span.holidays').text() == 'Congés : du 06 au 09/01, du 16 au 20/01.'
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_month_span(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
]
|
||||
|
||||
mocked_get_events.return_value = [
|
||||
# starts in december
|
||||
get_event('Jean', datetime.date(2022, 12, 25), datetime.date(2023, 1, 5)),
|
||||
# ends in february
|
||||
get_event('Jean', datetime.date(2023, 1, 30), datetime.date(2023, 2, 10)),
|
||||
]
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert resp.pyquery('.worker span.holidays').text() == 'Congés : du 02 au 04/01, les 30 et 31/01.'
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_split_on_day_off(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
]
|
||||
|
||||
holidays = get_event('Jean', datetime.date(2023, 1, 2), datetime.date(2023, 1, 7))
|
||||
events = mocked_get_events.return_value = [holidays]
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert resp.pyquery('.worker span.holidays').text() == 'Congés : du 02 au 06/01.'
|
||||
|
||||
# off on wednesday, holidays split in half
|
||||
event = get_event('Jean', datetime.date(2023, 1, 4), datetime.date(2023, 1, 5), category='Journées off')
|
||||
event.add('rrule').value = 'FREQ=WEEKLY;BYDAY=WE'
|
||||
events.append(event)
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert resp.pyquery('.worker span.holidays').text() == 'Congés : les 02 et 03/01, les 05 et 06/01.'
|
||||
|
||||
# off on friday, holidays end one day sooner
|
||||
event.rrule.value = 'FREQ=WEEKLY;BYDAY=FR'
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert resp.pyquery('.worker span.holidays').text() == 'Congés : du 02 au 05/01.'
|
||||
|
||||
# off on monday, holidays start one day later
|
||||
event.rrule.value = 'FREQ=WEEKLY;BYDAY=MO'
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert resp.pyquery('.worker span.holidays').text() == 'Congés : du 03 au 06/01.'
|
||||
|
||||
# long holidays get split multiple times
|
||||
holidays.dtend.value = datetime.date(2023, 1, 21)
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert (
|
||||
resp.pyquery('.worker span.holidays').text()
|
||||
== 'Congés : du 03 au 06/01, du 10 au 13/01, du 17 au 20/01.'
|
||||
)
|
||||
|
||||
# split on wednesday
|
||||
event.rrule.value = 'FREQ=WEEKLY;BYDAY=WE'
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert (
|
||||
resp.pyquery('.worker span.holidays').text()
|
||||
== 'Congés : les 02 et 03/01, du 05 au 10/01, du 12 au 17/01, les 19 et 20/01.'
|
||||
)
|
||||
|
||||
|
||||
@mock.patch('eo_gestion.eo_conges.views.WorkersMonthlyReport.get_events')
|
||||
def test_conges_split_on_day_off_single_day(mocked_get_events, app, settings):
|
||||
settings.WORKERS_CALENDAR_CONFIG['workers'] = [
|
||||
{'username': 'jdupont', 'display_name': 'Jean Dupont', 'number': 42},
|
||||
]
|
||||
|
||||
# off on wednesday
|
||||
off_event = get_event(
|
||||
'Jean', datetime.date(2023, 1, 4), datetime.date(2023, 1, 5), category='Journées off'
|
||||
)
|
||||
off_event.add('rrule').value = 'FREQ=WEEKLY;BYDAY=WE'
|
||||
|
||||
mocked_get_events.return_value = [
|
||||
off_event,
|
||||
# holidays on monday
|
||||
get_event('Jean', datetime.date(2023, 1, 2), datetime.date(2023, 1, 3)),
|
||||
]
|
||||
|
||||
login(app)
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert resp.pyquery('.worker span.holidays').text() == 'Congés : le 02/01.'
|
||||
|
||||
# off on monday
|
||||
off_event.rrule.value = 'FREQ=WEEKLY;BYDAY=MO'
|
||||
|
||||
resp = app.get('/conges/2023/01/')
|
||||
assert resp.pyquery('.worker span.holidays').text() == 'Congés : 0.'
|
|
@ -0,0 +1,8 @@
|
|||
def login(app, username='admin', password='admin'):
|
||||
login_page = app.get('/login/')
|
||||
login_form = login_page.forms[0]
|
||||
login_form['username'] = username
|
||||
login_form['password'] = password
|
||||
resp = login_form.submit()
|
||||
assert resp.status_int == 302
|
||||
return app
|
Loading…
Reference in New Issue