Skip to content
accounts_list.py 18.4 KiB
Newer Older
# -*- coding: utf-8 -*-

Florent's avatar
Florent committed
# Copyright(C) 2009-2014  Florent Fourcot, Romain Bignon
#
# 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/>.

Romain Bignon's avatar
Romain Bignon committed
from datetime import date, timedelta
import datetime
from weboob.capabilities.bank import Account, Investment, Loan
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.profile import Person
from weboob.browser.pages import HTMLPage, LoggedPage, JsonPage
from weboob.browser.elements import ListElement, TableElement, ItemElement, method, DataError
from weboob.browser.filters.standard import (
    CleanText, CleanDecimal, Eval, Filter, Field, MultiFilter, Date,
    Lower, Async, AsyncLoad, Format, Env,
    Regexp,
from weboob.browser.filters.json import Dict
from weboob.browser.filters.html import Attr, Link, TableCell
Romain Bignon's avatar
Romain Bignon committed
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
class Transaction(FrenchTransaction):
    PATTERNS = [(re.compile(u'^retrait dab (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{4}) (?P<text>.*)'), FrenchTransaction.TYPE_WITHDRAWAL),
                (re.compile(u'^carte (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{4}) (?P<text>.*)'), FrenchTransaction.TYPE_CARD),
                (re.compile(u'^virement (sepa )?(emis vers|recu|emis)? (?P<text>.*)'), FrenchTransaction.TYPE_TRANSFER),
                (re.compile(u'^remise cheque(?P<text>.*)'), FrenchTransaction.TYPE_DEPOSIT),
                (re.compile(u'^cheque (?P<text>.*)'), FrenchTransaction.TYPE_CHECK),
                (re.compile(u'^prelevement (?P<text>.*)'), FrenchTransaction.TYPE_ORDER),
Florent's avatar
Florent committed
                (re.compile(u'^prlv sepa (?P<text>.*)'), FrenchTransaction.TYPE_ORDER),
                (re.compile(u'^prélèvement sepa en faveur de (?P<text>.*)'), FrenchTransaction.TYPE_ORDER),
Florent's avatar
Florent committed
class AddPref(MultiFilter):
    prefixes = {u'Courant': u'CC-', u'Livret A': 'LA-', u'Orange': 'LEO-',
Florent's avatar
Florent committed
                u'Durable': u'LDD-', u"Titres": 'TITRE-', u'PEA': u'PEA-'}
Florent's avatar
Florent committed

    def filter(self, values):
        el, label = values
        for key, pref in self.prefixes.items():
            if key in label:
                return pref + el
        return el


class AddType(Filter):
    types = {u'Courant': Account.TYPE_CHECKING,
             u'Livret A': Account.TYPE_SAVINGS,
             u'Orange': Account.TYPE_SAVINGS,
             u'Durable': Account.TYPE_SAVINGS,
             u'Titres': Account.TYPE_MARKET,
             u'PEA': Account.TYPE_PEA,
             u'Direct Vie': Account.TYPE_LIFE_INSURANCE,
Vincent Paredes's avatar
Vincent Paredes committed
             u'Assurance Vie': Account.TYPE_LIFE_INSURANCE,
             u'Crédit Immobilier': Account.TYPE_LOAN,
             u'Prêt Personnel': Account.TYPE_LOAN,
             }
Florent's avatar
Florent committed

    def filter(self, label):
        for key, acc_type in self.types.items():
            if key in label:
                return acc_type
        return Account.TYPE_UNKNOWN


Florent's avatar
Florent committed
class PreHashmd5(MultiFilter):
    def filter(self, values):
        concat = ''
        for value in values:
            if type(value) is datetime.date:
                concat += value.strftime('%d/%m/%Y')
            else:
                concat += u'%s' % value
Florent's avatar
Florent committed
        return concat.encode('utf-8')
Florent's avatar
Florent committed

Florent's avatar
Florent committed
class INGDate(Date):
    monthvalue = {u'janv.': '01', u'févr.': '02', u'mars': '03', u'avr.': '04',
                  u'mai': '05', u'juin': '06', u'juil.': '07', u'août': '08',
Florent's avatar
Florent committed
                  u'sept.': '09', u'oct.': '10', u'nov.': '11', u'déc.': '12'}
Florent's avatar
Florent committed

    def filter(self, txt):
        if txt == 'hier':
            return date.today() - timedelta(days=1)
Florent's avatar
Florent committed
        elif txt == "aujourd'hui":
            return date.today()
        elif txt == 'demain':
            return date.today() + timedelta(days=1)
        frenchmonth = txt.split(' ')[1]
        month = self.monthvalue[frenchmonth]
        txt = txt.replace(' ', '')
        txt = txt.replace(frenchmonth, '/%s/' % month)
        return super(INGDate, self).filter(txt)
Florent's avatar
Florent committed


class INGCategory(Filter):
    catvalue = {u'virt': u"Virement", u'autre': u"Autre",
                u'plvt': u'Prélèvement', u'cb_ret': u"Carte retrait",
                u'cb_ach': u'Carte achat', u'chq': u'Chèque',
                u'frais': u'Frais bancaire', u'sepaplvt': u'Prélèvement'}

    def filter(self, txt):
        txt = txt.split('-')[0].lower()
        try:
            return self.catvalue[txt]
        except:
            return txt

Florent's avatar
Florent committed

class AccountsList(LoggedPage, HTMLPage):
Romain Bignon's avatar
Romain Bignon committed
    i = 0

    def has_error(self):
        return len(self.doc.xpath('//div[has-class("alert-warning")]')) > 0
Florent's avatar
Florent committed

    def has_link(self):
        return len(self.doc.xpath('//a[contains(@href, "goTo")]'))

    def get_card_list(self):
        card_list = []
        card_elements = self.doc.xpath('//div[has-class("ccinc_cards")]/div[has-class("accordion")]')
        for card in card_elements:
            card_properties = {}

            # Regexp parse the text to extract the card number that may be in different formats
            card_properties['number'] = Regexp(CleanText('.'), '(\d+[\s|*]+\d+)', default=NotAvailable)(card)
            debit_info = (CleanText('.//div[@class="debit-info"]', default='')(card))

            is_deferred = u'Débit différé' in debit_info
            is_immediate = u'Débit immédiat' in debit_info

            if is_immediate:
                card_properties['kind'] = self.browser.IMMEDIATE_CB
            elif is_deferred:
                card_properties['kind'] = self.browser.DEFERRED_CB
            else:
                raise DataError("Cannot tell if the card {} is deferred or immediate".format(card_properties['number']))

            card_list.append(card_properties)

        return card_list

Florent's avatar
Florent committed
    @method
    class get_list(ListElement):
        item_xpath = '//div[@id="bloc-menu-comptes"]//a[@class="mainclic"]'
Florent's avatar
Florent committed

        class item(ItemElement):
            klass = Account

            obj_currency = u'EUR'
            obj_label = CleanText('./span[@class="title"]')
Florent's avatar
Florent committed
            obj_id = AddPref(Field('_id'), Field('label'))
            obj_type = AddType(Field('label'))
            obj_coming = NotAvailable
            obj__jid = Attr('//input[@name="javax.faces.ViewState"]', 'value')

            def obj_balance(self):
                balance = CleanDecimal('./span[@class="solde"]/label', replace_dots=True)(self)
                return -abs(balance) if Field('type')(self) == Account.TYPE_LOAN else balance

            def obj__id(self):
                return CleanText('./span[@class="account-number"]')(self) or CleanText('./span[@class="life-insurance-application"]')(self)

    @method
    class get_detailed_loans(ListElement):
        item_xpath = '//div[@class="mainclic"]'

        class item(ItemElement):
            klass = Loan

            obj_currency = u'EUR'
            obj_label = CleanText('./span[@class="title"]')
            obj_id = AddPref(Field('_id'), Field('label'))
            obj_type = AddType(Field('label'))
            obj_coming = NotAvailable
            obj__jid = Attr('//input[@name="javax.faces.ViewState"]', 'value')
            obj__id = CleanText('./span[@class="account-number"]')

            def obj_balance(self):
                balance = CleanDecimal('./span[@class="solde"]/label', replace_dots=True)(self)
                return -abs(balance)
Florent's avatar
Florent committed
    class generic_transactions(ListElement):
        class item(ItemElement):
            klass = Transaction

Romain Bignon's avatar
Romain Bignon committed
            obj_id = None  # will be overwrited by the browser
            # we use lower for compatibility with the old website
Florent's avatar
Florent committed
            obj_amount = CleanDecimal('.//td[starts-with(@class, "amount")]', replace_dots=True)
Florent's avatar
Florent committed
            obj_date = INGDate(CleanText('.//td[@class="date"]'), dayfirst=True)
            obj_rdate = Field('date')
Florent's avatar
Florent committed
            obj__hash = PreHashmd5(Field('date'), Field('raw'), Field('amount'))
Florent's avatar
Florent committed
            obj_category = INGCategory(Attr('.//td[@class="picto"]/span', 'class'))
            def obj_raw(self):
                return Transaction.Raw(Lower('.//td[@class="lbl"]'))(self) or Format('%s %s', Field('date'), Field('amount'))(self)

            def condition(self):
                date_field = self.el.find('.//td[@class="date"]')
                if date_field is None or 'À venir' in CleanText().filter(date_field):
                    return False
                if 'index' in self.env and self.env['index'] > 0 and self.page.i < self.env['index']:
                    self.page.i += 1
                    return False
                return True

    @method
    class iter_asv_investments(ListElement):
        item_xpath = '//div[@id="index:accountdetail"]//div[has-class("asv_fond")]'

        class item(ItemElement):
            klass = Investment

            obj_portfolio_share = Eval(lambda x: x / 100, CleanDecimal('.//dl[@class="ligne-repartition"]/dd', replace_dots=True))
            obj_label = CleanText('.//span[@class="asv_cat_lbl"]')
Florent's avatar
Florent committed
    @method
    class get_coming(generic_transactions):
        item_xpath = '//div[@class="transactions cc future"]//table'

    @method
Florent's avatar
Florent committed
    class get_transactions_cc(generic_transactions):
Florent's avatar
Florent committed
        item_xpath = '//div[@class="temporaryTransactionList"]//table'

Florent's avatar
Florent committed
    @method
    class get_transactions_others(generic_transactions):
        item_xpath = '//table'
Florent's avatar
Florent committed
    def get_history_jid(self):
        span = Attr('//*[starts-with(@id, "index:j_id")]', 'id')(self.doc)
        jid = span.split(':')[1]
Florent's avatar
Florent committed
        return jid

    def get_asv_jid(self):
        return self.doc.xpath('//input[@id="javax.faces.ViewState"]/@value')[0]

    def islast(self):
        havemore = self.doc.getroot().cssselect('.show-more-transactions')
        if len(havemore) == 0:
            return True

        nomore = self.doc.getroot().cssselect('.no-more-transactions')
        return len(nomore) > 0
Romain Bignon's avatar
Romain Bignon committed

    @property
    def is_asv(self):
        span = self.doc.xpath('//span[@id="index:panelASV"]')
        return len(span) > 0

Baptiste Delpey's avatar
Baptiste Delpey committed
    def asv_has_detail(self):
        ap = self.doc.xpath('//a[@id="index:asvInclude:goToAsvPartner"] | //p[contains(text(), "Gestion Libre")]')
        return len(ap) > 0

    @property
    def asv_is_other(self):
        a = self.doc.xpath('//a[@id="index:asvInclude:goToAsvPartner"]')
        return len(a) > 0
        form = self.get_form(name="follow_link")
        form['follow_link:j_idcl'] = "follow_link:goToAsvPartner"
Célande Adrien's avatar
Célande Adrien committed
    def get_multispace(self):
        multispace = []

        for a in self.doc.xpath('//a[contains(@id, "mainMenu")]'):
            space = {}
            name = CleanText('.')(a)
            if 'Vos comptes' not in name:
                space['name'] = name
            else:
                space['name'] = CleanText('//div[@class="print-content"]/h1')(a)
Célande Adrien's avatar
Célande Adrien committed
            space['id'] = Regexp(Attr('.', 'id'), r'mainMenu:(.*)')(a)
            space['form'] = Attr('.', 'onclick')(a)
            space['is_active'] = 'active' in CleanText('./@class')(a)
Célande Adrien's avatar
Célande Adrien committed
            multispace.append(space)
Célande Adrien's avatar
Célande Adrien committed
        return multispace

    def fillup_form(self, form, regexp, string):
        # fill form depending on JS
        link = re.search(regexp, string).group(1)
        parts = link.split(',')
        for p in parts:
            f = p.split("':'")
            form[f[0].replace("'", '')] = f[1].replace("'", '')

Célande Adrien's avatar
Célande Adrien committed
    def change_space(self, space):
        form = self.get_form(id='mainMenu')
Célande Adrien's avatar
Célande Adrien committed
        self.fillup_form(form, r"':\{(.*)\}\s\}", space['form'])
        form['AJAXREQUEST'] = '_viewRoot'
        form.submit()

Célande Adrien's avatar
Célande Adrien committed
    def load_space_page(self):
        # The accounts page exists in two forms: with the spaces list and without
        # When having the spaceless page, a form must be submit to access the space page
        form = self.get_form(id='user-menu')
        on_click = self.doc.xpath('//a[contains(@class, "comptes")]/@onclick')[1]
        self.fillup_form(form, r"\),\{(.*)\},'", on_click)
        form.submit()

class IbanPage(LoggedPage, HTMLPage):
    def get_iban(self):
        iban = CleanText('//tr[td[1]//text()="IBAN"]/td[2]')(self.doc).strip().replace(' ', '')
        if not iban or 'null' in iban:
            return NotAvailable
        return iban


class LoanTokenPage(LoggedPage, HTMLPage):
    def on_load(self):
        form = self.get_form()
        form.submit()


class LoanDetailPage(LoggedPage, JsonPage):
    def getdetails(self, loan):
        loan.total_amount = CleanDecimal(Dict('amount'))(self.doc)
        loan.maturity_date = Date(Dict('loanEndDate'))(self.doc)
        loan.duration = Dict('loanDuration')(self.doc)
        loan.rate = CleanDecimal(Dict('variableInterestRate'))(self.doc) / 100
        loan.nb_payments_left = Dict('remainingMonth')(self.doc)
        loan.last_payment_date = Date(Dict('lastRefundDate'))(self.doc)
        loan.next_payment_date = Date(Dict('nextRefundDate'))(self.doc)
        loan.next_payment_amount = CleanDecimal(Dict('monthlyRefund'))(self.doc)


class TitreDetails(LoggedPage, HTMLPage):
    def submit(self):
        form = self.get_form()
        form.submit()
Baptiste Delpey's avatar
Baptiste Delpey committed
class ASVInvest(LoggedPage, HTMLPage):
    class iter_investments(TableElement):
        # Ignore the first line:
        # <tr>
        #     <td colspan="5" class="enteteTableau metaEnteteTableau enteteTableauFirstCol metaEnteteTableauFirstCol">R&eacute;partition de
        #         l'investissement
        #     </td>
        #     <td colspan="3" class="enteteTableau metaEnteteTableau enteteTableauFirstCol metaEnteteTableauFirstCol">
        #         Plus/moins-values&nbsp;(**)
        #     </td>
        # </tr>
        # Then, there is the line of column heads.
        # Ignore also information lines like that:
        # <tr>
        #       <td colspan="8" class="liTableau" align="left">Gestion Pilotée
        #       <td>
        # </tr>
        item_xpath = '//table[@class="Tableau"]//tr[position()>2 and count(./td) >= 8]'
        head_xpath = '//table[@class="Tableau"]//tr[position()=2]/td'

        col_label = u'Support(s)'
        col_vdate = re.compile('Date')
        col_unitvalue = u'Valeur de part'
        col_quantity = u'Nombre de parts'
        col_valuation = u'Contre-valeur'
        col_unitprice = [u'Prix revient', u'PAM']
        col_diff = u'Montant'
        col_diff_percent = u'%'
        class item(ItemElement):
            klass = Investment

            # Euro funds links are like that:
            # <td class="lpTableau lpTableauFirstCol"><a href="javascript:alert('Les performances de ce fond ne sont pas consultables.')" onclick="">Eurossima
            # </a></td>
            # So ignore them.
            load_details = Link('.//td[1]//a') & Regexp(pattern='^((?!javascript:).*)', default=NotAvailable) & AsyncLoad
                val = Async('details', CleanText('//td[@class="libelle-normal" and contains(.,"CodeISIN")]', default=NotAvailable))(self)
                if val:
                    return val.split('CodeISIN : ')[1] if val else val
                else:
                    return NotAvailable
            def obj_diff_percent(self):
                diff = CleanDecimal(TableCell('diff_percent'), replace_dots=True, default=NotAvailable)(self)
                if not diff:
                    return diff
                return diff / 100

            obj_label = CleanText(TableCell('label'))
            obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True)
            obj_unitvalue = CleanDecimal(TableCell('unitvalue'), replace_dots=True, default=NotAvailable)
            obj_quantity = CleanDecimal(TableCell('quantity'), default=NotAvailable)
            obj_valuation = CleanDecimal(TableCell('valuation'), replace_dots=True)
            obj_unitprice = CleanDecimal(TableCell('unitprice', default=None), replace_dots=True, default=NotAvailable)
            obj_diff = CleanDecimal(TableCell('diff'), replace_dots=True, default=NotAvailable)
class DetailFondsPage(LoggedPage, HTMLPage):


def MyInput(*args, **kwargs):
    args = (u'//input[contains(@name, "%s")]' % args[0], 'value',)
    kwargs.update(default=NotAvailable)
    return Attr(*args, **kwargs)


def MySelect(*args, **kwargs):
    args = (u'//select[contains(@name, "%s")]/option[@selected]' % args[0],)
    kwargs.update(default=NotAvailable)
    return CleanText(*args, **kwargs)


class ProfilePage(LoggedPage, HTMLPage):
    @method
    class get_profile(ItemElement):
        klass = Person

        obj_name = CleanText('//a[has-class("hme-pm")]/span[@title]')
        obj_address = CleanText('//ul[@class="newPostAdress"]//dd[@class="withMessage"]')
        obj_country = CleanText('//dt[label[contains(text(), "Pays")]]/following-sibling::dd')
        obj_email = CleanText('//dt[contains(text(), "Email")]/following-sibling::dd/text()')
        obj_phone = Env('phone')
        obj_mobile = Env('mobile')

        def parse(self, el):
            pattern = '//dt[contains(text(), "%s")]/following-sibling::dd/label'
            phone = CleanText(pattern % "professionnel")(self)
            mobile = CleanText(pattern % "portable")(self)
            self.env['phone'] = phone or mobile
            self.env['mobile'] = mobile

    @method
    class update_profile(ItemElement):
        obj_job = MyInput('category_pro')
        obj_job_contract_type = MySelect('contractType')
        obj_company_name = MyInput('category_empl')
        obj_socioprofessional_category = MySelect('personal_form:csp')

        def obj_job_activity_area(self):
            return MySelect('business_sector')(self) or NotAvailable

        def obj_main_bank(self):
            return MySelect('present_bank')(self) or NotAvailable

        def obj_housing_status(self):
            return MySelect('housingType')(self) or NotAvailable

        def obj_job_start_date(self):
            month = MySelect('seniority_Month')(self)
            year = MySelect('seniority_Year')(self)
            return Date(default=NotAvailable).filter('01/%s/%s' % (month, year)) if month and year else NotAvailable

        def obj_birth_date(self):
            birth_date = self.page.browser.birthday
            return Date().filter("%s/%s/%s" % (birth_date[2:4], birth_date[:2], birth_date[-4:]))