add investment to boursorama bank module

This commit is contained in:
smurail 2014-10-23 13:30:02 +02:00
commit 8672a6b443
5 changed files with 164 additions and 12 deletions

View file

@ -20,10 +20,14 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
import re
from collections import defaultdict
from weboob.deprecated.browser import Browser, BrowserIncorrectPassword from weboob.deprecated.browser import Browser, BrowserIncorrectPassword
from weboob.capabilities.bank import Account from weboob.capabilities.bank import Account
from .pages import LoginPage, AccountsList, AccountHistory, CardHistory, UpdateInfoPage, AuthenticationPage from .pages import (LoginPage, AccountsList, AccountHistory, CardHistory, UpdateInfoPage,
AuthenticationPage, AccountInvestment, InvestmentDetail)
__all__ = ['Boursorama'] __all__ = ['Boursorama']
@ -36,7 +40,8 @@ class BrowserIncorrectAuthenticationCode(BrowserIncorrectPassword):
class Boursorama(Browser): class Boursorama(Browser):
DOMAIN = 'www.boursorama.com' DOMAIN = 'www.boursorama.com'
PROTOCOL = 'https' PROTOCOL = 'https'
CERTHASH = ['6bdf8b6dd177bd417ddcb1cfb818ede153288e44115eb269f2ddd458c8461039', 'b290ef629c88f0508e9cc6305421c173bd4291175e3ddedbee05ee666b34c20e'] CERTHASH = ['6bdf8b6dd177bd417ddcb1cfb818ede153288e44115eb269f2ddd458c8461039',
'b290ef629c88f0508e9cc6305421c173bd4291175e3ddedbee05ee666b34c20e']
ENCODING = None # refer to the HTML encoding ENCODING = None # refer to the HTML encoding
PAGES = { PAGES = {
'.*/connexion/securisation/index.phtml': AuthenticationPage, '.*/connexion/securisation/index.phtml': AuthenticationPage,
@ -46,6 +51,8 @@ class Boursorama(Browser):
'.*/comptes/banque/cartes/mouvements.phtml.*': CardHistory, '.*/comptes/banque/cartes/mouvements.phtml.*': CardHistory,
'.*/comptes/epargne/mouvements.phtml.*': AccountHistory, '.*/comptes/epargne/mouvements.phtml.*': AccountHistory,
'.*/date_anniversaire.phtml.*': UpdateInfoPage, '.*/date_anniversaire.phtml.*': UpdateInfoPage,
'.*/detail.phtml.*': AccountInvestment,
'.*/opcvm.phtml.*': InvestmentDetail
} }
def __init__(self, device="weboob", enable_twofactors=False, def __init__(self, device="weboob", enable_twofactors=False,
@ -136,5 +143,29 @@ class Boursorama(Browser):
link = self.page.get_next_url() link = self.page.get_next_url()
def get_investment(self, account):
if account.type != Account.TYPE_MARKET or not account._detail_url:
raise NotImplementedError()
self.location(account._detail_url)
seen = defaultdict(int)
def slugify(label):
label = label.upper().replace('FONDS EN EUROS (', '')[:12]
slug = re.sub(r'[^A-Za-z0-9]', ' ', label).strip()
slug = re.sub(r'\s+', '-', slug)
if label in seen:
counter = str(seen[slug])
slug = slug[:-len(counter)] + counter
seen[label] += 1
return slug
for inv in self.page.get_investment():
if inv._detail_url:
self.location(inv._detail_url)
self.page.get_investment_detail(inv)
if not inv.id:
inv.id = inv.code = 'XX' + slugify(inv.label)
yield inv
def transfer(self, from_id, to_id, amount, reason=None): def transfer(self, from_id, to_id, amount, reason=None):
raise NotImplementedError() raise NotImplementedError()

View file

@ -70,6 +70,11 @@ class BoursoramaModule(Module, CapBank):
for history in self.browser.get_history(account): for history in self.browser.get_history(account):
yield history yield history
def iter_investment(self, account):
with self.browser:
for investment in self.browser.get_investment(account):
yield investment
# TODO # TODO
#def iter_coming(self, account): #def iter_coming(self, account):
# with self.browser: # with self.browser:

View file

@ -23,8 +23,8 @@ from .account_history import AccountHistory
from .card_history import CardHistory from .card_history import CardHistory
from .accounts_list import AccountsList from .accounts_list import AccountsList
from .login import LoginPage, UpdateInfoPage from .login import LoginPage, UpdateInfoPage
from .two_authentication import AuthenticationPage from .two_authentication import AuthenticationPage
from .investment import AccountInvestment, InvestmentDetail
class AccountPrelevement(AccountsList): class AccountPrelevement(AccountsList):
@ -36,4 +36,6 @@ __all__ = ['LoginPage',
'CardHistory', 'CardHistory',
'UpdateInfoPage', 'UpdateInfoPage',
'AuthenticationPage', 'AuthenticationPage',
'AccountInvestment',
'InvestmentDetail',
] ]

View file

@ -33,12 +33,16 @@ class AccountsList(Page):
def get_list(self): def get_list(self):
blocks = self.document.xpath('//div[@id="synthese-list"]//div[@class="block"]') blocks = self.document.xpath('//div[@id="synthese-list"]//div[@class="block"]')
for div in blocks: for div in blocks:
block_title = ''.join(div.xpath('.//span[@class="title"]//text()')).lower()
for tr in div.getiterator('tr'): for tr in div.getiterator('tr'):
account = Account() account = Account()
account.id = None account.id = None
account._link_id = None account._link_id = None
if 'assurance vie' in block_title:
# Life insurance accounts are investments
account.type = Account.TYPE_MARKET
for td in tr.getiterator('td'): for td in tr.getiterator('td'):
if td.attrib.get('class', '') == 'account-cb': if td.get('class', '') == 'account-cb':
try: try:
a = td.xpath('./*/a[@class="gras"]')[0] a = td.xpath('./*/a[@class="gras"]')[0]
except IndexError: except IndexError:
@ -47,11 +51,11 @@ class AccountsList(Page):
account.type = Account.TYPE_CARD account.type = Account.TYPE_CARD
account.label = self.parser.tocleanstring(a) account.label = self.parser.tocleanstring(a)
try: try:
account._link_id = td.xpath('.//a')[0].attrib['href'] account._link_id = td.xpath('.//a')[0].get('href')
except KeyError: except KeyError:
pass pass
elif td.attrib.get('class', '') == 'account-name': elif td.get('class', '') == 'account-name':
try: try:
span = td.xpath('./span[@class="label"]')[0] span = td.xpath('./span[@class="label"]')[0]
except IndexError: except IndexError:
@ -59,23 +63,24 @@ class AccountsList(Page):
break break
account.label = self.parser.tocleanstring(span) account.label = self.parser.tocleanstring(span)
try: try:
account._link_id = td.xpath('.//a')[0].attrib['href'] account._link_id = td.xpath('.//a')[0].get('href')
account._detail_url = account._link_id
except KeyError: except KeyError:
pass pass
elif td.attrib.get('class', '') == 'account-more-actions': elif td.get('class', '') == 'account-more-actions':
for a in td.getiterator('a'): for a in td.getiterator('a'):
# For normal account, two "account-more-actions" # For normal account, two "account-more-actions"
# One for the account, one for the credit card. Take the good one # One for the account, one for the credit card. Take the good one
if "mouvements.phtml" in a.attrib['href'] and "/cartes/" not in a.attrib['href']: if "mouvements.phtml" in a.get('href') and "/cartes/" not in a.get('href'):
account._link_id = a.attrib['href'] account._link_id = a.get('href')
elif td.attrib.get('class', '') == 'account-number': elif td.get('class', '') == 'account-number':
id = td.text id = td.text
id = id.strip(u' \n\t') id = id.strip(u' \n\t')
account.id = id account.id = id
elif td.attrib.get('class', '') == 'account-total': elif td.get('class', '') == 'account-total':
span = td.find('span') span = td.find('span')
if span is None: if span is None:
balance = td.text balance = td.text

View file

@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2014 Ta mère la pute
#
# 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 lxml.etree import XPath
from weboob.deprecated.browser import Page
from weboob.capabilities.bank import Investment
from weboob.browser.filters.standard import CleanDecimal
_el_to_string = XPath('string()')
def el_to_string(el):
return unicode(_el_to_string(el))
class IsinMixin(object):
def get_isin(self, s):
mobj = self._re_isin.search(s)
if mobj:
return mobj.group(1)
class AccountInvestment(IsinMixin, Page):
_re_isin = re.compile(r'isin=(\w+)')
_tr_list = XPath('//div[@id="content-gauche"]//table[@class="list"]/tbody/tr')
_td_list = XPath('./td')
_link = XPath('./td[1]/a/@href')
def get_investment(self):
Decimal = CleanDecimal(replace_dots=True).filter
for tr in self._tr_list(self.document):
cells = list(el_to_string(td) for td in self._td_list(tr))
link = unicode(self._link(tr)[0])
'''
Boursorama table cells
----------------------
0. Fonds
1. Date de valeur
2. Valeur de part
3. Nombre de parts
4. Contre valeur
5. Prix revient
6. +/- value en *
7. +/- value en %*
Investment model
----------------
label = StringField('Label of stocks')
code = StringField('Short code identifier of the stock')
description = StringField('Description of the stock')
quantity = IntField('Quantity of stocks')
unitprice = DecimalField('Buy price of one stock')
unitvalue = DecimalField('Current value of one stock')
valuation = DecimalField('Total current valuation of the Investment')
diff = DecimalField('Difference between the buy cost and the current valuation')
'''
inv = Investment()
isin = self.get_isin(link)
if isin:
inv.id = inv.code = isin
inv.label = cells[0]
inv.quantity = Decimal(cells[3])
inv.valuation = Decimal(cells[4])
inv.unitprice = Decimal(cells[5])
inv.unitvalue = Decimal(cells[2])
inv.diff = Decimal(cells[6])
inv._detail_url = link if '/cours.phtml' in link else None
yield inv
class InvestmentDetail(IsinMixin, Page):
_re_isin = re.compile('(\w+)')
_isin = XPath('//h2[@class and contains(concat(" ", normalize-space(@class), " "), " fv-isin ")]')
_description = XPath('//p[@class="taj"]')
def get_investment_detail(self, inv):
subtitle = el_to_string(self._isin(self.document)[0])
inv.id = inv.code = self.get_isin(subtitle)
inv.description = el_to_string(self._description(self.document)[0]).strip()