Newer
Older
# -*- coding: utf-8 -*-
# Copyright(C) 2012 Gilles-Alexandre Quenot
#
# 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/>.
from __future__ import unicode_literals
import json
from datetime import datetime, timedelta
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
from weboob.exceptions import AuthMethodNotImplemented, BrowserIncorrectPassword, ActionNeeded
from weboob.capabilities.bank import Account, AddRecipientStep, Recipient
from weboob.tools.capabilities.bank.transactions import sorted_transactions
from .pages.login import LoginPage, UnavailablePage
from .pages.accounts_list import (
AccountsList, AccountHistoryPage, CardHistoryPage, InvestmentHistoryPage, PeaHistoryPage, LoanPage, ProfilePage, ProfilePageCSV, SecurityPage,
from .pages.transfer import (
RegisterTransferPage, ValidateTransferPage, ConfirmTransferPage, RecipientsPage, RecipientSMSPage
class Fortuneo(LoginBrowser, StatesMixin):
login_page = URL(r'.*identification\.jsp.*', LoginPage)
accounts_page = URL(r'/fr/prive/default.jsp\?ANav=1',
r'.*prive/default\.jsp.*',
r'.*/prive/mes-comptes/synthese-mes-comptes\.jsp',
AccountsList)
account_history = URL(r'.*/prive/mes-comptes/livret/consulter-situation/consulter-solde\.jsp.*',
r'.*/prive/mes-comptes/compte-courant/consulter-situation/consulter-solde\.jsp.*',
r'.*/prive/mes-comptes/compte-especes.*',
AccountHistoryPage)
card_history = URL(r'.*/prive/mes-comptes/compte-courant/carte-bancaire/encours-debit-differe\.jsp.*', CardHistoryPage)
pea_history = URL(r'.*/prive/mes-comptes/pea/.*',
r'.*/prive/mes-comptes/compte-titres-pea/.*',
r'.*/prive/mes-comptes/ppe/.*', PeaHistoryPage)
invest_history = URL(r'.*/prive/mes-comptes/assurance-vie/.*', InvestmentHistoryPage)
loan_contract = URL(r'/fr/prive/mes-comptes/credit-immo/contrat-credit-immo/contrat-pret-immobilier.jsp.*', LoanPage)
unavailable = URL(r'/customError/indispo.html', UnavailablePage)
security_page = URL(r'/fr/prive/identification-carte-securite-forte.jsp.*', SecurityPage)
# transfer
recipients = URL(
r'/fr/prive/mes-comptes/compte-courant/realiser-operations/gerer-comptes-externes/consulter-comptes-externes.jsp',
r'/fr/prive/verifier-compte-externe.jsp',
r'fr/prive/mes-comptes/compte-courant/.*/gestion-comptes-externes.jsp',
recipient_sms = URL(
r'/fr/prive/appel-securite-forte-otp-bankone.jsp',
r'/fr/prive/mes-comptes/compte-courant/.*/confirmer-ajout-compte-externe.jsp',
RecipientSMSPage)
register_transfer = URL(
r'/fr/prive/mes-comptes/compte-courant/realiser-operations/saisie-virement.jsp\?ca=(?P<ca>)',
RegisterTransferPage)
validate_transfer = URL(
r'/fr/prive/mes-comptes/compte-courant/.*/verifier-saisie-virement.jsp',
ValidateTransferPage)
confirm_transfer = URL(
r'fr/prive/mes-comptes/compte-courant/.*/init-confirmer-saisie-virement.jsp',
r'/fr/prive/mes-comptes/compte-courant/.*/confirmer-saisie-virement.jsp',
ConfirmTransferPage)
profile = URL(r'/fr/prive/informations-client.jsp', ProfilePage)
profile_csv = URL(r'/PdfStruts\?*', ProfilePageCSV)
need_reload_state = None
__states__ = ['need_reload_state', 'add_recipient_form']
def __init__(self, *args, **kwargs):
LoginBrowser.__init__(self, *args, **kwargs)
self.action_needed_processed = False
def do_login(self):
if not self.login_page.is_here():
self.location('/fr/identification.jsp')
self.location('/fr/prive/default.jsp?ANav=1')
if self.accounts_page.is_here() and self.page.need_sms():
raise AuthMethodNotImplemented('Authentification with sms is not supported')
def load_state(self, state):
# reload state only for new recipient feature
if state.get('need_reload_state'):
# don't use locate browser for add recipient step
state.pop('url', None)
super(Fortuneo, self).load_state(state)
if hasattr(account, '_investment_link'):
if account.id in self.investments:
return self.investments[account.id]
else:
self.location(account._investment_link)
return self.page.get_investments(account)
def get_history(self, account):
self.location(account._history_link)
if not account.type == Account.TYPE_LOAN:
if self.page.select_period():
return sorted_transactions(self.page.get_operations())
def get_coming(self, account):
for cb_link in account._card_links:
for _ in range(3):
self.location(cb_link)
if not self.page.is_loading():
break
for tr in sorted_transactions(self.page.get_operations()):
# Note: if you want to debug process_action_needed() here,
# you must first set self.action_needed_processed to False
# otherwise it might not enter the "if" loop here below.
if not self.action_needed_processed:
self.process_action_needed()
assert self.accounts_page.is_here()
return self.page.get_list()
def process_action_needed(self):
# we have to go in an iframe to know if there are CGUs
url = self.page.get_iframe_url()
if url:
self.location(self.absurl(url, base=True)) # beware, the landing page might vary according to the referer page. So far I didn't figure out how the landing page is chosen.
if self.security_page.is_here():
# Some connections require reinforced security and we cannot bypass the OTP in order
# to get to the account information. Users have to provide a phone number in order to
# validate an OTP, so we must raise an ActionNeeded with the appropriate message.
raise ActionNeeded('Cette opération sensible doit être validée par un code sécurité envoyé par SMS ou serveur vocal. '
'Veuillez contacter le Service Clients pour renseigner vos coordonnées téléphoniques.')
# if there are skippable CGUs, skip them
if self.accounts_page.is_here() and self.page.has_action_needed():
# Look for the request in the event listener registered to the button
# can be harcoded, no variable part. It is a POST request without data.
self.location(self.absurl('ReloadContext?action=1&', base=True), method='POST')
self.accounts_page.go() # go back to the accounts page whenever there was an iframe or not
self.action_needed_processed = True
@need_login
def iter_recipients(self, origin_account):
self.register_transfer.go(ca=origin_account._ca)
if self.page.is_account_transferable(origin_account):
for internal_recipient in self.page.iter_internal_recipients(origin_account_id=origin_account.id):
yield internal_recipient
self.recipients.go()
for external_recipients in self.page.iter_external_recipients():
yield external_recipients
def copy_recipient(self, recipient):
rcpt = Recipient()
rcpt.iban = recipient.iban
rcpt.id = recipient.iban
rcpt.label = recipient.label
rcpt.category = recipient.category
rcpt.enabled_at = datetime.now().replace(microsecond=0) + timedelta(days=1)
rcpt.currency = u'EUR'
return rcpt
def new_recipient(self, recipient, **params):
if 'code' in params:
# to drop and use self.add_recipient_form instead in send_code()
recipient_form = json.loads(self.add_recipient_form)
self.send_code(recipient_form ,params['code'])
if self.page.rcpt_after_sms():
self.need_reload_state = None
return self.copy_recipient(recipient)
elif self.page.is_code_expired():
self.need_reload_state = True
raise AddRecipientStep(recipient, Value('code', label='Le code sécurité est expiré. Veuillez saisir le nouveau code reçu qui sera valable 5 minutes.'))
assert False, self.page.get_error()
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
return self.new_recipient_before_otp(recipient, **params)
@need_login
def new_recipient_before_otp(self, recipient, **params):
self.recipients.go()
self.page.check_external_iban_form(recipient)
self.page.check_recipient_iban()
# fill form
self.page.fill_recipient_form(recipient)
rcpt = self.page.get_new_recipient(recipient)
# get first part of confirm form
send_code_form = self.page.get_send_code_form()
data = {
'appelAjax': 'true',
'domicileUpdated': 'false',
'numeroSelectionne.value': '',
'portableUpdated': 'false',
'proUpdated': 'false',
'typeOperationSensible': 'AJOUT_BENEFICIAIRE'
}
# this send sms to user
self.location(self.absurl('/fr/prive/appel-securite-forte-otp-bankone.jsp', base=True) , data=data)
# get second part of confirm form
send_code_form.update(self.page.get_send_code_form_input())
# save form value and url for statesmixin
self.add_recipient_form = dict(send_code_form)
self.add_recipient_form.update({'url': send_code_form.url})
# storage can't handle dict with '.' in key
# to drop when dict with '.' in key is handled
self.add_recipient_form = json.dumps(self.add_recipient_form)
self.need_reload_state = True
raise AddRecipientStep(rcpt, Value('code', label='Veuillez saisir le code reçu.'))
def send_code(self, form_data, code):
form_url = form_data['url']
form_data['otp'] = code
form_data.pop('url')
self.location(self.absurl(form_url, base=True), data=form_data)
@need_login
def init_transfer(self, account, recipient, amount, label, exec_date):
self.register_transfer.go(ca=account._ca)
self.page.fill_transfer_form(account, recipient, amount, label, exec_date)
return self.page.handle_response(account, recipient, amount, label, exec_date)
@need_login
def execute_transfer(self, transfer):
self.page.validate_transfer()
self.page.confirm_transfer()
return self.page.transfer_confirmation(transfer)
@need_login
def get_profile(self):
self.profile.go()
csv_link = self.page.get_csv_link()
if csv_link:
self.location(csv_link)