Project

General

Profile

Download (32.6 KB) Statistics
| Branch: | Tag: | Revision:

root / extra / modules / abelium_domino_ws.py @ 84fd22a0

1
# -*- coding: utf-8 -*-
2
from decimal import Decimal
3
import time
4
import datetime
5
from xml.etree import ElementTree as etree
6
import logging
7

    
8
from suds.client import Client
9
from suds.bindings.binding import Binding
10

    
11
logger = logging.getLogger(__name__)
12
# Webdev is bugged and using an HTML generator to produce XML content, 
13
Binding.replyfilter = lambda self, x: x.replace(' ', ' ')
14

    
15
# cleaning and parsing functions
16

    
17
LINE_SEPARATOR = '\n'
18
COLUMN_SEPARATOR = '\t'
19

    
20
def unicode_and_strip(x):
21
    return unicode(x).strip()
22

    
23
def strip_and_int(x):
24
    try:
25
        return int(x.strip())
26
    except ValueError:
27
        return None
28

    
29
def strip_and_date(x):
30
    try:
31
        return datetime.datetime.strptime(x.strip(), '%Y%m%d').date()
32
    except ValueError:
33
        return None
34

    
35
def parse_date(date_string):
36
    if date_string:
37
        return datetime.datetime.strptime(date_string, "%Y%m%d")
38
    else:
39
        None
40

    
41
class DominoException(Exception):
42
    pass
43

    
44
def object_cached(function):
45
    '''Decorate an object method so that its results is cached on the object
46
       instance after the first call.
47
    '''
48
    def decorated_function(self, *args, **kwargs):
49
        cache_name = '__%s_cache' % function.__name__
50
        if not hasattr(self, cache_name):
51
            setattr(self, cache_name, (time.time(), {}))
52
        t, d = getattr(self, cache_name)
53
        if time.time() - t > 30:
54
            setattr(self, cache_name, (time.time(), {}))
55
            t, d = getattr(self, cache_name)
56
        k = tuple(*args) + tuple(sorted(kwargs.items()))
57
        if not k in d:
58
            d[k] = function(self, *args, **kwargs)
59
        return d[k]
60
    return decorated_function
61

    
62
# Data model
63
class SimpleObject(object):
64
    '''Base class for object returned by the web service'''
65

    
66
    '''Describe basic columns'''
67
    COLUMNS = ()
68
    '''Describe extended object columns'''
69
    MORE_COLUMNS = ()
70

    
71
    def __init__(self, **kwargs):
72
        self.__dict__.update(kwargs)
73

    
74
    def __repr__(self):
75
        c = {}
76
        for remote_name, name, converter, desc in self.COLUMNS:
77
            if hasattr(self, name):
78
                c[name] = getattr(self, name)
79
        return str(c)
80

    
81
    def serialize(self):
82
        l = []
83
        for remote_name, local_name, converter, desc in self.COLUMNS + self.MORE_COLUMNS:
84
            if local_name == 'id':
85
                continue
86
            v = getattr(self, local_name, None)
87
            if v is None:
88
                continue
89
            l.append(u'{0}: "{1}"'.format(remote_name, v))
90
        return u','.join(l)
91

    
92
    def debug(self):
93
        '''Output a debugging view of this object'''
94
        res = ''
95
        for remote_name, name, converter, desc in self.MORE_COLUMNS or self.COLUMNS:
96
            if hasattr(self, name):
97
                res += name + ':' + repr(getattr(self, name)) + '\n'
98
        return res
99

    
100
    def __int__(self):
101
        '''Return the object id'''
102
        return self.id
103

    
104
class UrgentContact(SimpleObject):
105
    COLUMNS = (
106
            ('IDENFANTS', 'id_enfant', strip_and_int, 'IDENFANTS'),
107
            ('IDCONTACT_AUTORISE', 'id', strip_and_int, 'IDCONTACT_AUTORISE'),
108
            ('LIENFAMILLE_CH', 'lien_de_famille', unicode_and_strip, 'LIENFAMILLE_CH'),
109
            ('PERE_MERE_CH', 'lien_pere_ou_pere', unicode_and_strip, 'PERE_MERE_CH'),
110
            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
111
            ('TYPE_CH', 'type', unicode_and_strip, 'TYPE_CH'),
112
            ('NOM_CH', 'nom', unicode_and_strip, 'NOM_CH'),
113
            ('PRENOM_CH', 'prenom', unicode_and_strip, 'PRENOM_CH'),
114
            ('RUE_CH', 'rue', unicode_and_strip, 'RUE_CH'),
115
            ('RUE2_CH', 'rue2', unicode_and_strip, 'RUE2_CH'),
116
            ('RUE3_CH', 'rue3', unicode_and_strip, 'RUE3_CH'),
117
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'CODEPOSTAL_CH'),
118
            ('VILLE_CH', 'ville', unicode_and_strip, 'VILLE_CH'),
119
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'TELEPHONE_CH'),
120
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'TELEPHONE2_CH'),
121
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'ADRESSEINT_CH'),
122
    )
123

    
124
class Child(SimpleObject):
125
    COLUMNS = (
126
            ('IDENFANTS', 'id', strip_and_int, 'Identifiant de ENFANTS'),
127
            ('NOM_CH', 'nom', unicode_and_strip, 'Nom'),
128
            ('PRENOM_CH', 'prenom', unicode_and_strip, 'Prénom'),
129
            ('NAISSANCE_DA', 'date_naissance', strip_and_date, 'Date de Naissance'),
130
            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'Commentaires / Notes'),
131
            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
132
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'Code Postal'),
133
            ('VILLE_CH', 'ville', unicode_and_strip, 'Ville'),
134
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'Code Interne'),
135
            ('LIEUNAISSANCE_CH', 'lieu_naissance', unicode_and_strip, 'Lieu de Naissance'),
136
            ('DEPNAISSANCE_CH', 'departement_naissance', unicode_and_strip, 'Département Naissance'),
137
            ('NUMSECU_CH', 'num_securite_sociale', unicode_and_strip, 'N° de SECU'),
138
            ('NATIONALITE_CH', 'nationalite', unicode_and_strip, 'Nationalité'),
139
            ('PRENOM2_CH', 'prenom2', unicode_and_strip, 'Prénom 2'),
140
            ('SEXE_CH', 'sexe', unicode_and_strip, 'Sexe'),
141
            ('IDTABLELIBRE1', 'IDTABLELIBRE1', unicode_and_strip, 'IDTABLELIBRE1'),
142
            ('IDTABLELIBRE2', 'IDTABLELIBRE2', unicode_and_strip, 'IDTABLELIBRE2'),
143
            ('IDTABLELIBRE3', 'IDTABLELIBRE3', unicode_and_strip, 'IDTABLELIBRE3'),
144
            ('IDTABLELIBRE4', 'IDTABLELIBRE4', unicode_and_strip, 'IDTABLELIBRE4'),
145
            ('CHAMPLIBRE1_CH', 'CHAMPLIBRE1_CH', unicode_and_strip, 'Valeur Champ Libre 1'),
146
            ('CHAMPLIBRE2_CH', 'CHAMPLIBRE2_CH', unicode_and_strip, 'Valeur Champ Libre 2'),
147
            ('CHAMPCALCULE1_CH', 'CHAMPCALCULE1_CH', unicode_and_strip, 'Valeur Champ Calculé 1'),
148
            ('CHAMPCALCULE2_CH', 'CHAMPCALCULE2_CH', unicode_and_strip, 'Valeur Champ Calculé 2'),
149
            ('SOMMEIL_ME', 'sommeil', unicode_and_strip, 'Sommeil'),
150
            ('ACTIVITE_ME', 'activite', unicode_and_strip, 'Activités'),
151
            ('HABITUDE_ME', 'habitude', unicode_and_strip, 'Habitudes'),
152
            ('PHOTO_CH', 'photographie', unicode_and_strip, 'Photographie'),
153
            ('NUMCOMPTE_CH', 'numcompte', unicode_and_strip, 'N° Compte Comptable'),
154
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'Téléphone'),
155
            ('IDFAMILLES2', 'id_famille2', unicode_and_strip, 'Identifiant famille 2'),
156
            ('PERE_CH', 'pere', unicode_and_strip, 'Nom du père'),
157
            ('MERE_CH', 'mere', unicode_and_strip, 'Nom de la mère'),
158
            ('AUTOPARENTALEMERE_IN', 'autorisation_parentale_mere', unicode_and_strip, 'Autorisation Parentale Mère'),
159
            ('AUTOPARENTALEPERE_IN', 'autorisation_parentale_pere', unicode_and_strip, 'Autorisation Parentale de Père'),
160
            ('IDPORTAIL_ENFANTS', 'id_portail_enfants', unicode_and_strip, 'Identifiant de PORTAIL_ENFANTS'),
161
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'Adresse Internet'),
162
    )
163

    
164
    def save(self):
165
        if hasattr(self, 'id'):
166
            self.client.update_child(self)
167
        else:
168
            self.id = self.client.add_child(self)
169
        self.client.clear_cache()
170

    
171
class Family(SimpleObject):
172
    COLUMNS = (
173
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
174
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
175
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
176
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
177
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
178
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
179
    )
180

    
181
    MORE_COLUMNS = (
182
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
183
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
184
            ('CIVILITE_CH', 'civilite', unicode_and_strip, 'civilité'),
185
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
186
            ('RUE_CH', 'rue', unicode_and_strip, 'rue'),
187
            ('RUE2_CH', 'rue2', unicode_and_strip, 'rue 2'),
188
            ('RUE3_CH', 'rue3', unicode_and_strip, 'rue 3'),
189
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'code postal'),
190
            ('VILLE_CH', 'ville', unicode_and_strip, 'ville'),
191
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'téléphone'),
192
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'téléphone 2'),
193
            ('TELECOPIE_CH', 'telecopie', unicode_and_strip, 'télécopie'),
194
            ('TELECOPIE2_CH', 'telecopie2', unicode_and_strip, 'télécopie 2'),
195
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
196
            ('SITUATION_CH', 'situation', unicode_and_strip, 'situation familiale'),
197
            ('REVENUMENSUEL_MO', 'revenu_mensuel', unicode_and_strip, 'revenu mensuel de la famille'),
198
            ('REVENUANNUEL_MO', 'revenu_annuel', unicode_and_strip, 'revenu annuel de la famille'),
199
            ('QUOTIENTFAMILIAL_MO', 'quotient_familial', unicode_and_strip, 'quotient familial'),
200
            ('NBTOTALENFANTS_EN', 'nb_total_enfants', unicode_and_strip, 'nombre total d\'enfants'),
201
            ('NBENFANTSACHARGE_EN', 'nb_enfants_a_charge', unicode_and_strip, 'nombre d\'enfants à charge'),
202
            ('NOMPERE_CH', 'nom_pere', unicode_and_strip, 'monsieur'),
203
            ('PRENOMPERE_CH', 'prenom_pere', unicode_and_strip, 'prénom monsieur'),
204
            ('AUTOPARENTALEPERE_IN', 'autoparentale_pere', unicode_and_strip, 'autorisation parentale de père'),
205
            ('DATENAISPERE_DA', 'date_naissance_pere', strip_and_date, 'date de naisance du père'),
206
            ('DEPNAISPERE_EN', 'departement_naissance_pere', unicode_and_strip, 'département de naissance du père'),
207
            ('LIEUNAISPERE_CH', 'lieu_naissance_pere', unicode_and_strip, 'lieu de naissance du père'),
208
            ('RUEPERE_CH', 'rue_pere', unicode_and_strip, 'rue père'),
209
            ('RUE2PERE_CH', 'rue2_pere', unicode_and_strip, 'rue 2 père'),
210
            ('RUE3PERE_CH', 'rue3_pere', unicode_and_strip, 'rue 3 père'),
211
            ('CODEPOSTALPERE_CH', 'code_postal_pere', unicode_and_strip, 'code postal père'),
212
            ('VILLEPERE_CH', 'ville_pere', unicode_and_strip, 'ville père'),
213
            ('TELEPHONEPERE_CH', 'telephone_pere', unicode_and_strip, 'téléphone père'),
214
            ('TELEPHONE2PERE_CH', 'telephone2_pere', unicode_and_strip, 'téléphone 2 père'),
215
            ('TELPERE_LR_IN', 'tel_pere_liste_rouge', unicode_and_strip, 'téléphone liste rouge père'),
216
            ('TEL2PERE_LR_IN', 'tel2_pere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge père'),
217
            ('TEL_LR_IN', 'tel_liste_rourge', unicode_and_strip, 'téléphone liste rouge'),
218
            ('TEL2_LR_IN', 'tel2_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge'),
219
            ('NOMMERE_CH', 'nom_mere', unicode_and_strip, 'madame'),
220
            ('PRENOMMERE_CH', 'prenom_mere', unicode_and_strip, 'prénom madame'),
221
            ('AUTOPARENTALEMERE_IN', 'autoparentale_mere', unicode_and_strip, 'autorisation parentale mère'),
222
            ('DATENAISMERE_DA', 'date_naissance_mere', strip_and_date, 'date de naissance de la mère'),
223
            ('DEPNAISMERE_EN', 'departement_naissance_mere', unicode_and_strip, 'département de naissance de la mère'),
224
            ('LIEUNAISMERE_CH', 'lieu_naissance_mere', unicode_and_strip, 'lieu de naissance de la mère'),
225
            ('RUEMERE_CH', 'rue_mere', unicode_and_strip, 'rue mère'),
226
            ('REVMENSUELPERE_MO', 'revenu_mensuel_pere', unicode_and_strip, 'revenu mensuel du père'),
227
            ('RUE2MERE_CH', 'rue2_mere', unicode_and_strip, 'rue 2 mère'),
228
            ('RUE3MERE_CH', 'rue3_mere', unicode_and_strip, 'rue 3 mère'),
229
            ('CODEPOSTALMERE_CH', 'code_postal_mere', unicode_and_strip, 'code postal de la mère'),
230
            ('VILLEMERE_CH', 'ville_mere', unicode_and_strip, 'ville de la mère'),
231
            ('REVMENSUELMERE_MO', 'revenu_mensuel_mere', unicode_and_strip, 'revenu mensuel mère'),
232
            ('REVANNUELPERE_MO', 'revenu_annuel_pere', unicode_and_strip, 'revenu annuel père'),
233
            ('REVANNUELMERE_MO', 'revenu_annuel_mere', unicode_and_strip, 'revenu annuel mère'),
234
            ('TELEPHONEMERE_CH', 'telephone_mere', unicode_and_strip, 'téléphone mère'),
235
            ('TELEPHONE2MERE_CH', 'telephone2_mere', unicode_and_strip, 'téléphone 2 mère'),
236
            ('TELMERE_LR_IN', 'telephone_mere_liste_rouge', unicode_and_strip, 'téléphone liste rouge mère'),
237
            ('TEL2MERE_LR_IN', 'telephone2_mere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge mère'),
238
            ('TELECOPIEPERE_CH', 'telecopie_pere', unicode_and_strip, 'télécopie du père'),
239
            ('TELECOPIE2PERE_CH', 'telecopie2_pere', unicode_and_strip, 'télécopie 2 du père'),
240
            ('TELECOPIEMERE_CH', 'telecopie_mere', unicode_and_strip, 'télécopie de la mère'),
241
            ('TELECOPIE2MERE_CH', 'telecopie2_mere', unicode_and_strip, 'télécopie 2 de la mère'),
242
            ('PROFPERE_CH', 'profession_pere', unicode_and_strip, 'profession du père'),
243
            ('PROFMERE_CH', 'profession_mere', unicode_and_strip, 'profession de la mère'),
244
            ('LIEUTRAVPERE_CH', 'lieu_travail_pere', unicode_and_strip, 'lieu de travail du père'),
245
            ('LIEUTRAVMERE_CH', 'lieu_travail_mere', unicode_and_strip, 'lieu de travail de la mère'),
246
            ('RUETRAVPERE_CH', 'rue_travail_pere', unicode_and_strip, 'rue travail père'),
247
            ('RUE2TRAVPERE_CH', 'rue2_travail_pere', unicode_and_strip, 'rue 2 travail père'),
248
            ('RUE3TRAVPERE_CH', 'rue3_travail_pere', unicode_and_strip, 'rue 3 travail père'),
249
            ('CPTRAVPERE_CH', 'code_postal_travail_pere', unicode_and_strip, 'code postal travail père'),
250
            ('VILLETRAVPERE_CH', 'ville_travail_pere', unicode_and_strip, 'ville travail père'),
251
            ('RUETRAVMERE_CH', 'rue_travail_mere', unicode_and_strip, 'rue travail mère'),
252
            ('RUE2TRAVMERE_CH', 'rue2_travail_mere', unicode_and_strip, 'rue 2 travail mère'),
253
            ('RUE3TRAVMERE_CH', 'rue3_travail_mere', unicode_and_strip, 'rue 3 travail mère'),
254
            ('CPTRAVMERE_CH', 'code_postal_travail_mere', unicode_and_strip, 'code postal travail mère'),
255
            ('VILLETRAVMERE_CH', 'ville_travail_mere', unicode_and_strip, 'ville travail mère'),
256
            ('TELPROFPERE_CH', 'telephone_travail_pere', unicode_and_strip, 'téléphone professionnel père'),
257
            ('TEL2PROFPERE_CH', 'telephone2_travail_pere', unicode_and_strip, 'téléphone 2 professionnel père'),
258
            ('TELMOBILPERE_CH', 'telephone_mobile_pere', unicode_and_strip, 'téléphone mobile'),
259
            ('TELPROFMERE_CH', 'telephone_travail_mere', unicode_and_strip, 'téléphone travail mère'),
260
            ('TEL2PROFMERE_CH', 'telephone2_travail_mere', unicode_and_strip, 'téléphone 2 travail mère'),
261
            ('TELMOBILMERE_CH', 'telephone_mobile_mere', unicode_and_strip, 'téléphone mobile mère'),
262
            ('TOTALDU_MO', 'total_du', unicode_and_strip, 'total dû'),
263
            ('TOTALREGLE_MO', 'total_regle', unicode_and_strip, 'total réglé'),
264
            ('NUMCENTRESS_CH', 'num_centre_securite_sociale', unicode_and_strip, 'n° centre sécurité sociale'),
265
            ('NOMCENTRESS_CH', 'nom_centre_securite_sociale', unicode_and_strip, 'nom centre sécurité sociale'),
266
            ('NUMASSURANCE_CH', 'num_assurance', unicode_and_strip, 'n° assurance'),
267
            ('NOMASSURANCE_CH', 'nom_assurance', unicode_and_strip, 'nom assurance'),
268
            ('RIVOLI_EN', 'code_rivoli', unicode_and_strip, 'identifiant code rivoli'),
269
            ('NUMCOMPTE_CH', 'numero_compte_comptable', unicode_and_strip, 'n° compte comptable'),
270
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
271
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
272
            ('NUMALLOCATAIRE_CH', 'numero_allocataire', unicode_and_strip, 'n° allocataire'),
273
            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'commentaires / notes'),
274
            ('IDCSPPERE', 'identifiant_csp_pere', unicode_and_strip, 'référence identifiant csp'),
275
            ('IDCSPMERE', 'identifiant_csp_mere', unicode_and_strip, 'référence identifiant csp'),
276
            ('IDSECTEURS', 'identifiant_secteurs', unicode_and_strip, 'référence identifiant secteurs'),
277
            ('IDZONES', 'identifiant_zones', unicode_and_strip, 'référence identifiant zones'),
278
            ('IDRUES', 'identifiant_rues', unicode_and_strip, 'référence identifiant rues'),
279
            ('IDVILLES', 'identifiant_villes', unicode_and_strip, 'référence identifiant villes'),
280
            ('IDREGIMES', 'identifiant_regimes', unicode_and_strip, 'référence identifiant regimes'),
281
            ('IDSITUATIONFAMILLE', 'identifiant_situation_famille', unicode_and_strip, 'référence identifiant situationfamille'),
282
            ('NUMSECUPERE_CH', 'num_securite_sociale_pere', unicode_and_strip, 'n° secu père'),
283
            ('NUMSECUMERE_CH', 'num_securite_sociale_mere', unicode_and_strip, 'n° secu mère'),
284
            ('NATIONPERE_CH', 'nation_pere', unicode_and_strip, 'nationalité père'),
285
            ('NATIONMERE_CH', 'nation_mere', unicode_and_strip, 'nationalité mère'),
286
            ('NOMJEUNEFILLE_CH', 'nom_jeune_fille', unicode_and_strip, 'nom jeune fille'),
287
            ('IDCAFS', 'idcafs', unicode_and_strip, 'référence identifiant cafs'),
288
            ('CHAMPLIBRE1_CH', 'champ_libre1', unicode_and_strip, 'valeur champ libre 1'),
289
            ('CHAMPLIBRE2_CH', 'champ_libre2', unicode_and_strip, 'valeur champ libre 2'),
290
            ('CHAMPCALCULE1_CH', 'champ_calcule1', unicode_and_strip, 'valeur champ calculé 1'),
291
            ('CHAMPCALCULE2_CH', 'champ_calcule2', unicode_and_strip, 'valeur champ calculé 2'),
292
            ('IDTABLELIBRE1', 'id_table_libre1', unicode_and_strip, 'idtablelibre1'),
293
            ('IDTABLELIBRE3', 'id_table_libre3', unicode_and_strip, 'idtablelibre3'),
294
            ('IDTABLELIBRE2', 'id_table_libre2', unicode_and_strip, 'idtablelibre2'),
295
            ('IDTABLELIBRE4', 'id_table_libre4', unicode_and_strip, 'idtablelibre4'),
296
            ('NOMURSSAF_CH', 'nom_urssaf', unicode_and_strip, 'nom urssaf'),
297
            ('NUMURSSAF_CH', 'num_urssaf', unicode_and_strip, 'n° urssaf'),
298
            ('IDPROFPERE', 'identifiant_profession_pere', unicode_and_strip, 'référence identifiant profession'),
299
            ('IDPROFMERE', 'identifiant_profession_mere', unicode_and_strip, 'référence identifiant profession'),
300
            ('ALLOCATAIRE_CH', 'allocataire', unicode_and_strip, 'allocataire père ou mère (p,m)'),
301
#            ('PHOTOPERE_CH', 'photo_pere', unicode_and_strip, 'photographie père'),
302
#            ('PHOTOMERE_CH', 'photo_mere', unicode_and_strip, 'photographie mère'),
303
            ('NUMRUE_CH', 'numero_rue', unicode_and_strip, 'numéro de rue'),
304
            ('NUMRUEPERE_CH', 'numero_rue_pere', unicode_and_strip, 'numéro de rue père'),
305
            ('NUMRUEMERE_CH', 'numero_rue_mere', unicode_and_strip, 'numéro de rue mère'),
306
            ('IDPORTAIL_FAMILLES', 'identifiant_portail_familles', unicode_and_strip, 'identifiant de portail_familles'),
307
            ('ECHEANCEASSURANCE_DA', 'echeance_assurance', unicode_and_strip, 'date echéance assurance'),
308
            ('RM_MIKADO_MO', 'rm_mikado', unicode_and_strip, 'revenus mensuels mikado'),
309
            ('RA_MIKADO_MO', 'ra_mikado', unicode_and_strip, 'revenus annuels mikado'),
310
            ('QF_MIKADO_MO', 'qf_mikado', unicode_and_strip, 'quotient familial mikado'),
311
            ('RM_DIABOLO_MO', 'rm_diabolo', unicode_and_strip, 'revenus mensuels diabolo'),
312
            ('RA_DIABOLO_MO', 'ra_diabolo', unicode_and_strip, 'revenus annuels diabolo'),
313
            ('QF_DIABOLO_MO', 'qf_diabolo', unicode_and_strip, 'quotient familial diabolo'),
314
            ('RM_OLIGO_MO', 'rm_oligo', unicode_and_strip, 'revenus mensuels oligo'),
315
            ('RA_OLIGO_MO', 'ra_oligo', unicode_and_strip, 'revenus annuels oligo'),
316
            ('QF_OLIGO_MO', 'qf_oligo', unicode_and_strip, 'quotient familial oligo'),
317
            ('APPLICATION_REV_MIKADO_DA', 'application_rev_mikado', unicode_and_strip, 'date d\'application des revenus de mikado'),
318
            ('APPLICATION_REV_DIABOLO_DA', 'application_rev_diabolo', unicode_and_strip, 'date d\'application des revenus de diabolo'),
319
            ('APPLICATION_REV_OLIGO_DA', 'application_rev_oligo', unicode_and_strip, 'date d\'application des revenus de oligo'),
320
    )
321

    
322
    def __init__(self, *args, **kwargs):
323
        self.children = []
324
        super(Family, self).__init__(*args, **kwargs)
325

    
326
    def complete(self):
327
        k = [a for a,b,c,d in self.MORE_COLUMNS]
328
        list(self.client('LISTER_FAMILLES', args=(','.join(k), self.id),
329
                columns=self.MORE_COLUMNS, instances=(self,)))
330
        l = self.client.get_children(self.id).values()
331
        self.children = sorted(l, key=lambda c: c.id)
332
        return self
333

    
334
    @property
335
    def invoices(self):
336
        return [invoice for id, invoice in self.client.invoices.iteritems() if invoice.id_famille == self.id]
337

    
338
    def add_child(self, child):
339
        if hasattr(self, 'id'):
340
            child.id_famille = self.id
341
            child.client = self.client
342
        self.children.append(child)
343

    
344
    def save(self):
345
        if hasattr(self, 'id'):
346
            self.client.update_family(self)
347
        else:
348
            self.code_interne = self.client.new_code_interne()
349
            self.id = self.client.add_family(self)
350
        for child in self.children:
351
            child.id_famille = self.id
352
            child.save()
353
        self.client.clear_cache()
354

    
355
class Invoice(SimpleObject):
356
    COLUMNS = (
357
        ('', 'id_famille', int, ''),
358
        ('', 'id', int, ''),
359
        ('', 'numero', str, ''),
360
        ('', 'debut_periode', parse_date, ''),
361
        ('', 'fin_periode', parse_date, ''),
362
        ('', 'creation', parse_date, ''),
363
        ('', 'echeance', parse_date, ''),
364
        ('', 'montant', Decimal, ''),
365
        ('', 'reste_du', Decimal, ''),
366
    )
367
    _detail = None
368

    
369
    def detail(self):
370
        if not self._detail:
371
            self.client.factures_detail([self])
372
        return self._detail
373

    
374
    @property
375
    def family(self):
376
        return self.client.families[self.id_famille]
377

    
378
    def paid(self):
379
        return self.reste_du == Decimal(0)
380

    
381
class DominoWs(object):
382
    '''Interface to the WebService exposed by Abelium Domino.
383

    
384
       It allows to retrieve family and invoices.
385

    
386
       Beware that it does a lot of caching to limit call to the webservice, so
387
       if you need fresh data, call clear_cache()
388

    
389
       All family are in the families dictionnary and all invoices in the
390
       invoices dictionnary.
391
    '''
392

    
393
    def __init__(self, url, domain, login, password, location=None):
394
        if not Client:
395
            raise ValueError('You need python suds')
396
        logger.debug('creating DominoWs(%r, %r, %r, %r, location=%r)', url, domain, login, password, location)
397
        self.url = url
398
        self.domain = domain
399
        self.login = login
400
        self.password = password
401
        self.client = Client(url, location=location, timeout=60)
402
        self.client.options.cache.setduration(seconds=60)
403

    
404
    def clear_cache(self):
405
        '''Remove cached attributes from the instance.'''
406

    
407
        for key, value in self.__dict__.items():
408
            if key.startswith('__') and key.endswith('_cache'):
409
                del self.__dict__[key]
410

    
411
    def call(self, function_name, *args):
412
        '''Call SOAP method named function_name passing args list as parameters.
413

    
414
           Any error is converted into the DominoException class.'''
415
        print 'call', function_name, args
416

    
417
        try:
418
            logger.debug('soap call to %s%r', function_name, args)
419
            data = getattr(self.client.service, function_name)(self.domain, self.login, self.password, *args)
420
            logger.debug('result: %s', data)
421
            self.data = data
422
        except IOError, e:
423
            raise DominoException('Erreur IO', e)
424
        if data is None:
425
           data = ''
426
        if data.startswith('ERREUR'):
427
            raise DominoException(data[9:].encode('utf8'))
428
        return data
429

    
430
    def parse_tabular_data(self, data):
431
        '''Row are separated by carriage-return, ASCII #13, characters and columns by tabs.
432
           Empty lines (ignoring spaces) are ignored.
433
        '''
434

    
435
        rows = data.split(LINE_SEPARATOR)
436
        rows = [[cell.strip() for cell in row.split(COLUMN_SEPARATOR)] for row in rows if row.strip() != '']
437
        return rows
438

    
439
    def __call__(self, function_name, cls=None, args=[], instances=None, columns=None):
440
        '''Call SOAP method named function_name, splitlines, map tab separated
441
        values to _map keys in a dictionnary, and use this dictionnary to
442
        initialize an object of class cls.
443

    
444
        - If instances is present, the given instances are updated with the
445
          returned content, in order, row by row.
446
        - If cls is not None and instances is None, a new instance of the class
447
          cls is instancied for every row and initialized with the content of
448
          the row.
449
        - If cls and instances are None, the raw data returned by the SOAP call
450
          is returned.
451
        '''
452

    
453
        data = self.call(function_name, *args)
454
        if cls or instances:
455
            rows = self.parse_tabular_data(data)
456
            kwargs = {}
457
            if instances:
458
                rows = zip(rows, instances)
459
            for row in rows:
460
                if instances:
461
                    row, instance = row
462
                if not row[0]:
463
                    continue
464
                for a, b in zip(columns or cls.COLUMNS, row):
465
                    x, name, converter, desc = a
466
                    kwargs[name] = converter(b.strip())
467
                if instances:
468
                    instance.__dict__.update(kwargs)
469
                    yield instance
470
                else:
471
                    yield cls(client=self, **kwargs)
472
        else:
473
            yield data
474

    
475
    def add_family(self, family):
476
        result = self.call('AJOUTER_FAMILLE', family.serialize())
477
        return int(result.strip())
478

    
479
    def update_family(self, family):
480
        if not hasattr(family, 'id'):
481
            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
482
        result = self.call('MODIFIER_FAMILLE', unicode(family.id), family.serialize())
483
        return result.strip() == 'OK'
484

    
485
    def add_child(self, child):
486
        result = self.call('AJOUTER_ENFANT', child.serialize())
487
        return int(result.strip())
488

    
489
    def update_child(self, child):
490
        if not hasattr(child, 'id'):
491
            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
492
        result = self.call('MODIFIER_ENFANT', unicode(child.id), child.serialize())
493
        return result.strip() == 'OK'
494

    
495
    @property
496
    @object_cached
497
    def families(self):
498
        '''Dictionary of all families indexed by their id.
499

    
500
           After the first use, the value is cached. Use clear_cache() to reset
501
           it.
502
        '''
503

    
504
        return self.get_families()
505

    
506
    def get_families(self, id_famille=0, full=False):
507
        '''Get families informations.
508
           There is no caching.
509

    
510
           id_famille - if not 0, the family with this id is retrieved. If 0
511
           all families are retrieved. Default to 0.
512
           full - If True return all the columns of the family table. Default
513
           to False.
514
        '''
515
        columns = Family.MORE_COLUMNS if full else Family.COLUMNS
516
        families = self('LISTER_FAMILLES',
517
                Family,
518
                args=(','.join([x[0] for x in columns]), id_famille))
519
        return dict([(int(x), x) for x in families])
520

    
521
    def get_children(self, id_famille=0):
522
        columns = Child.COLUMNS
523
        if id_famille == 0:
524
            children = self('LISTER_ENFANTS',
525
                Child,
526
                args=((','.join([x[0] for x in columns])),))
527
        else:
528
            children = self('LISTER_ENFANTS_FAMILLE',
529
                Child,
530
                args=(id_famille, (','.join([x[0] for x in columns]))))
531
        return dict([(int(x), x) for x in children])
532

    
533
    def get_urgent_contacts(self, id_enfant):
534
        columns = UrgentContact.COLUMNS
535
        urgent_contacts = self('LISTER_PERSONNES_URGENCE',
536
                UrgentContact,
537
                
538
                args=((id_enfant, ','.join([x[0] for x in columns]))))
539
        return dict([(int(x), x) for x in urgent_contacts])
540

    
541
    @property
542
    @object_cached
543
    def invoices(self):
544
        '''Dictionnary of all invoices indexed by their id.
545

    
546
           After the first use, the value is cached. Use clear_cache() to reset
547
           it.
548
        '''
549
        invoices = self.get_invoices()
550
        for invoice in invoices.values():
551
            invoice.famille = self.families[invoice.id_famille]
552
        return invoices
553

    
554
    def new_code_interne(self):
555
        max_ci = 0
556
        for family in self.families.values():
557
            try:
558
                max_ci = max(max_ci, int(family.code_interne))
559
            except:
560
                pass
561
        return '%05d' % (max_ci+1)
562

    
563
    def get_invoices(self, id_famille=0, state='TOUTES'):
564
        '''Get invoices informations.
565

    
566
           id_famille - If value is not 0, only invoice for the family with
567
           this id are retrieved. If value is 0, invoices for all families are
568
           retrieved. Default to 0.
569
           etat - state of the invoices to return, possible values are
570
           'SOLDEES', 'NON_SOLDEES', 'TOUTES'.
571
        '''
572
        invoices = self('LISTER_FACTURES_FAMILLE', Invoice,
573
            args=(id_famille, state))
574
        invoices = list(invoices)
575
        for invoice in invoices:
576
            invoice.famille = self.families[invoice.id_famille]
577
        return dict(((int(x), x) for x in invoices))
578

    
579
    FACTURE_DETAIL_HEADERS = ['designation', 'quantite', 'prix', 'montant']
580
    def factures_detail(self, invoices):
581
        '''Retrieve details of some invoice'''
582
        data = self.call('DETAILLER_FACTURES', (''.join(("%s;" % int(x) for x in invoices)),))
583
        try:
584
            tree = etree.fromstring(data.encode('utf8'))
585
            for invoice, facture_node in zip(invoices, tree.findall('facture')):
586
                rows = []
587
                for ligne in facture_node.findall('detail_facture/ligne'):
588
                    row = []
589
                    rows.append(row)
590
                    for header in self.FACTURE_DETAIL_HEADERS:
591
                        if header in ligne.attrib:
592
                            row.append((header, ligne.attrib[header]))
593
                etablissement = facture_node.find('detail_etablissements/etablissement')
594
                if etablissement is not None:
595
                    nom = etablissement.get('nom').strip()
596
                else:
597
                    nom = ''
598
                d = { 'etablissement': nom, 'lignes': rows }
599
                invoice._detail = d
600
        except Exception, e:
601
            raise
602
            raise DominoException('Exception when retrieving invoice details', e)
603

    
604
    def get_family_by_mail(self, email):
605
        '''Return the first whose one email attribute matches the given email'''
606
        for famille in self.families.values():
607
            if email in (famille.email_pere, famille.email_mere,
608
                    famille.adresse_internet):
609
                return famille
610
        return None
611

    
612
    def get_family_by_code_interne(self, code_interne):
613
        '''Return the first whose one email attribute matches the given email'''
614
        for famille in self.families.values():
615
            if getattr(famille, 'code_interne', None) == code_interne:
616
                return famille
617
        return None
618

    
619
    def pay_invoice(self, id_invoices, amount, other_information, date=None):
620
        '''Notify Domino of the payment of some invoices.
621

    
622
           id_invoices - integer if of the invoice or Invoice instances
623
           amount - amount as a Decimal object
624
           other_information - free content to attach to the payment, like a
625
           bank transaction number for correlation.
626
           date - date of the payment, must be a datetime object. If None,
627
           now() is used. Default to None.
628
        '''
629

    
630
        if not date:
631
            date = datetime.datetime.now()
632
        due = sum([self.invoices[int(id_invoice)].due for id_invoice in id_invoices])
633
        if Decimal(amount) == Decimal(due):
634
            return self('SOLDER_FACTURE', None, args=(str(amount), 
635
                    ''.join([ '%s;' % int(x) for x in id_invoices]),
636
                    date.strftime('%Y-%m-%d'), other_information))
637
        else:
638
            raise DominoException('Amount due and paid do not match', { 'due': due, 'paid': amount})
(6-6/32)