358 lines
15 KiB
Python
358 lines
15 KiB
Python
# passerelle - uniform access to multiple data sources and services
|
|
# Copyright (C) 2015 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
|
|
from datetime import datetime
|
|
import logging
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from django.db import models
|
|
from django.http import HttpResponse, HttpResponseNotFound
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.utils.encoding import smart_text
|
|
|
|
|
|
from jsonfield import JSONField
|
|
|
|
from passerelle.base.models import BaseResource
|
|
from passerelle.compat import json_loads
|
|
from passerelle.soap import client_to_jsondict
|
|
from passerelle.utils.api import endpoint
|
|
from passerelle.utils.jsonresponse import APIError
|
|
from passerelle.views import WrongParameter
|
|
|
|
from . import soap
|
|
from .utils import normalize_person, normalize_invoice
|
|
|
|
logger = logging.getLogger('passerelle.contrib.teamnet_axel')
|
|
|
|
ADULT1 = '1'
|
|
ADULT2 = '2'
|
|
CHILD = '3'
|
|
|
|
DATE_IN_FORMAT = '%Y-%m-%dT%H:%M:%S'
|
|
DATE_OUT_FORMAT = '%d/%m/%Y %H:%M:%S'
|
|
|
|
|
|
def get_name_id(request):
|
|
if 'NameID' not in request.GET:
|
|
raise WrongParameter(['NameID'], [])
|
|
return request.GET['NameID']
|
|
|
|
|
|
class TeamnetAxel(BaseResource):
|
|
wsdl_url = models.CharField(
|
|
max_length=128, blank=False,
|
|
verbose_name=_('WSDL URL'),
|
|
help_text=_('Teamnet Axel WSDL URL'))
|
|
verify_cert = models.BooleanField(
|
|
default=True,
|
|
verbose_name=_('Check HTTPS Certificate validity'))
|
|
username = models.CharField(
|
|
max_length=128, blank=True,
|
|
verbose_name=_('Username'))
|
|
password = models.CharField(
|
|
max_length=128, blank=True,
|
|
verbose_name=_('Password'))
|
|
keystore = models.FileField(
|
|
upload_to='teamnet_axel', null=True, blank=True,
|
|
verbose_name=_('Keystore'),
|
|
help_text=_('Certificate and private key in PEM format'))
|
|
|
|
billing_regies = JSONField(_('Mapping between regie ids and billing ids'))
|
|
|
|
category = _('Business Process Connectors')
|
|
manager_view_template_name = 'passerelle/contrib/teamnet_axel/detail.html'
|
|
|
|
class Meta:
|
|
verbose_name = _('Teamnet Axel')
|
|
|
|
@classmethod
|
|
def get_verbose_name(cls):
|
|
return cls._meta.verbose_name
|
|
|
|
#
|
|
# Axel SOAP call: getData
|
|
#
|
|
def get_data(self, operation, args):
|
|
# args is a XML node (ElementTree.Element)
|
|
portail = ET.Element('PORTAIL')
|
|
portail.append(args)
|
|
streamId = operation
|
|
xmlParams = smart_text(ET.tostring(portail, encoding='UTF-8'))
|
|
user = ''
|
|
logger.debug(u'getData(streamId=%s, xmlParams=%s, user=%s)', streamId, xmlParams, user)
|
|
result = soap.get_client(self).service.getData(streamId, smart_text(xmlParams), user)
|
|
logger.debug(u'getData(%s) result: %s', streamId, smart_text(result))
|
|
xml_result = ET.fromstring(result)
|
|
if xml_result.find('RESULTAT/STATUS').text != 'OK':
|
|
msg = xml_result.find('RESULTAT/COMMENTAIRES').text
|
|
raise APIError(msg)
|
|
return xml_result.find('DATA')
|
|
|
|
# Axel authentication
|
|
def authenticate(self, login, pwd):
|
|
'''return False or an AXEL user dict:
|
|
{
|
|
"login": "23060A",
|
|
"estidentifie": true,
|
|
"estbloque": false,
|
|
"estchangement_mdp_requis": false,
|
|
"nbechec": "0",
|
|
"idpersonne": "47747",
|
|
"idfamille": "23060",
|
|
}
|
|
'''
|
|
xml_utilisateur = ET.Element('UTILISATEUR')
|
|
ET.SubElement(xml_utilisateur, 'LOGIN').text = login
|
|
ET.SubElement(xml_utilisateur, 'PWD').text = pwd
|
|
try:
|
|
data = self.get_data('ConnexionCompteFamille', xml_utilisateur)
|
|
except APIError:
|
|
return False
|
|
data = data.find('PORTAIL/UTILISATEUR')
|
|
data = soap.xml_to_dict(data)
|
|
for key, value in data.items():
|
|
if key.startswith('est'):
|
|
data[key] = value == 'true'
|
|
if data.get('estbloque'):
|
|
return False
|
|
if not data.get('estidentifie'):
|
|
return False
|
|
return data
|
|
|
|
def get_family_id(self, request):
|
|
nameid = get_name_id(request)
|
|
links = Link.objects.filter(resource=self, nameid=nameid)
|
|
if len(links) > 1:
|
|
raise APIError('multiple links')
|
|
if not links:
|
|
return None
|
|
user = self.authenticate(links[0].login, links[0].pwd)
|
|
if not user:
|
|
raise APIError('authentication failed')
|
|
if 'idfamille' not in user:
|
|
raise APIError('user without idfamille')
|
|
return user['idfamille']
|
|
|
|
def get_family_data(self, idfamille, annee=None):
|
|
xml_famille = ET.Element('FAMILLE')
|
|
ET.SubElement(xml_famille, 'IDFAMILLE').text = idfamille
|
|
if annee:
|
|
ET.SubElement(xml_famille, 'ANNEE').text = annee
|
|
data = self.get_data('DonneesFamille', xml_famille)
|
|
xml_individus = data.findall('PORTAIL/INDIVIDUS')
|
|
if not xml_individus:
|
|
raise APIError('PORTAIL/INDIVIDUS is empty')
|
|
individus = [dict((k.lower(), v) for k, v in i.attrib.items()) for i in xml_individus]
|
|
for individu in individus:
|
|
individu['id'] = individu['idindividu']
|
|
individu['text'] = '%(prenom)s %(nom)s' % individu
|
|
adults = [normalize_person(i) for i in individus if i['indtype'] in (ADULT1, ADULT2)]
|
|
children = [normalize_person(i) for i in individus if i['indtype'] == CHILD]
|
|
return {'family': idfamille, 'adults': adults, 'children': children}
|
|
|
|
@endpoint(perm='can_access')
|
|
def ping(self, request, *args, **kwargs):
|
|
try:
|
|
client = soap.get_client(self)
|
|
except (Exception, ) as exc:
|
|
raise APIError('Client Error: %s' % exc)
|
|
res = {'ping': 'pong'}
|
|
if 'debug' in request.GET:
|
|
res['client'] = client_to_jsondict(client)
|
|
return {'data': res}
|
|
|
|
@endpoint(perm='can_access')
|
|
def auth(self, request, *args, **kwargs):
|
|
login = request.GET.get('login')
|
|
pwd = request.GET.get('pwd')
|
|
return {'data': self.authenticate(login, pwd)}
|
|
|
|
@endpoint(name='family', perm='can_access')
|
|
def family_data(self, request, *args, **kwargs):
|
|
idfamille = self.get_family_id(request)
|
|
if not idfamille:
|
|
return {'data': None}
|
|
data = self.get_family_data(idfamille)
|
|
return {'data': data}
|
|
|
|
@endpoint(name='family', perm='can_access', pattern='^adults/$')
|
|
def family_adults(self, request, *args, **kwargs):
|
|
idfamille = self.get_family_id(request)
|
|
if not idfamille:
|
|
return {'data': None}
|
|
data = self.get_family_data(idfamille)
|
|
return {'data': data.get('adults')}
|
|
|
|
@endpoint(name='family', perm='can_access', pattern='^children/$')
|
|
def family_children(self, request, *args, **kwargs):
|
|
idfamille = self.get_family_id(request)
|
|
if not idfamille:
|
|
return {'data': None}
|
|
data = self.get_family_data(idfamille)
|
|
return {'data': data.get('children')}
|
|
|
|
@endpoint(name='family', perm='can_access', pattern='^link/$')
|
|
def family_link(self, request, *args, **kwargs):
|
|
nameid = get_name_id(request)
|
|
login = request.GET.get('login')
|
|
pwd = request.GET.get('password')
|
|
user = self.authenticate(login, pwd)
|
|
if not user:
|
|
raise APIError('authentication failed')
|
|
if 'idfamille' not in user:
|
|
raise APIError('user without idfamille')
|
|
famille = self.get_family_data(user['idfamille'])
|
|
Link.objects.update_or_create(
|
|
resource=self, nameid=nameid, defaults={'login': login, 'pwd': pwd})
|
|
user['_famille'] = famille
|
|
user['_nameid'] = nameid
|
|
return {'data': user}
|
|
|
|
@endpoint(name='family', perm='can_access', pattern='^unlink/$')
|
|
def family_unlink(self, request, *args, **kwargs):
|
|
nameid = get_name_id(request)
|
|
logins = [v['login'] for v in Link.objects.filter(resource=self, nameid=nameid).values('login')]
|
|
Link.objects.filter(resource=self, nameid=nameid).delete()
|
|
if logins:
|
|
return {'data': {'login_was': logins}}
|
|
else:
|
|
return {'data': None}
|
|
|
|
def get_teamnet_payable_invoices(self, regie_id, family_id):
|
|
operation = 'FacturesApayerRegie'
|
|
xml_invoices = ET.Element('LISTFACTURE')
|
|
ET.SubElement(xml_invoices, 'IDREGIE').text = regie_id
|
|
ET.SubElement(xml_invoices, 'IDFAMILLE').text = family_id
|
|
data = self.get_data(operation, xml_invoices)
|
|
xml_invoices = data.findall('PORTAIL/FACTURES')
|
|
payable_invoices = {}
|
|
if xml_invoices:
|
|
for i in xml_invoices:
|
|
payable_invoices.update(normalize_invoice(i.attrib, family_id))
|
|
return payable_invoices
|
|
|
|
@endpoint(name='regie', pattern='^(?P<regie_id>\w+)/invoices/$')
|
|
def active_invoices(self, request, regie_id, **kwargs):
|
|
family_id = self.get_family_id(request)
|
|
if not family_id:
|
|
return {'data': []}
|
|
invoices = self.get_teamnet_payable_invoices(regie_id, family_id)
|
|
invoices = sorted([p for i, p in invoices.items()], key=lambda i: i['created'], reverse=True)
|
|
return {'data': invoices}
|
|
|
|
def get_teamnet_historical_invoices(self, regie_id, family_id):
|
|
"""
|
|
returns historical invoices for a given regie.
|
|
The list contains also payable invoices.
|
|
"""
|
|
operation = 'HistoriqueFacturesRegie'
|
|
xml_invoices = ET.Element('LISTFACTURE')
|
|
ET.SubElement(xml_invoices, 'IDREGIE').text = regie_id
|
|
ET.SubElement(xml_invoices, 'IDFAMILLE').text = family_id
|
|
ET.SubElement(xml_invoices, 'NBMOIS').text = '12'
|
|
data = self.get_data(operation, xml_invoices)
|
|
xml_invoices = data.findall('PORTAIL/FACTURES')
|
|
historical_invoices = {}
|
|
if xml_invoices:
|
|
for i in xml_invoices:
|
|
historical_invoices.update(normalize_invoice(i.attrib, family_id, historical=True))
|
|
return historical_invoices
|
|
|
|
@endpoint(name='regie', perm='can_access', pattern='^(?P<regie_id>\w+)/invoices/history/$')
|
|
def invoices_history(self, request, regie_id, **kwargs):
|
|
family_id = self.get_family_id(request)
|
|
if not family_id:
|
|
return {'data': []}
|
|
payable = self.get_teamnet_payable_invoices(regie_id, family_id)
|
|
historical = self.get_teamnet_historical_invoices(regie_id, family_id)
|
|
historical = [v for i, v in historical.items() if i not in payable]
|
|
invoices = sorted(historical, key=lambda i: i['created'], reverse=True)
|
|
return {'data': invoices}
|
|
|
|
@endpoint(name='regie', perm='can_access',
|
|
pattern='^(?P<regie_id>\w+)/invoice/(?P<invoice_id>[\w,-]+)/$')
|
|
def get_invoice_details(self, request, regie_id, invoice_id, **kwargs):
|
|
family_id, i = invoice_id.split('-', 1)
|
|
payable = self.get_teamnet_payable_invoices(regie_id, family_id)
|
|
if invoice_id in payable:
|
|
return {'data': payable[invoice_id]}
|
|
historical = self.get_teamnet_historical_invoices(regie_id, family_id)
|
|
if invoice_id in historical:
|
|
return {'data': historical[invoice_id]}
|
|
return {'data': None}
|
|
|
|
@endpoint(name='regie', perm='can_access',
|
|
pattern='^(?P<regie_id>\w+)/invoice/(?P<invoice_id>[\w,-]+)/pdf/$')
|
|
def invoice_pdf(self, request, regie_id, invoice_id, **kwargs):
|
|
family_id, invoice = invoice_id.split('-', 1)
|
|
invoice_xml = ET.Element('FACTUREPDF')
|
|
ET.SubElement(invoice_xml, 'IDFAMILLE').text = family_id
|
|
ET.SubElement(ET.SubElement(invoice_xml, 'FACTURES'), 'NOFACTURE').text = invoice
|
|
data = self.get_data('FacturesPDF', invoice_xml)
|
|
pdf = data.find('PORTAIL/PDF')
|
|
b64content = base64.b64decode(pdf.get('FILE'))
|
|
if not b64content:
|
|
return HttpResponseNotFound()
|
|
response = HttpResponse(content_type='application/pdf')
|
|
response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % invoice_id
|
|
response.write(b64content)
|
|
return response
|
|
|
|
@endpoint(name='regie', methods=['post'],
|
|
perm='can_access', pattern='^(?P<regie_id>\w+)/invoice/(?P<invoice_id>[\w,-]+)/pay/$')
|
|
def pay_invoice(self, request, regie_id, invoice_id, **kwargs):
|
|
data = json_loads(request.body)
|
|
transaction_id = data.get('transaction_id')
|
|
transaction_date = data.get('transaction_date')
|
|
email = data.get('email')
|
|
|
|
family_id, invoice = invoice_id.split('-', 1)
|
|
payable_invoices = self.get_teamnet_payable_invoices(regie_id, family_id)
|
|
|
|
if invoice_id not in payable_invoices:
|
|
return {'data': False}
|
|
|
|
invoice_to_pay = payable_invoices[invoice_id]
|
|
t_date = datetime.strptime(transaction_date, DATE_IN_FORMAT)
|
|
|
|
payment_xml = ET.Element('PAIEMENT')
|
|
ET.SubElement(payment_xml, 'IDDEMANDE')
|
|
ET.SubElement(payment_xml, 'IDFAMILLE').text = family_id
|
|
ET.SubElement(payment_xml, 'IDREGIEENC').text = self.billing_regies.get(regie_id)
|
|
ET.SubElement(payment_xml, 'MODEREGLEMENT').text = 'PAY'
|
|
ET.SubElement(payment_xml, 'MONTANT').text = str(invoice_to_pay['amount'])
|
|
ET.SubElement(payment_xml, 'URL')
|
|
if email:
|
|
ET.SubElement(payment_xml, 'COURRIEL').text = email
|
|
else:
|
|
ET.SubElement(payment_xml, 'COURRIEL')
|
|
ET.SubElement(payment_xml, 'REFPAIEMENT').text = transaction_id
|
|
ET.SubElement(payment_xml, 'DATEENVOI').text = t_date.strftime(DATE_OUT_FORMAT)
|
|
ET.SubElement(payment_xml, 'DATERETOUR').text = t_date.strftime(DATE_OUT_FORMAT)
|
|
ET.SubElement(payment_xml, 'CODERETOUR')
|
|
ET.SubElement(ET.SubElement(payment_xml, 'FACTURE'), 'NOFACTURE').text = invoice
|
|
self.get_data('PaiementFactures', payment_xml)
|
|
return {'data': True}
|
|
|
|
|
|
class Link(models.Model):
|
|
resource = models.ForeignKey(TeamnetAxel, on_delete=models.CASCADE)
|
|
nameid = models.CharField(blank=False, max_length=256)
|
|
login = models.CharField(blank=False, max_length=128)
|
|
pwd = models.CharField(blank=False, max_length=128)
|