wealth.py 7.84 KB
Newer Older
1 2 3 4
# -*- coding: utf-8 -*-

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

Vincent A's avatar
Vincent A committed
20
from __future__ import unicode_literals
21 22 23

import re

Vincent A's avatar
Vincent A committed
24 25 26
from decimal import Decimal
from weboob.browser.pages import HTMLPage, LoggedPage, JsonPage
from weboob.browser.elements import ListElement, DictElement, ItemElement, method, TableElement
27
from .compat.weboob_browser_filters_standard import (
Vincent A's avatar
Vincent A committed
28
    CleanDecimal, CleanText, Currency, Date, Eval, Field, Lower, MapIn, QueryValue, Regexp,
29
)
30
from weboob.browser.filters.html import Attr, Link, TableCell
Vincent A's avatar
Vincent A committed
31
from weboob.browser.filters.json import Dict
32
from .compat.weboob_capabilities_bank import Account, Investment
33
from weboob.capabilities.profile import Person
34
from weboob.capabilities.base import NotAvailable, NotLoaded
35 36 37
from weboob.tools.capabilities.bank.transactions import FrenchTransaction


Vincent A's avatar
Vincent A committed
38 39
def float_to_decimal(f):
    return Decimal(str(f))
40 41 42 43 44


class AccountsPage(LoggedPage, HTMLPage):
    @method
    class iter_accounts(ListElement):
45
        item_xpath = '//div[contains(@data-route, "/savings/")]'
46 47 48 49

        class item(ItemElement):
            klass = Account

50 51 52 53 54 55 56
            TYPES = {u'assurance vie': Account.TYPE_LIFE_INSURANCE,
                     u'perp': Account.TYPE_PERP,
                     u'epargne retraite agipi pair': Account.TYPE_PERP,
                     u'novial avenir': Account.TYPE_MADELIN,
                     u'epargne retraite novial': Account.TYPE_LIFE_INSURANCE,
                    }

57 58
            condition = lambda self: Field('balance')(self) is not NotAvailable

Vincent A's avatar
Vincent A committed
59
            obj_id = Regexp(CleanText('.//span[has-class("small-title")]'), r'([\d/]+)')
60
            obj_label = CleanText('.//h3[has-class("card-title")]')
Vincent A's avatar
Vincent A committed
61 62
            obj_balance = CleanDecimal.French('.//p[has-class("amount-card")]')
            obj_valuation_diff = CleanDecimal.French('.//p[@class="performance"]', default=NotAvailable)
63 64 65 66 67 68 69 70

            def obj_url(self):
                url = Attr('.', 'data-route')(self)
                # The Assurance Vie xpath recently changed so we must verify that all
                # the accounts now have "/savings/" instead of "/assurances-vie/".
                assert "/savings/" in url
                return url

71
            obj_currency = Currency('.//p[has-class("amount-card")]')
72
            obj__acctype = "investment"
73

74
            obj_type = MapIn(Lower(Field('label')), TYPES, Account.TYPE_UNKNOWN)
75 76


77
class InvestmentPage(LoggedPage, HTMLPage):
78
    @method
79
    class iter_investment(TableElement):
80
        item_xpath = '//table/tbody/tr[td[2]]'
81 82 83
        head_xpath = '//table/thead//th'

        col_label = 'Nom des supports'
84
        col_valuation = re.compile('.*Montant')
85 86
        col_vdate = 'Date de valorisation'
        col_portfolio_share = u'Répartition'
87 88
        col_quantity = re.compile('Nombre de parts')
        col_unitvalue = re.compile('Valeur de la part')
89 90 91 92

        class item(ItemElement):
            klass = Investment

93
            obj_label = CleanText(TableCell('label'))
94
            obj_code = QueryValue(Link('.//a[contains(@href, "isin")]', default=''), 'isin', default=NotAvailable)
95

96
            def valuation(self):
97
                td = TableCell('valuation')(self)[0]
98
                return CleanDecimal('.')(td)
99

100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
            def obj_quantity(self):
                if not self.page.is_detail():
                    return NotAvailable
                td = TableCell('quantity')(self)[0]
                return CleanDecimal('.//span[1]', replace_dots=True)(td)

            def obj_valuation(self):
                if self.obj_original_currency():
                    return NotAvailable
                return self.valuation()

            def obj_original_valuation(self):
                if self.obj_original_currency():
                    return self.valuation()
                return NotLoaded

116 117 118 119 120
            def obj_vdate(self):
                td = TableCell('vdate')(self)[0]
                txt = CleanText('./text()')(td)
                return Date('.', dayfirst=True, default=NotAvailable).filter(txt)

121 122 123 124 125 126 127 128
            def obj_code_type(self):
                lst = self.el.xpath('./th/a')
                if not lst:
                    return NotAvailable
                return Investment.CODE_TYPE_ISIN

            obj_code = Regexp(Link('./th/a', default=''), r'isin=(.{12})$', default=NotAvailable)

129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
            def unitvalue(self):
                return CleanDecimal(TableCell('unitvalue'), replace_dots=True)(self)

            def obj_unitvalue(self):
                if not self.page.is_detail() or self.obj_original_currency():
                    return NotAvailable
                return self.unitvalue()

            def obj_original_unitvalue(self):
                if self.page.is_detail() and self.obj_original_currency():
                    return self.unitvalue()
                return NotLoaded

            def obj_portfolio_share(self):
                if self.page.is_detail():
                    return NotAvailable
                return Eval(lambda x: x / 100, CleanDecimal(TableCell('portfolio_share'), replace_dots=True))(self)

            def obj_original_currency(self):
                cur = Currency(TableCell('valuation'))(self)
                return cur if self.env['currency'] != cur else NotLoaded

    def detailed_view(self):
152
        return Attr(u'//button[contains(text(), "Vision détaillée")]', 'data-url', default=None)(self.doc)
153 154

    def is_detail(self):
155
        return bool(self.doc.xpath(u'//th[contains(text(), "Valeur de la part")]'))
156 157


Vincent A's avatar
Vincent A committed
158
class AccountDetailsPage(LoggedPage, HTMLPage):
159
    def get_account_url(self, url):
Vincent A's avatar
Vincent A committed
160
        return Attr('//a[@href="%s"]' % url, 'data-target')(self.doc)
161 162

    def get_investment_url(self):
163
        return Attr('//div[has-class("card-distribution")]', 'data-url', default=None)(self.doc)
164 165


Vincent A's avatar
Vincent A committed
166 167 168 169 170
class Transaction(FrenchTransaction):
    PATTERNS = [
        (re.compile('^(?P<text>souscription.*)'), FrenchTransaction.TYPE_DEPOSIT),
        (re.compile('^(?P<text>.*)'), FrenchTransaction.TYPE_BANK),
    ]
171 172


Vincent A's avatar
Vincent A committed
173
class HistoryPage(LoggedPage, JsonPage):
174
    @method
Vincent A's avatar
Vincent A committed
175
    class iter_history(DictElement):
176 177 178 179

        class item(ItemElement):
            klass = Transaction

Vincent A's avatar
Vincent A committed
180 181 182
            obj_raw = Transaction.Raw(Dict('label'))
            obj_date = Date(Dict('date'))
            obj_amount = Eval(float_to_decimal, Dict('gross_amount/value'))
183

Vincent A's avatar
Vincent A committed
184 185
            def validate(self, obj):
                return CleanText(Dict('status'))(self) == 'DONE'
186

Vincent A's avatar
Vincent A committed
187 188 189 190 191
    def get_error_code(self):
        # The server returns a list if it worked and a dict in case of error
        if isinstance(self.doc, dict) and 'return' in self.doc:
            return self.doc['return']['error']['code']
        return None
192 193 194 195 196 197 198 199 200 201 202 203 204


class ProfilePage(LoggedPage, HTMLPage):
    def get_profile(self):
        form = self.get_form(xpath='//div[@class="popin-card"]')

        profile = Person()

        profile.name = '%s %s' % (form['party.first_name'], form['party.preferred_last_name'])
        profile.address = '%s %s %s' % (form['mailing_address.street_line'], form['mailing_address.zip_postal_code'], form['mailing_address.locality'])
        profile.email = CleanText('//label[@class="email-editable"]')(self.doc)
        profile.phone = CleanText('//div[@class="info-title colorized phone-disabled"]//label', children=False)(self.doc)
        return profile