Project

General

Profile

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

root / extra / modules / abelium_domino_ws.py @ 5aa61d1d

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

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

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

    
14
# cleaning and parsing functions
15

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

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

    
22
def strip_and_int(x):
23
    return int(x.strip())
24

    
25
def parse_date(date_string):
26
    if date_string:
27
        return datetime.datetime.strptime(date_string, "%Y%m%d")
28
    else:
29
        None
30

    
31
class DominoException(Exception):
32
    pass
33

    
34
def object_cached(function):
35
    '''Decorate an object method so that its results is cached on the object
36
       instance after the first call.
37
    '''
38
    def decorated_function(self, *args, **kwargs):
39
        cache_name = '__%s_cache' % function.__name__
40
        if not hasattr(self, cache_name):
41
            setattr(self, cache_name, {})
42
        d = getattr(self, cache_name)
43
        k = tuple(*args) + tuple(sorted(kwargs.items()))
44
        if not k in d:
45
            d[k] = function(self, *args, **kwargs)
46
        return d[k]
47
    return decorated_function
48

    
49
# Data model
50
class SimpleObject(object):
51
    '''Base class for object returned by the web service'''
52

    
53
    '''Describe basic columns'''
54
    COLUMNS = ()
55
    '''Describe extended object columns'''
56
    MORE_COLUMNS = ()
57

    
58
    def __init__(self, **kwargs):
59
        self.__dict__.update(kwargs)
60

    
61
    def __repr__(self):
62
        c = {}
63
        for remote_name, name, converter, desc in self.COLUMNS:
64
            if hasattr(self, name):
65
                c[name] = getattr(self, name)
66
        return str(c)
67

    
68
    def debug(self):
69
        '''Output a debugging view of this object'''
70
        for remote_name, name, converter, desc in self.MORE_COLUMNS or self.COLUMNS:
71
            if hasattr(self, name):
72
                print name, ':', getattr(self, name)
73

    
74
    def __int__(self):
75
        '''Return the object id'''
76
        return self.id
77

    
78
class Family(SimpleObject):
79
    COLUMNS = (
80
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
81
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
82
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
83
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
84
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
85
    )
86

    
87
    MORE_COLUMNS = (
88
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
89
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
90
            ('CIVILITE_CH', 'civilite', unicode_and_strip, 'civilité'),
91
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
92
            ('RUE_CH', 'rue', unicode_and_strip, 'rue'),
93
            ('RUE2_CH', 'rue2', unicode_and_strip, 'rue 2'),
94
            ('RUE3_CH', 'rue3', unicode_and_strip, 'rue 3'),
95
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'code postal'),
96
            ('VILLE_CH', 'ville', unicode_and_strip, 'ville'),
97
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'téléphone'),
98
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'téléphone 2'),
99
            ('TELECOPIE_CH', 'telecopie', unicode_and_strip, 'télécopie'),
100
            ('TELECOPIE2_CH', 'telecopie2', unicode_and_strip, 'télécopie 2'),
101
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
102
            ('SITUATION_CH', 'situation', unicode_and_strip, 'situation familiale'),
103
            ('REVENUMENSUEL_MO', 'revenu_mensuel_mo', unicode_and_strip, 'revenu mensuel de la famille'),
104
            ('REVENUANNUEL_MO', 'revenu_annuel_mo', unicode_and_strip, 'revenu annuel de la famille'),
105
            ('QUOTIENTFAMILIAL_MO', 'quotient_familial_mo', unicode_and_strip, 'quotient familial'),
106
            ('NBTOTALENFANTS_EN', 'nb_total_enfants_en', unicode_and_strip, 'nombre total d\'enfants'),
107
            ('NBENFANTSACHARGE_EN', 'nb_enfants_a_charge_en', unicode_and_strip, 'nombre d\'enfants à charge'),
108
            ('NOMPERE_CH', 'nom_pere', unicode_and_strip, 'monsieur'),
109
            ('PRENOMPERE_CH', 'prenom_pere', unicode_and_strip, 'prénom monsieur'),
110
            ('AUTOPARENTALEPERE_IN', 'autoparentale_pere_in', unicode_and_strip, 'autorisation parentale de père'),
111
            ('DATENAISPERE_DA', 'date_naissance_pere_da', unicode_and_strip, 'date de naisance du père'),
112
            ('DEPNAISPERE_EN', 'departement_naissance_pere_en', unicode_and_strip, 'département de naissance du père'),
113
            ('LIEUNAISPERE_CH', 'lieu_naissance_pere', unicode_and_strip, 'lieu de naissance du père'),
114
            ('RUEPERE_CH', 'rue_pere', unicode_and_strip, 'rue père'),
115
            ('RUE2PERE_CH', 'rue2_pere', unicode_and_strip, 'rue 2 père'),
116
            ('RUE3PERE_CH', 'rue3_pere', unicode_and_strip, 'rue 3 père'),
117
            ('CODEPOSTALPERE_CH', 'code_postal_pere', unicode_and_strip, 'code postal père'),
118
            ('VILLEPERE_CH', 'ville_pere', unicode_and_strip, 'ville père'),
119
            ('TELEPHONEPERE_CH', 'telephone_pere', unicode_and_strip, 'téléphone père'),
120
            ('TELEPHONE2PERE_CH', 'telephone2_pere', unicode_and_strip, 'téléphone 2 père'),
121
            ('TELPERE_LR_IN', 'tel_pere_liste_rouge_in', unicode_and_strip, 'téléphone liste rouge père'),
122
            ('TEL2PERE_LR_IN', 'tel2_pere_liste_rouge_in', unicode_and_strip, 'téléphone 2 liste rouge père'),
123
            ('TEL_LR_IN', 'tel_liste_rourge_in', unicode_and_strip, 'téléphone liste rouge'),
124
            ('TEL2_LR_IN', 'tel2_liste_rouge_in', unicode_and_strip, 'téléphone 2 liste rouge'),
125
            ('NOMMERE_CH', 'nom_mere', unicode_and_strip, 'madame'),
126
            ('PRENOMMERE_CH', 'prenom_mere', unicode_and_strip, 'prénom madame'),
127
            ('AUTOPARENTALEMERE_IN', 'autoparentale_mere_in', unicode_and_strip, 'autorisation parentale mère'),
128
            ('DATENAISMERE_DA', 'date_naissance_mere_da', unicode_and_strip, 'date de naissance de la mère'),
129
            ('DEPNAISMERE_EN', 'departement_naissance_mere_en', unicode_and_strip, 'département de naissance de la mère'),
130
            ('LIEUNAISMERE_CH', 'lieu_naissance_mere', unicode_and_strip, 'lieu de naissance de la mère'),
131
            ('RUEMERE_CH', 'rue_mere', unicode_and_strip, 'rue mère'),
132
            ('REVMENSUELPERE_MO', 'revenu_mensuel_pere_mo', unicode_and_strip, 'revenu mensuel du père'),
133
            ('RUE2MERE_CH', 'rue2_mere', unicode_and_strip, 'rue 2 mère'),
134
            ('RUE3MERE_CH', 'rue3_mere', unicode_and_strip, 'rue 3 mère'),
135
            ('CODEPOSTALMERE_CH', 'code_postal_mere', unicode_and_strip, 'code postal de la mère'),
136
            ('VILLEMERE_CH', 'ville_mere', unicode_and_strip, 'ville de la mère'),
137
            ('REVMENSUELMERE_MO', 'revenu_mensuel_mere_mo', unicode_and_strip, 'revenu mensuel mère'),
138
            ('REVANNUELPERE_MO', 'revenu_annuel_pere_mo', unicode_and_strip, 'revenu annuel père'),
139
            ('REVANNUELMERE_MO', 'revenu_annuel_mere_mo', unicode_and_strip, 'revenu annuel mère'),
140
            ('TELEPHONEMERE_CH', 'telephone_mere', unicode_and_strip, 'téléphone mère'),
141
            ('TELEPHONE2MERE_CH', 'telephone2_mere', unicode_and_strip, 'téléphone 2 mère'),
142
            ('TELMERE_LR_IN', 'telephone_mere_liste_rouge_in', unicode_and_strip, 'téléphone liste rouge mère'),
143
            ('TEL2MERE_LR_IN', 'telephone2_mere_liste_rouge_in', unicode_and_strip, 'téléphone 2 liste rouge mère'),
144
            ('TELECOPIEPERE_CH', 'telecopie_pere', unicode_and_strip, 'télécopie du père'),
145
            ('TELECOPIE2PERE_CH', 'telecopie2_pere', unicode_and_strip, 'télécopie 2 du père'),
146
            ('TELECOPIEMERE_CH', 'telecopie_mere', unicode_and_strip, 'télécopie de la mère'),
147
            ('TELECOPIE2MERE_CH', 'telecopie2_mere', unicode_and_strip, 'télécopie 2 de la mère'),
148
            ('PROFPERE_CH', 'profession_pere', unicode_and_strip, 'profession du père'),
149
            ('PROFMERE_CH', 'profession_mere', unicode_and_strip, 'profession de la mère'),
150
            ('LIEUTRAVPERE_CH', 'lieu_travail_pere', unicode_and_strip, 'lieu de travail du père'),
151
            ('LIEUTRAVMERE_CH', 'lieu_travail_mere', unicode_and_strip, 'lieu de travail de la mère'),
152
            ('RUETRAVPERE_CH', 'rue_travail_pere', unicode_and_strip, 'rue travail père'),
153
            ('RUE2TRAVPERE_CH', 'rue2_travail_pere', unicode_and_strip, 'rue 2 travail père'),
154
            ('RUE3TRAVPERE_CH', 'rue3_travail_pere', unicode_and_strip, 'rue 3 travail père'),
155
            ('CPTRAVPERE_CH', 'code_postal_travail_pere', unicode_and_strip, 'code postal travail père'),
156
            ('VILLETRAVPERE_CH', 'ville_travail_pere', unicode_and_strip, 'ville travail père'),
157
            ('RUETRAVMERE_CH', 'rue_travail_mere', unicode_and_strip, 'rue travail mère'),
158
            ('RUE2TRAVMERE_CH', 'rue2_travail_mere', unicode_and_strip, 'rue 2 travail mère'),
159
            ('RUE3TRAVMERE_CH', 'rue3_travail_mere', unicode_and_strip, 'rue 3 travail mère'),
160
            ('CPTRAVMERE_CH', 'code_postal_travail_mere', unicode_and_strip, 'code postal travail mère'),
161
            ('VILLETRAVMERE_CH', 'ville_travail_mere', unicode_and_strip, 'ville travail mère'),
162
            ('TELPROFPERE_CH', 'telephone_travail_pere', unicode_and_strip, 'téléphone professionnel père'),
163
            ('TEL2PROFPERE_CH', 'telephone2_travail_pere', unicode_and_strip, 'téléphone 2 professionnel père'),
164
            ('TELMOBILPERE_CH', 'telephone_mobile_pere', unicode_and_strip, 'téléphone mobile'),
165
            ('TELPROFMERE_CH', 'telephone_travail_mere', unicode_and_strip, 'téléphone travail mère'),
166
            ('TEL2PROFMERE_CH', 'telephone2_travail_mere', unicode_and_strip, 'téléphone 2 travail mère'),
167
            ('TELMOBILMERE_CH', 'telephone_mobile_mere', unicode_and_strip, 'téléphone mobile mère'),
168
            ('TOTALDU_MO', 'total_du_mo', unicode_and_strip, 'total dû'),
169
            ('TOTALREGLE_MO', 'total_regle_mo', unicode_and_strip, 'total réglé'),
170
            ('NUMCENTRESS_CH', 'num_centre_securite_sociale', unicode_and_strip, 'n° centre sécurité sociale'),
171
            ('NOMCENTRESS_CH', 'nom_centre_securite_sociale', unicode_and_strip, 'nom centre sécurité sociale'),
172
            ('NUMASSURANCE_CH', 'num_assurance', unicode_and_strip, 'n° assurance'),
173
            ('NOMASSURANCE_CH', 'nom_assurance', unicode_and_strip, 'nom assurance'),
174
            ('RIVOLI_EN', 'code_rivoli_en', unicode_and_strip, 'identifiant code rivoli'),
175
            ('NUMCOMPTE_CH', 'numero_compte_comptable', unicode_and_strip, 'n° compte comptable'),
176
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
177
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
178
            ('NUMALLOCATAIRE_CH', 'numero_allocataire', unicode_and_strip, 'n° allocataire'),
179
            ('COMMENTAIRE_ME', 'commentaire_me', unicode_and_strip, 'commentaires / notes'),
180
            ('IDCSPPERE', 'identifiant_csp_pere', unicode_and_strip, 'référence identifiant csp'),
181
            ('IDCSPMERE', 'identifiant_csp_mere', unicode_and_strip, 'référence identifiant csp'),
182
            ('IDSECTEURS', 'identifiant_secteurs', unicode_and_strip, 'référence identifiant secteurs'),
183
            ('IDZONES', 'identifiant_zones', unicode_and_strip, 'référence identifiant zones'),
184
            ('IDRUES', 'identifiant_rues', unicode_and_strip, 'référence identifiant rues'),
185
            ('IDVILLES', 'identifiant_villes', unicode_and_strip, 'référence identifiant villes'),
186
            ('IDREGIMES', 'identifiant_regimes', unicode_and_strip, 'référence identifiant regimes'),
187
            ('IDSITUATIONFAMILLE', 'identifiant_situation_famille', unicode_and_strip, 'référence identifiant situationfamille'),
188
            ('NUMSECUPERE_CH', 'num_securite_sociale_pere', unicode_and_strip, 'n° secu père'),
189
            ('NUMSECUMERE_CH', 'num_securite_sociale_mere', unicode_and_strip, 'n° secu mère'),
190
            ('NATIONPERE_CH', 'nation_pere', unicode_and_strip, 'nationalité père'),
191
            ('NATIONMERE_CH', 'nation_mere', unicode_and_strip, 'nationalité mère'),
192
            ('NOMJEUNEFILLE_CH', 'nom_jeune_fille', unicode_and_strip, 'nom jeune fille'),
193
            ('IDCAFS', 'idcafs', unicode_and_strip, 'référence identifiant cafs'),
194
            ('CHAMPLIBRE1_CH', 'champ_libre1', unicode_and_strip, 'valeur champ libre 1'),
195
            ('CHAMPLIBRE2_CH', 'champ_libre2', unicode_and_strip, 'valeur champ libre 2'),
196
            ('CHAMPCALCULE1_CH', 'champ_calcule1', unicode_and_strip, 'valeur champ calculé 1'),
197
            ('CHAMPCALCULE2_CH', 'champ_calcule2', unicode_and_strip, 'valeur champ calculé 2'),
198
            ('IDTABLELIBRE1', 'id_table_libre1', unicode_and_strip, 'idtablelibre1'),
199
            ('IDTABLELIBRE3', 'id_table_libre3', unicode_and_strip, 'idtablelibre3'),
200
            ('IDTABLELIBRE2', 'id_table_libre2', unicode_and_strip, 'idtablelibre2'),
201
            ('IDTABLELIBRE4', 'id_table_libre4', unicode_and_strip, 'idtablelibre4'),
202
            ('NOMURSSAF_CH', 'nom_urssaf', unicode_and_strip, 'nom urssaf'),
203
            ('NUMURSSAF_CH', 'num_urssaf', unicode_and_strip, 'n° urssaf'),
204
            ('IDPROFPERE', 'identifiant_profession_pere', unicode_and_strip, 'référence identifiant profession'),
205
            ('IDPROFMERE', 'identifiant_profession_mere', unicode_and_strip, 'référence identifiant profession'),
206
            ('ALLOCATAIRE_CH', 'allocataire', unicode_and_strip, 'allocataire père ou mère (p,m)'),
207
#            ('PHOTOPERE_CH', 'photo_pere', unicode_and_strip, 'photographie père'),
208
#            ('PHOTOMERE_CH', 'photo_mere', unicode_and_strip, 'photographie mère'),
209
            ('NUMRUE_CH', 'numero_rue', unicode_and_strip, 'numéro de rue'),
210
            ('NUMRUEPERE_CH', 'numero_rue_pere', unicode_and_strip, 'numéro de rue père'),
211
            ('NUMRUEMERE_CH', 'numero_rue_mere', unicode_and_strip, 'numéro de rue mère'),
212
            ('IDPORTAIL_FAMILLES', 'identifiant_portail_familles', unicode_and_strip, 'identifiant de portail_familles'),
213
            ('ECHEANCEASSURANCE_DA', 'echeance_assurance_da', unicode_and_strip, 'date echéance assurance'),
214
            ('RM_MIKADO_MO', 'rm_mikado_mo', unicode_and_strip, 'revenus mensuels mikado'),
215
            ('RA_MIKADO_MO', 'ra_mikado_mo', unicode_and_strip, 'revenus annuels mikado'),
216
            ('QF_MIKADO_MO', 'qf_mikado_mo', unicode_and_strip, 'quotient familial mikado'),
217
            ('RM_DIABOLO_MO', 'rm_diabolo_mo', unicode_and_strip, 'revenus mensuels diabolo'),
218
            ('RA_DIABOLO_MO', 'ra_diabolo_mo', unicode_and_strip, 'revenus annuels diabolo'),
219
            ('QF_DIABOLO_MO', 'qf_diabolo_mo', unicode_and_strip, 'quotient familial diabolo'),
220
            ('RM_OLIGO_MO', 'rm_oligo_mo', unicode_and_strip, 'revenus mensuels oligo'),
221
            ('RA_OLIGO_MO', 'ra_oligo_mo', unicode_and_strip, 'revenus annuels oligo'),
222
            ('QF_OLIGO_MO', 'qf_oligo_mo', unicode_and_strip, 'quotient familial oligo'),
223
            ('APPLICATION_REV_MIKADO_DA', 'application_rev_mikado_da', unicode_and_strip, 'date d\'application des revenus de mikado'),
224
            ('APPLICATION_REV_DIABOLO_DA', 'application_rev_diabolo_da', unicode_and_strip, 'date d\'application des revenus de diabolo'),
225
            ('APPLICATION_REV_OLIGO_DA', 'application_rev_oligo_da', unicode_and_strip, 'date d\'application des revenus de oligo'),
226
    )
227

    
228
    def complete(self):
229
        k = [a for a,b,c,d in self.MORE_COLUMNS]
230
        list(self.client('LISTER_FAMILLES', args=(','.join(k), self.id),
231
                columns=self.MORE_COLUMNS, instances=(self,)))
232
        return self
233

    
234
    @property
235
    def invoices(self):
236
        return [invoice for id, invoice in self.client.invoices.iteritems() if invoice.id_famille == self.id]
237

    
238
class Invoice(SimpleObject):
239
    COLUMNS = (
240
        ('', 'id_famille', int, ''),
241
        ('', 'id', int, ''),
242
        ('', 'numero', str, ''),
243
        ('', 'debut_periode', parse_date, ''),
244
        ('', 'fin_periode', parse_date, ''),
245
        ('', 'creation', parse_date, ''),
246
        ('', 'echeance', parse_date, ''),
247
        ('', 'montant', Decimal, ''),
248
        ('', 'reste_du', Decimal, ''),
249
    )
250
    _detail = None
251

    
252
    def detail(self):
253
        if not self._detail:
254
            self.client.factures_detail([self])
255
        return self._detail
256

    
257
    @property
258
    def family(self):
259
        return self.client.families[self.id_famille]
260

    
261
    def paid(self):
262
        return self.reste_du == Decimal(0)
263

    
264
class DominoWs(object):
265
    '''Interface to the WebService exposed by Abelium Domino.
266

    
267
       It allows to retrieve family and invoices.
268

    
269
       Beware that it does a lot of caching to limit call to the webservice, so
270
       if you need fresh data, call clear_cache()
271

    
272
       All family are in the families dictionnary and all invoices in the
273
       invoices dictionnary.
274
    '''
275

    
276
    def __init__(self, url, domain, login, password, location=None):
277
        if not Client:
278
            raise ValueError('You need python suds')
279
        logger.debug('creating DominoWs(%r, %r, %r, %r, location=%r)', url, domain, login, password, location)
280
        self.url = url
281
        self.domain = domain
282
        self.login = login
283
        self.password = password
284
        self.client = Client(url, location=location)
285
        self.client.options.cache.setduration(seconds=60)
286

    
287
    def clear_cache(self):
288
        '''Remove cached attributes from the instance.'''
289

    
290
        for key, value in self.__dict__.items():
291
            if key.startswith('__') and key.endswith('_cache'):
292
                del self.__dict__[key]
293

    
294
    def call(self, function_name, *args):
295
        '''Call SOAP method named function_name passing args list as parameters.
296

    
297
           Any error is converted into the DominoException class.'''
298

    
299
        try:
300
            logger.debug('soap call to %s%r', function_name, args)
301
            data = getattr(self.client.service, function_name)(self.domain, self.login, self.password, *args)
302
            with open('dump.txt', 'w') as f:
303
                f.write(data.encode('utf8'))
304
            logger.debug('result: %s', data)
305
            self.data = data
306
        except IOError, e:
307
            raise DominoException('Erreur IO', e)
308
        if data.startswith('ERREUR'):
309
            raise DominoException(data[9:].encode('utf8'))
310
        return data
311

    
312
    def parse_tabular_data(self, data):
313
        '''Row are separated by carriage-return, ASCII #13, characters and columns by tabs.
314
           Empty lines (ignoring spaces) are ignored.
315
        '''
316

    
317
        rows = data.split(LINE_SEPARATOR)
318
        rows = [[cell.strip() for cell in row.split(COLUMN_SEPARATOR)] for row in rows if row.strip() != '']
319
        return rows
320

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

    
326
        - If instances is present, the given instances are updated with the
327
          returned content, in order, row by row.
328
        - If cls is not None and instances is None, a new instance of the class
329
          cls is instancied for every row and initialized with the content of
330
          the row.
331
        - If cls and instances are None, the raw data returned by the SOAP call
332
          is returned.
333
        '''
334

    
335
        data = self.call(function_name, *args)
336
        if cls or instances:
337
            rows = self.parse_tabular_data(data)
338
            kwargs = {}
339
            if instances:
340
                rows = zip(rows, instances)
341
            for row in rows:
342
                if instances:
343
                    row, instance = row
344
                if not row[0]:
345
                    continue
346
                for a, b in zip(columns or cls.COLUMNS, row):
347
                    x, name, converter, desc = a
348
                    kwargs[name] = converter(b.strip())
349
                if instances:
350
                    instance.__dict__.update(kwargs)
351
                    yield instance
352
                else:
353
                    yield cls(client=self, **kwargs)
354
        else:
355
            yield data
356

    
357
    @property
358
    @object_cached
359
    def families(self):
360
        '''Dictionary of all families indexed by their id.
361

    
362
           After the first use, the value is cached. Use clear_cache() to reset
363
           it.
364
        '''
365

    
366
        return self.get_families()
367

    
368
    def get_families(self, id_famille=0, full=False):
369
        '''Get families informations.
370
           There is no caching.
371

    
372
           id_famille - if not 0, the family with this id is retrieved. If 0
373
           all families are retrieved. Default to 0.
374
           full - If True return all the columns of the family table. Default
375
           to False.
376
        '''
377
        columns = Family.MORE_COLUMNS if full else Family.COLUMNS
378
        families = self('LISTER_FAMILLES',
379
                Family,
380
                args=(','.join([x[0] for x in columns]), id_famille))
381
        return dict([(int(x), x) for x in families])
382

    
383
    @property
384
    @object_cached
385
    def invoices(self):
386
        '''Dictionnary of all invoices indexed by their id.
387

    
388
           After the first use, the value is cached. Use clear_cache() to reset
389
           it.
390
        '''
391
        invoices = self.get_invoices()
392
        for invoice in invoices.values():
393
            invoice.famille = self.families[invoice.id_famille]
394
        self.factures_detail(invoices.values())
395
        return invoices
396

    
397
    def get_invoices(self, id_famille=0, state='NON_SOLDEES'):
398
        '''Get invoices informations.
399

    
400
           id_famille - If value is not 0, only invoice for the family with
401
           this id are retrieved. If value is 0, invoices for all families are
402
           retrieved. Default to 0.
403
           etat - state of the invoices to return, possible values are
404
           'SOLDEES', 'NON_SOLDEES', 'TOUTES'.
405
        '''
406
        invoices = self('LISTER_FACTURES_FAMILLE', Invoice,
407
            args=(id_famille, state))
408
        invoices = list(invoices)
409
        for invoice in invoices:
410
            invoice.famille = self.families[invoice.id_famille]
411
        return dict(((int(x), x) for x in invoices))
412

    
413
    FACTURE_DETAIL_HEADERS = ['designation', 'quantite', 'prix', 'montant']
414
    def factures_detail(self, invoices):
415
        '''Retrieve details of some invoice'''
416
        data = self.call('DETAILLER_FACTURES', (''.join(("%s;" % int(x) for x in invoices)),))
417
        try:
418
            tree = etree.fromstring(data.encode('utf8'))
419
            for invoice, facture_node in zip(invoices, tree.findall('facture')):
420
                rows = []
421
                for ligne in facture_node.findall('detail_facture/ligne'):
422
                    row = []
423
                    rows.append(row)
424
                    for header in self.FACTURE_DETAIL_HEADERS:
425
                        if header in ligne.attrib:
426
                            row.append((header, ligne.attrib[header]))
427
                d = { 'etablissement': facture_node.find('detail_etablissements/etablissement').get('nom').strip(),
428
                        'lignes': rows }
429
                invoice._detail = d
430
        except Exception, e:
431
            raise DominoException('Exception when retrieving invoice details', e)
432

    
433
    def get_family_by_mail(self, email):
434
        '''Return the first whose one email attribute matches the given email'''
435
        for famille in self.families.values():
436
            if email in (famille.email_pere, famille.email_mere,
437
                    famille.adresse_internet):
438
                return famille
439
        return None
440

    
441
    def pay_invoice(self, id_invoices, amount, other_information, date=None):
442
        '''Notify Domino of the payment of some invoices.
443

    
444
           id_invoices - integer if of the invoice or Invoice instances
445
           amount - amount as a Decimal object
446
           other_information - free content to attach to the payment, like a
447
           bank transaction number for correlation.
448
           date - date of the payment, must be a datetime object. If None,
449
           now() is used. Default to None.
450
        '''
451

    
452
        if not date:
453
            date = datetime.datetime.now()
454
        due = sum([self.invoices[int(id_invoice)].due for id_invoice in id_invoices])
455
        if Decimal(amount) == Decimal(due):
456
            return self('SOLDER_FACTURE', None, args=(str(amount), 
457
                    ''.join([ '%s;' % int(x) for x in id_invoices]),
458
                    date.strftime('%Y-%m-%d'), other_information))
459
        else:
460
            raise DominoException('Amount due and paid do not match', { 'due': due, 'paid': amount})
(5-5/30)