From 14b6d75533d52a7aca949782d6f8d946dfe4c247 Mon Sep 17 00:00:00 2001 From: Kitof Date: Sun, 14 Jul 2013 00:25:06 +0200 Subject: [PATCH] Add module for HelloBank support (closes #1276) Signed-off-by: Romain Bignon --- modules/hellobank/__init__.py | 23 +++ modules/hellobank/backend.py | 151 +++++++++++++++ modules/hellobank/browser.py | 228 +++++++++++++++++++++++ modules/hellobank/perso/__init__.py | 0 modules/hellobank/perso/accounts_list.py | 95 ++++++++++ modules/hellobank/perso/login.py | 118 ++++++++++++ modules/hellobank/perso/messages.py | 82 ++++++++ modules/hellobank/perso/transactions.py | 96 ++++++++++ modules/hellobank/perso/transfer.py | 111 +++++++++++ modules/hellobank/test.py | 38 ++++ 10 files changed, 942 insertions(+) create mode 100644 modules/hellobank/__init__.py create mode 100755 modules/hellobank/backend.py create mode 100755 modules/hellobank/browser.py create mode 100644 modules/hellobank/perso/__init__.py create mode 100755 modules/hellobank/perso/accounts_list.py create mode 100755 modules/hellobank/perso/login.py create mode 100755 modules/hellobank/perso/messages.py create mode 100755 modules/hellobank/perso/transactions.py create mode 100755 modules/hellobank/perso/transfer.py create mode 100755 modules/hellobank/test.py diff --git a/modules/hellobank/__init__.py b/modules/hellobank/__init__.py new file mode 100644 index 00000000..1bcadf01 --- /dev/null +++ b/modules/hellobank/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010-2011 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 . + + +from .backend import HelloBankBackend + +__all__ = ['HelloBankBackend'] diff --git a/modules/hellobank/backend.py b/modules/hellobank/backend.py new file mode 100755 index 00000000..9e977744 --- /dev/null +++ b/modules/hellobank/backend.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Christophe Lampin +# Copyright(C) 2010-2012 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 . + + +# python2.5 compatibility +from __future__ import with_statement + +from decimal import Decimal +from datetime import datetime, timedelta + +from weboob.capabilities.bank import ICapBank, AccountNotFound, Account, Recipient +from weboob.capabilities.messages import ICapMessages, Thread +from weboob.tools.backend import BaseBackend, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import HelloBank + + +__all__ = ['HelloBankBackend'] + + +class HelloBankBackend(BaseBackend, ICapBank, ICapMessages): + NAME = 'hellobank' + MAINTAINER = u'Christophe Lampin' + EMAIL = 'weboob@lampin.net' + VERSION = '0.g' + LICENSE = 'AGPLv3+' + DESCRIPTION = 'Hello Bank ! website' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), + ValueBackendPassword('password', label='Password', regexp='^(\d{6}|)$')) + BROWSER = HelloBank + STORAGE = {'seen': []} + + # Store the messages *list* for this duration + CACHE_THREADS = timedelta(seconds=3 * 60 * 60) + + def __init__(self, *args, **kwargs): + BaseBackend.__init__(self, *args, **kwargs) + self._threads = None + self._threads_age = datetime.utcnow() + + def create_default_browser(self): + return self.create_browser(self.config['login'].get(), + self.config['password'].get()) + + def iter_accounts(self): + for account in self.browser.get_accounts_list(): + yield account + + def get_account(self, _id): + if not _id.isdigit(): + raise AccountNotFound() + with self.browser: + account = self.browser.get_account(_id) + if account: + return account + else: + raise AccountNotFound() + + def iter_history(self, account): + with self.browser: + return self.browser.iter_history(account) + + def iter_coming(self, account): + with self.browser: + return self.browser.iter_coming_operations(account) + + def iter_transfer_recipients(self, ignored): + for account in self.browser.get_transfer_accounts().itervalues(): + recipient = Recipient() + recipient.id = account.id + recipient.label = account.label + yield recipient + + def transfer(self, account, to, amount, reason=None): + if isinstance(account, Account): + account = account.id + + try: + assert account.isdigit() + assert to.isdigit() + amount = Decimal(amount) + except (AssertionError, ValueError): + raise AccountNotFound() + + with self.browser: + return self.browser.transfer(account, to, amount, reason) + + def iter_threads(self, cache=False): + """ + If cache is False, always fetch the threads from the website. + """ + old = self._threads_age < datetime.utcnow() - self.CACHE_THREADS + threads = self._threads + if not cache or threads is None or old: + with self.browser: + threads = list(self.browser.iter_threads()) + # the website is stupid and does not have the messages in the proper order + threads = sorted(threads, key=lambda t: t.date, reverse=True) + self._threads = threads + seen = self.storage.get('seen', default=[]) + for thread in threads: + if thread.id not in seen: + thread.root.flags |= thread.root.IS_UNREAD + else: + thread.root.flags &= ~thread.root.IS_UNREAD + yield thread + + def fill_thread(self, thread, fields=None): + if fields is None or 'root' in fields: + return self.get_thread(thread) + + def get_thread(self, _id): + if isinstance(_id, Thread): + thread = _id + _id = thread.id + else: + thread = Thread(_id) + with self.browser: + thread = self.browser.get_thread(thread) + return thread + + def iter_unread_messages(self): + threads = list(self.iter_threads(cache=True)) + for thread in threads: + if thread.root.flags & thread.root.IS_UNREAD: + thread = self.fillobj(thread) or thread + yield thread.root + + def set_message_read(self, message): + self.storage.get('seen', default=[]).append(message.thread.id) + self.storage.save() + + OBJECTS = {Thread: fill_thread} diff --git a/modules/hellobank/browser.py b/modules/hellobank/browser.py new file mode 100755 index 00000000..f13a2747 --- /dev/null +++ b/modules/hellobank/browser.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Christophe Lampin +# Copyright(C) 2009-2013 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 . + + +import urllib +import mechanize +from datetime import datetime +from logging import warning + +from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword, BrowserPasswordExpired +from weboob.capabilities.bank import TransferError, Transfer + +from .perso.accounts_list import AccountsList, AccountPrelevement +from .perso.transactions import AccountHistory, AccountComing +from .perso.transfer import TransferPage, TransferConfirmPage, TransferCompletePage +from .perso.login import LoginPage, ConfirmPage, InfoMessagePage +from .perso.messages import MessagePage, MessagesPage + +__all__ = ['HelloBank'] + + +class HelloBank(BaseBrowser): + DOMAIN = 'client.hellobank.fr' + PROTOCOL = 'https' + ENCODING = None # refer to the HTML encoding + PAGES = {'.*TableauBord.*': AccountsList, + '.*type=folder.*': AccountHistory, + '.*pageId=mouvementsavenir.*': AccountComing, + '.*NS_AVEDP.*': AccountPrelevement, + '.*NS_VIRDF.*': TransferPage, + '.*NS_VIRDC.*': TransferConfirmPage, + '.*/NS_VIRDA\?stp=(?P\d+).*': TransferCompletePage, + '.*type=homeconnex.*': LoginPage, + '.*layout=HomeConnexion.*': ConfirmPage, + '.*SAF_CHM_VALID.*': ConfirmPage, + '.*Action=DSP_MSG.*': InfoMessagePage, + '.*Messages_recus.*': MessagesPage, + '.*Lire_Message.*': MessagePage, + } + + def __init__(self, *args, **kwargs): + BaseBrowser.__init__(self, *args, **kwargs) + + def home(self): + self.location('https://client.hellobank.fr/banque/portail/digitale/HomeConnexion?type=homeconnex') + + def is_logged(self): + return not self.is_on_page(LoginPage) + + def login(self): + assert isinstance(self.username, basestring) + assert isinstance(self.password, basestring) + assert self.password.isdigit() + + if not self.is_on_page(LoginPage): + self.home() + + self.page.login(self.username, self.password) + self.location('/NSFR?Action=DSP_VGLOBALE', no_login=True) + + if self.is_on_page(LoginPage): + raise BrowserIncorrectPassword() + + def get_accounts_list(self): + # We have to parse transfer page to get the IBAN numbers + if not self.is_on_page(TransferPage): + now = datetime.now() + self.location('/NS_VIRDF?Origine=DSP_VIR&stp=%s' % now.strftime("%Y%m%d%H%M%S")) + + accounts = self.page.get_accounts() + if len(accounts) == 0: + print 'no accounts' + # oops, no accounts? check if we have not exhausted the allowed use + # of this password + for img in self.document.getroot().cssselect('img[align="middle"]'): + if img.attrib.get('alt', '') == 'Changez votre code secret': + raise BrowserPasswordExpired('Your password has expired') + self.location('/NSFR?Action=DSP_VGLOBALE') + return self.page.get_list(accounts) + + def get_account(self, id): + assert isinstance(id, basestring) + + l = self.get_accounts_list() + for a in l: + if a.id == id: + return a + + return None + + def get_IBAN_from_account(self, account): + self.go_to_history_page(account) + return self.page.get_IBAN() + + def go_to_history_page(self,account): + if account._link_id is None: + return iter([]) + + if not self.is_on_page(AccountsList): + self.location('/NSFR?Action=DSP_VGLOBALE') + + data = {'gt': 'homepage:basic-theme', + 'externalIAId': 'IAStatements', + 'cboFlowName': 'flow/iastatement', + 'contractId': account._link_id, + 'groupId': '-2', + 'pastOrPendingOperations': 1, + 'groupSelected':'-2', + 'step': 'STAMENTS', + 'pageId': 'releveoperations', + 'sendEUD': 'true', + } + self.location('/udc', urllib.urlencode(data)) + + return None + + def go_to_coming_operations_page(self,account): + if account._link_id is None: + return iter([]) + + if not self.is_on_page(AccountsList): + self.location('/NSFR?Action=DSP_VGLOBALE') + + data = {'gt': 'homepage:basic-theme', + 'externalIAId': 'IAStatements', + 'cboFlowName': 'flow/iastatement', + 'contractId': account._link_id, + 'groupId': '-2', + 'pastOrPendingOperations': 2, + 'groupSelected':'-2', + 'step': 'STAMENTS', + 'pageId': 'mouvementsavenir', + 'sendEUD': 'true', + } + self.location('/udc', urllib.urlencode(data)) + + return None + + def iter_history(self, account): + self.go_to_history_page(account) + return self.page.iter_operations() + + def iter_coming_operations(self, account): + self.go_to_coming_operations_page(account) + return self.page.iter_coming_operations() + + def get_transfer_accounts(self): + if not self.is_on_page(TransferPage): + self.location('/NS_VIRDF') + + assert self.is_on_page(TransferPage) + return self.page.get_accounts() + + def transfer(self, from_id, to_id, amount, reason=None): + if not self.is_on_page(TransferPage): + self.location('/NS_VIRDF') + + # Need to clean HTML before parse it + html = self.response().get_data().replace(". + + +import re +from decimal import Decimal + +from weboob.tools.capabilities.bank.transactions import FrenchTransaction +from weboob.capabilities.bank import Account +from weboob.capabilities.base import NotAvailable +from weboob.tools.browser import BasePage, BrokenPageError, BrowserPasswordExpired +from weboob.tools.json import json +import unicodedata as ud + +__all__ = ['AccountsList', 'AccountPrelevement'] + + +class AccountsList(BasePage): + ACCOUNT_TYPES = { + 1: Account.TYPE_CHECKING, + 2: Account.TYPE_SAVINGS, + 3: Account.TYPE_DEPOSIT, + 5: Account.TYPE_MARKET, # FIX ME : I don't know the right code + 9: Account.TYPE_LOAN, + } + + def on_loaded(self): + pass + + def get_list(self, accounts_ids): + l = [] + # Read the json data + json_data = self.browser.readurl('/banque/PA_Autonomy-war/ProxyIAService?cleOutil=IA_SMC_UDC&service=getlstcpt&dashboard=true&refreshSession=true&cre=udc&poka=true') + json_infos = json.loads(json_data) + for famille in json_infos['smc']['data']['familleCompte']: + id_famille = famille['idFamilleCompte'] + for compte in famille['compte']: + account = Account() + account.label = u''+compte['libellePersoProduit'] + account.currency = account.get_currency(compte['devise']) + account.balance = Decimal(compte['soldeDispo']) + account.coming = Decimal(compte['soldeAVenir']) + account.type = self.ACCOUNT_TYPES.get(id_famille, Account.TYPE_UNKNOWN) + account.id = 0 + account._link_id = 'KEY'+compte['key'] + + # IBAN aren't in JSON + # Fast method, get it from transfer page. + for i,a in accounts_ids.items(): + if a.label == account.label: + account.id = i + # But it's doesn't work with LOAN and MARKET, so use slow method : Get it from transaction page. + if account.id == 0: + account.id = self.browser.get_IBAN_from_account(account) + l.append(account) + + if len(l) == 0: + print 'no accounts' + # oops, no accounts? check if we have not exhausted the allowed use + # of this password + for img in self.document.getroot().cssselect('img[align="middle"]'): + if img.attrib.get('alt', '') == 'Changez votre code secret': + raise BrowserPasswordExpired('Your password has expired') + return l + + def get_execution_id(self): + return self.document.xpath('//input[@name="_flowExecutionKey"]')[0].attrib['value'] + + def get_messages_link(self): + """ + Get the link to the messages page, which seems to have an identifier in it. + """ + return self.document.xpath('//a[@title="Messagerie"]')[0].attrib['href'] + + + +class AccountPrelevement(AccountsList): + pass diff --git a/modules/hellobank/perso/login.py b/modules/hellobank/perso/login.py new file mode 100755 index 00000000..8d61db8e --- /dev/null +++ b/modules/hellobank/perso/login.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Christophe Lampin +# Copyright(C) 2009-2011 Romain Bignon, Pierre Mazière +# +# 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 . + + +import time +import re +from weboob.tools.mech import ClientForm +import urllib + +from weboob.tools.browser import BasePage, BrowserUnavailable +from weboob.tools.captcha.virtkeyboard import VirtKeyboard,VirtKeyboardError + +__all__ = ['LoginPage', 'ConfirmPage', 'ChangePasswordPage'] + + +class HelloBankVirtKeyboard(VirtKeyboard): + symbols={'0':'4d1e060efb694ee60e4bd062d800401c', + '1':'509134b5c09980e282cdd5867815e9e3', + '2':'4cd09c9c44405e00b12e0371e2f972ae', + '3':'227d854efc5623292eda4ca2f9bfc4d7', + '4':'be8d23e7f5fce646193b7b520ff80443', + '5':'5fe450b35c946c3a983f1df6e5b41fd1', + '6':'113a6f63714f5094c7f0b25caaa66f78', + '7':'de0e93ba880a8a052aea79237f08f3f8', + '8':'3d70474c05c240b606556c89baca0568', + '9':'040954a5e5e93ec2fb03ac0cfe592ac2' + } + + + url="/NSImgBDGrille?timestamp=%d" + + color=17 + + def __init__(self,basepage): + coords = {} + coords["01"] = (31,28,49,49) + coords["02"] = (108,28,126,49) + coords["03"] = (185,28,203,49) + coords["04"] = (262,28,280,49) + coords["05"] = (339,28,357,49) + coords["06"] = (31,100,49,121) + coords["07"] = (108,100,126,121) + coords["08"] = (185,100,203,121) + coords["09"] = (262,100,280,121) + coords["10"] = (339,100,357,121) + + VirtKeyboard.__init__(self,basepage.browser.openurl(self.url % time.time()),coords,self.color) + self.check_symbols(self.symbols,basepage.browser.responses_dirname) + + def get_symbol_code(self,md5sum): + code=VirtKeyboard.get_symbol_code(self,md5sum) + return code + + def get_string_code(self,string): + code='' + for c in string: + code+=self.get_symbol_code(self.symbols[c]) + return code + + +class LoginPage(BasePage): + def on_loaded(self): + for td in self.document.getroot().cssselect('td.LibelleErreur'): + if td.text is None: + continue + msg = td.text.strip() + if 'indisponible' in msg: + raise BrowserUnavailable(msg) + + def login(self, login, password): + try: + vk=HelloBankVirtKeyboard(self) + except VirtKeyboardError,err: + self.logger.error("Error: %s"%err) + return False + + self.browser.select_form('logincanalnet') + self.browser.set_all_readonly(False) + self.browser['ch1'] = login.encode('utf-8') + self.browser['ch5'] = vk.get_string_code(password) + self.browser.submit() + + +class ConfirmPage(BasePage): + def get_error(self): + for td in self.document.xpath('//td[@class="hdvon1"]'): + if td.text: + return td.text.strip() + return None + + def get_relocate_url(self): + script = self.document.xpath('//script')[0] + m = re.match('document.location.replace\("(.*)"\)', script.text[script.text.find('document.location.replace'):]) + if m: + return m.group(1) + + +class InfoMessagePage(BasePage): + def on_loaded(self): + pass + diff --git a/modules/hellobank/perso/messages.py b/modules/hellobank/perso/messages.py new file mode 100755 index 00000000..6a608880 --- /dev/null +++ b/modules/hellobank/perso/messages.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2012 Laurent Bachelier +# +# 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 . + + +from weboob.tools.browser import BasePage, BrokenPageError +from weboob.capabilities.messages import Message, Thread +from weboob.capabilities.base import NotLoaded +from weboob.tools.capabilities.messages.genericArticle import try_drop_tree + +import re +from datetime import datetime + +from lxml.html import make_links_absolute + +__all__ = ['MessagesPage', 'MessagePage'] + + +class MessagesPage(BasePage): + def iter_threads(self): + table = self.parser.select(self.document.getroot(), 'table#listeMessages', 1) + for tr in table.xpath('./tr'): + if tr.attrib.get('class', '') not in ('msgLu', 'msgNonLu'): + continue + author = unicode(self.parser.select(tr, 'td.colEmetteur', 1).text) + link = self.parser.select(tr, 'td.colObjet a', 1) + date_raw = self.parser.select(tr, 'td.colDate1', 1).attrib['data'] + jsparams = re.search('\((.+)\)', link.attrib['onclick']).groups()[0] + jsparams = [i.strip('\'" ') for i in jsparams.split(',')] + page_id, _id, unread = jsparams + # this means unread on the website + unread = False if unread == "false" else True + # 2012/02/29:01h30min45sec + dt_match = re.match('(\d+)/(\d+)/(\d+):(\d+)h(\d+)min(\d+)sec', date_raw).groups() + dt_match = [int(d) for d in dt_match] + thread = Thread(_id) + thread._link_id = (page_id, unread) + thread.date = datetime(*dt_match) + thread.title = unicode(link.text) + message = Message(thread, 0) + message.set_empty_fields(None) + message.flags = message.IS_HTML + message.title = thread.title + message.date = thread.date + message.sender = author + message.content = NotLoaded # This is the only thing we are missing + thread.root = message + yield thread + + +class MessagePage(BasePage): + def get_content(self): + """ + Get the message content. + This page has a date, but it is less precise than the main list page, + so we only use it for the message content. + """ + try: + content = self.parser.select(self.document.getroot(), + 'div.txtMessage div.contenu', 1) + except BrokenPageError: + # This happens with some old messages (2007) + content = self.parser.select(self.document.getroot(), 'div.txtMessage', 1) + + content = make_links_absolute(content, self.url) + try_drop_tree(self.parser, content, 'script') + return self.parser.tostring(content) diff --git a/modules/hellobank/perso/transactions.py b/modules/hellobank/perso/transactions.py new file mode 100755 index 00000000..bacb2a50 --- /dev/null +++ b/modules/hellobank/perso/transactions.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Christophe Lampin +# Copyright(C) 2009-2012 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 . + + +import re + +from weboob.tools.browser import BasePage +from weboob.tools.capabilities.bank.transactions import FrenchTransaction + + +__all__ = ['AccountHistory', 'AccountComing'] + + +class Transaction(FrenchTransaction): + PATTERNS = [(re.compile(u'^(?PCHEQUE)(?P.*)'), FrenchTransaction.TYPE_CHECK), + (re.compile('^(?PFACTURE CARTE) DU (?P
\d{2})(?P\d{2})(?P\d{2}) (?P.*?)( CA?R?T?E? ?\d*X*\d*)?$'), + FrenchTransaction.TYPE_CARD), + (re.compile('^(?P(PRELEVEMENT|TELEREGLEMENT|TIP)) (?P.*)'), + FrenchTransaction.TYPE_ORDER), + (re.compile('^(?PECHEANCEPRET)(?P.*)'), FrenchTransaction.TYPE_LOAN_PAYMENT), + (re.compile('^(?PRETRAIT DAB) (?P
\d{2})/(?P\d{2})/(?P\d{2})( (?P\d+)H(?P\d+))? (?P.*)'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('^(?PVIR(EMEN)?T? ((RECU|FAVEUR) TIERS|SEPA RECU)?)( /FRM)?(?P.*)'), + FrenchTransaction.TYPE_TRANSFER), + (re.compile('^(?PREMBOURST) CB DU (?P
\d{2})(?P\d{2})(?P\d{2}) (?P.*)'), + FrenchTransaction.TYPE_PAYBACK), + (re.compile('^(?PREMBOURST)(?P.*)'), FrenchTransaction.TYPE_PAYBACK), + (re.compile('^(?PCOMMISSIONS)(?P.*)'), FrenchTransaction.TYPE_BANK), + (re.compile('^(?P(?PREMUNERATION).*)'), FrenchTransaction.TYPE_BANK), + (re.compile('^(?PREMISE CHEQUES)(?P.*)'), FrenchTransaction.TYPE_DEPOSIT), + ] + + +class AccountHistory(BasePage): + def iter_operations(self): + for tr in self.document.xpath('//table[@id="tableCompte"]//tr'): + if len(tr.xpath('td[@class="debit"]')) == 0: + continue + + id = tr.find('td').find('input').attrib['id'].lstrip('_') + op = Transaction(id) + op.parse(date=tr.findall('td')[1].text, + raw=tr.findall('td')[2].text.replace(u'\xa0', u'')) + + debit = tr.xpath('.//td[@class="debit"]')[0].text + credit = tr.xpath('.//td[@class="credit"]')[0].text + + op.set_amount(credit, debit) + + yield op + + def iter_coming_operations(self): + i = 0 + for tr in self.document.xpath('//table[@id="tableauOperations"]//tr'): + if 'typeop' in tr.attrib: + tds = tr.findall('td') + if len(tds) != 3: + continue + + text = tds[1].text or u'' + text = text.replace(u'\xa0', u'') + for child in tds[1].getchildren(): + if child.text: + text += child.text + if child.tail: + text += child.tail + + i += 1 + operation = Transaction(i) + operation.parse(date=tr.attrib['dateop'], + raw=text) + operation.set_amount(tds[2].text) + yield operation + + def get_IBAN(self): + return self.document.xpath('//a[@class="lien_perso_libelle"]')[0].attrib['id'][10:26] + +class AccountComing(AccountHistory): + pass diff --git a/modules/hellobank/perso/transfer.py b/modules/hellobank/perso/transfer.py new file mode 100755 index 00000000..55e9804b --- /dev/null +++ b/modules/hellobank/perso/transfer.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Christophe Lampin +# Copyright(C) 2010-2011 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 . + + +import re + +from weboob.tools.browser import BasePage, BrowserPasswordExpired +from weboob.tools.ordereddict import OrderedDict +from weboob.capabilities.bank import TransferError + + +__all__ = ['TransferPage', 'TransferConfirmPage', 'TransferCompletePage'] + + +class Account(object): + def __init__(self, id, label, send_checkbox, receive_checkbox): + self.id = id + self.label = label + self.send_checkbox = send_checkbox + self.receive_checkbox = receive_checkbox + + +class TransferPage(BasePage): + def on_loaded(self): + for td in self.document.xpath('//td[@class="hdvon1"]'): + if td.text and 'Vous avez atteint le seuil de' in td.text: + raise BrowserPasswordExpired(td.text.strip()) + + def get_accounts(self): + accounts = OrderedDict() + first = True + for table in self.document.xpath('//table[@class="tableCompte"]'): + if first: + first = False + for tr in table.cssselect('tr.hdoc1, tr.hdotc1'): + tds = tr.findall('td') + id = tds[1].text.replace(u'\xa0', u'') + label = tds[0].text.replace(u'\xa0', u' ') + if label is None and tds[0].find('nobr') is not None: + label = tds[0].find('nobr').text + send_checkbox = tds[4].find('input').attrib['value'] if tds[4].find('input') is not None else None + receive_checkbox = tds[5].find('input').attrib['value'] if tds[5].find('input') is not None else None + account = Account(id, label, send_checkbox, receive_checkbox) + accounts[id] = account + return accounts + + def transfer(self, from_id, to_id, amount, reason): + accounts = self.get_accounts() + + # Transform RIBs to short IDs + if len(str(from_id)) == 23: + from_id = str(from_id)[5:21] + if len(str(to_id)) == 23: + to_id = str(to_id)[5:21] + + try: + sender = accounts[from_id] + except KeyError: + raise TransferError('Account %s not found' % from_id) + + try: + recipient = accounts[to_id] + except KeyError: + raise TransferError('Recipient %s not found' % to_id) + + if sender.send_checkbox is None: + raise TransferError('Unable to make a transfer from %s' % sender.label) + if recipient.receive_checkbox is None: + raise TransferError('Unable to make a transfer to %s' % recipient.label) + + self.browser.select_form(nr=0) + self.browser['C1'] = [sender.send_checkbox] + self.browser['C2'] = [recipient.receive_checkbox] + self.browser['T6'] = str(amount).replace('.', ',') + if reason: + self.browser['T5'] = reason.encode('utf-8') + self.browser.submit() + + +class TransferConfirmPage(BasePage): + def on_loaded(self): + for td in self.document.getroot().cssselect('td#size2'): + raise TransferError(td.text.strip()) + + for a in self.document.getiterator('a'): + m = re.match('/NSFR\?Action=VIRDA&stp=(\d+)', a.attrib['href']) + if m: + self.browser.location('/NS_VIRDA?stp=%s' % m.group(1)) + return + + +class TransferCompletePage(BasePage): + def get_id(self): + return self.group_dict['id'] diff --git a/modules/hellobank/test.py b/modules/hellobank/test.py new file mode 100755 index 00000000..3e191316 --- /dev/null +++ b/modules/hellobank/test.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2010-2011 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 . + + +from weboob.tools.test import BackendTest +from random import choice + + +class HelloBankTest(BackendTest): + BACKEND = 'hellobank' + + def test_bank(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)) + + def test_msgs(self): + threads = list(self.backend.iter_threads()) + thread = self.backend.fillobj(choice(threads), ['root']) + assert len(thread.root.content)