diff --git a/modules/bred/__init__.py b/modules/bred/__init__.py
new file mode 100644
index 00000000..3b649d6c
--- /dev/null
+++ b/modules/bred/__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 BredBackend
+
+__all__ = ['BredBackend']
diff --git a/modules/bred/backend.py b/modules/bred/backend.py
new file mode 100644
index 00000000..b7cf3ce6
--- /dev/null
+++ b/modules/bred/backend.py
@@ -0,0 +1,70 @@
+# -*- 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 __future__ import with_statement
+
+from weboob.capabilities.bank import ICapBank, AccountNotFound
+from weboob.tools.backend import BaseBackend, BackendConfig
+from weboob.tools.value import ValueBackendPassword
+
+from .browser import BredBrowser
+
+
+__all__ = ['BredBackend']
+
+
+class BredBackend(BaseBackend, ICapBank):
+ NAME = 'bred'
+ MAINTAINER = 'Romain Bignon'
+ EMAIL = 'romain@weboob.org'
+ VERSION = '0.d'
+ DESCRIPTION = u'Bred French bank website'
+ LICENSE = 'AGPLv3+'
+ CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False),
+ ValueBackendPassword('password', label='Password of account'))
+ BROWSER = BredBrowser
+
+ def create_default_browser(self):
+ return self.create_browser(self.config['login'].get(), self.config['password'].get())
+
+ def iter_accounts(self):
+ with self.browser:
+ for account in self.browser.get_accounts_list():
+ yield account
+
+ 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:
+ transactions = list(self.browser.get_history(account))
+ transactions.sort(key=lambda tr: tr.rdate, reverse=True)
+ return [tr for tr in transactions if not tr._is_coming]
+
+ def iter_coming(self, account):
+ with self.browser:
+ transactions = list(self.browser.get_card_operations(account))
+ transactions.sort(key=lambda tr: tr.rdate, reverse=True)
+ return [tr for tr in transactions if tr._is_coming]
diff --git a/modules/bred/browser.py b/modules/bred/browser.py
new file mode 100644
index 00000000..5819dbcb
--- /dev/null
+++ b/modules/bred/browser.py
@@ -0,0 +1,106 @@
+# -*- 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 .
+
+
+import urllib
+
+from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword
+
+from .pages import LoginPage, LoginResultPage, AccountsPage, EmptyPage, TransactionsPage
+
+
+__all__ = ['BredBrowser']
+
+
+class BredBrowser(BaseBrowser):
+ PROTOCOL = 'https'
+ DOMAIN = 'www.bred.fr'
+ PAGES = {'https://www.bred.fr/': LoginPage,
+ 'https://www.bred.fr/Andromede/MainAuth.*': LoginResultPage,
+ 'https://www.bred.fr/Andromede/Main': AccountsPage,
+ 'https://www.bred.fr/Andromede/Ecriture': TransactionsPage,
+ 'https://www.bred.fr/Andromede/applications/index.jsp': EmptyPage,
+ }
+
+ def is_logged(self):
+ return self.page and not self.is_on_page(LoginPage)
+
+ def home(self):
+ return self.location('https://www.bred.fr/')
+
+ def login(self):
+ assert isinstance(self.username, basestring)
+ assert isinstance(self.password, basestring)
+
+ if not self.is_on_page(LoginPage):
+ self.location('https://www.bred.fr/', no_login=True)
+
+ self.page.login(self.username, self.password)
+
+ assert self.is_on_page(LoginResultPage)
+
+ error = self.page.get_error()
+ if error is not None:
+ raise BrowserIncorrectPassword(error)
+
+ self.page.confirm()
+
+ def get_accounts_list(self):
+ if not self.is_on_page(AccountsPage):
+ self.location('https://www.bred.fr/Andromede/Main')
+ 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 iter_transactions(self, id):
+ numero_compte, numero_poste = id.split('.')
+ data = {'typeDemande': 'recherche',
+ 'motRecherche': '',
+ 'numero_compte': numero_compte,
+ 'numero_poste': numero_poste,
+ 'detail': '',
+ 'tri': 'date',
+ 'sens': 'sort',
+ 'monnaie': 'EUR',
+ 'index_hist': 4
+ }
+ self.location('https://www.bred.fr/Andromede/Ecriture', urllib.urlencode(data))
+
+ assert self.is_on_page(TransactionsPage)
+ return self.page.get_history()
+
+ def get_history(self, account):
+ for tr in self.iter_transactions(account.id):
+ yield tr
+
+ for tr in self.get_card_operations(account):
+ yield tr
+
+ def get_card_operations(self, account):
+ for id in account._card_links:
+ for tr in self.iter_transactions(id):
+ yield tr
diff --git a/modules/bred/favicon.png b/modules/bred/favicon.png
new file mode 100644
index 00000000..2925f407
Binary files /dev/null and b/modules/bred/favicon.png differ
diff --git a/modules/bred/pages.py b/modules/bred/pages.py
new file mode 100644
index 00000000..dbc3538a
--- /dev/null
+++ b/modules/bred/pages.py
@@ -0,0 +1,175 @@
+# -*- 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.tools.misc import to_unicode
+from weboob.capabilities.bank import Account
+from weboob.tools.capabilities.bank.transactions import FrenchTransaction
+
+
+__all__ = ['LoginPage', 'LoginResultPage', 'AccountsPage', 'TransactionsPage', 'EmptyPage']
+
+
+class LoginPage(BasePage):
+ def login(self, login, passwd):
+ self.browser.select_form(name='authen')
+ self.browser['id'] = login
+ self.browser['pass'] = passwd
+ self.browser.submit(nologin=True)
+
+class LoginResultPage(BasePage):
+ def confirm(self):
+ self.browser.location('MainAuth?typeDemande=AC', no_login=True)
+
+ def get_error(self):
+ error = self.document.xpath('//td[@class="txt_norm2"]/b')
+ if len(error) == 0:
+ return None
+
+ return error[0].text.strip()
+
+class EmptyPage(BasePage):
+ pass
+
+class BredBasePage(BasePage):
+ def js2args(self, s):
+ cur_arg = None
+ args = {}
+ # For example:
+ # javascript:reloadApplication('nom_application', 'compte_telechargement', 'numero_poste', '000', 'numero_compte', '12345678901','monnaie','EUR');
+ for sub in re.findall("'([^']+)'", s):
+ if cur_arg is None:
+ cur_arg = sub
+ else:
+ args[cur_arg] = sub
+ cur_arg = None
+
+ return args
+
+class AccountsPage(BredBasePage):
+ def get_list(self):
+ accounts = []
+
+ for tr in self.document.xpath('//table[@class="compteTable"]/tr'):
+ if not tr.attrib.get('class', '').startswith('ligne_'):
+ continue
+
+ cols = tr.findall('td')
+
+ amount = Decimal(u''.join([txt.strip() for txt in cols[-1].itertext()]).strip(' EUR').replace(' ', '').replace(',', '.'))
+ a = cols[0].find('a')
+ if a is None:
+ # this line is a cards line. attach it on the first account.
+ if len(accounts) == 0:
+ self.logger.warning('There is a card link but no accounts!')
+ continue
+
+ for a in cols[0].xpath('.//li/a'):
+ args = self.js2args(a.attrib['href'])
+ if not 'numero_compte' in args or not 'numero_poste' in args:
+ self.logger.warning('Card link with strange args: %s' % args)
+ continue
+
+ accounts[0]._card_links.append('%s.%s' % (args['numero_compte'], args['numero_poste']))
+ if not accounts[0].coming:
+ accounts[0].coming = Decimal('0.0')
+ accounts[0].coming += amount
+ continue
+
+ args = self.js2args(a.attrib['href'])
+
+ account = Account()
+ account.id = u'%s.%s' % (args['numero_compte'], args['numero_poste'])
+ account.label = to_unicode(a.attrib.get('alt', a.text.strip()))
+ account.balance = amount
+ account._card_links = []
+ accounts.append(account)
+
+ return accounts
+
+class Transaction(FrenchTransaction):
+ PATTERNS = [(re.compile('^RETRAIT G.A.B. \d+ (?P.*?)( CARTE .*)? LE (?P\d{2})/(?P\d{2})/(?P\d{2}).*'),
+ FrenchTransaction.TYPE_WITHDRAWAL),
+ (re.compile('^VIR(EMENT)? (?P.*)'), FrenchTransaction.TYPE_TRANSFER),
+ (re.compile('^PRLV (?P.*)'), FrenchTransaction.TYPE_ORDER),
+ (re.compile('^(?P.*) TRANSACTION( CARTE .*)? LE (?P\d{2})/(?P\d{2})/(?P\d{2}) ?(.*)$'),
+ FrenchTransaction.TYPE_CARD),
+ (re.compile('^CHEQUE.*'), FrenchTransaction.TYPE_CHECK),
+ (re.compile('^(CONVENTION \d+ )?COTISATION (?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):
+ is_coming = None
+
+ for tr in self.document.xpath('//div[@class="scrollTbody"]/table//tr'):
+ cols = tr.findall('td')
+
+ # check if it's a card page, so by default transactions are not yet debited.
+ if len(cols) == 6 and is_coming is None:
+ is_coming = True
+
+ col_label = cols[1]
+ if col_label.find('a') is not None:
+ col_label = col_label.find('a')
+
+ date = u''.join([txt.strip() for txt in cols[0].itertext()])
+ label = unicode(col_label.text.strip())
+
+ # always strip card debits transactions. if we are on a card page, all next
+ # transactions will be probably already debited.
+ if label.startswith('DEBIT MENSUEL '):
+ is_coming = False
+ continue
+
+ t = Transaction(col_label.attrib['id'])
+
+ # an optional tooltip on page contain the second part of the transaction label.
+ tooltip = self.document.xpath('//div[@id="tooltip%s"]' % t.id)
+ raw = label
+ if len(tooltip) > 0:
+ raw += u' ' + u' '.join([txt.strip() for txt in tooltip[0].itertext()])
+
+ raw = re.sub(r'[ ]+', ' ', raw)
+
+ t.parse(date, raw)
+
+ # as only the first part of label is important to user, if there are no subpart
+ # taken by FrenchTransaction regexps, reset the label as first part.
+ if t.label == t.raw:
+ t.label = label
+
+ debit = u''.join([txt.strip() for txt in cols[-2].itertext()])
+ credit = u''.join([txt.strip() for txt in cols[-1].itertext()])
+ t.set_amount(credit, debit)
+
+ t._is_coming = bool(is_coming)
+
+ yield t
diff --git a/modules/bred/test.py b/modules/bred/test.py
new file mode 100644
index 00000000..4e24f66d
--- /dev/null
+++ b/modules/bred/test.py
@@ -0,0 +1,31 @@
+# -*- 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 BredTest(BackendTest):
+ BACKEND = 'bred'
+
+ def test_bred(self):
+ l = list(self.backend.iter_accounts())
+
+ a = l[0]
+ list(self.backend.iter_history(a))
+ list(self.backend.iter_coming(a))