diff --git a/modules/barclays/__init__.py b/modules/barclays/__init__.py new file mode 100644 index 00000000..0a9e0b12 --- /dev/null +++ b/modules/barclays/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 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 . + + +from .backend import BarclaysBackend + +__all__ = ['BarclaysBackend'] diff --git a/modules/barclays/backend.py b/modules/barclays/backend.py new file mode 100644 index 00000000..f286af66 --- /dev/null +++ b/modules/barclays/backend.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 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 . + + +from weboob.capabilities.bank import ICapBank, AccountNotFound +from weboob.tools.backend import BaseBackend, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import Barclays + + +__all__ = ['BarclaysBackend'] + + +class BarclaysBackend(BaseBackend, ICapBank): + NAME = 'barclays' + MAINTAINER = u'Romain Bignon' + EMAIL = 'romain@weboob.org' + VERSION = '0.e' + DESCRIPTION = u'Barclays French bank website' + LICENSE = 'AGPLv3+' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), + ValueBackendPassword('password', label='Password'), + ValueBackendPassword('secret', label='Secret word')) + BROWSER = Barclays + + def create_default_browser(self): + return self.create_browser(self.config['secret'].get(), + self.config['login'].get(), + self.config['password'].get()) + + def iter_accounts(self): + with self.browser: + return self.browser.get_accounts_list() + + def get_account(self, _id): + 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.get_history(account) + + def iter_coming(self, account): + with self.browser: + return self.browser.get_coming_operations(account) diff --git a/modules/barclays/browser.py b/modules/barclays/browser.py new file mode 100644 index 00000000..ae92fbd1 --- /dev/null +++ b/modules/barclays/browser.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 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 . + + +from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword + +from .pages import LoginPage, Login2Page, IndexPage, AccountsPage, TransactionsPage, CardPage, ValuationPage + + +__all__ = ['Barclays'] + + +class Barclays(BaseBrowser): + PROTOCOL = 'https' + DOMAIN = 'www.barclays.fr' + PAGES = {'https?://.*.barclays.fr/\d-index.html': IndexPage, + 'https://.*.barclays.fr/barclaysnetV2/logininstit.do.*': LoginPage, + 'https://.*.barclays.fr/barclaysnetV2/loginSecurite.do.*': Login2Page, + 'https://.*.barclays.fr/barclaysnetV2/tbord.do.*': AccountsPage, + 'https://.*.barclays.fr/barclaysnetV2/releve.do.*': TransactionsPage, + 'https://.*.barclays.fr/barclaysnetV2/cartes.do.*': CardPage, + 'https://.*.barclays.fr/barclaysnetV2/valuationViewBank.do.*': ValuationPage, + } + + def __init__(self, secret, *args, **kwargs): + self.secret = secret + + BaseBrowser.__init__(self, *args, **kwargs) + + def is_logged(self): + return self.page is not None and not self.is_on_page((LoginPage, IndexPage, LoginPage)) + + def home(self): + if self.is_logged(): + self.location('tbord.do') + else: + self.login() + + def login(self): + """ + Attempt to log in. + Note: this method does nothing if we are already logged in. + """ + assert isinstance(self.username, basestring) + assert isinstance(self.password, basestring) + + if self.is_logged(): + return + + if not self.is_on_page(LoginPage): + self.location('https://b-net.barclays.fr/barclaysnetV2/logininstit.do?lang=fr&nodoctype=0', no_login=True) + + self.page.login(self.username, self.password) + + if not self.page.has_redirect(): + raise BrowserIncorrectPassword() + + self.location('loginSecurite.do', no_login=True) + + self.page.login(self.secret) + + if not self.is_logged(): + raise BrowserIncorrectPassword() + + def get_accounts_list(self): + if not self.is_on_page(AccountsPage): + self.location('tbord.do') + return self.page.get_list() + + 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_history(self, account): + self.location(account._link) + + assert self.is_on_page((TransactionsPage, ValuationPage)) + + return self.page.get_history() + + def get_coming_operations(self, account): + for card in account._card_links: + self.location(card) + + assert self.is_on_page(CardPage) + + for tr in self.page.get_history(): + yield tr diff --git a/modules/barclays/favicon.png b/modules/barclays/favicon.png new file mode 100644 index 00000000..fe46a7a2 Binary files /dev/null and b/modules/barclays/favicon.png differ diff --git a/modules/barclays/pages.py b/modules/barclays/pages.py new file mode 100644 index 00000000..02ebe2b8 --- /dev/null +++ b/modules/barclays/pages.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 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 . + + +from decimal import Decimal +import re + +from weboob.tools.browser import BasePage +from weboob.capabilities.bank import Account +from weboob.tools.capabilities.bank.transactions import FrenchTransaction + + +__all__ = ['LoginPage', 'Login2Page', 'IndexPage', 'AccountsPage', 'TransactionsPage', + 'CardPage', 'ValuationPage'] + + +class LoginPage(BasePage): + def login(self, login, passwd): + self.browser.select_form(name='frmLogin') + self.browser['username'] = login.encode(self.browser.ENCODING) + self.browser['password'] = passwd.encode(self.browser.ENCODING) + self.browser.submit(nologin=True) + + def has_redirect(self): + if len(self.document.getroot().xpath('//form')) > 0: + return False + else: + return True + +class Login2Page(BasePage): + def login(self, secret): + label = self.document.xpath('//span[@class="PF_LABEL"]')[0].text.strip() + letters = '' + for n in re.findall('(\d+)', label): + letters += secret[int(n) - 1] + + self.browser.select_form(name='frmControl') + self.browser['word'] = letters + self.browser.submit(name='valider', nologin=True) + +class IndexPage(BasePage): + pass + +class AccountsPage(BasePage): + ACCOUNT_TYPES = {u'Epargne': Account.TYPE_SAVINGS, + u'Liquidités': Account.TYPE_CHECKING, + } + + def get_list(self): + accounts = [] + + for block in self.document.xpath('//div[@class="pave"]/div'): + head_type = block.xpath('./div/span[@class="accGroupLabel"]')[0].text.strip() + account_type = self.ACCOUNT_TYPES.get(head_type, Account.TYPE_UNKNOWN) + for tr in block.cssselect('ul li.tbord_account'): + id = tr.attrib.get('id', '') + if id.find('contratId') != 0: + self.logger.warning('Unable to parse contract ID: %r' % id) + continue + id = id[id.find('contratId')+len('contratId'):] + + link = tr.cssselect('span.accountLabel a')[0] + balance = Decimal(FrenchTransaction.clean_amount(tr.cssselect('span.accountTotal')[0].text)) + + if id.endswith('CRT'): + account = accounts[-1] + account._card_links.append(link.attrib['href']) + if not account.coming: + account.coming = Decimal('0.0') + account.coming += balance + continue + + account = Account() + account.id = id + account.label = unicode(link.text.strip()) + account.type = account_type + account.balance = balance + account.currency = account.get_currency(tr.cssselect('span.accountDev')[0].text) + account._link = link.attrib['href'] + account._card_links = [] + accounts.append(account) + + return accounts + + +class Transaction(FrenchTransaction): + PATTERNS = [(re.compile('^RET DAB (?P.*?) RETRAIT DU (?P
\d{2})(?P\d{2})(?P\d{2}).*'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('^RET DAB (?P.*?) CARTE ?:.*'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('^(?P.*) RETRAIT DU (?P
\d{2})(?P\d{2})(?P\d{2}) .*'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('(\w+) (?P
\d{2})(?P\d{2})(?P\d{2}) CB[:\*][^ ]+ (?P.*)'), + FrenchTransaction.TYPE_CARD), + (re.compile('^(?PVIR(EMEN)?T? (SEPA)?(RECU|FAVEUR)?)( /FRM)?(?P.*)'), + FrenchTransaction.TYPE_TRANSFER), + (re.compile('^PRLV (?P.*) (REF \w+)?$'),FrenchTransaction.TYPE_ORDER), + (re.compile('^CHEQUE.*? (REF \w+)?$'), FrenchTransaction.TYPE_CHECK), + (re.compile('^(AGIOS /|FRAIS) (?P.*)'), FrenchTransaction.TYPE_BANK), + (re.compile('^(CONVENTION \d+ )?COTIS(ATION)? (?P.*)'), + FrenchTransaction.TYPE_BANK), + (re.compile('^REMISE (?P.*)'), FrenchTransaction.TYPE_DEPOSIT), + (re.compile('^(?P.*)( \d+)? QUITTANCE .*'), + FrenchTransaction.TYPE_ORDER), + (re.compile('^.* LE (?P
\d{2})/(?P\d{2})/(?P\d{2})$'), + FrenchTransaction.TYPE_UNKNOWN), + ] + + +class TransactionsPage(BasePage): + def get_history(self): + for tr in self.document.xpath('//table[@id="operation"]/tbody/tr'): + tds = tr.findall('td') + + if len(tds) < 5: + continue + + t = Transaction(tds[-1].findall('img')[-1].attrib.get('id', '')) + + date = u''.join([txt.strip() for txt in tds[0].itertext()]) + raw = u' '.join([txt.strip() for txt in tds[1].itertext()]) + debit = u''.join([txt.strip() for txt in tds[-3].itertext()]) + credit = u''.join([txt.strip() for txt in tds[-2].itertext()]) + t.parse(date, re.sub(r'[ ]+', ' ', raw)) + t.set_amount(credit, debit) + + if t.raw.startswith('ACHAT CARTE -DEBIT DIFFERE'): + continue + + yield t + + +class CardPage(BasePage): + def get_history(self): + for tr in self.document.xpath('//table[@class="report"]/tbody/tr'): + tds = tr.findall('td') + + if len(tds) != 3: + #header + continue + + t = Transaction(0) + date = u''.join([txt.strip() for txt in tds[0].itertext()]) + raw = u' '.join([txt.strip() for txt in tds[1].itertext()]) + amount = u''.join([txt.strip() for txt in tds[-1].itertext()]) + t.parse(date, re.sub(r'[ ]+', ' ', raw)) + t.label = tds[1].find('span').text.strip() + t.type = t.TYPE_CARD + t.set_amount(amount) + yield t + +class ValuationPage(BasePage): + def get_history(self): + return iter([]) diff --git a/modules/barclays/test.py b/modules/barclays/test.py new file mode 100644 index 00000000..ab919703 --- /dev/null +++ b/modules/barclays/test.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 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 . + + +from weboob.tools.test import BackendTest + +class BarclaysTest(BackendTest): + BACKEND = 'barclays' + + def test_banquepop(self): + l = list(self.backend.iter_accounts()) + if len(l) > 0: + a = l[0] + list(self.backend.iter_history(a))