Projet

Général

Profil

0001-remove-obsolete-abelium-domino-modules-17244.patch

Frédéric Péters, 03 octobre 2020 20:05

Télécharger (61,1 ko)

Voir les différences:

Subject: [PATCH] remove obsolete abelium domino modules (#17244)

 auquotidien/auquotidien.py                    |   7 -
 auquotidien/modules/abelium_domino_synchro.py | 165 -----
 auquotidien/modules/abelium_domino_ui.py      | 170 -----
 auquotidien/modules/abelium_domino_vars.py    |  86 ---
 .../modules/abelium_domino_workflow.py        | 148 ----
 auquotidien/modules/abelium_domino_ws.py      | 647 ------------------
 auquotidien/modules/admin.py                  |   7 +-
 auquotidien/modules/connectors.py             |   1 -
 8 files changed, 1 insertion(+), 1230 deletions(-)
 delete mode 100644 auquotidien/modules/abelium_domino_synchro.py
 delete mode 100644 auquotidien/modules/abelium_domino_ui.py
 delete mode 100644 auquotidien/modules/abelium_domino_vars.py
 delete mode 100644 auquotidien/modules/abelium_domino_workflow.py
 delete mode 100644 auquotidien/modules/abelium_domino_ws.py
auquotidien/auquotidien.py
13 13
from modules import root
14 14
from modules import payments
15 15
from modules import connectors
16
from modules import abelium_domino_ui
17
from modules import abelium_domino_vars
18
from modules import abelium_domino_synchro
19 16

  
20 17
get_publisher_class().register_translation_domain('auquotidien')
21 18
get_publisher_class().default_configuration_path = 'au-quotidien-wcs-settings.xml'
......
32 29
import wcs.admin.forms
33 30
wcs.admin.forms.FormsDirectory.categories = categories_admin.CategoriesDirectory()
34 31

  
35
import wcs.admin.settings
36
wcs.admin.settings.SettingsDirectory.domino = abelium_domino_ui.AbeliumDominoDirectory()
37
wcs.admin.settings.SettingsDirectory._q_exports.append('domino')
38

  
39 32
import wcs.categories
40 33
wcs.categories.Category.XML_NODES = [('name', 'str'), ('url_name', 'str'),
41 34
        ('description', 'str'), ('position', 'int'),
auquotidien/modules/abelium_domino_synchro.py
1
import sys
2
from datetime import datetime
3
import collections
4
from decimal import Decimal
5

  
6
from wcs.qommon import _
7
from wcs.qommon.cron import CronJob
8
from wcs.qommon.publisher import get_publisher_class
9
from wcs.qommon import get_logger
10

  
11
from wcs.users import User
12

  
13
from .abelium_domino_ui import (get_client, is_activated, get_invoice_regie,
14
        abelium_domino_ws)
15
from .payments import Invoice, Transaction
16

  
17
DOMINO_ID_PREFIX = 'DOMINO-'
18

  
19
def synchronize_domino(publisher):
20
    regie = get_invoice_regie(publisher)
21
    logger = get_logger()
22
    if not is_activated(publisher) or not regie:
23
        return
24
    client = get_client(publisher)
25
    if client is None:
26
        logger.warning('Unable to create a DominoWS object')
27
        return
28
    client.clear_cache()
29
    users = User.values()
30
    users_by_mail = dict(((user.email, user) for user in users))
31
    users_by_code_interne = {}
32
    for user in users:
33
        if hasattr(user, 'abelium_domino_code_famille'):
34
            users_by_code_interne[user.abelium_domino_code_famille] = user
35
    try:
36
        invoices = client.invoices
37
    except abelium_domino_ws.DominoException as e:
38
        publisher.notify_of_exception(sys.exc_info(), context='[DOMINO]')
39
        logger.error('domino cron: failure to retrieve invoice list from domino '
40
                'for synchronization [error:%s]', e)
41
        return
42
    # import new invoices
43
    logger.info('domino cron: retrieved %i invoices', len(invoices))
44
    for invoice_id, invoice in invoices.items():
45
        user = None
46
        if invoice.family.code_interne in users_by_code_interne:
47
            user = users_by_code_interne[invoice.family.code_interne]
48
        if user is None:
49
            for email in (invoice.family.email_pere, invoice.family.email_mere,
50
                    invoice.family.adresse_internet):
51
                user = users_by_mail.get(email)
52
                if user:
53
                    break
54
            else:
55
                continue
56
        external_id = '%s%s' % (DOMINO_ID_PREFIX, invoice.id)
57
        payment_invoice = Invoice.get_on_index(external_id, 'external_id', ignore_errors=True)
58
        if payment_invoice:
59
            continue
60
        if invoice.reste_du == Decimal(0) or invoice.reste_du < Decimal(0):
61
            continue
62
        payment_invoice = Invoice()
63
        payment_invoice.user_id = user.id
64
        payment_invoice.external_id = external_id
65
        payment_invoice.regie_id = regie.id
66
        payment_invoice.formdef_id = None
67
        payment_invoice.formdata_id = None
68
        payment_invoice.amount = invoice.reste_du
69
        payment_invoice.date = invoice.creation
70
        payment_invoice.domino_synchro_date = datetime.now()
71
        if 'etablissement' in invoice._detail:
72
            etablissement = invoice._detail['etablissement'].encode('utf-8')
73
            payment_invoice.subject = _('%s - Childcare services') % etablissement
74
        else:
75
            payment_invoice.subject = _('Childcare services')
76
        if invoice._detail.get('lignes'):
77
            details = []
78
            details.append('<table class="invoice-details"><thead>')
79
            tpl = '''<tr>
80
    <td>%(designation)s</td>
81
    <td>%(quantite)s</td>
82
    <td>%(prix)s</td>
83
    <td>%(montant)s</td>
84
</tr>'''
85
            captions = {
86
                    'designation': _('Caption'),
87
                    'quantite': _('Quantity'),
88
                    'prix': _('Price'),
89
                    'amount': _('Amount')
90
            }
91
            details.append(tpl % captions)
92
            details.append('</thead>')
93
            details.append('<tbody>')
94
            for ligne in invoice._detail['lignes']:
95
                def encode(x):
96
                    a, b = x
97
                    b = b.encode('utf-8')
98
                    return (a,b)
99
                ligne = map(encode, ligne)
100
                ligne = dict(ligne)
101
                base = collections.defaultdict(lambda:'')
102
                base.update(ligne)
103
                details.append(tpl % base)
104
            details.append('</tbody></table>')
105
            payment_invoice.details = '\n'.join(details)
106
        payment_invoice.store()
107
        logger.info('domino cron: remote invoice %s for family %s added to user %s invoices with id %s',
108
                invoice.id, invoice.family.id, user.id, payment_invoice.id)
109

  
110
    # update invoices
111
    invoices_ids = dict(invoices.items())
112
    for payment_invoice in Invoice.values():
113
        if payment_invoice.external_id is None or not payment_invoice.external_id.startswith(DOMINO_ID_PREFIX):
114
            continue # not a payment related to domino we skip
115
        i = payment_invoice.external_id[len(DOMINO_ID_PREFIX):]
116
        i = int(i)
117
        invoice = invoices_ids.get(i)
118
        if payment_invoice.paid:
119
            if not invoice: 
120
                # invoice has been paid (locally or not) but remote invoice has
121
                # been deleted do, we do nothing.
122
                continue
123
            if getattr(payment_invoice, 'domino_knows_its_paid', None) or getattr(payment_invoice, 'paid_by_domino', None):
124
                # synchronization of payment already done, skip
125
                continue
126
            transactions = list(Transaction.get_with_indexed_value('invoice_ids',
127
                                                                   payment_invoice.id))
128
            if not transactions:
129
                logger.warning("domino cron: invoice %s is marked paid but does "
130
                        "not have any linked transaction.", payment_invoice.id)
131
                details = '' # no details about the payment, problem
132
            else:
133
                details = repr(transactions[0].__dict__)
134
            if invoice.montant != payment_invoice.amount:
135
                pass # add warning logs
136
            try:
137
                client.pay_invoice([invoice], invoice.montant, details,
138
                        payment_invoice.paid_date)
139
            except abelium_domino_ws.DominoException as e:
140
                logger.error('domino cron: invoice %s has been paid, but the remote system '
141
                        'is unreachable, notification will be done again '
142
                        'later [error: %s]', invoice.id, e)
143
            else:
144
                # memorize the date of synchronization
145
                payment_invoice.domino_knows_its_paid = datetime.now()
146
                payment_invoice.store()
147
                logger.info('domino cron: domino: invoice %s has been paid; remote system has been '
148
                        'notified', payment_invoice.id)
149
        else: # unpaid
150
            if not invoice:
151
                logger.info('domino cron: remote invoice %s disapearred, so its '
152
                        'still-unpaid local counterpart invoice %s was deleted.',
153
                        i, payment_invoice.id)
154
                payment_invoice.remove_self()
155
            elif invoice.paid():
156
                payment_invoice.paid_by_domino = True
157
                payment_invoice.pay()
158
                logger.info('domino cron: remote invoice %s has beend paid, '
159
                        'local invoice %s of user %s is now marked as paid.',
160
                        invoice.id, payment_invoice.id, payment_invoice.user_id)
161
            else: # not invoice.paid()
162
                pass # still waiting for the payment
163

  
164
get_publisher_class().register_cronjob(CronJob(function=synchronize_domino,
165
    hours=range(0, 24), minutes=range(0, 60, 30)))
auquotidien/modules/abelium_domino_ui.py
1
from quixote import get_publisher, redirect, get_request
2
from quixote.directory import Directory, AccessControlled
3
from quixote.html import TemplateIO, htmltext
4

  
5
from wcs.qommon import _, N_
6
from wcs.qommon import get_cfg, get_logger
7
from wcs.qommon.form import Form, StringWidget, CheckboxWidget, SingleSelectWidget
8
from wcs.qommon.backoffice.menu import html_top
9
from quixote.html import htmltext
10

  
11
from .payments import Regie
12

  
13

  
14
# constants
15
ABELIUM_DOMINO = 'abelium_domino'
16
ACTIVATED = 'activated'
17
WSDL_URL = 'wsdl_url'
18
SERVICE_URL = 'service_url'
19
DOMAIN = 'domain'
20
LOGIN = 'login'
21
PASSWORD = 'password'
22
INVOICE_REGIE = 'invoice_regie'
23

  
24
try:
25
    import abelium_domino_ws
26
except ImportError as e:
27
    abelium_domino_ws = None
28
    import_error = e
29

  
30
def get_abelium_cfg(publisher):
31
    if not publisher:
32
        publisher = get_publisher()
33
    return publisher.cfg.get(ABELIUM_DOMINO, {})
34

  
35
def is_activated(publisher=None):
36
    cfg = get_abelium_cfg(publisher)
37
    return cfg.get(ACTIVATED, False) and abelium_domino_ws is not None
38

  
39
def get_client(publisher=None):
40
    publisher = publisher or get_publisher()
41

  
42
    cfg = get_abelium_cfg(publisher)
43
    try:
44
        publisher._ws_cache = abelium_domino_ws.DominoWs(
45
            url=cfg.get(WSDL_URL, ''),
46
            domain=cfg.get(DOMAIN,''),
47
            login=cfg.get(LOGIN, ''),
48
            password=cfg.get(PASSWORD, ''),
49
            location=cfg.get(SERVICE_URL),
50
            logger=get_logger())
51
    except IOError:
52
        return None
53
    return publisher._ws_cache
54

  
55
def get_family(user, publisher=None):
56
    family = None
57
    if user is None:
58
        return None
59
    client = get_client(publisher)
60
    if not client:
61
        return None
62
    if hasattr(user, 'abelium_domino_code_famille'):
63
        family = client.get_family_by_code_interne(
64
            user.abelium_domino_code_famille)
65
    if family is None and user.email:
66
        family = client.get_family_by_mail(user.email)
67
    return family
68

  
69
def get_invoice_regie(publisher=None):
70
    cfg = get_abelium_cfg(publisher)
71
    regie_id = cfg.get(INVOICE_REGIE)
72
    if not regie_id:
73
        return None
74
    return Regie.get(regie_id, ignore_errors=True)
75

  
76
class AbeliumDominoDirectory(Directory):
77
    _q_exports = [ '' , 'debug' ]
78
    label = N_('Domino')
79

  
80
    def debug(self):
81
        from abelium_domino_vars import SESSION_CACHE
82
        html_top(ABELIUM_DOMINO)
83
        r = TemplateIO(html=True)
84
        r += htmltext('<form method="post"><button>Lancer le cron</button></form>')
85
        if get_request().get_method() == 'POST':
86
            try:
87
                from abelium_domino_synchro import synchronize_domino
88
                synchronize_domino(get_publisher())
89
            except Exception as e:
90
                r += htmltext('<pre>%s</pre>') % repr(e)
91
        r += htmltext('<p>code interne: %s</p>') % getattr(get_request().user, str('abelium_domino_code_famille'), None)
92
        r += htmltext('<dl>')
93
        context = get_publisher().substitutions.get_context_variables()
94
        for var in sorted(context.keys()):
95
            value = context[var]
96
            if value:
97
                r += htmltext('<dt>%s</dt>') % var
98
                r += htmltext('<dd>%s</dt>') % value
99
        r += htmltext('</dl>')
100
        delattr(get_request().session, SESSION_CACHE)
101

  
102
    def _q_index(self):
103
        publisher = get_publisher()
104
        cfg = get_cfg(ABELIUM_DOMINO, {})
105
        form = self.form(cfg)
106

  
107
        title = _('Abelium Domino')
108
        html_top(ABELIUM_DOMINO, title = title)
109
        r = TemplateIO(html=True)
110
        r += htmltext('<h2>%s</h2>') % title
111

  
112
        if form.is_submitted() and not form.has_errors():
113
            if form.get_widget('cancel').parse():
114
                return redirect('..')
115
            if form.get_widget('submit').parse():
116
                for name in [f[0] for f in self.form_desc] + [INVOICE_REGIE]:
117
                    widget = form.get_widget(name)
118
                    if widget:
119
                        cfg[name] = widget.parse()
120
                publisher.cfg[ABELIUM_DOMINO] = cfg
121
                publisher.write_cfg()
122
                return redirect('.')
123

  
124
        if abelium_domino_ws:
125
            r += form.render()
126
        else:
127
            message = _('The Abelium Domino module is not '
128
                    'activated because of this error when '
129
                    'loading it: %r') % import_error
130
            r += htmltext('<p class="errornotice">%s</p>') % message
131
        r += htmltext('<dl style="display: none">')
132
        context = get_publisher().substitutions.get_context_variables()
133
        for var in sorted(context.keys()):
134
            value = context[var]
135
            if value:
136
                r += htmltext('<dt>%s</dt>') % var
137
                r += htmltext('<dd>%s</dt>') % value
138
        r += htmltext('</dl>')
139
        return r.getvalue()
140

  
141
    form_desc = (
142
            # name, required, title, kind
143
            (ACTIVATED, False, _('Activated'), bool),
144
            (WSDL_URL, True, _('WSDL URL'), str),
145
            (SERVICE_URL, False, _('Service URL'), str),
146
            (DOMAIN, True, _('Domain'), str),
147
            (LOGIN, True, _('Login'), str),
148
            (PASSWORD, True, _('Password'), str),
149
    )
150

  
151

  
152
    def form(self, initial_value={}):
153
        form = Form(enctype='multipart/form-data')
154
        kinds = { str: StringWidget, bool: CheckboxWidget }
155
        for name, required, title, kind in self.form_desc:
156
            widget = kinds[kind]
157
            form.add(widget, name, required=required, title=title,
158
                    value=initial_value.get(name, ''))
159
        options = [(regie.id, regie.label) \
160
                        for regie in Regie.values()]
161
        options.insert(0, (None, _('None')))
162
        form.add(SingleSelectWidget, INVOICE_REGIE,
163
                title=_('Regie which will receive payments'),
164
                value=initial_value.get(INVOICE_REGIE),
165
                options=options)
166

  
167
        form.add_submit('submit', _('Submit'))
168
        form.add_submit('cancel', _('Cancel'))
169

  
170
        return form
auquotidien/modules/abelium_domino_vars.py
1
from decimal import Decimal
2
import logging
3

  
4
from quixote.publish import get_publisher
5

  
6
from wcs.qommon import _
7
from wcs.qommon.substitution import Substitutions
8
from wcs.publisher import WcsPublisher
9

  
10
from .abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
11

  
12
SESSION_CACHE = 'abelium_domino_variable_cache'
13

  
14
class DominoVariables(object):
15
    VARIABLE_TEMPLATE = 'domino_var_%s'
16
    CHILD_VARIABLE_TEMPLATE = 'domino_var_%s_enfant%s'
17

  
18
    @classmethod
19
    def CHILD_COLUMNS(cls):
20
        return abelium_domino_ws.Child.COLUMNS
21

  
22
    @classmethod
23
    def FAMILY_COLUMNS(cls):
24
        return abelium_domino_ws.Family.COLUMNS + abelium_domino_ws.Family.MORE_COLUMNS
25

  
26
    def __init__(self, publisher=None, request=None):
27
        self.publisher = publisher
28
        self.request = request
29

  
30
    def get_substitution_variables(self):
31
        vars = {}
32
        if not is_activated() or not self.request or not self.request.user \
33
                or not getattr(self.request.user, 'email'):
34
            return vars
35

  
36
        # test cache
37
        cache = getattr(self.request.session, SESSION_CACHE, None)
38
        if cache is not None:
39
            return cache
40
        # call the web service
41
        try:
42
            charset = get_publisher().site_charset
43
            family = get_family(self.request.user)
44
            if family:
45
                family.complete()
46
                for i, child in enumerate(family.children):
47
                    for remote_name, name, converter, desc in self.CHILD_COLUMNS():
48
                        v = getattr(child, name, None)
49
                        if v is None:
50
                            continue
51
                        if hasattr(v, 'encode'):
52
                            v = v.encode(charset)
53
                        vars[self.CHILD_VARIABLE_TEMPLATE % (name, i+1)] = v
54
                vars[self.VARIABLE_TEMPLATE % 'nombre_enfants'] = len(family.children)
55
                for remote_name, name, converted, desc in self.FAMILY_COLUMNS():
56
                    if hasattr(family, name):
57
                        v = getattr(family, name)
58
                        if v is None:
59
                            continue
60
                        if hasattr(v, 'encode'):
61
                            v = v.encode(charset)
62
                        vars[self.VARIABLE_TEMPLATE % name] = v
63
                amount = Decimal(0)
64
                for invoice in family.invoices:
65
                    amount += invoice.reste_du
66
                if amount:
67
                    vars['user_famille_reste_du'] = str(amount)
68
        except abelium_domino_ws.DominoException:
69
            logging.exception('unable to call the domino ws for user %s', self.request.user.id)
70
        setattr(self.request.session, SESSION_CACHE, vars)
71
        self.request.session.store()
72
        return vars
73

  
74
    def get_substitution_variables_list(cls):
75
        if not is_activated():
76
            return ()
77
        vars = []
78
        for remote_name, name, converted, desc in cls.FAMILY_COLUMNS():
79
            vars.append((_('Domino'), cls.VARIABLE_TEMPLATE % name, desc))
80
        for remote_name, name, converted, desc in cls.CHILD_COLUMNS():
81
            vars.append((_('Domino'), cls.CHILD_VARIABLE_TEMPLATE % (name, '{0,1,2,..}'), desc))
82
        return vars
83
    get_substitution_variables_list = classmethod(get_substitution_variables_list)
84

  
85
Substitutions.register_dynamic_source(DominoVariables)
86
WcsPublisher.register_extra_source(DominoVariables)
auquotidien/modules/abelium_domino_workflow.py
1
import re
2
import time
3

  
4
from quixote import get_request, get_publisher, get_session
5
from quixote.directory import Directory
6

  
7
from wcs.qommon import _, N_
8
from wcs.qommon.substitution import Substitutions
9
from wcs.qommon.form import Form, ValidatedStringWidget
10
from wcs.qommon import get_logger
11

  
12
from wcs.workflows import Workflow, WorkflowStatusJumpItem, register_item_class
13
from wcs.forms.common import FormStatusPage
14

  
15
from .abelium_domino_ui import (is_activated, abelium_domino_ws, get_client, get_family)
16
from . import abelium_domino_ws
17

  
18
class InternalCodeStringWidget(ValidatedStringWidget):
19
    regex = '\d*'
20

  
21
class AbeliumDominoRegisterFamilyWorkflowStatusItem(WorkflowStatusJumpItem):
22
    status = None
23
    description = N_('Abelium Domino: Register a Family')
24
    key = 'abelium-domino-register-family'
25
    category = 'interaction'
26
    label = None
27

  
28
    def render_as_line(self):
29
        return _('Register a Family into Abelium Domino')
30

  
31
    def get_family(self, formdata):
32
        try:
33
            user = formdata.get_user()
34
            if user:
35
                family = get_family(user)
36
                if family:
37
                    family.complete()
38
                return family
39
        except abelium_domino_ws.DominoException:
40
            pass
41
        return None
42

  
43
    def fill_form(self, form, formdata, user):
44
        family = self.get_family(formdata)
45
        if 'family_id' not in form._names:
46
            form.add(InternalCodeStringWidget, 'family_id',
47
                     title=_('Family internal code'),
48
                     value=family and family.code_interne.encode('utf8'),
49
                     hint=_('If a family internal code is present, the '
50
                            'family is updated, if not it is created'))
51
            form.add_submit('create_update_button%s' % self.id,
52
                    _('Create or update the family'))
53

  
54
    def update(self, form, formdata, user, evo):
55
        fid_widget = form.get_widget('family_id')
56
        code_interne = fid_widget.parse()
57
        try:
58
            code_interne = int(code_interne)
59
        except ValueError:
60
            raise ValueError('Le code interne est invalide')
61
        code_interne = '%05d' % code_interne
62
        family = get_client().get_family_by_code_interne(code_interne)
63
        if not family:
64
            raise ValueError('Le code interne est invalide')
65
        family.complete()
66
        self.extract_family(form, formdata, user, evo, family)
67
        family.save()
68
        return family
69

  
70
    def create(self, form, formdata, user, evo):
71
        family = abelium_domino_ws.Family(client=get_client())
72
        self.extract_family(form, formdata, user, evo, family)
73
        return family
74

  
75
    def extract_family(self, form, formdata, user, evo, family):
76
        formdef = formdata.formdef
77
        children = [abelium_domino_ws.Child() for i in range(5)]
78
        max_i = 0
79
        for field in formdef.fields:
80
            value = formdata.data.get(field.id)
81
            if value in (None, ''):
82
                continue
83
            if hasattr(field, 'date_in_the_past'):
84
                value = time.strftime('%Y%m%d', value)
85
            value = unicode(value, 'utf8')
86
            if field.prefill and \
87
               field.prefill.get('type') == 'formula':
88
                v = field.prefill.get('value', '').strip()
89
                i = None
90
                name = None
91
                m = re.search('domino_var_([^ ]*)_enfant([0-9]*)', v)
92
                m2 = re.search('domino_var_([^ ]*)', v)
93
                if m:
94
                    name, i = m.groups()
95
                    try:
96
                        i = int(i)
97
                    except ValueError:
98
                        continue
99
                    max_i = max(i, max_i)
100
                    setattr(children[i-1], name, value)
101
                elif m2:
102
                    name = m2.group(1)
103
                    setattr(family, name, value)
104
        for child1, child2 in zip(family.children, children[:max_i]):
105
            child1.__dict__.update(child2.__dict__)
106
        family.save()
107
        if max_i > len(family.children): # add new children
108
            for child in children[len(family.children):max_i]:
109
                family.add_child(child)
110

  
111
    def submit_form(self, form, formdata, user, evo):
112
        logger = get_logger()
113
        if form.get_submit() != 'create_update_button%s' % self.id:
114
            return
115
        try:
116
            if form.get_widget('family_id').parse():
117
                family = self.update(form, formdata, user, evo)
118
                msg = _('Sucessfully updated the family %s')
119
                log_msg = _('Sucessfully updated the family %(code)s of %(user)s')
120
            else:
121
                family = self.create(form, formdata, user, evo)
122
                msg = _('Sucessfully created the family %s')
123
                log_msg = _('Sucessfully created the family %(code)s of %(user)s')
124
            code_interne = family.code_interne.encode('utf8')
125
            msg = msg % code_interne
126
            logger.info(log_msg, {'code': code_interne, 'user': formdata.get_user()})
127
            form_user = formdata.get_user()
128
            form_user.abelium_domino_code_famille = code_interne
129
            form_user.store()
130
        except Exception as e:
131
            if form.get_widget('family_id').parse():
132
                msg = _('Unable to update family: %s') % str(e)
133
            else:
134
                msg = _('Unable to create family: %s') % str(e)
135
            evo.comment = msg
136
            logger.exception(msg  % formdata.get_user())
137
        else:
138
            evo.comment = msg
139
            wf_status = self.get_target_status()
140
            if wf_status:
141
                evo.status = 'wf-%s' % wf_status[0].id
142
        return False
143

  
144
    def is_available(self, workflow=None):
145
        return get_publisher().has_site_option('domino')
146
    is_available = classmethod(is_available)
147

  
148
register_item_class(AbeliumDominoRegisterFamilyWorkflowStatusItem)
auquotidien/modules/abelium_domino_ws.py
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('&nbsp;', '&#160;')
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.items() 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

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

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

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

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

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

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

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

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

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

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

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

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

  
514
        return self.get_families()
515

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  
638
        if not date:
639
            date = datetime.datetime.now()
640
        due = sum([self.invoices[int(id_invoice)].reste_du
641
                 for id_invoice in id_invoices])
642
        if Decimal(amount) == Decimal(due):
643
            return self('SOLDER_FACTURE', None, args=(str(amount), 
644
                    ''.join([ '%s;' % int(x) for x in id_invoices]),
645
                    date.strftime('%Y-%m-%d'), other_information))
646
        else:
647
            raise DominoException('Amount due and paid do not match', { 'due': due, 'paid': amount})
auquotidien/modules/admin.py
18 18
from wcs.qommon.backoffice.menu import html_top
19 19

  
20 20
import re
21
from .abelium_domino_ui import AbeliumDominoDirectory
22 21

  
23 22

  
24 23
class PanelDirectory(Directory):
25
    _q_exports = ['', 'update', 'permissions', 'domino']
24
    _q_exports = ['', 'update', 'permissions']
26 25
    label = N_('Control Panel')
27 26

  
28
    domino = AbeliumDominoDirectory()
29

  
30 27
    def _verify_mask(self, form):
31 28
        if form.is_submitted():
32 29
            if not re.match("[0-9Xx]*$", form.get('mobile_mask') or ''):
......
76 73
        r += htmltext('<h2>%s</h2>') % _('Extra Options')
77 74
        r += htmltext('<ul>')
78 75
        r += htmltext('<li><a href="aq/permissions">%s</a></li>') % _('Permissions')
79
        if get_publisher().has_site_option('domino'):
80
            r += htmltext('<li><a href="aq/domino">%s</a></li>') % _('Abelium Domino Integration')
81 76
        r += htmltext('</ul>')
82 77
        r += htmltext('</div>')
83 78
        r += htmltext('</div>')
auquotidien/modules/connectors.py
1
from . import abelium_domino_workflow
2 1
from . import clicrdv
3
-