The new woob repository is here: https://gitlab.com/woob/woob. This gitlab will be removed soon.

The new woob repository is here: https://gitlab.com/woob/woob. This gitlab will be removed soon.

pages.py 9.87 KB
Newer Older
Edouard Lambert's avatar
Edouard Lambert committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# -*- 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/>.


import re

from weboob.browser.pages import HTMLPage, LoggedPage
24
from weboob.browser.elements import ItemElement, TableElement, method
25
from weboob.browser.filters.standard import CleanText, Date, Regexp, CleanDecimal, \
26
                                            TableCell, Field, Async, AsyncLoad, Eval
Edouard Lambert's avatar
Edouard Lambert committed
27 28
from weboob.browser.filters.html import Attr, Link
from weboob.capabilities.bank import Account, Investment, Transaction
29
from weboob.capabilities.base import NotAvailable, empty
30
from weboob.exceptions import BrowserUnavailable
Edouard Lambert's avatar
Edouard Lambert committed
31 32 33 34 35 36 37


def MyDecimal(*args, **kwargs):
    kwargs.update(replace_dots=True, default=NotAvailable)
    return CleanDecimal(*args, **kwargs)


38 39 40 41 42
class MaintenancePage(HTMLPage):
   def on_load(self):
        raise BrowserUnavailable(CleanText().filter(self.doc.xpath('//p')))


Edouard Lambert's avatar
Edouard Lambert committed
43 44 45 46 47 48 49 50
class LoginPage(HTMLPage):
    def login(self, login, password):
        form = self.get_form('//form[@id="loginForm"]')
        form['loginForm:name'] = login
        form['loginForm:password'] = password
        form['loginForm:login'] = "loginForm:login"
        form.submit()

51 52 53
    def get_error(self):
        return CleanText('//li[@class="erreurBox"]')(self.doc)

Edouard Lambert's avatar
Edouard Lambert committed
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83

class AccountsPage(LoggedPage, HTMLPage):
    TYPES = {'Assurance Vie': Account.TYPE_LIFE_INSURANCE, 'Unknown': Account.TYPE_UNKNOWN}

    @method
    class iter_accounts(TableElement):
        item_xpath = '//table[@role]/tbody/tr'
        head_xpath = '//table[@role]/thead/tr/th'

        col_label = u'Produit'
        col_id = u'Numéro de contrat'
        col_balance = u'Montant (€)'

        class item(ItemElement):
            klass = Account

            load_details = Field('_link') & AsyncLoad

            obj_id = CleanText(TableCell('id'), replace=[(' ', '')])
            obj_label = CleanText(TableCell('label'))
            obj_balance = MyDecimal(TableCell('balance'))
            obj_valuation_diff = Async('details') & MyDecimal('//tr[1]/td[contains(text(), \
                                    "value du contrat")]/following-sibling::td')
            obj__link = Link('.//a')

            def obj_type(self):
                return self.page.TYPES[Async('details', CleanText('//td[contains(text(), \
                    "Option fiscale")]/following-sibling::td', default="Unknown"))(self)]


84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
class TableInvestment(TableElement):
    col_label = u'Support'
    col_vdate = u'Date de valeur'
    col_unitvalue = u'Valeur de part'
    col_quantity = u'Nombre de parts'
    col_portfolio_share = u'%'


class ItemInvestment(ItemElement):
    klass = Investment

    obj_label = CleanText(TableCell('label'))
    obj_quantity = MyDecimal(TableCell('quantity', default=None))
    obj_unitvalue = MyDecimal(TableCell('unitvalue', default=None))
    obj_vdate = Date(CleanText(TableCell('vdate', default="")), dayfirst=True, default=NotAvailable)

    def obj_valuation(self):
        valuation = MyDecimal(TableCell('valuation', default=None))(self)
        h2 = CleanText('./ancestor::div[contains(@id, "Histo")][1]/preceding-sibling::h2[1]')(self)
        return -valuation if valuation and any(word in h2.lower() for word in self.page.DEBIT_WORDS) else valuation

    def obj_portfolio_share(self):
        ps = MyDecimal(TableCell('portfolio_share', default=None))(self)
107
        return Eval(lambda x: x / 100, ps)(self) if not empty(ps) else NotAvailable
108 109 110 111 112 113 114 115 116 117 118 119 120


class TableTransactionsInvestment(TableInvestment):
    item_xpath = './tbody/tr'
    head_xpath = './thead/tr/th'

    col_code = u'ISIN'
    col_valuation = [u'Montant brut', u'Montant net']

    class item(ItemInvestment):
        obj_code = Regexp(CleanText(TableCell('code')), pattern='([A-Z]{2}\d{10})', default=NotAvailable)


121 122 123 124 125
class ProfileTableInvestment(TableInvestment):
    # used only when portfolio is divided in multiple "profiles"
    head_xpath = '//thead[ends-with(@id, ":contratProfilTable_head")]/tr/th'


Edouard Lambert's avatar
Edouard Lambert committed
126 127 128 129 130 131 132 133 134 135 136 137
class DetailsPage(LoggedPage, HTMLPage):
    DEBIT_WORDS = [u'arrêté', 'rachat', 'frais', u'désinvestir']

    def get_investment_form(self):
        form = self.get_form('//form[contains(@id, "j_idt")]')
        form['ongletSituation:ongletContratTab_newTab'] = \
                Link().filter(self.doc.xpath('//a[contains(text(), "Prix de revient moyen")]'))[1:]
        form['javax.faces.source'] = "ongletSituation:ongletContratTab"
        form['javax.faces.behavior.event'] = "tabChange"
        return form

    @method
138
    class iter_investment(TableInvestment):
139 140
        item_xpath = '//div[contains(@id,"INVESTISSEMENT")]//div[ends-with(@id, ":tableDetailSituationCompte")]//table/tbody/tr[@data-ri]'
        head_xpath = '//div[contains(@id,"INVESTISSEMENT")]//div[ends-with(@id, ":tableDetailSituationCompte")]//table/thead/tr/th'
Edouard Lambert's avatar
Edouard Lambert committed
141 142 143

        col_valuation = re.compile('Contre')

144
        class item(ItemInvestment):
Edouard Lambert's avatar
Edouard Lambert committed
145 146 147 148 149 150 151 152 153 154
            obj_code = Regexp(CleanText('.//td[contains(text(), "Isin")]'), ':[\s]+([\w]+)', default=NotAvailable)

            def obj_unitprice(self):
                return MyDecimal('//div[contains(@id, "PRIX_REVIENT")]//a[contains(text(), \
                            "%s")]/ancestor::tr/td[5]' % Field('label')(self))(self)

            def obj_diff(self):
                return MyDecimal('//div[contains(@id, "PRIX_REVIENT")]//a[contains(text(), \
                            "%s")]/ancestor::tr/td[6]' % Field('label')(self))(self)

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
            def obj_portfolio_share(self):
                inv_share = ItemInvestment.obj_portfolio_share(self)
                if self.xpath('ancestor::tbody[ends-with(@id, "contratProfilTable_data")]'):
                    # investments are nested in profiles, row share is relative to profile share
                    profile_table_el = self.xpath('ancestor::tr/ancestor::table[position() = 1]')[0]
                    profile_table = ProfileTableInvestment(self.page, self, profile_table_el)
                    share_idx = profile_table.get_colnum('portfolio_share')
                    assert share_idx

                    path = 'ancestor::tr/preceding-sibling::tr[@data-ri][position() = 1][1]/td[%d]' % (share_idx + 1)

                    profile_share = MyDecimal(path)(self)
                    assert profile_share
                    #raise Exception('dtc')
                    profile_share = Eval(lambda x: x / 100, profile_share)(self)
                    return inv_share * profile_share
                else:
                    return inv_share

Edouard Lambert's avatar
Edouard Lambert committed
174 175 176 177 178 179 180 181 182 183 184 185
    def get_historytab_form(self):
        form = self.get_form('//form[contains(@id, "j_idt")]')
        idt = Attr(None, 'name').filter(self.doc.xpath('//input[contains(@name, "j_idt") \
                        and contains(@name, "activeIndex")]')).rsplit('_', 1)[0]
        form['%s_contentLoad' % idt] = "true"
        form['%s_newTab' % idt] = Link().filter(self.doc.xpath('//a[contains(@href, "HISTORIQUE")]'))[1:]
        form['%s_activeIndex' % idt] = "1"
        form['javax.faces.source'] = idt
        form['javax.faces.behavior.event'] = "tabChange"
        return form

    def get_historyallpages_form(self):
186 187 188 189 190 191 192
        onclick = self.doc.xpath('//a[contains(text(), "Tout")]/@onclick')
        if onclick:
            idt = re.search('{[^\w]+([\w\d:]+)', onclick[0]).group(1)
            form = self.get_form('//form[contains(@id, "j_idt")]')
            form[idt] = idt
            return form
        return False
Edouard Lambert's avatar
Edouard Lambert committed
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209

    def get_historyexpandall_form(self):
        form = self.get_form('//form[contains(@id, "j_idt")]')
        form['javax.faces.source'] = "ongletHistoOperations:newoperations"
        form['javax.faces.behavior.event'] = "rowToggle"
        form['ongletHistoOperations:newoperations_rowExpansion'] = "true"
        for data in self.doc.xpath('//tr[@data-ri]/@data-ri'):
            form['ongletHistoOperations:newoperations_expandedRowIndex'] = data
            yield form

    @method
    class iter_history(TableElement):
        item_xpath = '//table/tbody[@id and not(contains(@id, "j_idt"))]/tr[@data-ri]'
        head_xpath = '//table/thead[@id and not(contains(@id, "j_idt"))]/tr/th'

        col_label = u'Type'
        col_status = u'Etat'
210 211
        col_brut = [u'Montant brut', u'Brut']
        col_net = [u'Montant net', u'Net']
Edouard Lambert's avatar
Edouard Lambert committed
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
        col_date = u'Date de réception'
        col_vdate = u'Date de valeur'

        class item(ItemElement):
            klass = Transaction

            obj_label = CleanText(TableCell('label'))
            obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True)
            obj_type = Transaction.TYPE_BANK

            def obj_amount(self):
                amount = MyDecimal(TableCell('net') if not CleanText(TableCell('brut'))(self) else TableCell('brut'))(self)
                return -amount if amount and any(word in Field('label')(self).lower() for word in self.page.DEBIT_WORDS) else amount

            def obj_date(self):
                return Date(CleanText(TableCell('date')), dayfirst=True, default=Field('vdate')(self))(self)

            def condition(self):
                return u"Validé" in CleanText(TableCell('status'))(self)

232 233 234
            def obj_investments(self):
                investments = []
                for table in self.el.xpath('./following-sibling::tr[1]//span[contains(text(), "ISIN")]/ancestor::table[1]'):
235 236
                    investments.extend(TableTransactionsInvestment(self.page, el=table)())
                return investments