# -*- coding: utf-8 -*-
from decimal import Decimal
import time
import datetime
from xml.etree import ElementTree as etree
import logging

try:
    from suds.client import Client
    from suds.bindings.binding import Binding
    # Webdev is bugged and using an HTML generator to produce XML content, 
    Binding.replyfilter = lambda self, x: x.replace('&nbsp;', '&#160;')
except ImportError:
    Client = None
    Binding = None

logger = logging.getLogger(__name__)

# cleaning and parsing functions

LINE_SEPARATOR = '\n'
COLUMN_SEPARATOR = '\t'

def unicode_and_strip(x):
    return unicode(x).strip()

def strip_and_int(x):
    try:
        return int(x.strip())
    except ValueError:
        return None

def strip_and_date(x):
    try:
        return datetime.datetime.strptime(x.strip(), '%Y%m%d').date()
    except ValueError:
        return None

def parse_date(date_string):
    if date_string:
        return datetime.datetime.strptime(date_string, "%Y%m%d")
    else:
        None

class DominoException(Exception):
    pass

def object_cached(function):
    '''Decorate an object method so that its results is cached on the object
       instance after the first call.
    '''
    def decorated_function(self, *args, **kwargs):
        cache_name = '__%s_cache' % function.__name__
        if not hasattr(self, cache_name):
            setattr(self, cache_name, (time.time(), {}))
        t, d = getattr(self, cache_name)
        if time.time() - t > 80:
            setattr(self, cache_name, (time.time(), {}))
            t, d = getattr(self, cache_name)
        k = tuple(*args) + tuple(sorted(kwargs.items()))
        if not k in d:
            d[k] = function(self, *args, **kwargs)
        return d[k]
    return decorated_function

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

    '''Describe basic columns'''
    COLUMNS = ()
    '''Describe extended object columns'''
    MORE_COLUMNS = ()

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

    def __repr__(self):
        c = {}
        for remote_name, name, converter, desc in self.COLUMNS:
            if hasattr(self, name):
                c[name] = getattr(self, name)
        return str(c)

    def serialize(self):
        l = []
        for remote_name, local_name, converter, desc in self.COLUMNS + self.MORE_COLUMNS:
            if local_name == 'id':
                continue
            v = getattr(self, local_name, None)
            if v is None:
                continue
            if isinstance(v, (datetime.date, datetime.datetime)):
                v = v.strftime('%Y%m%d')
            if remote_name.endswith('_DA') and '-' in v:
                v = v.replace('-', '')
            l.append(u'{0}: "{1}"'.format(remote_name, v))
        return u','.join(l)

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

    def __int__(self):
        '''Return the object id'''
        return self.id

class UrgentContact(SimpleObject):
    COLUMNS = (
            ('IDENFANTS', 'id_enfant', strip_and_int, 'IDENFANTS'),
            ('IDCONTACT_AUTORISE', 'id', strip_and_int, 'IDCONTACT_AUTORISE'),
            ('LIENFAMILLE_CH', 'lien_de_famille', unicode_and_strip, 'LIENFAMILLE_CH'),
            ('PERE_MERE_CH', 'lien_pere_ou_pere', unicode_and_strip, 'PERE_MERE_CH'),
            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
            ('TYPE_CH', 'type', unicode_and_strip, 'TYPE_CH'),
            ('NOM_CH', 'nom', unicode_and_strip, 'NOM_CH'),
            ('PRENOM_CH', 'prenom', unicode_and_strip, 'PRENOM_CH'),
            ('RUE_CH', 'rue', unicode_and_strip, 'RUE_CH'),
            ('RUE2_CH', 'rue2', unicode_and_strip, 'RUE2_CH'),
            ('RUE3_CH', 'rue3', unicode_and_strip, 'RUE3_CH'),
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'CODEPOSTAL_CH'),
            ('VILLE_CH', 'ville', unicode_and_strip, 'VILLE_CH'),
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'TELEPHONE_CH'),
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'TELEPHONE2_CH'),
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'ADRESSEINT_CH'),
    )

class Child(SimpleObject):
    COLUMNS = (
            ('IDENFANTS', 'id', strip_and_int, 'Identifiant de ENFANTS'),
            ('NOM_CH', 'nom', unicode_and_strip, 'Nom'),
            ('PRENOM_CH', 'prenom', unicode_and_strip, 'Prénom'),
            ('NAISSANCE_DA', 'date_naissance', strip_and_date, 'Date de Naissance'),
            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'Commentaires / Notes'),
            ('IDFAMILLES', 'id_famille', unicode_and_strip, 'IDFAMILLES'),
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'Code Postal'),
            ('VILLE_CH', 'ville', unicode_and_strip, 'Ville'),
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'Code Interne'),
            ('LIEUNAISSANCE_CH', 'lieu_naissance', unicode_and_strip, 'Lieu de Naissance'),
            ('DEPNAISSANCE_CH', 'departement_naissance', unicode_and_strip, 'Département Naissance'),
            ('NUMSECU_CH', 'num_securite_sociale', unicode_and_strip, 'N° de SECU'),
            ('NATIONALITE_CH', 'nationalite', unicode_and_strip, 'Nationalité'),
            ('PRENOM2_CH', 'prenom2', unicode_and_strip, 'Prénom 2'),
            ('SEXE_CH', 'sexe', unicode_and_strip, 'Sexe'),
            ('IDTABLELIBRE1', 'IDTABLELIBRE1', unicode_and_strip, 'IDTABLELIBRE1'),
            ('IDTABLELIBRE2', 'IDTABLELIBRE2', unicode_and_strip, 'IDTABLELIBRE2'),
            ('IDTABLELIBRE3', 'IDTABLELIBRE3', unicode_and_strip, 'IDTABLELIBRE3'),
            ('IDTABLELIBRE4', 'IDTABLELIBRE4', unicode_and_strip, 'IDTABLELIBRE4'),
            ('CHAMPLIBRE1_CH', 'CHAMPLIBRE1_CH', unicode_and_strip, 'Valeur Champ Libre 1'),
            ('CHAMPLIBRE2_CH', 'CHAMPLIBRE2_CH', unicode_and_strip, 'Valeur Champ Libre 2'),
            ('CHAMPCALCULE1_CH', 'CHAMPCALCULE1_CH', unicode_and_strip, 'Valeur Champ Calculé 1'),
            ('CHAMPCALCULE2_CH', 'CHAMPCALCULE2_CH', unicode_and_strip, 'Valeur Champ Calculé 2'),
            ('SOMMEIL_ME', 'sommeil', unicode_and_strip, 'Sommeil'),
            ('ACTIVITE_ME', 'activite', unicode_and_strip, 'Activités'),
            ('HABITUDE_ME', 'habitude', unicode_and_strip, 'Habitudes'),
            ('PHOTO_CH', 'photographie', unicode_and_strip, 'Photographie'),
            ('NUMCOMPTE_CH', 'numcompte', unicode_and_strip, 'N° Compte Comptable'),
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'Téléphone'),
            ('IDFAMILLES2', 'id_famille2', unicode_and_strip, 'Identifiant famille 2'),
            ('PERE_CH', 'pere', unicode_and_strip, 'Nom du père'),
            ('MERE_CH', 'mere', unicode_and_strip, 'Nom de la mère'),
            ('AUTOPARENTALEMERE_IN', 'autorisation_parentale_mere', unicode_and_strip, 'Autorisation Parentale Mère'),
            ('AUTOPARENTALEPERE_IN', 'autorisation_parentale_pere', unicode_and_strip, 'Autorisation Parentale de Père'),
            ('IDPORTAIL_ENFANTS', 'id_portail_enfants', unicode_and_strip, 'Identifiant de PORTAIL_ENFANTS'),
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'Adresse Internet'),
    )

    def save(self):
        if hasattr(self, 'id'):
            self.client.update_child(self)
        else:
            self.id = self.client.add_child(self)
        self.client.clear_cache()

class Family(SimpleObject):
    COLUMNS = (
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
    )

    MORE_COLUMNS = (
            ('IDFAMILLES', 'id', strip_and_int, 'identifiant de famille'),
            ('CODEINTERNE_CH', 'code_interne', unicode_and_strip, 'code interne'),
            ('CIVILITE_CH', 'civilite', unicode_and_strip, 'civilité'),
            ('NOMFAMILLE_CH', 'famille_nom', unicode_and_strip, 'nom de famille'),
            ('RUE_CH', 'rue', unicode_and_strip, 'rue'),
            ('RUE2_CH', 'rue2', unicode_and_strip, 'rue 2'),
            ('RUE3_CH', 'rue3', unicode_and_strip, 'rue 3'),
            ('CODEPOSTAL_CH', 'code_postal', unicode_and_strip, 'code postal'),
            ('VILLE_CH', 'ville', unicode_and_strip, 'ville'),
            ('TELEPHONE_CH', 'telephone', unicode_and_strip, 'téléphone'),
            ('TELEPHONE2_CH', 'telephone2', unicode_and_strip, 'téléphone 2'),
            ('TELECOPIE_CH', 'telecopie', unicode_and_strip, 'télécopie'),
            ('TELECOPIE2_CH', 'telecopie2', unicode_and_strip, 'télécopie 2'),
            ('ADRESSEINT_CH', 'adresse_internet', unicode_and_strip, 'adresse internet'),
            ('SITUATION_CH', 'situation', unicode_and_strip, 'situation familiale'),
            ('REVENUMENSUEL_MO', 'revenu_mensuel', unicode_and_strip, 'revenu mensuel de la famille'),
            ('REVENUANNUEL_MO', 'revenu_annuel', unicode_and_strip, 'revenu annuel de la famille'),
            ('QUOTIENTFAMILIAL_MO', 'quotient_familial', unicode_and_strip, 'quotient familial'),
            ('NBTOTALENFANTS_EN', 'nb_total_enfants', unicode_and_strip, 'nombre total d\'enfants'),
            ('NBENFANTSACHARGE_EN', 'nb_enfants_a_charge', unicode_and_strip, 'nombre d\'enfants à charge'),
            ('NOMPERE_CH', 'nom_pere', unicode_and_strip, 'monsieur'),
            ('PRENOMPERE_CH', 'prenom_pere', unicode_and_strip, 'prénom monsieur'),
            ('AUTOPARENTALEPERE_IN', 'autoparentale_pere', unicode_and_strip, 'autorisation parentale de père'),
            ('DATENAISPERE_DA', 'date_naissance_pere', strip_and_date, 'date de naisance du père'),
            ('DEPNAISPERE_EN', 'departement_naissance_pere', unicode_and_strip, 'département de naissance du père'),
            ('LIEUNAISPERE_CH', 'lieu_naissance_pere', unicode_and_strip, 'lieu de naissance du père'),
            ('RUEPERE_CH', 'rue_pere', unicode_and_strip, 'rue père'),
            ('RUE2PERE_CH', 'rue2_pere', unicode_and_strip, 'rue 2 père'),
            ('RUE3PERE_CH', 'rue3_pere', unicode_and_strip, 'rue 3 père'),
            ('CODEPOSTALPERE_CH', 'code_postal_pere', unicode_and_strip, 'code postal père'),
            ('VILLEPERE_CH', 'ville_pere', unicode_and_strip, 'ville père'),
            ('TELEPHONEPERE_CH', 'telephone_pere', unicode_and_strip, 'téléphone père'),
            ('TELEPHONE2PERE_CH', 'telephone2_pere', unicode_and_strip, 'téléphone 2 père'),
            ('TELPERE_LR_IN', 'tel_pere_liste_rouge', unicode_and_strip, 'téléphone liste rouge père'),
            ('TEL2PERE_LR_IN', 'tel2_pere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge père'),
            ('TEL_LR_IN', 'tel_liste_rourge', unicode_and_strip, 'téléphone liste rouge'),
            ('TEL2_LR_IN', 'tel2_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge'),
            ('NOMMERE_CH', 'nom_mere', unicode_and_strip, 'madame'),
            ('PRENOMMERE_CH', 'prenom_mere', unicode_and_strip, 'prénom madame'),
            ('AUTOPARENTALEMERE_IN', 'autoparentale_mere', unicode_and_strip, 'autorisation parentale mère'),
            ('DATENAISMERE_DA', 'date_naissance_mere', strip_and_date, 'date de naissance de la mère'),
            ('DEPNAISMERE_EN', 'departement_naissance_mere', unicode_and_strip, 'département de naissance de la mère'),
            ('LIEUNAISMERE_CH', 'lieu_naissance_mere', unicode_and_strip, 'lieu de naissance de la mère'),
            ('RUEMERE_CH', 'rue_mere', unicode_and_strip, 'rue mère'),
            ('REVMENSUELPERE_MO', 'revenu_mensuel_pere', unicode_and_strip, 'revenu mensuel du père'),
            ('RUE2MERE_CH', 'rue2_mere', unicode_and_strip, 'rue 2 mère'),
            ('RUE3MERE_CH', 'rue3_mere', unicode_and_strip, 'rue 3 mère'),
            ('CODEPOSTALMERE_CH', 'code_postal_mere', unicode_and_strip, 'code postal de la mère'),
            ('VILLEMERE_CH', 'ville_mere', unicode_and_strip, 'ville de la mère'),
            ('REVMENSUELMERE_MO', 'revenu_mensuel_mere', unicode_and_strip, 'revenu mensuel mère'),
            ('REVANNUELPERE_MO', 'revenu_annuel_pere', unicode_and_strip, 'revenu annuel père'),
            ('REVANNUELMERE_MO', 'revenu_annuel_mere', unicode_and_strip, 'revenu annuel mère'),
            ('TELEPHONEMERE_CH', 'telephone_mere', unicode_and_strip, 'téléphone mère'),
            ('TELEPHONE2MERE_CH', 'telephone2_mere', unicode_and_strip, 'téléphone 2 mère'),
            ('TELMERE_LR_IN', 'telephone_mere_liste_rouge', unicode_and_strip, 'téléphone liste rouge mère'),
            ('TEL2MERE_LR_IN', 'telephone2_mere_liste_rouge', unicode_and_strip, 'téléphone 2 liste rouge mère'),
            ('TELECOPIEPERE_CH', 'telecopie_pere', unicode_and_strip, 'télécopie du père'),
            ('TELECOPIE2PERE_CH', 'telecopie2_pere', unicode_and_strip, 'télécopie 2 du père'),
            ('TELECOPIEMERE_CH', 'telecopie_mere', unicode_and_strip, 'télécopie de la mère'),
            ('TELECOPIE2MERE_CH', 'telecopie2_mere', unicode_and_strip, 'télécopie 2 de la mère'),
            ('PROFPERE_CH', 'profession_pere', unicode_and_strip, 'profession du père'),
            ('PROFMERE_CH', 'profession_mere', unicode_and_strip, 'profession de la mère'),
            ('LIEUTRAVPERE_CH', 'lieu_travail_pere', unicode_and_strip, 'lieu de travail du père'),
            ('LIEUTRAVMERE_CH', 'lieu_travail_mere', unicode_and_strip, 'lieu de travail de la mère'),
            ('RUETRAVPERE_CH', 'rue_travail_pere', unicode_and_strip, 'rue travail père'),
            ('RUE2TRAVPERE_CH', 'rue2_travail_pere', unicode_and_strip, 'rue 2 travail père'),
            ('RUE3TRAVPERE_CH', 'rue3_travail_pere', unicode_and_strip, 'rue 3 travail père'),
            ('CPTRAVPERE_CH', 'code_postal_travail_pere', unicode_and_strip, 'code postal travail père'),
            ('VILLETRAVPERE_CH', 'ville_travail_pere', unicode_and_strip, 'ville travail père'),
            ('RUETRAVMERE_CH', 'rue_travail_mere', unicode_and_strip, 'rue travail mère'),
            ('RUE2TRAVMERE_CH', 'rue2_travail_mere', unicode_and_strip, 'rue 2 travail mère'),
            ('RUE3TRAVMERE_CH', 'rue3_travail_mere', unicode_and_strip, 'rue 3 travail mère'),
            ('CPTRAVMERE_CH', 'code_postal_travail_mere', unicode_and_strip, 'code postal travail mère'),
            ('VILLETRAVMERE_CH', 'ville_travail_mere', unicode_and_strip, 'ville travail mère'),
            ('TELPROFPERE_CH', 'telephone_travail_pere', unicode_and_strip, 'téléphone professionnel père'),
            ('TEL2PROFPERE_CH', 'telephone2_travail_pere', unicode_and_strip, 'téléphone 2 professionnel père'),
            ('TELMOBILPERE_CH', 'telephone_mobile_pere', unicode_and_strip, 'téléphone mobile'),
            ('TELPROFMERE_CH', 'telephone_travail_mere', unicode_and_strip, 'téléphone travail mère'),
            ('TEL2PROFMERE_CH', 'telephone2_travail_mere', unicode_and_strip, 'téléphone 2 travail mère'),
            ('TELMOBILMERE_CH', 'telephone_mobile_mere', unicode_and_strip, 'téléphone mobile mère'),
            ('TOTALDU_MO', 'total_du', unicode_and_strip, 'total dû'),
            ('TOTALREGLE_MO', 'total_regle', unicode_and_strip, 'total réglé'),
            ('NUMCENTRESS_CH', 'num_centre_securite_sociale', unicode_and_strip, 'n° centre sécurité sociale'),
            ('NOMCENTRESS_CH', 'nom_centre_securite_sociale', unicode_and_strip, 'nom centre sécurité sociale'),
            ('NUMASSURANCE_CH', 'num_assurance', unicode_and_strip, 'n° assurance'),
            ('NOMASSURANCE_CH', 'nom_assurance', unicode_and_strip, 'nom assurance'),
            ('RIVOLI_EN', 'code_rivoli', unicode_and_strip, 'identifiant code rivoli'),
            ('NUMCOMPTE_CH', 'numero_compte_comptable', unicode_and_strip, 'n° compte comptable'),
            ('EMAILPERE_CH', 'email_pere', unicode_and_strip, 'email du père'),
            ('EMAILMERE_CH', 'email_mere', unicode_and_strip, 'email de la mère'),
            ('NUMALLOCATAIRE_CH', 'numero_allocataire', unicode_and_strip, 'n° allocataire'),
            ('COMMENTAIRE_ME', 'commentaire', unicode_and_strip, 'commentaires / notes'),
            ('IDCSPPERE', 'identifiant_csp_pere', unicode_and_strip, 'référence identifiant csp'),
            ('IDCSPMERE', 'identifiant_csp_mere', unicode_and_strip, 'référence identifiant csp'),
            ('IDSECTEURS', 'identifiant_secteurs', unicode_and_strip, 'référence identifiant secteurs'),
            ('IDZONES', 'identifiant_zones', unicode_and_strip, 'référence identifiant zones'),
            ('IDRUES', 'identifiant_rues', unicode_and_strip, 'référence identifiant rues'),
            ('IDVILLES', 'identifiant_villes', unicode_and_strip, 'référence identifiant villes'),
            ('IDREGIMES', 'identifiant_regimes', unicode_and_strip, 'référence identifiant regimes'),
            ('IDSITUATIONFAMILLE', 'identifiant_situation_famille', unicode_and_strip, 'référence identifiant situationfamille'),
            ('NUMSECUPERE_CH', 'num_securite_sociale_pere', unicode_and_strip, 'n° secu père'),
            ('NUMSECUMERE_CH', 'num_securite_sociale_mere', unicode_and_strip, 'n° secu mère'),
            ('NATIONPERE_CH', 'nation_pere', unicode_and_strip, 'nationalité père'),
            ('NATIONMERE_CH', 'nation_mere', unicode_and_strip, 'nationalité mère'),
            ('NOMJEUNEFILLE_CH', 'nom_jeune_fille', unicode_and_strip, 'nom jeune fille'),
            ('IDCAFS', 'idcafs', unicode_and_strip, 'référence identifiant cafs'),
            ('CHAMPLIBRE1_CH', 'champ_libre1', unicode_and_strip, 'valeur champ libre 1'),
            ('CHAMPLIBRE2_CH', 'champ_libre2', unicode_and_strip, 'valeur champ libre 2'),
            ('CHAMPCALCULE1_CH', 'champ_calcule1', unicode_and_strip, 'valeur champ calculé 1'),
            ('CHAMPCALCULE2_CH', 'champ_calcule2', unicode_and_strip, 'valeur champ calculé 2'),
            ('IDTABLELIBRE1', 'id_table_libre1', unicode_and_strip, 'idtablelibre1'),
            ('IDTABLELIBRE3', 'id_table_libre3', unicode_and_strip, 'idtablelibre3'),
            ('IDTABLELIBRE2', 'id_table_libre2', unicode_and_strip, 'idtablelibre2'),
            ('IDTABLELIBRE4', 'id_table_libre4', unicode_and_strip, 'idtablelibre4'),
            ('NOMURSSAF_CH', 'nom_urssaf', unicode_and_strip, 'nom urssaf'),
            ('NUMURSSAF_CH', 'num_urssaf', unicode_and_strip, 'n° urssaf'),
            ('IDPROFPERE', 'identifiant_profession_pere', unicode_and_strip, 'référence identifiant profession'),
            ('IDPROFMERE', 'identifiant_profession_mere', unicode_and_strip, 'référence identifiant profession'),
            ('ALLOCATAIRE_CH', 'allocataire', unicode_and_strip, 'allocataire père ou mère (p,m)'),
#            ('PHOTOPERE_CH', 'photo_pere', unicode_and_strip, 'photographie père'),
#            ('PHOTOMERE_CH', 'photo_mere', unicode_and_strip, 'photographie mère'),
            ('NUMRUE_CH', 'numero_rue', unicode_and_strip, 'numéro de rue'),
            ('NUMRUEPERE_CH', 'numero_rue_pere', unicode_and_strip, 'numéro de rue père'),
            ('NUMRUEMERE_CH', 'numero_rue_mere', unicode_and_strip, 'numéro de rue mère'),
            ('IDPORTAIL_FAMILLES', 'identifiant_portail_familles', unicode_and_strip, 'identifiant de portail_familles'),
            ('ECHEANCEASSURANCE_DA', 'echeance_assurance', unicode_and_strip, 'date echéance assurance'),
            ('RM_MIKADO_MO', 'rm_mikado', unicode_and_strip, 'revenus mensuels mikado'),
            ('RA_MIKADO_MO', 'ra_mikado', unicode_and_strip, 'revenus annuels mikado'),
            ('QF_MIKADO_MO', 'qf_mikado', unicode_and_strip, 'quotient familial mikado'),
            ('RM_DIABOLO_MO', 'rm_diabolo', unicode_and_strip, 'revenus mensuels diabolo'),
            ('RA_DIABOLO_MO', 'ra_diabolo', unicode_and_strip, 'revenus annuels diabolo'),
            ('QF_DIABOLO_MO', 'qf_diabolo', unicode_and_strip, 'quotient familial diabolo'),
            ('RM_OLIGO_MO', 'rm_oligo', unicode_and_strip, 'revenus mensuels oligo'),
            ('RA_OLIGO_MO', 'ra_oligo', unicode_and_strip, 'revenus annuels oligo'),
            ('QF_OLIGO_MO', 'qf_oligo', unicode_and_strip, 'quotient familial oligo'),
            ('APPLICATION_REV_MIKADO_DA', 'application_rev_mikado', unicode_and_strip, 'date d\'application des revenus de mikado'),
            ('APPLICATION_REV_DIABOLO_DA', 'application_rev_diabolo', unicode_and_strip, 'date d\'application des revenus de diabolo'),
            ('APPLICATION_REV_OLIGO_DA', 'application_rev_oligo', unicode_and_strip, 'date d\'application des revenus de oligo'),
    )

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

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

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

    def add_child(self, child):
        if hasattr(self, 'id'):
            child.id_famille = self.id
            child.client = self.client
        self.children.append(child)

    def save(self):
        if hasattr(self, 'id'):
            self.client.update_family(self)
        else:
            self.code_interne = self.client.new_code_interne()
            self.id = self.client.add_family(self)
        for child in self.children:
            child.id_famille = self.id
            child.save()
        self.client.clear_cache()

class Invoice(SimpleObject):
    COLUMNS = (
        ('', 'id_famille', int, ''),
        ('', 'id', int, ''),
        ('', 'numero', str, ''),
        ('', 'debut_periode', parse_date, ''),
        ('', 'fin_periode', parse_date, ''),
        ('', 'creation', parse_date, ''),
        ('', 'echeance', parse_date, ''),
        ('', 'montant', Decimal, ''),
        ('', 'reste_du', Decimal, ''),
    )
    _detail = {}

    def detail(self):
        if not self._detail:
            self.client.factures_detail([self])
        return self._detail

    @property
    def family(self):
        return self.client.families[self.id_famille]

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

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

       It allows to retrieve family and invoices.

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

       All family are in the families dictionnary and all invoices in the
       invoices dictionnary.
    '''

    def __init__(self, url, domain, login, password, location=None,
            logger=logger):
        if not Client:
            raise ValueError('You need python suds')
        self.logger = logger
        self.logger.debug('creating DominoWs(%r, %r, %r, %r, location=%r)',
                url, domain, login, password, location)
        self.url = url
        self.domain = domain
        self.login = login
        self.password = password
        self.client = Client(url, location=location, timeout=60)
        self.client.options.cache.setduration(seconds=60)

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

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

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

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

        try:
            self.logger.debug(('soap call to %s(%s)' % (function_name, args)).encode('utf-8'))
            data = getattr(self.client.service, function_name)(self.domain, self.login, self.password, *args)
            self.logger.debug((u'result: %s' % data).encode('utf-8'))
            self.data = data
        except IOError, e:
            raise DominoException('Erreur IO', e)
        if data is None:
           data = ''
        if data.startswith('ERREUR'):
            raise DominoException(data[9:].encode('utf8'))
        return data

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

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

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

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

        data = self.call(function_name, *args)
        if cls or instances:
            rows = self.parse_tabular_data(data)
            kwargs = {}
            if instances:
                rows = zip(rows, instances)
            for row in rows:
                if instances:
                    row, instance = row
                if not row[0]:
                    continue
                for a, b in zip(columns or cls.COLUMNS, row):
                    x, name, converter, desc = a
                    kwargs[name] = converter(b.strip())
                if instances:
                    instance.__dict__.update(kwargs)
                    yield instance
                else:
                    yield cls(client=self, **kwargs)
        else:
            yield data

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

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

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

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

    @property
    @object_cached
    def families(self):
        '''Dictionary of all families indexed by their id.

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

        return self.get_families()

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

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

    def get_children(self, id_famille=0):
        columns = Child.COLUMNS
        if id_famille == 0:
            children = self('LISTER_ENFANTS',
                Child,
                args=((','.join([x[0] for x in columns])),))
        else:
            children = self('LISTER_ENFANTS_FAMILLE',
                Child,
                args=(id_famille, (','.join([x[0] for x in columns]))))
        return dict([(int(x), x) for x in children])

    def get_urgent_contacts(self, id_enfant):
        columns = UrgentContact.COLUMNS
        urgent_contacts = self('LISTER_PERSONNES_URGENCE',
                UrgentContact,
                args=((id_enfant, ','.join([x[0] for x in columns]))))
        return dict([(int(x), x) for x in urgent_contacts])

    @property
    @object_cached
    def invoices(self):
        '''Dictionnary of all invoices indexed by their id.

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

    def new_code_interne(self):
        max_ci = 0
        for family in self.families.values():
            try:
                max_ci = max(max_ci, int(family.code_interne))
            except:
                pass
        return '%05d' % (max_ci+1)

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

           id_famille - If value is not 0, only invoice for the family with
           this id are retrieved. If value is 0, invoices for all families are
           retrieved. Default to 0.
           etat - state of the invoices to return, possible values are
           'SOLDEES', 'NON_SOLDEES', 'TOUTES'.
        '''
        invoices = self('LISTER_FACTURES_FAMILLE', Invoice,
            args=(id_famille, state))
        invoices = list(invoices)
        for invoice in invoices:
            invoice.famille = self.families[invoice.id_famille]
        return dict(((int(x), x) for x in invoices))

    FACTURE_DETAIL_HEADERS = ['designation', 'quantite', 'prix', 'montant']
    def factures_detail(self, invoices):
        '''Retrieve details of some invoice'''
        data = self.call('DETAILLER_FACTURES', (''.join(("%s;" % int(x) for x in invoices)),))
        try:
            tree = etree.fromstring(data.encode('utf8'))
            for invoice, facture_node in zip(invoices, tree.findall('facture')):
                rows = []
                for ligne in facture_node.findall('detail_facture/ligne'):
                    row = []
                    rows.append(row)
                    for header in self.FACTURE_DETAIL_HEADERS:
                        if header in ligne.attrib:
                            row.append((header, ligne.attrib[header]))
                etablissement = facture_node.find('detail_etablissements/etablissement')
                if etablissement is not None:
                    nom = etablissement.get('nom').strip()
                else:
                    nom = ''
                d = { 'etablissement': nom, 'lignes': rows }
                invoice._detail = d
        except Exception, e:
            raise DominoException('Exception when retrieving invoice details', e)

    def get_family_by_mail(self, email):
        '''Return the first whose one email attribute matches the given email'''
        for famille in self.families.values():
            if email in (famille.email_pere, famille.email_mere,
                    famille.adresse_internet):
                return famille
        return None

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

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

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

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