pages.py 24.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# -*- coding: utf-8 -*-

# Copyright(C) 2016      Edouard Lambert
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.

20
from __future__ import unicode_literals
21

22 23 24
import re
import requests
import json
25
import datetime as dt
26

27
from collections import OrderedDict
28 29

from weboob.browser.pages import HTMLPage, JsonPage, RawPage, LoggedPage, pagination
30
from weboob.browser.elements import DictElement, ItemElement, TableElement, SkipItem, method
31
from weboob.browser.filters.standard import CleanText, Upper, Date, Regexp, Format, CleanDecimal, Filter, Env, Slugify, Field
32
from weboob.browser.filters.json import Dict
33
from weboob.browser.filters.html import Attr, Link, TableCell
34
from weboob.browser.exceptions import ServerError
35
from weboob.capabilities.bank import Account, Investment, Loan
36
from weboob.capabilities.contact import Advisor
37
from weboob.capabilities.base import NotAvailable
38
from weboob.capabilities.profile import Profile
39
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
40
from weboob.exceptions import ParseError
41
from weboob.tools.capabilities.bank.investments import is_isin_valid
Nicolas Gattolin's avatar
Nicolas Gattolin committed
42
from weboob.tools.compat import unicode
43

44

45 46 47 48 49
def MyDecimal(*args, **kwargs):
    kwargs.update(replace_dots=True, default=NotAvailable)
    return CleanDecimal(*args, **kwargs)


50 51 52 53
class LoginPage(HTMLPage):
    pass


54 55 56 57 58 59 60
class LogoutPage(RawPage):
    pass


class InfosPage(LoggedPage, HTMLPage):
    def get_typelist(self):
        url = Attr(None, 'src').filter(self.doc.xpath('//script[contains(@src, "comptes/scripts")]'))
61
        m = re.search(r'synthesecomptes[^\w]+([^:]+)[^\w]+([^"]+)', self.browser.open(url).text)
62 63 64 65
        return {m.group(1): m.group(2)}


class AccountsPage(LoggedPage, JsonPage):
66 67 68 69 70 71 72
    TYPES = OrderedDict([('courant',             Account.TYPE_CHECKING),
                         ('pee',                 Account.TYPE_PEE),
                         ('epargne en actions',  Account.TYPE_PEA),
                         ('pea',                 Account.TYPE_PEA),
                         ('preference',          Account.TYPE_LOAN),
                         ('livret',              Account.TYPE_SAVINGS),
                         ('vie',                 Account.TYPE_LIFE_INSURANCE),
73
                         ('previ_option',        Account.TYPE_LIFE_INSURANCE),
74 75 76 77
                         ('actions',             Account.TYPE_MARKET),
                         ('titres',              Account.TYPE_MARKET),
                         ('ldd cm',              Account.TYPE_SAVINGS),
                         ('librissime',          Account.TYPE_SAVINGS),
78 79
                         ('epargne logement',    Account.TYPE_SAVINGS),
                         ('plan bleu',           Account.TYPE_SAVINGS),
80
                         ('capital plus',        Account.TYPE_SAVINGS),
81
                       ])
82 83

    def get_keys(self):
84
        """Returns the keys for which the value is a list or dict"""
85 86 87
        if "exception" in self.doc:
            return []
        return [k for k, v in self.doc.items() if v and isinstance(v, (dict, list))]
88 89 90

    def check_response(self):
        if "exception" in self.doc:
91 92 93 94
            self.logger.warning("There are no checking accounts: exception '{}' with code {}".format(
                                self.doc['exception']['message'],
                                self.doc['exception']['code'])
                            )
95

96 97 98 99 100
    def get_numbers(self):
        keys = self.get_keys()
        numbers = {}
        for key in keys:
            if isinstance(self.doc[key], dict):
101 102
                keys_ = [k for k in self.doc[key] if isinstance(k, unicode)]
                contracts = [v for k in keys_ for v in self.doc[key][k]]
103 104 105 106 107
            else:
                contracts = [v for v in self.doc[key]]
            numbers.update({c['index']: c['numeroContratSouscrit'] for c in contracts})
        return numbers

108 109 110 111 112 113 114
    @method
    class iter_accounts(DictElement):
        def parse(self, el):
            self.item_xpath = "%s/*" % Env('key')(self)

        def find_elements(self):
            selector = self.item_xpath.split('/')
115 116 117 118
            for sub_element in selector:
                if isinstance(self.el, dict) and self.el and sub_element == '*':
                    self.el = next(iter(self.el.values())) # replace self.el with its first value
                if sub_element == '*':
119
                    continue
120 121 122
                self.el = self.el[sub_element]
            for sub_element in self.el:
                yield sub_element
123 124 125 126

        class item(ItemElement):
            klass = Account

127 128
            condition = lambda self: "LIVRET" not in Dict('accountType')(self.el)

129
            obj_id = Dict('numeroContratSouscrit')
130 131 132
            obj_label = Upper(Dict('lib'))
            obj_currency =  Dict('deviseCompteCode')
            obj_coming = CleanDecimal(Dict('AVenir', default=None), default=NotAvailable)
133 134
            # Iban is available without last 5 numbers, or by sms
            obj_iban = NotAvailable
135
            obj__index = Dict('index')
136

137 138 139 140
            def obj_balance(self):
                balance = CleanDecimal(Dict('soldeEuro', default="0"))(self)
                return -abs(balance) if Field('type')(self) == Account.TYPE_LOAN else balance

141 142 143 144
            # It can have revolving credit on this page
            def obj__total_amount(self):
                return CleanDecimal(Dict('grantedAmount', default=None), default=NotAvailable)(self)

145 146 147 148
            def obj_type(self):
                return self.page.TYPES.get(Dict('accountType', default=None)(self).lower(), Account.TYPE_UNKNOWN)

    @method
149 150 151 152 153
    class iter_savings(DictElement):
        @property
        def item_xpath(self):
            return "%s/*/savingsProducts" % Env('key')(self)

154 155 156 157 158 159 160 161 162 163 164 165
        def store(self, obj):
            id = obj.id
            n = 1
            while id in self.objects:
                self.logger.warning('There are two objects with the same ID! %s' % id)
                n += 1
                id = '%s-%s' % (obj.id, n)

            obj.id = id
            self.objects[obj.id] = obj
            return obj

166 167 168 169 170 171 172 173
        # the accounts really are deeper, but the account type is in a middle-level
        class iter_accounts(DictElement):
            item_xpath = 'savingsAccounts'

            def parse(self, el):
                # accounts may have a user-entered label, so it shouldn't be relied too much on for parsing the account type
                self.env['type_label'] = el['libelleProduit']

174 175 176 177 178 179 180 181 182 183 184 185
            def store(self, obj):
                id = obj.id
                n = 1
                while id in self.objects:
                    self.logger.warning('There are two objects with the same ID! %s' % id)
                    n += 1
                    id = '%s-%s' % (obj.id, n)

                obj.id = id
                self.objects[obj.id] = obj
                return obj

186 187 188 189 190
            class item(ItemElement):
                klass = Account

                obj_label = Upper(Dict('libelleContrat'))
                obj_balance = CleanDecimal(Dict('solde', default="0"))
Nicolas Gattolin's avatar
Nicolas Gattolin committed
191
                obj_currency = 'EUR'
192 193
                obj_coming = CleanDecimal(Dict('AVenir', default=None), default=NotAvailable)
                obj__index = Dict('index')
194
                obj__owner = Dict('nomTitulaire')
195 196

                def obj_id(self):
197 198
                    type = Field('type')(self)
                    if type == Account.TYPE_LIFE_INSURANCE:
199 200 201
                        number = self.get_lifenumber()
                        if number:
                            return number
202
                    elif type in (Account.TYPE_PEA, Account.TYPE_MARKET):
203 204 205
                        number = self.get_market_number()
                        if number:
                            return number
206 207 208 209 210 211 212 213 214 215 216 217 218

                    try:
                        return Env('numbers')(self)[Dict('index')(self)]
                    except KeyError:
                        # index often changes, so we can't use it... and have to do something ugly
                        return Slugify(Format('%s-%s', Dict('libelleContrat'), Dict('nomTitulaire')))(self)

                def obj_type(self):
                    for key in self.page.TYPES:
                        if key in Env('type_label')(self).lower():
                            return self.page.TYPES[key]
                    return Account.TYPE_UNKNOWN

219 220 221
                def get_market_number(self):
                    label = Field('label')(self)
                    page = self.page.browser._go_market_history()
222
                    return page.get_account_id(label, Field('_owner')(self))
223

224 225
                def get_lifenumber(self):
                    index = Dict('index')(self)
226 227 228 229
                    data = json.loads(self.page.browser.lifeinsurance.open(accid=index).content)
                    if not data:
                        raise SkipItem('account seems unavailable')
                    url = data['url']
230 231
                    page = self.page.browser.open(url).page
                    return page.get_account_id()
232

233 234 235
    @method
    class iter_loans(DictElement):
        def parse(self, el):
236
            self.item_xpath = Env('key')(self)
237
            if "Pret" in Env('key')(self):
238
                self.item_xpath = "%s/*/lstPret" % self.item_xpath
239 240

        class item(ItemElement):
241 242 243 244 245
            klass = Loan

            def obj_id(self):
                # it seems that if we don't have "numeroContratSouscrit", "identifiantTechnique" is unique : only this direction !
                return Dict('numeroContratSouscrit', default=None)(self) or Dict('identifiantTechnique')(self)
246 247

            obj_label = Dict('libelle')
Nicolas Gattolin's avatar
Nicolas Gattolin committed
248
            obj_currency = 'EUR'
249
            obj_type = Account.TYPE_LOAN
250 251 252

            def obj_total_amount(self):
                # Json key change depending on loan type, consumer credit or revolving credit
253
                return CleanDecimal(Dict('montantEmprunte', default=None)(self) or Dict('montantUtilise'))(self)
254 255

            # Key not always available, when revolving credit not yet consummed
256
            obj_next_payment_amount = CleanDecimal(Dict('montantProchaineEcheance', default=None), default=NotAvailable)
257

258
            # obj_rate = can't find the info on website except pdf :(
259

260
            # Dates scraped are timestamp, to remove last '000' we divide by 1000
261
            def obj_maturity_date(self):
262
                # Key not always available, when revolving credit not yet consummed
263 264 265 266
                timestamp = Dict('dateFin', default=None)(self)
                if timestamp:
                    return dt.date.fromtimestamp(timestamp/1000)
                return NotAvailable
267 268

            def obj_next_payment_date(self):
269
                # Key not always available, when revolving credit not yet consummed
270 271 272 273
                timestamp = Dict('dateProchaineEcheance', default=None)(self)
                if timestamp:
                    return dt.date.fromtimestamp(timestamp/1000)
                return NotAvailable
274

275
            def obj_balance(self):
Sylvie Ye's avatar
Sylvie Ye committed
276
                return -abs(CleanDecimal().filter(self.el.get('montantRestant', self.el.get('montantUtilise'))))
277 278

            # only for revolving loans
279
            obj_available_amount = CleanDecimal(Dict('montantDisponible', default=None), default=NotAvailable)
280

281

282
class Transaction(FrenchTransaction):
Nicolas Gattolin's avatar
Nicolas Gattolin committed
283 284 285 286 287 288 289 290
    PATTERNS = [(re.compile(r'^CARTE (?P<dd>\d{2})/(?P<mm>\d{2}) (?P<text>.*)'), FrenchTransaction.TYPE_CARD),
                (re.compile(r'^(?P<text>(PRLV|PRELEVEMENTS).*)'), FrenchTransaction.TYPE_ORDER),
                (re.compile(r'^(?P<text>RET DAB.*)'), FrenchTransaction.TYPE_WITHDRAWAL),
                (re.compile(r'^(?P<text>ECH.*)'), FrenchTransaction.TYPE_LOAN_PAYMENT),
                (re.compile(r'^(?P<text>VIR.*)'), FrenchTransaction.TYPE_TRANSFER),
                (re.compile(r'^(?P<text>ANN.*)'), FrenchTransaction.TYPE_PAYBACK),
                (re.compile(r'^(?P<text>(VRST|VERSEMENT).*)'), FrenchTransaction.TYPE_DEPOSIT),
                (re.compile(r'^(?P<text>.*)'), FrenchTransaction.TYPE_BANK)
291 292 293 294
               ]


class HistoryPage(LoggedPage, JsonPage):
295 296 297
    def has_deferred_cards(self):
        return Dict('pendingDeferredDebitCardList/currentMonthCardList', default=None)

298
    def get_keys(self):
299 300 301
        if 'exception' in self.doc:
            return []
        return [k for k, v in self.doc.items() if v and isinstance(v, (dict, list))]
302

303
    @pagination
304 305
    @method
    class iter_history(DictElement):
306 307 308 309 310 311
        def next_page(self):
            if len(Env('nbs', default=[])(self)):
                data = {'index': Env('index')(self),
                        'filtreOperationsComptabilisees': "MOIS_MOINS_%s" % Env('nbs')(self)[0]
                       }
                Env('nbs')(self).pop(0)
312
                return requests.Request('POST', data=json.dumps(data), headers={'Content-Type': 'application/json'})
313

314 315 316
        def parse(self, el):
            # Key only if coming
            key = Env('key', default=None)(self)
317 318 319 320 321 322 323 324 325
            if key:
                if "CardList" in key:
                    self.item_xpath = "%s/currentMonthCardList/*/listeOperations" % key
                elif "futureOperationList" in key:
                    self.item_xpath = "%s/futurePrelevementList" % key
                else:
                    self.item_xpath = "%s/operationList" % key
            else:
                self.item_xpath = "listOperationProxy"
326 327 328 329

        class item(ItemElement):
            klass = Transaction

330 331
            class FromTimestamp(Filter):
                def filter(self, timestamp):
332 333 334 335
                    try:
                        return dt.date.fromtimestamp(int(timestamp[:-3]))
                    except TypeError:
                        return self.default_or_raise(ParseError('Element %r not found' % self.selector))
336

337
            obj_date = FromTimestamp(Dict('dateOperation', default=NotAvailable), default=NotAvailable)
338
            obj_raw = Transaction.Raw(Dict('libelleCourt'))
339
            obj_vdate = Date(Dict('dateValeur', NotAvailable), dayfirst=True, default=NotAvailable)
340 341
            obj_amount = CleanDecimal(Dict('montantEnEuro'), default=NotAvailable)

342
            def parse(self, el):
343 344
                key = Env('key', default=None)(self)
                if key and "DeferredDebit" in key:
345 346 347 348
                    for x in Dict('%s/currentMonthCardList' % key)(self.page.doc):
                        deferred_date = Dict('dateDiffere', default=None)(x)
                        if deferred_date:
                            break
349 350
                    setattr(self.obj, '_deferred_date', self.FromTimestamp().filter(deferred_date))

351
                # Skip duplicate transactions
352
                amount = Dict('montantEnEuro', default=None)(self)
353
                tr = Dict('libelleCourt')(self) + Dict('dateOperation', '')(self) + str(amount)
354
                if amount is None or (tr in self.page.browser.trs['list'] and self.page.browser.trs['lastdate'] <= Field('date')(self)):
355
                    raise SkipItem()
356

357 358 359
                self.page.browser.trs['lastdate'] = Field('date')(self)
                self.page.browser.trs['list'].append(tr)

360 361

class LifeinsurancePage(LoggedPage, HTMLPage):
362
    def get_account_id(self):
Nicolas Gattolin's avatar
Nicolas Gattolin committed
363
        account_id = Regexp(CleanText('//h1[@class="portlet-title"]'), r'n° ([\d\s]+)', default=NotAvailable)(self.doc)
364 365
        if account_id:
            return re.sub(r'\s', '', account_id)
366

367
    def get_link(self, page):
Nicolas Gattolin's avatar
Nicolas Gattolin committed
368
        return Link(default=NotAvailable).filter(self.doc.xpath('//a[contains(text(), "%s")]' % page))
369 370 371 372 373 374 375 376

    @pagination
    @method
    class iter_history(TableElement):
        item_xpath = '//table/tbody/tr[contains(@class, "results")]'
        head_xpath = '//table/thead/tr/th'

        col_date = re.compile('Date')
Nicolas Gattolin's avatar
Nicolas Gattolin committed
377
        col_label = re.compile('Libellé')
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
        col_amount = re.compile('Montant')

        next_page = Link('//a[contains(text(), "Suivant") and not(contains(@href, "javascript"))]', default=None)

        class item(ItemElement):
            klass = Transaction

            obj_raw = Transaction.Raw(TableCell('label'))
            obj_date = Date(CleanText(TableCell('date')), dayfirst=True)
            obj_amount = MyDecimal(TableCell('amount'))

    @method
    class iter_investment(TableElement):
        item_xpath = '//table/tbody/tr[contains(@class, "results")]'
        head_xpath = '//table/thead/tr/th'

Nicolas Gattolin's avatar
Nicolas Gattolin committed
394
        col_label = re.compile('Libellé')
395 396 397 398 399 400 401 402 403 404
        col_quantity = re.compile('Nb parts')
        col_vdate = re.compile('Date VL')
        col_unitvalue = re.compile('VL')
        col_unitprice = re.compile('Prix de revient')
        col_valuation = re.compile('Solde')

        class item(ItemElement):
            klass = Investment

            obj_label = CleanText(TableCell('label'))
Nicolas Gattolin's avatar
Nicolas Gattolin committed
405
            obj_code = Regexp(Link('./td/a'), r'Isin%253D([^%]+)')
406 407 408 409 410 411
            obj_quantity = MyDecimal(TableCell('quantity'))
            obj_unitprice = MyDecimal(TableCell('unitprice'))
            obj_unitvalue = MyDecimal(TableCell('unitvalue'))
            obj_valuation = MyDecimal(TableCell('valuation'))
            obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True, default=NotAvailable)

412 413 414
            def obj_code_type(self):
                return Investment.CODE_TYPE_ISIN if is_isin_valid(Field('code')(self)) else NotAvailable

415 416

class MarketPage(LoggedPage, HTMLPage):
417 418 419
    def find_account(self, acclabel, accowner):
        accowner = sorted(accowner.lower().split()) # first name and last name may not be ordered the same way on market site...

420 421
        # Check if history is present
        if CleanText(default=None).filter(self.doc.xpath('//body/p[contains(text(), "indisponible pour le moment")]')):
Baptiste Delpey's avatar
Baptiste Delpey committed
422
            return False
423

424
        ids = None
425
        for a in self.doc.xpath('//a[contains(@onclick, "indiceCompte")]'):
426
            self.logger.debug("get investment from onclick")
427 428 429 430 431 432

            label = CleanText('.')(a)
            owner = CleanText('./ancestor::tr/preceding-sibling::tr[@class="LnMnTiers"][1]')(a)
            owner = sorted(owner.lower().split())

            if label == acclabel and owner == accowner:
433 434
                ids = list(re.search(r'indiceCompte[^\d]+(\d+).*idRacine[^\d]+(\d+)', Attr('.', 'onclick')(a)).groups())
                ids.append(CleanText('./ancestor::td/preceding-sibling::td')(a))
435
                self.logger.debug("assign value to ids: {}".format(ids))
436
                return ids
437

438 439 440 441 442 443 444 445
        for a in self.doc.xpath('//a[contains(@href, "indiceCompte")]'):
            self.logger.debug("get investment from href")
            if CleanText('.')(a) == acclabel:
                ids = list(re.search(r'indiceCompte[^\d]+(\d+).*idRacine[^\d]+(\d+)', Attr('.', 'href')(a)).groups())
                ids.append(CleanText('./ancestor::td/preceding-sibling::td')(a))
                self.logger.debug("assign value to ids: {}".format(ids))
                return ids

446
    def get_account_id(self, acclabel, owner):
447 448 449
        account = self.find_account(acclabel, owner)
        if account:
            return account[2].replace(' ', '')
450

451 452
    def go_account(self, acclabel, owner):
        ids = self.find_account(acclabel, owner)
453 454 455
        if not ids:
            return

456
        form = self.get_form(name="formCompte")
Baptiste Delpey's avatar
Baptiste Delpey committed
457 458
        form['indiceCompte'] = ids[0]
        form['idRacine'] = ids[1]
459 460 461
        try:
            return form.submit()
        except ServerError:
462
            return False
463

464
    def go_account_full(self):
465 466
        form = self.get_form(name="formOperation")
        form['dateDebut'] = "02/01/1970"
467 468 469 470
        try:
            return form.submit()
        except ServerError:
            return False
471 472 473 474 475 476 477

    @method
    class iter_history(TableElement):
        item_xpath = '//table[has-class("domifrontTb")]/tr[not(has-class("LnTit") or has-class("LnTot"))]'
        head_xpath = '//table[has-class("domifrontTb")]/tr[1]/td'

        col_date = re.compile('Date')
Nicolas Gattolin's avatar
Nicolas Gattolin committed
478 479 480
        col_label = 'Opération'
        col_code = 'Code'
        col_quantity = 'Quantité'
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
        col_amount = re.compile('Montant')

        class item(ItemElement):
            klass = Transaction

            obj_label = CleanText(TableCell('label'))
            obj_type = Transaction.TYPE_BANK
            obj_date = Date(CleanText(TableCell('date')), dayfirst=True)
            obj_amount = CleanDecimal(TableCell('amount'))
            obj_investments = Env('investments')

            def parse(self, el):
                i = Investment()
                i.label = Field('label')(self)
                i.code = CleanText(TableCell('code'))(self)
                i.quantity = MyDecimal(TableCell('quantity'))(self)
                i.valuation = Field('amount')(self)
                i.vdate = Field('date')(self)
                self.env['investments'] = [i]

    @method
    class iter_investment(TableElement):
503 504
        item_xpath = '//table[has-class("domifrontTb")]/tr[not(has-class("LnTit") or has-class("LnTot"))]'
        head_xpath = '//table[has-class("domifrontTb")]/tr[1]/td'
505

Nicolas Gattolin's avatar
Nicolas Gattolin committed
506 507 508 509 510
        col_label = 'Valeur'
        col_code = 'Code'
        col_quantity = 'Qté'
        col_vdate = 'Date cours'
        col_unitvalue = 'Cours'
511
        col_unitprice = re.compile('P.R.U')
Nicolas Gattolin's avatar
Nicolas Gattolin committed
512
        col_valuation = 'Valorisation'
513 514 515 516

        class item(ItemElement):
            klass = Investment

517 518
            condition = lambda self: not CleanText('//div[has-class("errorConteneur")]', default=None)(self.el)

519
            obj_label = Upper(TableCell('label'))
520
            obj_quantity = MyDecimal(TableCell('quantity'))
521 522
            obj_unitprice = MyDecimal(TableCell('unitprice'))
            obj_unitvalue = MyDecimal(TableCell('unitvalue'))
523
            obj_valuation = CleanDecimal(TableCell('valuation'), replace_dots=True)
524 525
            obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True, default=NotAvailable)

526 527 528 529 530 531 532 533 534
            def obj_code(self):
                if Field('label')(self) == "LIQUIDITES":
                    return 'XX-liquidity'
                code = CleanText(TableCell('code'))(self)
                return code if is_isin_valid(code) else NotAvailable

            def obj_code_type(self):
                return Investment.CODE_TYPE_ISIN if is_isin_valid(Field('code')(self)) else NotAvailable

535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551

class AdvisorPage(LoggedPage, JsonPage):
    @method
    class get_advisor(ItemElement):
        klass = Advisor

        obj_name = Dict('nomPrenom')
        obj_email = obj_mobile = NotAvailable

        def obj_phone(self):
            return Dict('numeroTelephone')(self) or NotAvailable

    @method
    class update_agency(ItemElement):
        obj_fax = CleanText(Dict('numeroFax'), replace=[(' ', '')])
        obj_agency = Dict('nom')
        obj_address = Format('%s %s', Dict('adresse1'), Dict('adresse3'))
552 553 554 555


class RecipientsPage(LoggedPage, JsonPage):
    def get_numbers(self):
556 557 558 559
        # If account information is not available when asking for the
        # recipients (server error for ex.), return an empty dictionary
        # that will be filled later after being returned the json of the
        # account page (containing the accounts IDs too).
560
        if 'listCompteTitulaireCotitulaire' not in self.doc and 'exception' in self.doc:
561
            return {}
562

563 564 565
        ret = {}

        ret.update({
566 567
            d['index']: d['numeroContratSouscrit']
            for d in self.doc['listCompteTitulaireCotitulaire']
568 569 570 571 572 573 574 575 576 577 578 579 580
        })
        ret.update({
            d['index']: d['numeroContratSouscrit']
            for p in self.doc['listCompteMandataire'].values()
            for d in p
        })
        ret.update({
            d['index']: d['numeroContratSouscrit']
            for p in self.doc['listCompteLegalRep'].values()
            for d in p
        })

        return ret
581 582 583


class ProfilePage(LoggedPage, JsonPage):
584 585
    # be careful, this page is used in CmsoProBrowser too!

586 587 588 589
    @method
    class get_profile(ItemElement):
        klass = Profile

590 591 592 593
        def obj_id(self):
            return (Dict('identifiantExterne',default=None)(self)
                or Dict('login')(self))

594
        obj_name = Format('%s %s', Dict('firstName'), Dict('lastName'))
595
        obj_email = Dict('email', default=NotAvailable) # can be unavailable on pro website for example