pax_global_header 0000666 0000000 0000000 00000000064 13434604312 0014512 g ustar 00root root 0000000 0000000 52 comment=a5f04d414183a4592547763b947b57b4cbc00678
woob-a5f04d414183a4592547763b947b57b4cbc00678-modules-boursorama/ 0000775 0000000 0000000 00000000000 13434604312 0022724 5 ustar 00root root 0000000 0000000 woob-a5f04d414183a4592547763b947b57b4cbc00678-modules-boursorama/modules/ 0000775 0000000 0000000 00000000000 13434604312 0024374 5 ustar 00root root 0000000 0000000 woob-a5f04d414183a4592547763b947b57b4cbc00678-modules-boursorama/modules/boursorama/ 0000775 0000000 0000000 00000000000 13434604312 0026546 5 ustar 00root root 0000000 0000000 woob-a5f04d414183a4592547763b947b57b4cbc00678-modules-boursorama/modules/boursorama/__init__.py 0000664 0000000 0000000 00000001575 13434604312 0030667 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Gabriel Kerneis
# Copyright(C) 2010-2011 Jocelyn Jaubert
#
# 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 Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this weboob module. If not, see .
from .module import BoursoramaModule
__all__ = ['BoursoramaModule']
woob-a5f04d414183a4592547763b947b57b4cbc00678-modules-boursorama/modules/boursorama/browser.py 0000664 0000000 0000000 00000054273 13434604312 0030616 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Baptiste Delpey
#
# 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 Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this weboob module. If not, see .
import requests
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
from dateutil import parser
from weboob.browser.retry import login_method, retry_on_logout, RetryLoginBrowser
from weboob.browser.browsers import need_login, StatesMixin
from weboob.browser.url import URL
from weboob.exceptions import BrowserIncorrectPassword, BrowserHTTPNotFound, NoAccountsException, BrowserUnavailable
from weboob.browser.exceptions import LoggedOut, ClientError
from weboob.capabilities.bank import (
Account, AccountNotFound, TransferError, TransferInvalidAmount,
TransferInvalidEmitter, TransferInvalidLabel, TransferInvalidRecipient,
AddRecipientStep, Recipient, Rate
)
from weboob.capabilities.contact import Advisor
from weboob.tools.captcha.virtkeyboard import VirtKeyboardError
from weboob.tools.value import Value
from weboob.tools.compat import basestring, urlsplit
from weboob.tools.capabilities.bank.transactions import sorted_transactions
from .pages import (
LoginPage, VirtKeyboardPage, AccountsPage, AsvPage, HistoryPage, AuthenticationPage,
MarketPage, LoanPage, SavingMarketPage, ErrorPage, IncidentPage, IbanPage, ProfilePage, ExpertPage,
CardsNumberPage, CalendarPage, HomePage, PEPPage,
TransferAccounts, TransferRecipients, TransferCharac, TransferConfirm, TransferSent,
AddRecipientPage, StatusPage, CardHistoryPage, CardCalendarPage, CurrencyListPage, CurrencyConvertPage,
AccountsErrorPage, NoAccountPage, TransferMainPage,
)
__all__ = ['BoursoramaBrowser']
class BrowserIncorrectAuthenticationCode(BrowserIncorrectPassword):
pass
class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
BASEURL = 'https://clients.boursorama.com'
TIMEOUT = 60.0
STATE_DURATION = 10
home = URL('/$', HomePage)
keyboard = URL('/connexion/clavier-virtuel\?_hinclude=300000', VirtKeyboardPage)
status = URL(r'/aide/messages/dashboard\?showza=0&_hinclude=1', StatusPage)
calendar = URL('/compte/cav/.*/calendrier', CalendarPage)
card_calendar = URL('https://api.boursorama.com/services/api/files/download.phtml.*', CardCalendarPage)
error = URL('/connexion/compte-verrouille',
'/infos-profil', ErrorPage)
login = URL('/connexion/', LoginPage)
accounts = URL('/dashboard/comptes\?_hinclude=300000', AccountsPage)
accounts_error = URL('/dashboard/comptes\?_hinclude=300000', AccountsErrorPage)
pro_accounts = URL(r'/dashboard/comptes-professionnels\?_hinclude=1', AccountsPage)
no_account = URL('/dashboard/comptes\?_hinclude=300000',
'/dashboard/comptes-professionnels\?_hinclude=1', NoAccountPage)
history = URL('/compte/(cav|epargne)/(?P.*)/mouvements.*', HistoryPage)
card_transactions = URL('/compte/cav/(?P.*)/carte/.*', HistoryPage)
deffered_card_history = URL('https://api.boursorama.com/services/api/files/download.phtml.*', CardHistoryPage)
budget_transactions = URL('/budget/compte/(?P.*)/mouvements.*', HistoryPage)
other_transactions = URL('/compte/cav/(?P.*)/mouvements.*', HistoryPage)
saving_transactions = URL('/compte/epargne/csl/(?P.*)/mouvements.*', HistoryPage)
saving_pep = URL('/compte/epargne/pep', PEPPage)
incident = URL('/compte/cav/(?P.*)/mes-incidents.*', IncidentPage)
# transfer
transfer_main_page = URL(r'/compte/(?P[^/]+)/(?P\w+)/virements$', TransferMainPage)
transfer_accounts = URL(r'/compte/(?P[^/]+)/(?P\w+)/virements/nouveau$',
r'/compte/(?P[^/]+)/(?P\w+)/virements/nouveau/(?P\w+)/1', TransferAccounts)
recipients_page = URL(r'/compte/(?P[^/]+)/(?P\w+)/virements$',
r'/compte/(?P[^/]+)/(?P\w+)/virements/nouveau/(?P\w+)/2',
TransferRecipients)
transfer_charac = URL(r'/compte/(?P[^/]+)/(?P\w+)/virements/nouveau/(?P\w+)/3',
TransferCharac)
transfer_confirm = URL(r'/compte/(?P[^/]+)/(?P\w+)/virements/nouveau/(?P\w+)/4',
TransferConfirm)
transfer_sent = URL(r'/compte/(?P[^/]+)/(?P\w+)/virements/nouveau/(?P\w+)/5',
TransferSent)
rcpt_page = URL(r'/compte/(?P[^/]+)/(?P\w+)/virements/comptes-externes/nouveau/(?P\w+)/\d',
AddRecipientPage)
asv = URL('/compte/assurance-vie/.*', AsvPage)
saving_history = URL('/compte/cefp/.*/(positions|mouvements)',
'/compte/.*ord/.*/mouvements',
'/compte/pea/.*/mouvements',
'/compte/0%25pea/.*/mouvements',
'/compte/pea-pme/.*/mouvements', SavingMarketPage)
market = URL('/compte/(?!assurance|cav|epargne).*/(positions|mouvements)',
'/compte/ord/.*/positions', MarketPage)
loans = URL('/credit/immobilier/.*/informations',
'/credit/immobilier/.*/caracteristiques',
'/credit/consommation/.*/informations',
'/credit/lombard/.*/caracteristiques', LoanPage)
authentication = URL('/securisation', AuthenticationPage)
iban = URL('/compte/(?P.*)/rib', IbanPage)
profile = URL('/mon-profil/', ProfilePage)
expert = URL('/compte/derive/', ExpertPage)
cards = URL('/compte/cav/cb', CardsNumberPage)
currencylist = URL('https://www.boursorama.com/bourse/devises/parite/_detail-parite', CurrencyListPage)
currencyconvert = URL('https://www.boursorama.com/bourse/devises/convertisseur-devises/convertir', CurrencyConvertPage)
__states__ = ('auth_token',)
def __init__(self, config=None, *args, **kwargs):
self.config = config
self.auth_token = None
self.accounts_list = None
self.cards_list = None
self.deferred_card_calendar = None
kwargs['username'] = self.config['login'].get()
kwargs['password'] = self.config['password'].get()
super(BoursoramaBrowser, self).__init__(*args, **kwargs)
def locate_browser(self, state):
try:
self.location(state['url'])
except (requests.exceptions.HTTPError, requests.exceptions.TooManyRedirects, LoggedOut):
pass
def load_state(self, state):
if ('expire' in state and parser.parse(state['expire']) > datetime.now()) or state.get('auth_token'):
return super(BoursoramaBrowser, self).load_state(state)
def handle_authentication(self):
if self.authentication.is_here():
if self.config['enable_twofactors'].get():
self.page.sms_first_step()
self.page.sms_second_step()
else:
raise BrowserIncorrectAuthenticationCode(
"""Boursorama - activate the two factor authentication in boursorama config."""
""" You will receive SMS code but are limited in request per day (around 15)"""
)
@login_method
def do_login(self):
assert isinstance(self.config['device'].get(), basestring)
assert isinstance(self.config['enable_twofactors'].get(), bool)
if not self.password.isalnum():
raise BrowserIncorrectPassword()
if self.auth_token and self.config['pin_code'].get():
self.page.authenticate()
else:
for _ in range(3):
self.login.go()
try:
self.page.login(self.username, self.password)
except VirtKeyboardError:
self.logger.error('Failed to process VirtualKeyboard')
else:
break
else:
raise VirtKeyboardError()
if self.login.is_here() or self.error.is_here():
raise BrowserIncorrectPassword()
# After login, we might be redirected to the two factor authentication page.
self.handle_authentication()
if self.authentication.is_here():
raise BrowserIncorrectAuthenticationCode('Invalid PIN code')
def go_cards_number(self, link):
self.location(link)
self.location(self.page.get_cards_number_link())
@retry_on_logout()
@need_login
def get_accounts_list(self):
self.status.go()
exc = None
for x in range(3):
if self.accounts_list is not None:
break
self.accounts_list = []
self.loans_list = []
# Check that there is at least one account for this user
has_account = False
self.pro_accounts.go()
if self.pro_accounts.is_here():
self.accounts_list.extend(self.page.iter_accounts())
has_account = True
else:
# We dont want to let has_account=False if we landed on an unknown page
# it has to be the no_accounts page
assert self.no_account.is_here()
try:
self.accounts.go()
except BrowserUnavailable as e:
self.logger.warning('par accounts seem unavailable, retrying')
exc = e
self.accounts_list = None
continue
else:
if self.accounts.is_here():
self.accounts_list.extend(self.page.iter_accounts())
has_account = True
else:
# We dont want to let has_account=False if we landed on an unknown page
# it has to be the no_accounts page
assert self.no_account.is_here()
exc = None
if not has_account:
# if we landed twice on NoAccountPage, it means there is neither pro accounts nor pp accounts
raise NoAccountsException()
for account in list(self.accounts_list):
if account.type == Account.TYPE_LOAN:
# Loans details are present on another page so we create
# a Loan object and remove the corresponding Account:
self.location(account.url)
loan = self.page.get_loan()
loan.url = account.url
self.loans_list.append(loan)
self.accounts_list.remove(account)
self.accounts_list.extend(self.loans_list)
self.cards_list = [acc for acc in self.accounts_list if acc.type == Account.TYPE_CARD]
if self.cards_list:
self.go_cards_number(self.cards_list[0].url)
if self.cards.is_here():
self.page.populate_cards_number(self.cards_list)
# Cards without a number are not activated yet:
for card in self.cards_list:
if not card.number:
self.accounts_list.remove(card)
for account in self.accounts_list:
if account.type not in (Account.TYPE_CARD, Account.TYPE_LOAN, Account.TYPE_CONSUMER_CREDIT, Account.TYPE_MORTGAGE, Account.TYPE_REVOLVING_CREDIT, Account.TYPE_LIFE_INSURANCE):
account.iban = self.iban.go(webid=account._webid).get_iban()
for card in self.cards_list:
checking, = [account for account in self.accounts_list if account.type == Account.TYPE_CHECKING and account.url in card.url]
card.parent = checking
if exc:
raise exc
return self.accounts_list
def get_account(self, id):
assert isinstance(id, basestring)
for a in self.get_accounts_list():
if a.id == id:
return a
return None
def get_debit_date(self, debit_date):
for i, j in zip(self.deferred_card_calendar, self.deferred_card_calendar[1:]):
if i[0] < debit_date <= j[0]:
return j[1]
@retry_on_logout()
@need_login
def get_history(self, account, coming=False):
if account.type in (Account.TYPE_LOAN, Account.TYPE_CONSUMER_CREDIT) or '/compte/derive' in account.url:
return []
if account.type is Account.TYPE_SAVINGS and u"PLAN D'ÉPARGNE POPULAIRE" in account.label:
return []
if account.type in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_MARKET):
return self.get_invest_transactions(account, coming)
elif account.type == Account.TYPE_CARD:
return self.get_card_transactions(account, coming)
return self.get_regular_transactions(account, coming)
def get_regular_transactions(self, account, coming):
# We look for 3 years of history.
params = {}
params['movementSearch[toDate]'] = (date.today() + relativedelta(days=40)).strftime('%d/%m/%Y')
params['movementSearch[fromDate]'] = (date.today() - relativedelta(years=3)).strftime('%d/%m/%Y')
params['movementSearch[selectedAccounts][]'] = account._webid
self.location('%s/mouvements' % account.url.rstrip('/'), params=params)
for t in self.page.iter_history():
yield t
if coming and account.type == Account.TYPE_CHECKING:
self.location('%s/mouvements-a-venir' % account.url.rstrip('/'), params=params)
for t in self.page.iter_history(coming=True):
yield t
def get_card_transactions(self, account, coming):
# All card transactions can be found in the CSV (history and coming),
# however the CSV shows a maximum of 1000 transactions from all accounts.
self.location(account.url)
if self.home.is_here():
# for some cards, the site redirects us to '/'...
return
if self.deferred_card_calendar is None:
self.location(self.page.get_calendar_link())
params = {}
params['movementSearch[fromDate]'] = (date.today() - relativedelta(years=3)).strftime('%d/%m/%Y')
params['fullSearch'] = 1
self.location(account.url, params=params)
csv_link = self.page.get_csv_link()
if csv_link:
self.location(csv_link)
# Yield past transactions as 'history' and
# transactions in the future as 'coming':
for tr in sorted_transactions(self.page.iter_history(account_number=account.number)):
if coming and tr.date > date.today():
tr._is_coming = True
yield tr
elif not coming and tr.date < date.today():
yield tr
def get_invest_transactions(self, account, coming):
if coming:
return
transactions = []
self.location('%s/mouvements' % account.url.rstrip('/'))
account._history_pages = []
for t in self.page.iter_history(account=account):
transactions.append(t)
for t in self.page.get_transactions_from_detail(account):
transactions.append(t)
for t in sorted(transactions, key=lambda tr: tr.date, reverse=True):
yield t
@need_login
def get_investment(self, account):
if '/compte/derive' in account.url:
return iter([])
if not account.type in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_MARKET, Account.TYPE_PEA):
raise NotImplementedError()
self.location(account.url)
# We might deconnect at this point.
if self.login.is_here():
return self.get_investment(account)
return self.page.iter_investment()
@need_login
def get_profile(self):
return self.profile.stay_or_go().get_profile()
@need_login
def get_advisor(self):
# same for everyone
advisor = Advisor()
advisor.name = u"Service clientèle"
advisor.phone = u"0146094949"
return iter([advisor])
@need_login
def iter_transfer_recipients(self, account):
if account.type in (Account.TYPE_LOAN, Account.TYPE_LIFE_INSURANCE):
return []
assert account.url
# url transfer preparation
url = urlsplit(account.url)
parts = [part for part in url.path.split('/') if part]
assert len(parts) > 2, 'Account url missing some important part to iter recipient'
account_type = parts[1] # cav, ord, epargne ...
account_webid = parts[-1]
try:
self.transfer_main_page.go(acc_type=account_type, webid=account_webid)
except BrowserHTTPNotFound:
return []
# can check all account available transfer option
if self.transfer_main_page.is_here():
self.transfer_accounts.go(acc_type=account_type, webid=account_webid)
if self.transfer_accounts.is_here():
try:
self.page.submit_account(account.id)
except AccountNotFound:
return []
assert self.recipients_page.is_here()
return self.page.iter_recipients()
def check_basic_transfer(self, transfer):
if transfer.amount <= 0:
raise TransferInvalidAmount('transfer amount must be positive')
if transfer.recipient_id == transfer.account_id:
raise TransferInvalidRecipient('recipient must be different from emitter')
if not transfer.label:
raise TransferInvalidLabel('transfer label cannot be empty')
@need_login
def init_transfer(self, transfer, **kwargs):
self.check_basic_transfer(transfer)
account = self.get_account(transfer.account_id)
if not account:
raise AccountNotFound()
recipients = list(self.iter_transfer_recipients(account))
if not recipients:
raise TransferInvalidEmitter('The account cannot emit transfers')
recipients = [rcpt for rcpt in recipients if rcpt.id == transfer.recipient_id]
if len(recipients) == 0:
raise TransferInvalidRecipient('The recipient cannot be used with the emitter account')
assert len(recipients) == 1
self.page.submit_recipient(recipients[0]._tempid)
assert self.transfer_charac.is_here()
self.page.submit_info(transfer.amount, transfer.label, transfer.exec_date)
assert self.transfer_confirm.is_here()
if self.page.need_refresh():
# In some case we are not yet in the transfer_charac page, you need to refresh the page
self.location(self.url)
assert not self.page.need_refresh()
ret = self.page.get_transfer()
# at this stage, the site doesn't show the real ids/ibans, we can only guess
if recipients[0].label != ret.recipient_label:
if not recipients[0].label.startswith('%s - ' % ret.recipient_label):
# the label displayed here is just ""
# but in the recipients list it is " - "...
raise TransferError('Recipient label changed during transfer')
ret.recipient_id = recipients[0].id
ret.recipient_iban = recipients[0].iban
if account.label != ret.account_label:
raise TransferError('Account label changed during transfer')
ret.account_id = account.id
ret.account_iban = account.iban
return ret
@need_login
def execute_transfer(self, transfer, **kwargs):
assert self.transfer_confirm.is_here()
self.page.submit()
assert self.transfer_sent.is_here()
# the last page contains no info, return the last transfer object from init_transfer
return transfer
def build_recipient(self, recipient):
r = Recipient()
r.iban = recipient.iban
r.id = recipient.iban
r.label = recipient.label
r.category = recipient.category
r.enabled_at = date.today()
r.currency = u'EUR'
r.bank_name = recipient.bank_name
return r
@need_login
def new_recipient(self, recipient, **kwargs):
if 'code' in kwargs:
assert self.rcpt_page.is_here()
assert self.page.is_confirm_sms()
self.page.confirm_sms(kwargs['code'])
return self.rcpt_after_sms()
account = None
for account in self.get_accounts_list():
if account.url:
break
suffix = 'virements/comptes-externes/nouveau'
if account.url.endswith('/'):
target = account.url + suffix
else:
target = account.url + '/' + suffix
self.location(target)
assert self.page.is_charac()
self.page.submit_recipient(recipient)
if self.page.is_send_sms():
self.page.send_sms()
assert self.page.is_confirm_sms()
raise AddRecipientStep(self.build_recipient(recipient), Value('code', label='Veuillez saisir le code'))
# if the add recipient is restarted after the sms has been confirmed recently, the sms step is not presented again
return self.rcpt_after_sms()
def rcpt_after_sms(self):
assert self.page.is_confirm()
ret = self.page.get_recipient()
self.page.confirm()
assert self.page.is_created()
return ret
def iter_currencies(self):
return self.currencylist.go().get_currency_list()
def get_rate(self, curr_from, curr_to):
r = Rate()
params = {
'from': curr_from,
'to': curr_to,
'amount': '1'
}
r.currency_from = curr_from
r.currency_to = curr_to
r.datetime = datetime.now()
try:
self.currencyconvert.go(params=params)
r.value = self.page.get_rate()
# if a rate is no available the site return a 401 error...
except ClientError:
return
return r
woob-a5f04d414183a4592547763b947b57b4cbc00678-modules-boursorama/modules/boursorama/favicon.png 0000664 0000000 0000000 00000003227 13434604312 0030705 0 ustar 00root root 0000000 0000000 PNG
IHDR @ @ iq sRGB bKGD VKV pHYs tIMEDSS tEXtComment Created with GIMPW IDATx[lU]nCP.!AAD1^tD}1xK(a44*IF%SPPhׇ6Ιefɶ={|绝YȔ{ӱ ,VV`0h`8dBfcʆ
V`*0^u|(t:mJcXAI&
*m$ww4 t)
(p(Ux9,F`p{}5JAhPGo]_]Cg?r,,$
N r_L)!ϒkH=G^>Dz
nT/pmsv
w$u~Xv[[ oAA00:uvm°,Hd9T|xyK SGv!Sq
@u6hh4gE@f+
c±@vCmձ~dEA78TV_ eIF`pDުcEricRvv pBB=+[QNb4!59ݜjfqB'@Uw
_xݮ`uJ7oK0~KkR$H32;3)ۀ;(KxꕤXG]XxqzUg*T X2
ep~oh
3
_<^$0%y=a%aaє/nIO*Y`P jPK܊RJqEdȴ"*0D@!
|~M.Y^U{]oZ4KnP"qg(SQ $B+`X1w5si[MHOE)ΧR# o9Qsi@
Bv~X.{wH~4ciX1]7=sL;Փ(yH<&\ }hfJؗy Xc'ޒ<7"E^"y@r:%1O2RXKZS"`f)=bO'vX i`nغ1o 4"ZC5w,j}1K
Q鬧U;-e%Fપ|y1[O1v 3(ʮj#r(l2EI1~)U-Y )o#~jv`+D-. ]5V~,psREr_CkMQ2Jm5V1o ؘ)Ws2L^cݪ-7U[+gҮJMg_(!No:v{zDz( 7H+p~_mnn.# x/ IENDB` woob-a5f04d414183a4592547763b947b57b4cbc00678-modules-boursorama/modules/boursorama/module.py 0000664 0000000 0000000 00000010170 13434604312 0030404 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Gabriel Serme
# Copyright(C) 2011 Gabriel Kerneis
# Copyright(C) 2010-2011 Jocelyn Jaubert
#
# 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 Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this weboob module. If not, see .
from __future__ import unicode_literals
import re
from weboob.capabilities.bank import CapBankWealth, CapBankTransferAddRecipient, Account, AccountNotFound, CapCurrencyRate
from weboob.capabilities.profile import CapProfile
from weboob.capabilities.contact import CapContact
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword, ValueBool, Value
from .browser import BoursoramaBrowser
__all__ = ['BoursoramaModule']
class BoursoramaModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapProfile, CapContact, CapCurrencyRate):
NAME = 'boursorama'
MAINTAINER = u'Gabriel Kerneis'
EMAIL = 'gabriel@kerneis.info'
VERSION = '1.5'
LICENSE = 'AGPLv3+'
DESCRIPTION = u'Boursorama'
CONFIG = BackendConfig(ValueBackendPassword('login', label='Identifiant', masked=False),
ValueBackendPassword('password', label='Mot de passe'),
ValueBool('enable_twofactors', label='Send validation sms', default=False),
Value('device', label='Device name', regexp='\w*', default='weboob'),
Value('pin_code', label='Sms code', required=False, default=''),
)
BROWSER = BoursoramaBrowser
def create_default_browser(self):
return self.create_browser(self.config)
def iter_accounts(self):
return self.browser.get_accounts_list()
def get_account(self, _id):
account = self.browser.get_account(_id)
if account:
return account
else:
raise AccountNotFound()
def iter_history(self, account):
for tr in self.browser.get_history(account):
if not tr._is_coming:
yield tr
def iter_coming(self, account):
for tr in self.browser.get_history(account, coming=True):
if tr._is_coming:
yield tr
def iter_investment(self, account):
return self.browser.get_investment(account)
def get_profile(self):
return self.browser.get_profile()
def iter_contacts(self):
return self.browser.get_advisor()
def iter_transfer_recipients(self, account):
if not isinstance(account, Account):
account = self.get_account(account)
return self.browser.iter_transfer_recipients(account)
def init_transfer(self, transfer, **kwargs):
return self.browser.init_transfer(transfer, **kwargs)
def new_recipient(self, recipient, **kwargs):
return self.browser.new_recipient(recipient, **kwargs)
def execute_transfer(self, transfer, **kwargs):
return self.browser.execute_transfer(transfer, **kwargs)
def transfer_check_label(self, old, new):
# In the confirm page the '<' is interpeted like a html tag
# If no '>' is present the following chars are deleted
# Else: inside '<>' chars are deleted
old = re.sub(r'<[^>]*>', '', old).strip()
old = old.split('<')[0]
return super(BoursoramaModule, self).transfer_check_label(old, new)
def iter_currencies(self):
return self.browser.iter_currencies()
def get_rate(self, currency_from, currency_to):
return self.browser.get_rate(currency_from, currency_to)
woob-a5f04d414183a4592547763b947b57b4cbc00678-modules-boursorama/modules/boursorama/pages.py 0000664 0000000 0000000 00000131335 13434604312 0030225 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2016 Baptiste Delpey
#
# 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 Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this weboob module. If not, see .
from __future__ import unicode_literals
from base64 import b64decode
import datetime
from decimal import Decimal
import re
from io import BytesIO
from datetime import date
from weboob.browser.pages import HTMLPage, LoggedPage, pagination, NextPage, FormNotFound, PartialHTMLPage, LoginPage, CsvPage, RawPage, JsonPage
from weboob.browser.elements import ListElement, ItemElement, method, TableElement, SkipItem, DictElement
from weboob.browser.filters.standard import (
CleanText, CleanDecimal, Field, Format,
Regexp, Date, AsyncLoad, Async, Eval, Env,
Currency as CleanCurrency, Map,
)
from weboob.browser.filters.json import Dict
from weboob.browser.filters.html import Attr, Link, TableCell
from weboob.capabilities.bank import (
Account, Investment, Recipient, Transfer, AccountNotFound,
AddRecipientBankError, TransferInvalidAmount, Loan,
)
from weboob.tools.capabilities.bank.investments import create_french_liquidity
from weboob.capabilities.base import NotAvailable, Currency
from weboob.capabilities.profile import Person
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from weboob.tools.capabilities.bank.iban import is_iban_valid
from weboob.tools.value import Value
from weboob.tools.date import parse_french_date
from weboob.tools.captcha.virtkeyboard import VirtKeyboard, VirtKeyboardError
from weboob.tools.compat import urljoin
from weboob.exceptions import BrowserQuestion, BrowserIncorrectPassword, BrowserHTTPNotFound, BrowserUnavailable, ActionNeeded
class BrowserAuthenticationCodeMaxLimit(BrowserIncorrectPassword):
pass
class IncidentPage(HTMLPage):
pass
class IbanPage(LoggedPage, HTMLPage):
def get_iban(self):
if self.doc.xpath('//div[has-class("alert")]/p[contains(text(), "Une erreur est survenue")]') or \
self.doc.xpath('//div[has-class("alert")]/p[contains(text(), "Le compte est introuvable")]'):
return NotAvailable
return CleanText('//table[thead[tr[th[contains(text(), "Code I.B.A.N")]]]]/tbody/tr/td[2]', replace=[(' ', '')])(self.doc)
class AuthenticationPage(HTMLPage):
def authenticate(self):
self.logger.info('Using the PIN Code %s to login', self.browser.config['pin_code'].get())
self.logger.info('Using the auth_token %s to login', self.browser.auth_token)
form = self.get_form()
form['otp_confirm[otpCode]'] = self.browser.config['pin_code'].get()
form['flow_secureForm_instance'] = self.browser.auth_token
form['otp_confirm[validate]'] = ''
form['flow_secureForm_step'] = 2
form.submit()
self.browser.auth_token = None
def sms_first_step(self):
"""
This function simulates the registration of a device on
boursorama two factor authentification web page.
@param device device name to register
@exception BrowserAuthenticationCodeMaxLimit when daily limit is consumed
"""
form = self.get_form()
form.submit()
def sms_second_step(self):
#
error = CleanText('//div[has-class("form-errors")]')(self.doc)
if len(error) > 0:
raise BrowserIncorrectPassword(error)
form = self.get_form()
self.browser.auth_token = form['flow_secureForm_instance']
form['otp_prepare[receiveCode]'] = ''
form.submit()
raise BrowserQuestion(Value('pin_code', label='Enter the PIN Code'))
class Transaction(FrenchTransaction):
PATTERNS = [(re.compile('^(Virement .* )?VIR( SEPA)? (?P.*)'), FrenchTransaction.TYPE_TRANSFER),
(re.compile(u'^CHQ\. (?P.*)'), FrenchTransaction.TYPE_CHECK),
(re.compile('^(ACHAT|PAIEMENT) CARTE (?P\d{2})(?P\d{2})(?P\d{2}) (?P.*)'),
FrenchTransaction.TYPE_CARD),
(re.compile(r'^(?P.+)?(ACHAT|PAIEMENT) CARTE (?P\d{2})(?P\d{2})(?P\d{4}) (?P.*)'),
FrenchTransaction.TYPE_CARD),
(re.compile(r'^(?P.+)?((ACHAT|PAIEMENT)\s)?CARTE (?P\d{2})(?P\d{2})(?P\d{4}) (?P.*)'),
FrenchTransaction.TYPE_CARD),
(re.compile('^(PRLV SEPA |PRLV |TIP )(?P.*)'),
FrenchTransaction.TYPE_ORDER),
(re.compile('^RETRAIT DAB (?P\d{2})(?P\d{2})(?P\d{2}) (?P.*)'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^([A-Z][\sa-z]* )?RETRAIT DAB (?P\d{2})(?P\d{2})(?P\d{4}) (?P.*)'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^([A-Z][\sa-z]* )?Retrait dab (?P\d{2})(?P\d{2})(?P\d{4}) (?P.*)'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile('^AVOIR (?P\d{2})(?P\d{2})(?P\d{2}) (?P.*)'), FrenchTransaction.TYPE_PAYBACK),
(re.compile(r'^(?P[A-Z][\sa-z]* )?AVOIR (?P\d{2})(?P\d{2})(?P\d{4}) (?P.*)'), FrenchTransaction.TYPE_PAYBACK),
(re.compile('^REM CHQ (?P.*)'), FrenchTransaction.TYPE_DEPOSIT),
(re.compile(u'^([*]{3} solde des operations cb [*]{3} )?Relevé différé Carte (.*)'), FrenchTransaction.TYPE_CARD_SUMMARY),
(re.compile(r'^Ech pret'), FrenchTransaction.TYPE_LOAN_PAYMENT),
]
class VirtKeyboardPage(HTMLPage):
pass
class BoursoramaVirtKeyboard(VirtKeyboard):
symbols = {'0': (17, 7, 24, 17),
'1': (18, 6, 21, 18),
'2': (9, 7, 32, 34),
'3': (10, 7, 31, 34),
'4': (11, 6, 29, 34),
'5': (14, 6, 28, 34),
'6': (7, 7, 34, 34),
'7': (5, 6, 36, 34),
'8': (8, 7, 32, 34),
'9': (4, 7, 38, 34)}
color = (255,255,255)
def __init__(self, page):
self.md5 = {}
for button in page.doc.xpath('//ul[@class="password-input"]//button'):
c = button.attrib['data-matrix-key']
txt = button.attrib['style'].replace('background-image:url(data:image/png;base64,', '').rstrip(');')
img = BytesIO(b64decode(txt.encode('ascii')))
self.load_image(img, self.color, convert='RGB')
self.load_symbols((0, 0, 42, 42), c)
def load_symbols(self, coords, c):
coord = self.get_symbol_coords(coords)
if coord == (-1, -1, -1, -1):
return
self.md5[coord] = c
def get_code(self, password):
code = ''
for i, d in enumerate(password):
if i > 0:
code += '|'
try:
code += self.md5[self.symbols[d]]
except KeyError:
raise VirtKeyboardError()
return code
class LoginPage(LoginPage, HTMLPage):
TO_DIGIT = {'2': ['a', 'b', 'c'],
'3': ['d', 'e', 'f'],
'4': ['g', 'h', 'i'],
'5': ['j', 'k', 'l'],
'6': ['m', 'n', 'o'],
'7': ['p', 'q', 'r', 's'],
'8': ['t', 'u', 'v'],
'9': ['w', 'x', 'y', 'z']
}
def login(self, login, password):
if not password.isdigit():
password = ''.join([c if c.isdigit() else [k for k, v in self.TO_DIGIT.items() if c in v][0] for c in password.lower()])
form = self.get_form()
keyboard_page = self.browser.keyboard.open()
vk = BoursoramaVirtKeyboard(keyboard_page)
code = vk.get_code(password)
form['form[login]'] = login
form['form[fakePassword]'] = len(password) * '•'
form['form[password]'] = code
form['form[matrixRandomChallenge]'] = re.search('val\("(.*)"', CleanText('//script')(keyboard_page.doc)).group(1)
form.submit()
class StatusPage(LoggedPage, PartialHTMLPage):
def on_load(self):
# sometimes checking accounts are missing
msg = CleanText('//div[has-class("alert--danger")]', default=None)(self.doc)
if msg:
raise BrowserUnavailable(msg)
class AccountsPage(LoggedPage, HTMLPage):
def is_here(self):
# This id appears when there are no accounts (pro and pp)
return not self.doc.xpath('//div[contains(@id, "alert-random")]')
ACCOUNT_TYPES = {u'comptes courants': Account.TYPE_CHECKING,
u'cav': Account.TYPE_CHECKING,
'livret': Account.TYPE_SAVINGS,
'pel': Account.TYPE_SAVINGS,
'cel': Account.TYPE_SAVINGS,
u'comptes épargne': Account.TYPE_SAVINGS,
u'mon épargne': Account.TYPE_SAVINGS,
'csljeune': Account.TYPE_SAVINGS, # in url
u'ord': Account.TYPE_MARKET,
u'comptes bourse': Account.TYPE_MARKET,
u'mes placements financiers': Account.TYPE_MARKET,
u'av': Account.TYPE_LIFE_INSURANCE,
u'assurances vie': Account.TYPE_LIFE_INSURANCE,
u'assurance-vie': Account.TYPE_LIFE_INSURANCE,
u'mes crédits': Account.TYPE_LOAN,
u'crédit': Account.TYPE_LOAN,
u'prêt': Account.TYPE_LOAN,
u'pea': Account.TYPE_PEA,
'carte': Account.TYPE_CARD,
}
@method
class iter_accounts(ListElement):
item_xpath = '//table[@class="table table--accounts"]/tr[has-class("table__line--account") and count(descendant::td) > 1 and @data-line-account-href]'
class item(ItemElement):
klass = Account
load_details = Field('url') & AsyncLoad
def condition(self):
# Ignore externally aggregated accounts and insurances:
return not self.is_external() and not any(x in Field('url')(self) for x in ('automobile', 'assurance/protection', 'assurance/comptes', 'assurance/famille'))
obj_label = CleanText('.//a[has-class("account--name")] | .//div[has-class("account--name")]')
obj_currency = FrenchTransaction.Currency('.//a[has-class("account--balance")]')
obj_valuation_diff = Async('details') & CleanDecimal('//li[h4[text()="Total des +/- values"]]/h3 |\
//li[span[text()="Total des +/- values latentes"]]/span[has-class("overview__value")]', replace_dots=True, default=NotAvailable)
obj__holder = None
obj__amount = CleanDecimal('.//a[has-class("account--balance")]', replace_dots=True)
def obj_balance(self):
if Field('type')(self) != Account.TYPE_CARD:
balance = Field('_amount')(self)
if Field('type')(self) in [Account.TYPE_PEA, Account.TYPE_LIFE_INSURANCE, Account.TYPE_MARKET]:
page = Async('details').loaded_page(self)
if isinstance(page, MarketPage):
updated_balance = page.get_balance(Field('type')(self))
if updated_balance is not None:
return updated_balance
return balance
return Decimal('0')
def obj_coming(self):
# report deferred expenses in the coming attribute
if Field('type')(self) == Account.TYPE_CARD:
return Field('_amount')(self)
return Async('details', CleanDecimal(u'//li[h4[text()="Mouvements à venir"]]/h3', replace_dots=True, default=NotAvailable))(self)
def obj_id(self):
type = Field('type')(self)
if type == Account.TYPE_CARD:
# When card is opposed it still appears on accounts page with a dead link and so, no id. Skip it.
if Attr('.//a[has-class("account--name")]', 'href')(self) == '#':
raise SkipItem()
return self.obj__idparts()[1]
id = Async('details', Regexp(CleanText('//h3[has-class("account-number")]'), r'(\d+)', default=NotAvailable))(self)
if not id:
raise SkipItem()
return id
def obj_type(self):
# card url is /compte/cav/xxx/carte/yyy so reverse to match "carte" before "cav"
for word in Field('url')(self).lower().split('/')[::-1]:
v = self.page.ACCOUNT_TYPES.get(word)
if v:
return v
for word in Field('label')(self).replace('_', ' ').lower().split():
v = self.page.ACCOUNT_TYPES.get(word)
if v:
return v
category = CleanText('./preceding-sibling::tr[has-class("list--accounts--master")]//h4')(self)
v = self.page.ACCOUNT_TYPES.get(category.lower())
if v:
return v
page = Async('details').loaded_page(self)
if isinstance(page, LoanPage):
return Account.TYPE_LOAN
return Account.TYPE_UNKNOWN
def obj_url(self):
link = Attr('.//a[has-class("account--name")] | .//a[2] | .//div/a', 'href', default=NotAvailable)(self)
return urljoin(self.page.url, link)
def is_external(self):
return '/budget/' in Field('url')(self)
def obj__idparts(self):
return re.findall('[a-z\d]{32}', Field('url')(self))
def obj__webid(self):
parts = self.obj__idparts()
if parts:
return parts[0]
# We do not yield other banks accounts for the moment.
def validate(self, obj):
return not Async('details', CleanText(u'//h4[contains(text(), "Établissement bancaire")]'))(self) and not \
Async('details', CleanText(u'//h4/div[contains(text(), "Établissement bancaire")]'))(self)
class LoanPage(LoggedPage, HTMLPage):
LOAN_TYPES = {
"PRÊT PERSONNEL": Account.TYPE_CONSUMER_CREDIT,
}
@method
class get_loan(ItemElement):
klass = Loan
obj_id = CleanText('//h3[contains(@class, "account-number")]/strong')
obj_label = CleanText('//h2[contains(@class, "page-title__account")]//div[@class="account-edit-label"]/span')
obj_total_amount = CleanDecimal('//p[contains(text(), "Montant emprunt")]/span', replace_dots=True)
obj_currency = CleanCurrency('//p[contains(text(), "Montant emprunt")]/span')
obj_duration = CleanDecimal('//p[contains(text(), "Nombre prévisionnel d\'échéances restantes")]/span', default=NotAvailable)
obj_rate = CleanDecimal('//p[contains(text(), "Taux nominal en vigueur du prêt")]/span')
obj_nb_payments_left = CleanDecimal('//p[contains(text(), "Nombre prévisionnel d\'échéances restantes")]/span', default=NotAvailable)
obj_next_payment_amount = CleanDecimal('//p[contains(text(), "Montant de la prochaine échéance")]/span', replace_dots=True, default=NotAvailable)
obj_nb_payments_total = CleanDecimal('//p[contains(text(), "Nombre d\'écheances totales") or contains(text(), "Nombre total d\'échéances")]/span')
obj_subscription_date = Date(CleanText('//p[contains(text(), "Date de départ du prêt")]/span'), parse_func=parse_french_date)
obj_maturity_date = Date(CleanText('//p[contains(text(), "Date prévisionnelle d\'échéance finale")]/span'), parse_func=parse_french_date, default=NotAvailable)
def obj_balance(self):
balance = CleanDecimal('//p[contains(text(), "Capital restant dû")]/span', replace_dots=True)(self)
if balance > 0:
balance *= -1
return balance
def obj_type(self):
_type = CleanText('//h2[contains(@class, "page-title__account")]//div[@class="account-edit-label"]/span')
return Map(_type, self.page.LOAN_TYPES, default=Account.TYPE_LOAN)(self)
def obj_next_payment_date(self):
tmp = CleanText('//p[contains(text(), "Date de la prochaine échéance")]/span')(self)
if tmp == "-":
return NotAvailable
return Date(CleanText('//p[contains(text(), "Date de la prochaine échéance")]/span'), parse_func=parse_french_date)(self)
class NoAccountPage(LoggedPage, HTMLPage):
def is_here(self):
err = CleanText('//div[contains(@id, "alert-random")]/text()', children=False)(self.doc)
return "compte inconnu" in err.lower()
class CardCalendarPage(LoggedPage, RawPage):
def is_here(self):
return b'VCALENDAR' in self.doc
def on_load(self):
page_content = self.content.decode('utf-8')
self.browser.deferred_card_calendar = []
# handle ics calendar
dates = page_content.split('BEGIN:VEVENT')[1:]
assert len(dates)%2 == 0, 'List lenght should be even-numbered'
# get all dates
dates = [re.search(r'(?<=VALUE\=DATE:)(\d{8})', el).group(1) for el in dates]
dates.sort()
for i in range(0, len(dates), 2):
if len(dates[i:i+2]) == 2:
# list contains tuple like (vdate, date)
self.browser.deferred_card_calendar.append((Date().filter(dates[i]), Date().filter(dates[i+1])))
class CalendarPage(LoggedPage, HTMLPage):
def on_load(self):
# redirect
calendar_ics_url = urljoin(self.browser.BASEURL, CleanText('//a[contains(@href, "calendrier.ics")]/@href')(self.doc))
self.browser.location(calendar_ics_url)
class HistoryPage(LoggedPage, HTMLPage):
@method
class iter_history(ListElement):
item_xpath = '//ul[has-class("list__movement")]/li[div and not(contains(@class, "summary")) \
and not(contains(@class, "graph")) \
and not(contains(@class, "separator")) \
and not(contains(@class, "list__movement__line--deffered"))]'
class item(ItemElement):
klass = Transaction
obj_raw = Transaction.Raw(CleanText('.//div[has-class("list__movement__line--label__name")]'))
obj_date = Date(Attr('.//time', 'datetime'))
obj_amount = CleanDecimal('.//div[has-class("list__movement__line--amount")]', replace_dots=True)
obj_category = CleanText('.//span[has-class("category")]')
obj__account_name = CleanText('.//span[contains(@class, "account__name-xs")]', default=None)
def obj_id(self):
return Attr('.', 'data-id', default=NotAvailable)(self) or Attr('.', 'data-custom-id', default=NotAvailable)(self)
def obj_type(self):
# In order to set TYPE_DEFERRED_CARD transactions correctly,
# we must check if the transaction's account_name is in the list
# of deferred cards, but summary transactions must escape this rule.
if self.obj.type == Transaction.TYPE_CARD_SUMMARY:
return self.obj.type
deferred_card_labels = [card.label for card in self.page.browser.cards_list]
if 'cartes débit différé' in Field('category')(self) or Field('_account_name')(self).upper() in deferred_card_labels:
return Transaction.TYPE_DEFERRED_CARD
if not Env('is_card', default=False)(self):
if Env('coming', default=False)(self) and Field('raw')(self).startswith('CARTE '):
return Transaction.TYPE_CARD_SUMMARY
# keep the value previously set by Transaction.Raw
return self.obj.type
return Transaction.TYPE_UNKNOWN
def obj_rdate(self):
if self.obj.rdate:
# Transaction.Raw may have already set it
return self.obj.rdate
s = Regexp(Field('raw'), ' (\d{2}/\d{2}/\d{2}) | (?!NUM) (\d{6}) ', default=NotAvailable)(self)
if not s:
return Field('date')(self)
s = s.replace('/', '')
# Sometimes the user enters an invalid date 16/17/19 for example
return Date(dayfirst=True, default=NotAvailable).filter('%s-%s-%s' % (s[:2], s[2:4], s[4:]))
def obj__is_coming(self):
return Env('coming', default=False)(self) or len(self.xpath(u'.//span[@title="Mouvement à débit différé"]')) or self.obj_date() > date.today()
def obj_date(self):
date = Date(Attr('.//time', 'datetime'))(self)
if Env('is_card', default=False)(self):
if self.page.browser.deferred_card_calendar is None:
self.page.browser.location(Link('//a[contains(text(), "calendrier")]')(self))
closest = self.page.browser.get_debit_date(date)
if closest:
return closest
return date
def validate(self, obj):
# TYPE_DEFERRED_CARD transactions are already present in the card history
# so we only return TYPE_DEFERRED_CARD for the coming:
if not Env('coming', default=False)(self):
return not len(self.xpath(u'.//span[has-class("icon-carte-bancaire")]')) \
and not len(self.xpath(u'.//a[contains(@href, "/carte")]')) \
and obj.type != Transaction.TYPE_DEFERRED_CARD
elif Env('coming', default=False)(self):
# Do not return coming from deferred cards if their
# summary does not have a fixed amount yet:
if obj.type == Transaction.TYPE_CARD_SUMMARY:
return False
return True
def get_cards_number_link(self):
return Link('//a[small[span[contains(text(), "carte bancaire")]]]', default=NotAvailable)(self.doc)
def get_csv_link(self):
return Link('//a[@data-operations-export-button]')(self.doc)
def get_calendar_link(self):
return Link('//a[contains(text(), "calendrier")]')(self.doc)
class CardHistoryPage(LoggedPage, CsvPage):
ENCODING = 'latin-1'
FMTPARAMS = {'delimiter': str(';')}
HEADER = 1
@method
class iter_history(DictElement):
class item(ItemElement):
klass = Transaction
obj_raw = Transaction.Raw(Dict('label'))
obj_date = Date(Dict('dateVal'), dayfirst=True)
obj__account_label = Dict('accountLabel')
obj__is_coming = False
def obj_amount(self):
if Field('type')(self) == Transaction.TYPE_CARD_SUMMARY:
# '-' so the reimbursements appear positively in the card transactions:
return -CleanDecimal(Dict('amount'), replace_dots=True)(self)
return CleanDecimal(Dict('amount'), replace_dots=True)(self)
def obj_rdate(self):
if self.obj.rdate:
# Transaction.Raw may have already set it
return self.obj.rdate
s = Regexp(Field('raw'), ' (\d{2}/\d{2}/\d{2}) | (?!NUM) (\d{6}) ', default=NotAvailable)(self)
if not s:
return Field('date')(self)
s = s.replace('/', '')
# Sometimes the user enters an invalid date 16/17/19 for example
return Date(dayfirst=True, default=NotAvailable).filter('%s%s%s%s%s' % (s[:2], '-', s[2:4], '-', s[4:]))
def obj_type(self):
if 'CARTE' in self.obj.raw:
return Transaction.TYPE_DEFERRED_CARD
return self.obj.type
def obj_category(self):
return Dict('category')(self)
# The csv page shows every transactions of the card account AND the associated
# check account. Here we want only the card transactions.
# Also, if there is more than one card account, the csv page will show
# transactions of every card account (smart) ... So we need to check for
# account number.
def validate(self, obj):
if "Relevé" in obj.raw:
return Env('account_number')(self) in obj.raw
return ("CARTE" in obj.raw or "CARTE" in obj._account_label) and Env('account_number')(self) in Dict('accountNum')(self)
class Myiter_investment(TableElement):
# We do not scrape the investments contained in the "Engagements en liquidation" table
# so we must check that the before the does not contain this title.
item_xpath = '//div[preceding-sibling::h3[1][text()!="Engagements en liquidation"]]//table[contains(@class, "operations")]/tbody/tr'
head_xpath = '//div[preceding-sibling::h3[1][text()!="Engagements en liquidation"]]//table[contains(@class, "operations")]/thead/tr/th'
col_value = u'Valeur'
col_quantity = u'Quantité'
col_unitprice = u'Px. Revient'
col_unitvalue = u'Cours'
col_valuation = u'Montant'
col_diff = u'+/- latentes'
class Myitem(ItemElement):
klass = Investment
obj_quantity = CleanDecimal(TableCell('quantity'), default=NotAvailable)
obj_unitprice = CleanDecimal(TableCell('unitprice'), replace_dots=True, default=NotAvailable)
obj_unitvalue = CleanDecimal(TableCell('unitvalue'), replace_dots=True, default=NotAvailable)
obj_valuation = CleanDecimal(TableCell('valuation'), replace_dots=True, default=NotAvailable)
obj_diff = CleanDecimal(TableCell('diff'), replace_dots=True, default=NotAvailable)
def obj_label(self):
return CleanText().filter((TableCell('value')(self)[0]).xpath('.//a'))
def obj_code(self):
return CleanText().filter((TableCell('value')(self)[0]).xpath('./span')) or NotAvailable
def my_pagination(func):
def inner(page, *args, **kwargs):
while True:
try:
for r in func(page, *args, **kwargs):
yield r
except NextPage as e:
try:
result = page.browser.location(e.request)
page = result.page
except BrowserHTTPNotFound as e:
page.logger.warning(e)
return
else:
return
return inner
class MarketPage(LoggedPage, HTMLPage):
def get_balance(self, account_type):
txt = u"Solde au" if account_type is Account.TYPE_LIFE_INSURANCE else u"Total Portefeuille"
# HTML tags are usually h4-h3 but may also be span-span
h_balance = CleanDecimal('//li[h4[contains(text(), "%s")]]/h3' % txt, replace_dots=True, default=None)(self.doc)
span_balance = CleanDecimal('//li/span[contains(text(), "%s")]/following-sibling::span' % txt, replace_dots=True, default=None)(self.doc)
return h_balance or span_balance or None
@my_pagination
@method
class iter_history(TableElement):
item_xpath = '//table/tbody/tr'
head_xpath = '//table/thead/tr/th'
col_label = ['Nature', u'Opération']
col_amount = 'Montant'
col_date = ["Date d'effet", 'Date', u'Date opération']
next_page = Link('//li[@class="pagination__next"]/a')
class item(ItemElement):
klass = Transaction
def obj_date(self):
d = Date(CleanText(TableCell('date')), dayfirst=True, default=None)(self)
if d:
return d
return Date(CleanText(TableCell('date')), parse_func=parse_french_date)(self)
obj_raw = Transaction.Raw(CleanText(TableCell('label')))
obj_amount = CleanDecimal(TableCell('amount'), replace_dots=True, default=NotAvailable)
obj__is_coming = False
def parse(self, el):
if el.xpath('./td[2]/a'):
m = re.search('(\d+)', el.xpath('./td[2]/a')[0].get('data-modal-alert-behavior', ''))
if m:
self.env['account']._history_pages.append((Field('raw')(self),\
self.page.browser.open('%s%s%s' % (self.page.url.split('mouvements')[0], 'mouvement/', m.group(1))).page))
raise SkipItem()
@method
class get_investment(Myiter_investment):
class item (Myitem):
def obj_unitvalue(self):
return CleanDecimal(replace_dots=True, default=NotAvailable).filter((TableCell('unitvalue')(self)[0]).xpath('./span[not(@class)]'))
def iter_investment(self):
# Xpath can be h3/h4 or div/span; in both cases
# the first node contains "Solde Espèces":
valuation = CleanDecimal('//li/*[contains(text(), "Solde Espèces")]/following-sibling::*', replace_dots=True, default=None)(self.doc)
if valuation:
yield create_french_liquidity(valuation)
for inv in self.get_investment():
yield inv
def get_transactions_from_detail(self, account):
for label, page in account._history_pages:
amounts = page.doc.xpath('//span[contains(text(), "Montant")]/following-sibling::span')
if len(amounts) == 3:
amounts.pop(0)
for table in page.doc.xpath('//table'):
t = Transaction()
t.date = Date(CleanText(page.doc.xpath('//span[contains(text(), "Date d\'effet")]/following-sibling::span')), dayfirst=True)(page)
t.label = label
t.amount = CleanDecimal(replace_dots=True).filter(amounts[0])
amounts.pop(0)
t._is_coming = False
t.investments = []
sum_amount = 0
for tr in table.xpath('./tbody/tr'):
i = Investment()
i.label = CleanText().filter(tr.xpath('./td[1]'))
i.vdate = Date(CleanText(tr.xpath('./td[2]')), dayfirst=True)(tr)
i.unitvalue = CleanDecimal(replace_dots=True).filter(tr.xpath('./td[3]'))
i.quantity = CleanDecimal(replace_dots=True).filter(tr.xpath('./td[4]'))
i.valuation = CleanDecimal(replace_dots=True).filter(tr.xpath('./td[5]'))
sum_amount += i.valuation
t.investments.append(i)
if t.label == 'prélèvement':
t.amount = sum_amount
yield t
class SavingMarketPage(MarketPage):
@pagination
@method
class iter_history(TableElement):
item_xpath = '//table/tbody/tr'
head_xpath = '//table/thead/tr/th'
col_label = u'Opération'
col_amount = u'Montant'
col_date = u'Date opération'
col_vdate = u'Date Val'
next_page = Link('//li[@class="pagination__next"]/a')
class item(ItemElement):
klass = Transaction
obj_label = CleanText(TableCell('label'))
obj_amount = CleanDecimal(TableCell('amount'), replace_dots=True)
obj__is_coming = False
def obj_date(self):
return parse_french_date(CleanText(TableCell('date'))(self))
def obj_vdate(self):
return parse_french_date(CleanText(TableCell('vdate'))(self))
@method
class iter_investment(TableElement):
item_xpath = '//table/tbody/tr[count(descendant::td) > 4]'
head_xpath = '//table/thead/tr[count(descendant::th) > 4]/th'
col_label = u'Fonds'
col_code = u'Code Isin'
col_unitvalue = u'Valeur de la part'
col_quantity = u'Nombre de parts'
col_vdate = u'Date VL'
class item(ItemElement):
klass = Investment
obj_label = CleanText(TableCell('label'))
obj_code = CleanText(TableCell('code'))
obj_unitvalue = CleanDecimal(TableCell('unitvalue'), replace_dots=True)
obj_quantity = CleanDecimal(TableCell('quantity'), replace_dots=True)
obj_valuation = Eval(lambda x, y: x * y, Field('quantity'), Field('unitvalue'))
obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True)
class AsvPage(MarketPage):
@method
class iter_investment(Myiter_investment):
col_vdate = u'Date de Valeur'
col_label = u'Valeur'
class item(Myitem):
obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True, default=NotAvailable)
def obj_label(self):
return CleanText('.//strong/a')(self) or CleanText('.//strong', children=False)(self)
class ErrorPage(HTMLPage):
def on_load(self):
error = (Attr('//input[@required][@id="profile_lei_type_identifier"]', 'data-message', default=None)(self.doc) or
CleanText('//h2[@class="page-title"][contains(text(), "Actualisation")]', default=None)(self.doc))
if error:
raise ActionNeeded(error)
class ExpertPage(LoggedPage, HTMLPage):
pass
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 = Format('%s %s %s', MySelect('genderTitle'), MyInput('firstName'), MyInput('lastName'))
obj_nationality = CleanText(u'//span[contains(text(), "Nationalité")]/span')
obj_spouse_name = MyInput('spouseFirstName')
obj_children = CleanDecimal(MyInput('dependentChildren'), default=NotAvailable)
obj_family_situation = MySelect('maritalStatus')
obj_matrimonial = MySelect('matrimonial')
obj_housing_status = MySelect('housingSituation')
obj_job = MyInput('occupation')
obj_job_start_date = Date(MyInput('employeeSince'), default=NotAvailable)
obj_company_name = MyInput('employer')
obj_socioprofessional_category = MySelect('socioProfessionalCategory')
class CardsNumberPage(LoggedPage, HTMLPage):
def populate_cards_number(self, cards):
for card in cards:
# The second hash of the card's url is used to get
# the card's hash on the HTML page:
card_url_hash = re.search('carte\/(.*)', card.url).group(1)
card_hash = CleanText('//nav[ul[li[a[contains(@href, "%s")]]]]/@data-card-key' % card_url_hash)(self.doc)
# With the card hash we can get the card number.
# Non activated cards have no card_hash and therefore no
# card number so we can easily eliminate them afterwards.
card.number = CleanText('//div[@data-card-key="%s"]/div/span' % card_hash)(self.doc)
class HomePage(LoggedPage, HTMLPage):
pass
class NoTransferPage(LoggedPage, HTMLPage):
pass
class TransferMainPage(LoggedPage, HTMLPage):
pass
class TransferAccounts(LoggedPage, HTMLPage):
@method
class iter_accounts(ListElement):
item_xpath = '//a[has-class("next-step")][@data-value]'
class item(ItemElement):
klass = Account
obj_id = CleanText('.//div[@class="transfer__account-number"]')
obj__sender_id = Attr('.', 'data-value')
def submit_account(self, id):
for account in self.iter_accounts():
if account.id == id:
break
else:
raise AccountNotFound()
form = self.get_form(name='DebitAccount')
form['DebitAccount[debitAccountKey]'] = account._sender_id
form.submit()
class TransferRecipients(LoggedPage, HTMLPage):
@method
class iter_recipients(ListElement):
item_xpath = '//a[has-class("transfer__account-wrapper")]'
class item(ItemElement):
klass = Recipient
obj_id = CleanText('.//div[@class="transfer__account-number"]')
obj_bank_name = Regexp(CleanText('.//div[@class="transfer__account-name"]'), pattern=r'- ([^-]*)$', default=NotAvailable)
def obj_label(self):
label = Regexp(CleanText('.//div[@class="transfer__account-name"]'), pattern=r'^(.*?)(?: -[^-]*)?$')(self)
return label.rstrip('-').rstrip()
def obj_category(self):
text = CleanText('./ancestor::div[has-class("deploy--item")]//a[has-class("deploy__title")]')(self)
if 'Mes comptes Boursorama Banque' in text:
return 'Interne'
elif 'Comptes externes' in text or 'Comptes de tiers' in text:
return 'Externe'
def obj_iban(self):
if Field('category')(self) == 'Externe':
return Field('id')(self)
def obj_enabled_at(self):
return datetime.datetime.now().replace(microsecond=0)
obj__tempid = Attr('.', 'data-value')
def condition(self):
iban = Field('iban')(self)
if iban:
return is_iban_valid(iban)
# some internal accounts don't show iban
return True
def submit_recipient(self, tempid):
form = self.get_form(name='CreditAccount')
form['CreditAccount[creditAccountKey]'] = tempid
form.submit()
class TransferCharac(LoggedPage, HTMLPage):
def get_option(self, select, text):
for opt in select.xpath('option'):
if opt.text_content() == text:
return opt.attrib['value']
def submit_info(self, amount, label, exec_date):
form = self.get_form(name='Characteristics')
assert amount > 0
amount = str(amount.quantize(Decimal('0.00'))).replace('.', ',')
form['Characteristics[amount]'] = amount
form['Characteristics[label]'] = label
if not exec_date:
exec_date = datetime.date.today()
if datetime.date.today() == exec_date:
assert self.get_option(form.el.xpath('//select[@id="Characteristics_schedulingType"]')[0], 'Ponctuel') == '1'
form['Characteristics[schedulingType]'] = '1'
else:
assert self.get_option(form.el.xpath('//select[@id="Characteristics_schedulingType"]')[0], 'Différé') == '2'
form['Characteristics[schedulingType]'] = '2'
# If we let the 0 in the front of the month or the day like 02, the website will not interpret the good date
form['Characteristics[scheduledDate][day]'] = exec_date.strftime('%d').lstrip("0")
form['Characteristics[scheduledDate][month]'] = exec_date.strftime('%m').lstrip("0")
form['Characteristics[scheduledDate][year]'] = exec_date.strftime('%Y')
form['Characteristics[notice]'] = 'none'
form.submit()
class TransferConfirm(LoggedPage, HTMLPage):
def on_load(self):
errors = CleanText('//li[contains(text(), "Le montant du virement est inférieur au minimum")]')(self.doc)
if errors:
raise TransferInvalidAmount(message=errors)
def need_refresh(self):
return not self.doc.xpath('//form[@name="Confirm"]//button[contains(text(), "Je valide")]')
@method
class get_transfer(ItemElement):
klass = Transfer
obj_label = CleanText('//div[@id="transfer-label"]/span[@class="transfer__account-value"]')
obj_amount = CleanDecimal('//div[@id="transfer-amount"]/span[@class="transfer__account-value"]', replace_dots=True)
obj_currency = CleanCurrency('//div[@id="transfer-amount"]/span[@class="transfer__account-value"]')
obj_account_label = CleanText('//span[@id="transfer-origin-account"]')
obj_recipient_label = CleanText('//span[@id="transfer-destination-account"]')
def obj_exec_date(self):
type_ = CleanText('//div[@id="transfer-type"]/span[@class="transfer__account-value"]')(self)
if type_ == 'Ponctuel':
return datetime.date.today()
elif type_ == 'Différé':
return Date(CleanText('//div[@id="transfer-date"]/span[@class="transfer__account-value"]'), dayfirst=True)(self)
def submit(self):
form = self.get_form(name='Confirm')
form.submit()
class TransferSent(LoggedPage, HTMLPage):
pass
class AddRecipientPage(LoggedPage, HTMLPage):
def on_load(self):
super(AddRecipientPage, self).on_load()
err = CleanText('//div[@class="form-errors"]', default=None)(self.doc)
if err:
raise AddRecipientBankError(message=err)
def _is_form(self, **kwargs):
try:
self.get_form(**kwargs)
except FormNotFound:
return False
return True
def is_charac(self):
return self._is_form(name='externalAccountsPrepareType')
def submit_recipient(self, recipient):
form = self.get_form(name='externalAccountsPrepareType')
form['externalAccountsPrepareType[type]'] = 'tiers'
form['externalAccountsPrepareType[label]'] = recipient.label
# names are mandatory and are uneditable...
form['externalAccountsPrepareType[beneficiaryLastname]'] = recipient.label
form['externalAccountsPrepareType[beneficiaryFirstname]'] = recipient.label
form['externalAccountsPrepareType[bank]'] = recipient.bank_name or 'Autre'
form['externalAccountsPrepareType[iban]'] = recipient.iban
form.submit()
def is_send_sms(self):
return self._is_form(name='otp_prepare')
def send_sms(self):
form = self.get_form(name='otp_prepare')
form['otp_prepare[receiveCode]'] = ''
form.submit()
def is_confirm_sms(self):
return self._is_form(name='otp_confirm')
def confirm_sms(self, code):
form = self.get_form(name='otp_confirm')
form['otp_confirm[otpCode]'] = code
form.submit()
def is_confirm(self):
return self._is_form(name='externalAccountsConfirmType')
def confirm(self):
self.get_form(name='externalAccountsConfirmType').submit()
def get_recipient(self):
div = self.doc.xpath('//div[@class="confirmation__text"]')[0]
ret = Recipient()
ret.label = CleanText('//p[b[contains(text(),"Libellé du compte :")]]/text()')(div)
ret.iban = ret.id = CleanText('//p[b[contains(text(),"Iban :")]]/text()')(div)
ret.bank_name = CleanText(u'//p[b[contains(text(),"Établissement bancaire :")]]/text()')(div)
ret.currency = u'EUR'
ret.category = u'Externe'
ret.enabled_at = datetime.date.today()
assert ret.label
return ret
def is_created(self):
return CleanText('//p[contains(text(), "Le bénéficiaire a bien été ajouté.")]')(self.doc) != ""
class PEPPage(LoggedPage, HTMLPage):
pass
class CurrencyListPage(HTMLPage):
@method
class iter_currencies(ListElement):
item_xpath = '//select[@class="c-select currency-change"]/option'
class item(ItemElement):
klass = Currency
obj_id = Attr('./.', 'value')
def get_currency_list(self):
CurIDList = []
for currency in self.iter_currencies():
currency.id = currency.id[0:3]
if currency.id not in CurIDList:
CurIDList.append(currency.id)
yield currency
class CurrencyConvertPage(JsonPage):
def get_rate(self):
if not 'error' in self.doc:
return Decimal(str(self.doc['rate']))
class AccountsErrorPage(LoggedPage, HTMLPage):
def is_here(self):
# some braindead error seems to affect many accounts until we retry
return '[E10008]' in CleanText('//div')(self.doc)
def on_load(self):
raise BrowserUnavailable()
woob-a5f04d414183a4592547763b947b57b4cbc00678-modules-boursorama/modules/boursorama/test.py 0000664 0000000 0000000 00000002171 13434604312 0030100 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Gabriel Kerneis
# Copyright(C) 2010-2011 Jocelyn Jaubert
#
# 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 Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this weboob module. If not, see .
from weboob.tools.test import BackendTest
class BoursoramaTest(BackendTest):
MODULE = 'boursorama'
def test_boursorama(self):
l = list(self.backend.iter_accounts())
if len(l) > 0:
a = l[0]
list(self.backend.iter_coming(a))
list(self.backend.iter_history(a))