1410 lines
54 KiB
Python
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')
|