passerelle/passerelle/contrib/caluire_axel/models.py

1410 lines
54 KiB
Python

# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import datetime
from collections import defaultdict
from operator import itemgetter
from django.core.cache import cache
from django.db import models
from django.http import HttpResponse
from django.utils import dateformat
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from passerelle.base.models import BaseResource
from passerelle.contrib.utils import axel
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError
from . import schemas, utils
WEEKDAYS = {
0: 'monday',
1: 'tuesday',
2: 'wednesday',
3: 'thursday',
4: 'friday',
5: 'saturday',
6: 'sunday',
}
class CaluireAxel(BaseResource):
wsdl_url = models.CharField(
max_length=128, blank=False, verbose_name=_('WSDL URL'), help_text=_('Caluire Axel WSDL URL')
)
category = _('Business Process Connectors')
_category_ordering = [_('Family account'), _('Schooling'), _('Invoices')]
class Meta:
verbose_name = _('Caluire Axel')
def check_status(self):
response = self.requests.get(self.wsdl_url)
response.raise_for_status()
def check_individu(self, post_data):
family_id = post_data.pop('IDENTFAMILLE')
for key in ['NAISSANCE', 'CODEPOSTAL', 'VILLE', 'TEL', 'MAIL']:
post_data[key] = None
try:
result = schemas.find_individus(self, {'PORTAIL': {'FINDINDIVIDU': post_data}})
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
data = result.json_response['DATA']['PORTAIL']['FINDINDIVIDUS']
for individu in data.get('INDIVIDU') or []:
for famille in individu.get('FAMILLE') or []:
if famille['IDENTFAMILLE'] == family_id:
place = famille['PLACE']
if place not in ['1', '2']:
# not RL1 or RL2
raise APIError('Wrong place in family', err_code='family-place-error-%s' % place)
return individu, result
raise APIError('Person not found', err_code='not-found')
@endpoint(
display_category=_('Family account'),
display_order=1,
description=_('Create link between user and Caluire Axel'),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
post={
'request_body': {
'schema': {
'application/json': schemas.LINK_SCHEMA,
}
}
},
)
def link(self, request, NameID, post_data):
if not NameID:
raise APIError('NameID is empty', err_code='bad-request', http_status=400)
family_id = post_data['IDENTFAMILLE']
try:
data, result = self.check_individu(post_data)
except APIError as e:
if not hasattr(e, 'err_code') or e.err_code == 'error':
raise
raise APIError('Person not found', err_code='not-found')
link, created = self.link_set.get_or_create(
name_id=NameID, defaults={'family_id': family_id, 'person_id': data['IDENT']}
)
if not created and (link.family_id != family_id or link.person_id != data['IDENT']):
raise APIError('Data conflict', err_code='conflict')
return {
'link': link.pk,
'created': created,
'family_id': link.family_id,
'data': {
'xml_request': result.xml_request,
'xml_response': result.xml_response,
},
}
def get_link(self, name_id):
try:
return self.link_set.get(name_id=name_id)
except Link.DoesNotExist:
raise APIError('Person not found', err_code='not-found')
@endpoint(
display_category=_('Family account'),
display_order=2,
description=_('Delete link between user and Caluire Axel'),
methods=['post'],
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
)
def unlink(self, request, NameID):
link = self.get_link(NameID)
link_id = link.pk
link.delete()
return {'link': link_id, 'deleted': True, 'family_id': link.family_id}
def get_family_data(self, family_id):
cache_key = 'caluire-axel-%s-family-data-%s' % (self.pk, family_id)
result = cache.get(cache_key)
if result is not None:
return result
try:
result = schemas.get_famille_individus(
self, {'PORTAIL': {'GETFAMILLE': {'IDENTFAMILLE': family_id}}}
)
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
family_data = result.json_response['DATA']['PORTAIL']['GETFAMILLE']
family_data['CODE'] = family_data['CODE'][0]
if family_data.get('RESPONSABLE1'):
family_data['RESPONSABLE1'] = family_data['RESPONSABLE1'][0]
if family_data.get('RESPONSABLE2'):
family_data['RESPONSABLE2'] = family_data['RESPONSABLE2'][0]
for child in family_data.get('MEMBRE', []):
child['id'] = child['IDENT']
child['text'] = '{} {}'.format(child['PRENOM'].strip(), child['NOM'].strip()).strip()
cache.set(cache_key, family_data, 30) # 30 seconds
return family_data
def get_child_data(self, family_id, child_id):
family_data = self.get_family_data(family_id)
for child in family_data.get('MEMBRE', []):
if child['IDENT'] == child_id:
return child
return None
@endpoint(
display_category=_('Family account'),
display_order=3,
description=_("Get information about user's family"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
)
def family_info(self, request, NameID):
link = self.get_link(NameID)
family_data = self.get_family_data(link.family_id)
family_data['family_id'] = link.family_id
return {'data': family_data}
@endpoint(
display_category=_('Family account'),
display_order=4,
description=_("Get information about children"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
)
def children_info(self, request, NameID):
link = self.get_link(NameID)
family_data = self.get_family_data(link.family_id)
return {'data': family_data.get('MEMBRE', [])}
@endpoint(
display_category=_('Family account'),
display_order=5,
description=_("Get information about a child"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
'idpersonne': {'description': _('Child ID')},
},
)
def child_info(self, request, NameID, idpersonne):
link = self.get_link(NameID)
child_data = self.get_child_data(link.family_id, idpersonne)
if child_data is None:
raise APIError('Child not found', err_code='not-found')
return {'data': child_data}
@endpoint(
display_category=_('Family account'),
display_order=6,
description=_('Upload attachments for child or family'),
methods=['post'],
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
post={
'request_body': {
'schema': {
'application/json': schemas.UPLOAD_ATTACHMENTS_SCHEMA,
}
}
},
)
def upload_attachments(self, request, NameID, post_data):
link = self.get_link(NameID)
if post_data.get('child_id'):
child_data = self.get_child_data(link.family_id, post_data['child_id'])
if child_data is None:
raise APIError('Child not found', err_code='not-found')
reference_date = datetime.datetime.strptime(post_data['reference_date'], axel.json_date_format).date()
reference_year = utils.get_reference_year_from_date(reference_date)
attachments = []
for attachment in post_data.get('attachments', []):
attachments.append(
{
'TYPEPIECE': attachment['attachment_type'],
'LIBELLE': attachment['label'],
'URLFILE': attachment.get('url'),
}
)
data = {
'IDENTFAMILLE': link.family_id,
'IDENTINDIVIDU': post_data.get('child_id'),
'ANNEE': str(reference_year),
}
if attachments:
data['PIECE'] = attachments
try:
result = schemas.set_pieces(self, {'PORTAIL': {'SETPIECES': data}})
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
for status in result.json_response['DATA']['PORTAIL']['SETPIECES']['PIECE']:
code = status['CODE']
if code != 0:
raise APIError(
'Wrong upload-attachments status', err_code='upload-attachments-code-error-%s' % code
)
return {
'created': True,
'data': {
'xml_request': result.xml_request,
'xml_response': result.xml_response,
},
}
@endpoint(
display_category=_('Schooling'),
display_order=1,
description=_("Get school list"),
perm='can_access',
parameters={
'num': {'description': _('Address: number')},
'street': {'description': _('Address: street')},
'zipcode': {'description': _('Address: zipcode')},
'city': {'description': _('Address: city')},
'schooling_date': {'description': _('Schooling date (to get reference year)')},
'school_level': {'description': _('Requested school level')},
},
)
def school_list(self, request, num, street, zipcode, city, schooling_date, school_level=None):
try:
schooling_date = datetime.datetime.strptime(schooling_date, axel.json_date_format)
except ValueError:
raise APIError('bad date format, should be YYYY-MM-DD', err_code='bad-request', http_status=400)
reference_year = utils.get_reference_year_from_date(schooling_date)
try:
result = schemas.get_list_ecole(
self,
{
'PORTAIL': {
'GETLISTECOLE': {
'NORUE': num,
'ADRESSE1': street,
'CODEPOSTAL': zipcode,
'VILLE': city,
'IDENTNIVEAU': school_level or '',
'ANNEE': str(reference_year),
}
}
},
)
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
school_data = result.json_response['DATA']['PORTAIL']['GETLISTECOLE']
for school in school_data.get('ECOLE', []):
school['id'] = school['IDENT']
school['text'] = school['LIBELLE']
return {'data': school_data}
@endpoint(
display_category=_('Schooling'),
display_order=2,
description=_("Get information about schooling of a child"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
'idpersonne': {'description': _('Child ID')},
'schooling_date': {'description': _('Schooling date (to get reference year)')},
},
)
def child_schooling_info(self, request, NameID, idpersonne, schooling_date):
link = self.get_link(NameID)
try:
schooling_date = datetime.datetime.strptime(schooling_date, axel.json_date_format)
except ValueError:
raise APIError('bad date format, should be YYYY-MM-DD', err_code='bad-request', http_status=400)
child_data = self.get_child_data(link.family_id, idpersonne)
if child_data is None:
raise APIError('Child not found', err_code='not-found')
reference_year = utils.get_reference_year_from_date(schooling_date)
try:
result = schemas.get_individu(
self,
{'PORTAIL': {'GETINDIVIDU': {'IDENTINDIVIDU': idpersonne, 'ANNEE': str(reference_year)}}},
)
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
schooling_data = result.json_response['DATA']['PORTAIL']['GETINDIVIDU']
return {'data': schooling_data}
def get_child_activities(self, child_id, reference_year):
cache_key = 'caluire-axel-%s-child-activities-%s-%s' % (self.pk, child_id, reference_year)
result = cache.get(cache_key)
if result is not None:
return result
try:
result = schemas.get_list_activites(
self,
{'PORTAIL': {'GETLISTACTIVITES': {'IDENTINDIVIDU': child_id, 'ANNEE': str(reference_year)}}},
)
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
activities_data = result.json_response['DATA']['PORTAIL']['GETLISTACTIVITES']
cache.set(cache_key, activities_data, 30) # 30 seconds
return activities_data
def get_activity_type(self, activity_id):
if activity_id.startswith('CJ MER'):
return 'mercredi'
if activity_id.startswith('CJ'):
return 'vacances'
if activity_id in ['ACCMAT', 'NAV MATIN']:
return 'matin'
if activity_id in ['ETUDES', 'GARDERIES', 'NAV SOIR']:
return 'soir'
return 'midi'
def get_child_activity(self, child_id, activity_id, reference_year):
activities_data = self.get_child_activities(child_id, reference_year)
for activity in activities_data.get('ACTIVITE', []):
if activity['IDENTACTIVITE'] == activity_id:
return activity
return None
@endpoint(
display_category=_('Schooling'),
display_order=3,
description=_("Get information about activities of a child for the year"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
'idpersonne': {'description': _('Child ID')},
'schooling_date': {'description': _('Schooling date (to get reference year)')},
},
)
def child_activities_info(self, request, NameID, idpersonne, schooling_date):
link = self.get_link(NameID)
try:
schooling_date = datetime.datetime.strptime(schooling_date, axel.json_date_format)
except ValueError:
raise APIError('bad date format, should be YYYY-MM-DD', err_code='bad-request', http_status=400)
child_data = self.get_child_data(link.family_id, idpersonne)
if child_data is None:
raise APIError('Child not found', err_code='not-found')
reference_year = utils.get_reference_year_from_date(schooling_date)
activities_data = self.get_child_activities(idpersonne, reference_year)
return {'data': activities_data}
def get_start_and_end_dates(self, start_date, end_date):
try:
start_date = datetime.datetime.strptime(start_date, axel.json_date_format).date()
end_date = datetime.datetime.strptime(end_date, axel.json_date_format).date()
except ValueError:
raise APIError('bad date format, should be YYYY-MM-DD', err_code='bad-request', http_status=400)
if start_date > end_date:
raise APIError(
'start_date should be before end_date',
err_code='bad-request',
http_status=400,
)
reference_year = utils.get_reference_year_from_date(start_date)
end_reference_year = utils.get_reference_year_from_date(end_date)
if reference_year != end_reference_year:
raise APIError(
'start_date and end_date are in different reference year (%s != %s)'
% (reference_year, end_reference_year),
err_code='bad-request',
http_status=400,
)
return start_date, end_date, reference_year
@endpoint(
display_category=_('Schooling'),
display_order=4,
description=_("Register a child for an activity"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
post={
'request_body': {
'schema': {
'application/json': schemas.REGISTER_ACTIVITY_SCHEMA,
}
}
},
)
def register_activity(self, request, NameID, post_data):
link = self.get_link(NameID)
child_data = self.get_child_data(link.family_id, post_data['child_id'])
if child_data is None:
raise APIError('Child not found', err_code='not-found')
child_data.pop('id', None)
child_data.pop('text', None)
child_data.pop('FAMILLE', None)
start_date, end_date, reference_year = self.get_start_and_end_dates(
post_data['registration_start_date'], post_data['registration_end_date']
)
# build data
data = {
'IDENTFAMILLE': link.family_id,
'INDIVIDU': child_data,
'ACTIVITE': {
'ANNEE': str(reference_year),
'IDENTACTIVITE': post_data['activity_id'],
'ENTREE': start_date.strftime(axel.json_date_format),
'SORTIE': end_date.strftime(axel.json_date_format),
},
}
try:
result = schemas.create_inscription_activite(
self, {'PORTAIL': {'CREATEINSCRIPTIONACTIVITE': data}}
)
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
code = result.json_response['DATA']['PORTAIL']['CREATEINSCRIPTIONACTIVITE']['CODE']
if code < -1:
raise APIError(
'Wrong register-activity status', err_code='register-activity-code-error-%s' % code
)
# invalidate get_children_activities cache
cache_key = 'caluire-axel-%s-child-activities-%s-%s' % (
self.pk,
post_data['child_id'],
reference_year,
)
cache.delete(cache_key)
return {
'created': bool(code == 0),
'data': {
'xml_request': result.xml_request,
'xml_response': result.xml_response,
},
}
def get_bookings(
self,
child_id,
activity_id,
start_date,
end_date,
activity_label=None,
ignore_wednesday=False,
ignore_weekend=False,
):
if activity_id.startswith('DEC'):
# classe decouverte, ignore
return []
data = {
'IDENTINDIVIDU': child_id,
'IDENTACTIVITE': activity_id,
'DEBUT': start_date.strftime(axel.json_date_format),
'FIN': end_date.strftime(axel.json_date_format),
}
try:
result = schemas.get_agenda(self, {'PORTAIL': {'GETAGENDA': data}})
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
code = result.json_response['DATA']['PORTAIL']['GETAGENDA']['CODE']
if code < 1:
raise APIError('Wrong agenda status', err_code='agenda-code-error-%s' % code)
# get pivot date
date_now = localtime()
pivot_date = None
if activity_id.startswith('CJ'):
# mercredi or vacances: not bookable
pivot_date = None
else:
# cantine, accmat, garderies, etudes
# 2 opened days required
pivot_date = date_now.date()
if date_now.hour >= 12:
# after 12H, jump to tomorrow
pivot_date += datetime.timedelta(days=1)
if pivot_date.weekday() > 2:
# add a day if after wednesday
pivot_date += datetime.timedelta(days=1)
# add 3 days
pivot_date += datetime.timedelta(days=3)
if pivot_date.weekday() > 4:
# week-end, jump to next monday
pivot_date += datetime.timedelta(days=7 - pivot_date.weekday())
elif pivot_date.weekday() == 2:
# wednesday, jump to next thursday
pivot_date += datetime.timedelta(days=1)
bookings = []
activity_type = self.get_activity_type(activity_id)
days = result.json_response['DATA']['PORTAIL']['GETAGENDA'].get('JOUR', [])
for day in days:
if day.get('FERME'):
# hide closed days
continue
day_date = datetime.datetime.strptime(day['JOURDATE'], axel.json_date_format).date()
if day_date.weekday() == 2 and ignore_wednesday:
continue
if day_date.weekday() >= 5 and ignore_weekend:
continue
booking = {
'id': '%s:%s:%s' % (child_id, activity_id, day['JOURDATE']),
'text': dateformat.format(day_date, 'l j F Y'),
'prefill': day['MATIN'] in ['X', 'H', 'R'],
'details': day,
}
color = 'grey'
if day['MATIN'] in ['X', 'R']:
color = 'green'
elif day['MATIN'] in ['H']:
color = 'yellow'
elif day['MATIN'] in ['.']:
color = 'white'
elif day['MATIN'] in ['D', 'C', 'M']:
color = 'orange'
elif day['MATIN'] in ['A', 'N']:
color = 'red'
booking['details']['status_color'] = color
if pivot_date is None or day_date < pivot_date:
# not bookable or it's too late to book
booking['details']['out_of_delay'] = True
disabled = True
else:
booking['details']['out_of_delay'] = False
if color in ['grey', 'yellow', 'red']:
disabled = True
elif color == 'orange':
disabled = day['MATIN'] != 'D'
else:
disabled = False
booking['disabled'] = disabled
booking['details']['activity_id'] = activity_id
booking['details']['activity_type'] = activity_type
if activity_label:
booking['details']['activity_label'] = activity_label
booking['details']['child_id'] = child_id
bookings.append(booking)
return bookings
@endpoint(
display_category=_('Schooling'),
display_order=5,
description=_("Get agenda for an activity and a child"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
'idpersonne': {'description': _('Child ID')},
'activity_id': {'description': _('Activity ID')},
'start_date': {'description': _('Start date of the period')},
'end_date': {'description': _('End date of the period')},
},
)
def get_agenda(self, request, NameID, idpersonne, activity_id, start_date, end_date):
link = self.get_link(NameID)
start_date, end_date, reference_year = self.get_start_and_end_dates(start_date, end_date)
child_data = self.get_child_data(link.family_id, idpersonne)
if child_data is None:
raise APIError('Child not found', err_code='not-found')
activity_data = self.get_child_activity(idpersonne, activity_id, reference_year)
if activity_data is None:
raise APIError('Activity not found', err_code='not-found')
bookings = self.get_bookings(
idpersonne, activity_id, start_date, end_date, activity_label=activity_data['LIBELLEACTIVITE']
)
return {'data': bookings}
def _get_child_agenda(self, request, NameID, idpersonne, start_date, end_date, full=False):
link = self.get_link(NameID)
start_date, end_date, reference_year = self.get_start_and_end_dates(start_date, end_date)
child_data = self.get_child_data(link.family_id, idpersonne)
if child_data is None:
raise APIError('Child not found', err_code='not-found')
activities_data = self.get_child_activities(idpersonne, reference_year)
bookings = []
for activity in activities_data.get('ACTIVITE', []):
activity_id = activity['IDENTACTIVITE']
activity_label = activity['LIBELLEACTIVITE']
if activity_id.startswith('CJ') and not full:
# mercredi or vacances: ignore it
continue
bookings += self.get_bookings(
idpersonne,
activity_id,
start_date,
end_date,
activity_label=activity_label,
ignore_wednesday=not (full),
ignore_weekend=True,
)
# sort bookings
activity_types = ['matin', 'midi', 'soir', 'mercredi', 'vacances']
activity_ids = ['NAV MATIN', 'ACCMAT', 'ETUDES', 'GARDERIES', 'NAV SOIR']
bookings = [
(
b['details']['JOURDATE'],
activity_types.index(b['details']['activity_type']),
activity_ids.index(b['details']['activity_id'])
if b['details']['activity_id'] in activity_ids
else 0,
b['details']['activity_label'],
b,
)
for b in bookings
]
bookings = sorted(bookings, key=itemgetter(0, 1, 2, 3))
bookings = [b for d, a, i, l, b in bookings]
return {
'data': bookings,
'extra_data': {
'start_date': start_date,
'end_date': end_date,
'school_year': '%s/%s' % (reference_year, reference_year + 1),
},
}
@endpoint(
display_category=_('Schooling'),
display_order=6,
description=_("Get periscolaire agenda for a child"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
'idpersonne': {'description': _('Child ID')},
'start_date': {'description': _('Start date of the period')},
'end_date': {'description': _('End date of the period')},
},
)
def get_agenda_periscolaire(self, request, NameID, idpersonne, start_date, end_date):
return self._get_child_agenda(request, NameID, idpersonne, start_date, end_date)
@endpoint(
display_category=_('Schooling'),
display_order=7,
description=_("Get full agenda for a child"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
'idpersonne': {'description': _('Child ID')},
'start_date': {'description': _('Start date of the period')},
'end_date': {'description': _('End date of the period')},
},
)
def get_agenda_full(self, request, NameID, idpersonne, start_date, end_date):
return self._get_child_agenda(request, NameID, idpersonne, start_date, end_date, full=True)
def set_bookings(self, child_id, activity_id, start_date, end_date, booking_list, legacy_agenda=None):
agenda = legacy_agenda or self.get_bookings(child_id, activity_id, start_date, end_date)
legacy_days = [b['id'] for b in agenda if b['prefill'] is True]
available_days = [b['id'] for b in agenda if b['disabled'] is False]
booking_date = start_date
bookings = []
updated = []
while booking_date <= end_date:
day = booking_date.strftime(axel.json_date_format)
day_id = '%s:%s:%s' % (child_id, activity_id, day)
booked = None
if day_id not in available_days:
# disabled or not available: not bookable
booked = None
elif day_id not in legacy_days and day_id in booking_list:
booked = 'X'
elif day_id in legacy_days and day_id not in booking_list:
booked = 'D'
if booked is not None:
# no changes, don't send the day
bookings.append(
{
'JOURDATE': day,
'MATIN': booked,
'MIDI': None,
'APRESMIDI': None,
}
)
updated.append(
{
'activity_id': activity_id,
'day': booking_date,
'booked': booked == 'X',
}
)
booking_date += datetime.timedelta(days=1)
if not bookings:
# don't call axel if no changes
return updated
try:
data = {
'PORTAIL': {
'SETAGENDA': {
'IDENTINDIVIDU': child_id,
'ACTIVITE': {
'IDENTACTIVITE': activity_id,
'JOUR': bookings,
},
}
}
}
result = schemas.set_agenda(self, data)
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
code = result.json_response['DATA']['PORTAIL']['SETAGENDA']['CODE']
if code != 0:
raise APIError('Wrong agenda status', err_code='agenda-code-error-%s' % code)
return updated
def _set_agenda(self, child_id, start_date, end_date, activities_data, current_agenda, booking_list):
# check exclusive activities
exclusive_activity_ids = ['ACCMAT', 'NAV MATIN', 'GARDERIES', 'ETUDES', 'NAV SOIR']
# build list of requested booked days
exclusive_activities = defaultdict(set)
for booking in booking_list:
_child_id, activity_id, day = booking.split(':')
if _child_id != child_id:
continue
if activity_id not in exclusive_activity_ids:
continue
exclusive_activities["%s:%s" % (day, self.get_activity_type(activity_id))].add(booking)
# build list of existing booked days
legacy_exclusive_activities = defaultdict(set)
for activity_id in exclusive_activity_ids:
legacy_agenda = current_agenda.get(activity_id) or []
for booking in legacy_agenda:
if booking['prefill'] is not True:
continue
_child_id, activity_id, day = booking['id'].split(':')
if _child_id != child_id:
continue
if activity_id not in exclusive_activity_ids:
continue
legacy_exclusive_activities["%s:%s" % (day, self.get_activity_type(activity_id))].add(
booking['id']
)
# check booking exclusivity for changes only
for key, bookings in exclusive_activities.items():
if len(legacy_exclusive_activities.get(key) or []) > 1:
# it was already booked in Axel ...
continue
if len(bookings) > 1:
raise APIError(
'not possible to book %s the same day' % ' and '.join(sorted(list(bookings))),
err_code='bad-request',
http_status=400,
)
updated = []
for activity in activities_data.get('ACTIVITE', []):
activity_id = activity['IDENTACTIVITE']
if activity_id.startswith(('CJ', 'DEC')):
# mercredi, vacances or classe decouverte: not bookable
continue
bookings = self.set_bookings(
child_id,
activity_id,
start_date,
end_date,
booking_list,
legacy_agenda=current_agenda[activity_id],
)
for booking in bookings:
booking['activity_label'] = activity['LIBELLEACTIVITE']
booking['activity_type'] = self.get_activity_type(activity_id)
updated += bookings
# sort changes
activity_types = ['nav-matin', 'matin', 'midi', 'soir', 'nav-soir']
updated = [
(
not u['booked'],
activity_types.index(u['activity_type']),
u['day'],
u,
)
for u in updated
]
updated = sorted(updated, key=itemgetter(0, 1, 2))
updated = [u for b, a, d, u in updated]
updated = [
{
'booked': u['booked'],
'activity_id': u['activity_id'],
'activity_label': u['activity_label'],
'day': u['day'],
}
for u in updated
]
return {
'updated': True,
'count': len(updated),
'changes': updated,
}
@endpoint(
display_category=_('Schooling'),
display_order=8,
description=_("Set agenda for a child"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
post={
'request_body': {
'schema': {
'application/json': schemas.BOOKING_SCHEMA,
}
}
},
)
def set_agenda(self, request, NameID, post_data):
link = self.get_link(NameID)
child_id = post_data['child_id']
child_data = self.get_child_data(link.family_id, child_id)
if child_data is None:
raise APIError('Child not found', err_code='not-found')
start_date, end_date, reference_year = self.get_start_and_end_dates(
post_data['start_date'], post_data['end_date']
)
# get current agenda
activities_data = self.get_child_activities(child_id, reference_year)
agenda = {}
for activity in activities_data.get('ACTIVITE', []):
activity_id = activity['IDENTACTIVITE']
if activity_id.startswith('CJ'):
# mercredi or vacances: not bookable
continue
agenda[activity_id] = self.get_bookings(child_id, activity_id, start_date, end_date)
return self._set_agenda(
child_id, start_date, end_date, activities_data, agenda, post_data['booking_list']
)
@endpoint(
display_category=_('Schooling'),
display_order=9,
description=_("Set agenda for a child, from changes applied to another child"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
post={
'request_body': {
'schema': {
'application/json': schemas.CHANGES_SCHEMA,
}
}
},
)
def set_agenda_apply_changes(self, request, NameID, post_data):
link = self.get_link(NameID)
child_id = post_data['child_id']
child_data = self.get_child_data(link.family_id, child_id)
if child_data is None:
raise APIError('Child not found', err_code='not-found')
start_date, end_date, reference_year = self.get_start_and_end_dates(
post_data['start_date'], post_data['end_date']
)
# get current agenda
activities_data = self.get_child_activities(child_id, reference_year)
agenda = {}
current_child_cantine_activity_id = None
current_child_cantine_activity_ids = []
current_child_activity_ids = set()
for activity in activities_data.get('ACTIVITE', []):
activity_id = activity['IDENTACTIVITE']
if activity_id.startswith(('CJ', 'DEC')):
# mercredi, vacances or classe decouverte: not bookable
continue
if self.get_activity_type(activity_id) == 'midi':
current_child_cantine_activity_ids.append(activity_id)
current_child_activity_ids.add(activity_id)
agenda[activity_id] = self.get_bookings(child_id, activity_id, start_date, end_date)
if len(current_child_cantine_activity_ids) > 1:
raise APIError(
'more than one activity cantine found for this child (%s)'
% ', '.join(current_child_cantine_activity_ids),
err_code='bad-request',
http_status=400,
)
if current_child_cantine_activity_ids:
current_child_cantine_activity_id = current_child_cantine_activity_ids[0]
# create booking_list from current_agenda and changes
changes = {}
for change in post_data['changes']:
activity_id = change['activity_id']
if activity_id.startswith(('CJ', 'DEC')):
# mercredi, vacances or classe decouverte: not bookable
continue
# activity_id can be different for cantine activity
if self.get_activity_type(activity_id) == 'midi':
activity_id = current_child_cantine_activity_id
if activity_id not in current_child_activity_ids:
# current child is not registered for this activity, skip
continue
changes['%s:%s:%s' % (child_id, activity_id, change['day'])] = change['booked']
booking_list = []
for activity_id, bookings in agenda.items():
for booking in bookings:
bid = booking['id']
prefill = booking['prefill']
if bid not in changes and prefill is True:
# no change, but already booked
booking_list.append(bid)
elif bid in changes and changes[bid] is True:
# change, should be booked
booking_list.append(bid)
return self._set_agenda(child_id, start_date, end_date, activities_data, agenda, booking_list)
@endpoint(
display_category=_('Schooling'),
display_order=10,
description=_("Set activity agenda for a child with a typical week"),
perm='can_access',
parameters={
'NameID': {'description': _('Publik ID')},
},
post={
'request_body': {
'schema': {
'application/json': schemas.TYPICAL_WEEK_BOOKING_SCHEMA,
}
}
},
)
def set_activity_agenda_typical_week(self, request, NameID, post_data):
link = self.get_link(NameID)
child_id = post_data['child_id']
child_data = self.get_child_data(link.family_id, child_id)
if child_data is None:
raise APIError('Child not found', err_code='not-found')
start_date, end_date, reference_year = self.get_start_and_end_dates(
post_data['start_date'], post_data['end_date']
)
activity_id = post_data['activity_id']
activity_data = self.get_child_activity(child_id, activity_id, reference_year)
if activity_data is None:
raise APIError('Activity not found', err_code='not-found')
if activity_id.startswith(('CJ', 'DEC')):
raise APIError('Not available for this activity', err_code='bad-request', http_status=400)
booking_date = start_date
bookings = []
while booking_date <= end_date:
if WEEKDAYS[booking_date.weekday()] in post_data['booking_list']:
bookings.append(
'%s:%s:%s' % (child_id, activity_id, booking_date.strftime(axel.json_date_format))
)
booking_date += datetime.timedelta(days=1)
updated = self.set_bookings(child_id, activity_id, start_date, end_date, bookings)
return {
'updated': True,
'count': len(updated),
}
def get_invoices(self, regie_id, family_id):
try:
result = schemas.get_factures_a_payer(
self,
{
'PORTAIL': {
'GETFACTURESAPAYER': {
'IDENTFAMILLE': family_id,
'IDENTREGIEFACT': regie_id,
}
}
},
)
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
code = result.json_response['DATA']['PORTAIL']['GETFACTURESAPAYER']['CODE']
if code < 0:
raise APIError('Wrong get-invoices status', err_code='get-invoices-code-error-%s' % code)
data = result.json_response['DATA']['PORTAIL']['GETFACTURESAPAYER']
result = []
date_now = localtime().date()
for facture in data.get('FACTURE', []):
date_creation = datetime.datetime.strptime(facture['EMISSION'], axel.json_date_format).date()
if date_creation <= date_now:
result.append(utils.normalize_invoice(facture, family_id))
return result
def get_historical_invoices(self, regie_id, family_id, nb_mounts_limit):
try:
nb_mois = int(nb_mounts_limit)
except ValueError:
raise APIError('nb_mounts_limit must be an integer', err_code='bad-request', http_status=400)
try:
result = schemas.get_list_factures(
self,
{
'PORTAIL': {
'GETLISTFACTURES': {
'IDENTFAMILLE': family_id,
'IDENTREGIEFACT': regie_id,
'NBMOIS': nb_mois,
}
},
},
)
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
code = result.json_response['DATA']['PORTAIL']['GETLISTFACTURES']['CODE']
if code < 0:
raise APIError(
'Wrong get-historical-invoices status',
err_code='get-historical-invoices-code-error-%s' % code,
)
data = result.json_response['DATA']['PORTAIL']['GETLISTFACTURES']
date_now = localtime().date()
result = []
for facture in data.get('FACTURE', []):
date_creation = datetime.datetime.strptime(facture['EMISSION'], axel.json_date_format).date()
if date_creation <= date_now:
result.append(utils.normalize_invoice(facture, family_id, historical=True))
return result
def get_invoice(self, regie_id, invoice_id, family_id, historical=None, nb_mounts_limit=None):
if historical:
invoices_data = self.get_historical_invoices(
regie_id=regie_id, family_id=family_id, nb_mounts_limit=nb_mounts_limit
)
else:
invoices_data = self.get_invoices(regie_id=regie_id, family_id=family_id)
for invoice in invoices_data:
if invoice['display_id'] == invoice_id:
return invoice
@endpoint(
display_category=_('Invoices'),
display_order=1,
name='regie',
perm='can_access',
pattern=r'^(?P<regie_id>[\w-]+)/invoices/?$',
example_pattern='{regie_id}/invoices',
description=_("Get invoices to pay"),
parameters={
'NameID': {'description': _('Publik ID')},
'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'},
},
)
def invoices(self, request, regie_id, NameID):
link = self.get_link(NameID)
invoices_data = self.get_invoices(regie_id=regie_id, family_id=link.family_id)
return {'data': invoices_data}
@endpoint(
display_category=_('Invoices'),
display_order=2,
name='regie',
perm='can_access',
pattern=r'^(?P<regie_id>[\w-]+)/invoices/history/?$',
example_pattern='{regie_id}/invoices/history',
description=_("Get invoices already paid"),
parameters={
'NameID': {'description': _('Publik ID')},
'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'},
'nb_mounts_limit': {'description': _('Number of months of history'), 'example_value': '12'},
},
)
def invoices_history(self, request, regie_id, NameID, nb_mounts_limit='12'):
link = self.get_link(NameID)
invoices_data = self.get_historical_invoices(
regie_id, family_id=link.family_id, nb_mounts_limit=nb_mounts_limit
)
return {'data': invoices_data}
@endpoint(
display_category=_('Invoices'),
display_order=3,
name='regie',
perm='can_access',
pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>(historical-)?\w+-\d+)/?$',
example_pattern='{regie_id}/invoice/{invoice_id}',
description=_('Get invoice details'),
parameters={
'NameID': {'description': _('Publik ID')},
'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'},
'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'IDFAM-42'},
'nb_mounts_limit': {'description': _('Number of months of history'), 'example_value': '12'},
},
)
def invoice(self, request, regie_id, invoice_id, NameID=None, nb_mounts_limit='12'):
real_invoice_id = invoice_id.split('-')[-1]
historical = invoice_id.startswith('historical-')
if historical:
invoice_id = invoice_id[len('historical-') :]
family_id, real_invoice_id = invoice_id.split('-')
invoice = self.get_invoice(
regie_id=regie_id,
invoice_id=real_invoice_id,
family_id=family_id,
historical=historical,
nb_mounts_limit=nb_mounts_limit,
)
if invoice is None:
raise APIError('Invoice not found', err_code='not-found')
return {'data': invoice}
@endpoint(
display_category=_('Invoices'),
display_order=4,
name='regie',
perm='can_access',
pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>(historical-)?\w+-\d+)/pdf/?$',
example_pattern='{regie_id}/invoice/{invoice_id}/pdf',
description=_('Get invoice as a PDF file'),
parameters={
'NameID': {'description': _('Publik ID')},
'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'},
'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'IDFAM-42'},
'nb_mounts_limit': {'description': _('Number of months of history'), 'example_value': '12'},
},
)
def invoice_pdf(self, request, regie_id, invoice_id, NameID, nb_mounts_limit='12'):
# check that invoice is related to current user
real_invoice_id = invoice_id.split('-')[-1]
historical = invoice_id.startswith('historical-')
try:
link = self.get_link(NameID)
invoice = self.get_invoice(
regie_id=regie_id,
invoice_id=real_invoice_id,
family_id=link.family_id,
historical=historical,
nb_mounts_limit=nb_mounts_limit,
)
except APIError as e:
e.http_status = 404
raise
if invoice is None:
raise APIError('Invoice not found', err_code='not-found', http_status=404)
# check that PDF is available
if not invoice['has_pdf']:
raise APIError('PDF not available', err_code='not-available', http_status=404)
try:
result = schemas.get_pdf_facture(
self, {'PORTAIL': {'GETPDFFACTURE': {'IDFACTURE': int(invoice['display_id'])}}}
)
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
http_status=404,
data={'xml_request': e.xml_request, 'xml_response': e.xml_response},
)
b64content = base64.b64decode(
result.json_response['DATA']['PORTAIL']['GETPDFFACTURE']['FACTUREPDF'] or ''
)
if not b64content:
raise APIError('PDF error', err_code='error', http_status=404)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % invoice_id
response.write(b64content)
return response
@endpoint(
display_category=_('Invoices'),
display_order=5,
name='regie',
methods=['post'],
perm='can_access',
pattern=r'^(?P<regie_id>[\w-]+)/invoice/(?P<invoice_id>\w+-\d+)/pay/?$',
example_pattern='{regie_id}/invoice/{invoice_id}/pay',
description=_('Notify an invoice as paid'),
parameters={
'regie_id': {'description': _('Regie identifier'), 'example_value': 'ENF'},
'invoice_id': {'description': _('Invoice identifier'), 'example_value': 'IDFAM-42'},
},
post={
'request_body': {
'schema': {
'application/json': schemas.PAYMENT_SCHEMA,
}
}
},
)
def pay_invoice(self, request, regie_id, invoice_id, **kwargs):
family_id, invoice_id = invoice_id.split('-')
invoice = self.get_invoice(regie_id=regie_id, family_id=family_id, invoice_id=invoice_id)
if invoice is None:
raise APIError('Invoice not found', err_code='not-found')
transaction_amount = invoice['amount']
post_data = {
'IDFACTURE': int(invoice_id),
'IDENTREGIEENC': regie_id,
'MONTANT': transaction_amount,
'IDENTMODEREGLEMENT': 'INCB',
}
try:
result = schemas.set_paiement(self, {'PORTAIL': {'SETPAIEMENT': post_data}})
except axel.AxelError as e:
raise APIError(
'Axel error: %s' % e,
err_code='error',
data={
'xml_request': e.xml_request,
'xml_response': e.xml_response,
'regie_id': regie_id,
'family_id': family_id,
'invoice': invoice,
'post_data': post_data,
'kwargs': kwargs,
},
)
try:
code = int(result.json_response['DATA']['PORTAIL']['SETPAIEMENT']['CODE'])
except (KeyError, ValueError):
raise APIError(
'Wrong pay-invoice response',
err_code='pay-invoice-code-response-error',
data={
'regie_id': regie_id,
'family_id': family_id,
'invoice': invoice,
'post_data': post_data,
'kwargs': kwargs,
'result': result.json_response,
},
http_status=500,
)
if code < 0:
raise APIError('Wrong pay-invoice status', err_code='pay-invoice-code-error-%s' % code)
return {
'created': True,
'data': {
'xml_request': result.xml_request,
'xml_response': result.xml_response,
},
}
class Link(models.Model):
resource = models.ForeignKey(CaluireAxel, on_delete=models.CASCADE)
name_id = models.CharField(blank=False, max_length=256)
family_id = models.CharField(blank=False, max_length=128)
person_id = models.CharField(blank=False, max_length=128)
class Meta:
unique_together = ('resource', 'name_id')