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 10.3 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
# -*- 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
23
from weboob.browser.elements import ItemElement, TableElement, method
24
from weboob.browser.filters.standard import CleanText, Date, Regexp, CleanDecimal, \
25 26
                                            Field, Async, AsyncLoad, Eval
from weboob.browser.filters.html import Attr, Link, TableCell
Edouard Lambert's avatar
Edouard Lambert committed
27
from weboob.capabilities.bank import Account, Investment, Transaction
28
from weboob.capabilities.base import NotAvailable, empty
29
from weboob.exceptions import BrowserUnavailable
30
from weboob.tools.compat import urljoin
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

class AccountsPage(LoggedPage, HTMLPage):
56 57 58 59 60
    TYPES = {
        'Assurance Vie': Account.TYPE_LIFE_INSURANCE,
        'Capitalisation': Account.TYPE_MARKET,
        'Unknown': Account.TYPE_UNKNOWN,
    }
Edouard Lambert's avatar
Edouard Lambert committed
61 62 63 64 65 66 67 68 69 70 71 72 73

    @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

74
            load_details = Field('url') & AsyncLoad
Edouard Lambert's avatar
Edouard Lambert committed
75 76 77 78 79 80

            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')
81 82 83

            def obj_url(self):
                return urljoin(self.page.url, Link('.//a')(self))
Edouard Lambert's avatar
Edouard Lambert committed
84 85 86 87 88 89

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


90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
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)
113
        return Eval(lambda x: x / 100, ps)(self) if not empty(ps) else NotAvailable
114 115 116 117 118 119 120 121 122 123 124 125 126


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)


127 128 129 130 131
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
132 133 134 135 136 137 138 139 140 141 142 143
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
144
    class iter_investment(TableInvestment):
145 146
        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
147 148 149

        col_valuation = re.compile('Contre')

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

153 154 155 156 157 158 159
            def invest_link(self):
                label = Field('label')(self)
                for a in self.el.xpath('//div[contains(@id, "PRIX_REVIENT")]//a'):
                    if label in CleanText('.')(a):
                        return a
                assert 'fonds euro' in label.lower()

Edouard Lambert's avatar
Edouard Lambert committed
160
            def obj_unitprice(self):
161 162 163
                link = self.invest_link()
                if link:
                    return MyDecimal('./ancestor::tr/td[5]')(link)
Edouard Lambert's avatar
Edouard Lambert committed
164 165

            def obj_diff(self):
166 167 168
                link = self.invest_link()
                if link:
                    return MyDecimal('./ancestor::tr/td[6]')(link)
Edouard Lambert's avatar
Edouard Lambert committed
169

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
            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
189 190 191 192 193 194 195 196 197 198 199 200
    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):
201 202 203 204 205 206 207
        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
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224

    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'
225 226
        col_brut = [u'Montant brut', u'Brut']
        col_net = [u'Montant net', u'Net']
Edouard Lambert's avatar
Edouard Lambert committed
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
        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):
245
                return u"Validé" in CleanText(TableCell('status'))(self) and u"Arrêté annuel" not in Field('label')(self)
Edouard Lambert's avatar
Edouard Lambert committed
246

247 248 249
            def obj_investments(self):
                investments = []
                for table in self.el.xpath('./following-sibling::tr[1]//span[contains(text(), "ISIN")]/ancestor::table[1]'):
250 251
                    investments.extend(TableTransactionsInvestment(self.page, el=table)())
                return investments