Projet

Général

Profil

Télécharger (33 ko) Statistiques
| Branche: | Tag: | Révision:

root / auquotidien / modules / abelium_domino_ws.py @ 8b02623d

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
try:
9
    from suds.client import Client
10
    from suds.bindings.binding import Binding
11
    # Webdev is bugged and using an HTML generator to produce XML content, 
12
    Binding.replyfilter = lambda self, x: x.replace(' ', ' ')
13
except ImportError:
14
    Client = None
15
    Binding = None
16

    
17
logger = logging.getLogger(__name__)
18

    
19
# cleaning and parsing functions
20

    
21
LINE_SEPARATOR = '\n'
22
COLUMN_SEPARATOR = '\t'
23

    
24
def unicode_and_strip(x):
25
    return unicode(x).strip()
26

    
27
def strip_and_int(x):
28
    try:
29
        return int(x.strip())
30
    except ValueError:
31
        return None
32

    
33
def strip_and_date(x):
34
    try:
35
        return datetime.datetime.strptime(x.strip(), '%Y%m%d').date()
36
    except ValueError:
37
        return None
38

    
39
def parse_date(date_string):
40
    if date_string:
41
        return datetime.datetime.strptime(date_string, "%Y%m%d")
42
    else:
43
        None
44

    
45
class DominoException(Exception):
46
    pass
47

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

    
66
# Data model
67
class SimpleObject(object):
68
    '''Base class for object returned by the web service'''
69

    
70
    '''Describe basic columns'''
71
    COLUMNS = ()
72
    '''Describe extended object columns'''
73
    MORE_COLUMNS = ()
74

    
75
    def __init__(self, **kwargs):
76
        self.__dict__.update(kwargs)
77

    
78
    def __repr__(self):
79
        c = {}
80
        for remote_name, name, converter, desc in self.COLUMNS:
81
            if hasattr(self, name):
82
                c[name] = getattr(self, name)
83
        return str(c)
84

    
85
    def serialize(self):
86
        l = []
87
        for remote_name, local_name, converter, desc in self.COLUMNS + self.MORE_COLUMNS:
88
            if local_name == 'id':
89
                continue
90
            v = getattr(self, local_name, None)
91
            if v is None:
92
                continue
93
            if isinstance(v, (datetime.date, datetime.datetime)):
94
                v = v.strftime('%Y%m%d')
95
            if remote_name.endswith('_DA') and '-' in v:
96
                v = v.replace('-', '')
97
            l.append(u'{0}: "{1}"'.format(remote_name, v))
98
        return u','.join(l)
99

    
100
    def debug(self):
101
        '''Output a debugging view of this object'''
102
        res = ''
103
        for remote_name, name, converter, desc in self.MORE_COLUMNS or self.COLUMNS:
104
            if hasattr(self, name):
105
                res += name + ':' + repr(getattr(self, name)) + '\n'
106
        return res
107

    
108
    def __int__(self):
109
        '''Return the object id'''
110
        return self.id
111

    
112
class UrgentContact(SimpleObject):
113
    COLUMNS = (
114
            ('IDENFANTS', 'id_enfant', strip_and_int, 'IDENFANTS'),
115
            ('IDCONTACT_AUTORISE', 'id', strip_and_int, 'IDCONTACT_AUTORISE'),
116
            ('LIENFAMILLE_CH', 'lien_de_famille', unicode_and_strip, 'LIENFAMILLE_CH'),
117
            ('PERE_MERE_CH', 'lien_pere_ou_pere', unicode_and_strip, 'PERE_MERE_CH'),
118
            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
119
            ('TYPE_CH', 'type', unicode_and_strip, 'TYPE_CH'),
120
            ('NOM_CH', 'nom', unicode_and_strip, 'NOM_CH'),
121
            ('PRENOM_CH', 'prenom', unicode_and_strip, 'PRENOM_CH'),
122
            ('RUE_CH', 'rue', unicode_and_strip, 'RUE_CH'),
123
            ('RUE2_CH', 'rue2', unicode_and_strip, 'RUE2_CH'),
124
            ('RUE3_CH', 'rue3', unicode_and_strip, 'RUE3_CH'),
125
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'CODEPOSTAL_CH'),
126
            ('VILLE_CH', 'ville', unicode_and_strip, 'VILLE_CH'),
127
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'TELEPHONE_CH'),
128
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'TELEPHONE2_CH'),
129
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'ADRESSEINT_CH'),
130
    )
131

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

    
172
    def save(self):
173
        if hasattr(self, 'id'):
174
            self.client.update_child(self)
175
        else:
176
            self.id = self.client.add_child(self)
177
        self.client.clear_cache()
178

    
179
class Family(SimpleObject):
180
    COLUMNS = (
181
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
182
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
183
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
184
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
185
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
186
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
187
    )
188

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

    
330
    def __init__(self, *args, **kwargs):
331
        self.children = []
332
        super(Family, self).__init__(*args, **kwargs)
333

    
334
    def complete(self):
335
        k = [a for a,b,c,d in self.MORE_COLUMNS]
336
        list(self.client('LISTER_FAMILLES', args=(','.join(k), self.id),
337
                columns=self.MORE_COLUMNS, instances=(self,)))
338
        l = self.client.get_children(self.id).values()
339
        self.children = sorted(l, key=lambda c: c.id)
340
        return self
341

    
342
    @property
343
    def invoices(self):
344
        return [invoice for id, invoice in self.client.invoices.iteritems() if invoice.id_famille == self.id]
345

    
346
    def add_child(self, child):
347
        if hasattr(self, 'id'):
348
            child.id_famille = self.id
349
            child.client = self.client
350
        self.children.append(child)
351

    
352
    def save(self):
353
        if hasattr(self, 'id'):
354
            self.client.update_family(self)
355
        else:
356
            self.code_interne = self.client.new_code_interne()
357
            self.id = self.client.add_family(self)
358
        for child in self.children:
359
            child.id_famille = self.id
360
            child.save()
361
        self.client.clear_cache()
362

    
363
class Invoice(SimpleObject):
364
    COLUMNS = (
365
        ('', 'id_famille', int, ''),
366
        ('', 'id', int, ''),
367
        ('', 'numero', str, ''),
368
        ('', 'debut_periode', parse_date, ''),
369
        ('', 'fin_periode', parse_date, ''),
370
        ('', 'creation', parse_date, ''),
371
        ('', 'echeance', parse_date, ''),
372
        ('', 'montant', Decimal, ''),
373
        ('', 'reste_du', Decimal, ''),
374
    )
375
    _detail = {}
376

    
377
    def detail(self):
378
        if not self._detail:
379
            self.client.factures_detail([self])
380
        return self._detail
381

    
382
    @property
383
    def family(self):
384
        return self.client.families[self.id_famille]
385

    
386
    def paid(self):
387
        return self.reste_du == Decimal(0)
388

    
389
class DominoWs(object):
390
    '''Interface to the WebService exposed by Abelium Domino.
391

    
392
       It allows to retrieve family and invoices.
393

    
394
       Beware that it does a lot of caching to limit call to the webservice, so
395
       if you need fresh data, call clear_cache()
396

    
397
       All family are in the families dictionnary and all invoices in the
398
       invoices dictionnary.
399
    '''
400

    
401
    def __init__(self, url, domain, login, password, location=None,
402
            logger=logger):
403
        if not Client:
404
            raise ValueError('You need python suds')
405
        self.logger = logger
406
        self.logger.debug('creating DominoWs(%r, %r, %r, %r, location=%r)',
407
                url, domain, login, password, location)
408
        self.url = url
409
        self.domain = domain
410
        self.login = login
411
        self.password = password
412
        self.client = Client(url, location=location, timeout=60)
413
        self.client.options.cache.setduration(seconds=60)
414

    
415
    def clear_cache(self):
416
        '''Remove cached attributes from the instance.'''
417

    
418
        for key, value in self.__dict__.items():
419
            if key.startswith('__') and key.endswith('_cache'):
420
                del self.__dict__[key]
421

    
422
    def call(self, function_name, *args):
423
        '''Call SOAP method named function_name passing args list as parameters.
424

    
425
           Any error is converted into the DominoException class.'''
426
        print 'call', function_name, args
427

    
428
        try:
429
            self.logger.debug(('soap call to %s(%s)' % (function_name, args)).encode('utf-8'))
430
            data = getattr(self.client.service, function_name)(self.domain, self.login, self.password, *args)
431
            self.logger.debug((u'result: %s' % data).encode('utf-8'))
432
            self.data = data
433
        except IOError, e:
434
            raise DominoException('Erreur IO', e)
435
        if data is None:
436
           data = ''
437
        if data.startswith('ERREUR'):
438
            raise DominoException(data[9:].encode('utf8'))
439
        return data
440

    
441
    def parse_tabular_data(self, data):
442
        '''Row are separated by carriage-return, ASCII #13, characters and columns by tabs.
443
           Empty lines (ignoring spaces) are ignored.
444
        '''
445

    
446
        rows = data.split(LINE_SEPARATOR)
447
        rows = [[cell.strip() for cell in row.split(COLUMN_SEPARATOR)] for row in rows if row.strip() != '']
448
        return rows
449

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

    
455
        - If instances is present, the given instances are updated with the
456
          returned content, in order, row by row.
457
        - If cls is not None and instances is None, a new instance of the class
458
          cls is instancied for every row and initialized with the content of
459
          the row.
460
        - If cls and instances are None, the raw data returned by the SOAP call
461
          is returned.
462
        '''
463

    
464
        data = self.call(function_name, *args)
465
        if cls or instances:
466
            rows = self.parse_tabular_data(data)
467
            kwargs = {}
468
            if instances:
469
                rows = zip(rows, instances)
470
            for row in rows:
471
                if instances:
472
                    row, instance = row
473
                if not row[0]:
474
                    continue
475
                for a, b in zip(columns or cls.COLUMNS, row):
476
                    x, name, converter, desc = a
477
                    kwargs[name] = converter(b.strip())
478
                if instances:
479
                    instance.__dict__.update(kwargs)
480
                    yield instance
481
                else:
482
                    yield cls(client=self, **kwargs)
483
        else:
484
            yield data
485

    
486
    def add_family(self, family):
487
        result = self.call('AJOUTER_FAMILLE', family.serialize())
488
        return int(result.strip())
489

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

    
496
    def add_child(self, child):
497
        result = self.call('AJOUTER_ENFANT', child.serialize())
498
        return int(result.strip())
499

    
500
    def update_child(self, child):
501
        if not hasattr(child, 'id'):
502
            raise DominoException('Family lacks an "id" attribute, it usually means that it is new.')
503
        result = self.call('MODIFIER_ENFANT', unicode(child.id), child.serialize())
504
        return result.strip() == 'OK'
505

    
506
    @property
507
    @object_cached
508
    def families(self):
509
        '''Dictionary of all families indexed by their id.
510

    
511
           After the first use, the value is cached. Use clear_cache() to reset
512
           it.
513
        '''
514

    
515
        return self.get_families()
516

    
517
    def get_families(self, id_famille=0, full=False):
518
        '''Get families informations.
519
           There is no caching.
520

    
521
           id_famille - if not 0, the family with this id is retrieved. If 0
522
           all families are retrieved. Default to 0.
523
           full - If True return all the columns of the family table. Default
524
           to False.
525
        '''
526
        columns = Family.MORE_COLUMNS if full else Family.COLUMNS
527
        families = self('LISTER_FAMILLES',
528
                Family,
529
                args=(','.join([x[0] for x in columns]), id_famille))
530
        return dict([(int(x), x) for x in families])
531

    
532
    def get_children(self, id_famille=0):
533
        columns = Child.COLUMNS
534
        if id_famille == 0:
535
            children = self('LISTER_ENFANTS',
536
                Child,
537
                args=((','.join([x[0] for x in columns])),))
538
        else:
539
            children = self('LISTER_ENFANTS_FAMILLE',
540
                Child,
541
                args=(id_famille, (','.join([x[0] for x in columns]))))
542
        return dict([(int(x), x) for x in children])
543

    
544
    def get_urgent_contacts(self, id_enfant):
545
        columns = UrgentContact.COLUMNS
546
        urgent_contacts = self('LISTER_PERSONNES_URGENCE',
547
                UrgentContact,
548
                args=((id_enfant, ','.join([x[0] for x in columns]))))
549
        return dict([(int(x), x) for x in urgent_contacts])
550

    
551
    @property
552
    @object_cached
553
    def invoices(self):
554
        '''Dictionnary of all invoices indexed by their id.
555

    
556
           After the first use, the value is cached. Use clear_cache() to reset
557
           it.
558
        '''
559
        invoices = self.get_invoices()
560
        for invoice in invoices.values():
561
            invoice.famille = self.families[invoice.id_famille]
562
        return invoices
563

    
564
    def new_code_interne(self):
565
        max_ci = 0
566
        for family in self.families.values():
567
            try:
568
                max_ci = max(max_ci, int(family.code_interne))
569
            except:
570
                pass
571
        return '%05d' % (max_ci+1)
572

    
573
    def get_invoices(self, id_famille=0, state='TOUTES'):
574
        '''Get invoices informations.
575

    
576
           id_famille - If value is not 0, only invoice for the family with
577
           this id are retrieved. If value is 0, invoices for all families are
578
           retrieved. Default to 0.
579
           etat - state of the invoices to return, possible values are
580
           'SOLDEES', 'NON_SOLDEES', 'TOUTES'.
581
        '''
582
        invoices = self('LISTER_FACTURES_FAMILLE', Invoice,
583
            args=(id_famille, state))
584
        invoices = list(invoices)
585
        for invoice in invoices:
586
            invoice.famille = self.families[invoice.id_famille]
587
        return dict(((int(x), x) for x in invoices))
588

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

    
613
    def get_family_by_mail(self, email):
614
        '''Return the first whose one email attribute matches the given email'''
615
        for famille in self.families.values():
616
            if email in (famille.email_pere, famille.email_mere,
617
                    famille.adresse_internet):
618
                return famille
619
        return None
620

    
621
    def get_family_by_code_interne(self, code_interne):
622
        '''Return the first whose one email attribute matches the given email'''
623
        for famille in self.families.values():
624
            if getattr(famille, 'code_interne', None) == code_interne:
625
                return famille
626
        return None
627

    
628
    def pay_invoice(self, id_invoices, amount, other_information, date=None):
629
        '''Notify Domino of the payment of some invoices.
630

    
631
           id_invoices - integer if of the invoice or Invoice instances
632
           amount - amount as a Decimal object
633
           other_information - free content to attach to the payment, like a
634
           bank transaction number for correlation.
635
           date - date of the payment, must be a datetime object. If None,
636
           now() is used. Default to None.
637
        '''
638

    
639
        if not date:
640
            date = datetime.datetime.now()
641
        due = sum([self.invoices[int(id_invoice)].reste_du
642
                 for id_invoice in id_invoices])
643
        if Decimal(amount) == Decimal(due):
644
            return self('SOLDER_FACTURE', None, args=(str(amount), 
645
                    ''.join([ '%s;' % int(x) for x in id_invoices]),
646
                    date.strftime('%Y-%m-%d'), other_information))
647
        else:
648
            raise DominoException('Amount due and paid do not match', { 'due': due, 'paid': amount})
(6-6/27)