# -*- coding: utf-8 -*-
# Copyright(C) 2016 Edouard Lambert
#
# This file is part of a weboob module.
#
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see .
from __future__ import unicode_literals
import re
from decimal import Decimal
from weboob.browser.pages import HTMLPage, LoggedPage, JsonPage
from weboob.browser.elements import ListElement, DictElement, ItemElement, method, TableElement
from .compat.weboob_browser_filters_standard import (
CleanDecimal, CleanText, Currency, Date, Eval, Field, Lower, MapIn, QueryValue, Regexp,
)
from weboob.browser.filters.html import Attr, Link, TableCell
from weboob.browser.filters.json import Dict
from .compat.weboob_capabilities_bank import Account, Investment
from weboob.capabilities.profile import Person
from weboob.capabilities.base import NotAvailable, NotLoaded
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
def float_to_decimal(f):
return Decimal(str(f))
class AccountsPage(LoggedPage, HTMLPage):
@method
class iter_accounts(ListElement):
item_xpath = '//div[contains(@data-route, "/savings/")]'
class item(ItemElement):
klass = Account
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,
}
condition = lambda self: Field('balance')(self) is not NotAvailable
obj_id = Regexp(CleanText('.//span[has-class("small-title")]'), r'([\d/]+)')
obj_label = CleanText('.//h3[has-class("card-title")]')
obj_balance = CleanDecimal.French('.//p[has-class("amount-card")]')
obj_valuation_diff = CleanDecimal.French('.//p[@class="performance"]', default=NotAvailable)
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
obj_currency = Currency('.//p[has-class("amount-card")]')
obj__acctype = "investment"
obj_type = MapIn(Lower(Field('label')), TYPES, Account.TYPE_UNKNOWN)
class InvestmentPage(LoggedPage, HTMLPage):
@method
class iter_investment(TableElement):
item_xpath = '//table/tbody/tr[td[2]]'
head_xpath = '//table/thead//th'
col_label = 'Nom des supports'
col_valuation = re.compile('.*Montant')
col_vdate = 'Date de valorisation'
col_portfolio_share = u'Répartition'
col_quantity = re.compile('Nombre de parts')
col_unitvalue = re.compile('Valeur de la part')
class item(ItemElement):
klass = Investment
obj_label = CleanText(TableCell('label'))
obj_code = QueryValue(Link('.//a[contains(@href, "isin")]', default=''), 'isin', default=NotAvailable)
def valuation(self):
td = TableCell('valuation')(self)[0]
return CleanDecimal('.')(td)
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
def obj_vdate(self):
td = TableCell('vdate')(self)[0]
txt = CleanText('./text()')(td)
return Date('.', dayfirst=True, default=NotAvailable).filter(txt)
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)
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):
return Attr(u'//button[contains(text(), "Vision détaillée")]', 'data-url', default=None)(self.doc)
def is_detail(self):
return bool(self.doc.xpath(u'//th[contains(text(), "Valeur de la part")]'))
class AccountDetailsPage(LoggedPage, HTMLPage):
def get_account_url(self, url):
return Attr('//a[@href="%s"]' % url, 'data-target')(self.doc)
def get_investment_url(self):
return Attr('//div[has-class("card-distribution")]', 'data-url', default=None)(self.doc)
class Transaction(FrenchTransaction):
PATTERNS = [
(re.compile('^(?Psouscription.*)'), FrenchTransaction.TYPE_DEPOSIT),
(re.compile('^(?P.*)'), FrenchTransaction.TYPE_BANK),
]
class HistoryPage(LoggedPage, JsonPage):
@method
class iter_history(DictElement):
class item(ItemElement):
klass = Transaction
obj_raw = Transaction.Raw(Dict('label'))
obj_date = Date(Dict('date'))
obj_amount = Eval(float_to_decimal, Dict('gross_amount/value'))
def validate(self, obj):
return CleanText(Dict('status'))(self) == 'DONE'
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
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