diff --git a/modules/fortuneo/__init__.py b/modules/fortuneo/__init__.py
new file mode 100644
index 00000000..d0c8b64a
--- /dev/null
+++ b/modules/fortuneo/__init__.py
@@ -0,0 +1,23 @@
+# -*- 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 .
+
+
+from .backend import FortuneoBackend
+
+__all__ = ['SocieteGeneraleBackend']
diff --git a/modules/fortuneo/backend.py b/modules/fortuneo/backend.py
new file mode 100644
index 00000000..ddb85799
--- /dev/null
+++ b/modules/fortuneo/backend.py
@@ -0,0 +1,75 @@
+# -*- 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 .
+
+
+# python2.5 compatibility
+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 Fortuneo
+
+
+__all__ = ['FortuneoBackend']
+
+
+class FortuneoBackend(BaseBackend, ICapBank):
+ NAME = 'fortuneo'
+ MAINTAINER = 'Gilles-Alexandre Quenot'
+ EMAIL = 'gilles.quenot@gmail.com'
+ VERSION = '0.c'
+ LICENSE = 'AGPLv3+'
+ DESCRIPTION = u'Fortuneo French bank website'
+ CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False),
+ ValueBackendPassword('password', label='Password'))
+ BROWSER = Fortuneo
+
+ 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):
+ #pass
+ #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):
+ pass
+ #with self.browser:
+ # for tr in self.browser.iter_history(account._link_id):
+ # if not tr._coming:
+ # yield tr
+
+ def iter_coming(self, account):
+ with self.browser:
+ for tr in self.browser.iter_history(account._link_id):
+ if tr._coming:
+ yield tr
diff --git a/modules/fortuneo/browser.py b/modules/fortuneo/browser.py
new file mode 100644
index 00000000..4f427953
--- /dev/null
+++ b/modules/fortuneo/browser.py
@@ -0,0 +1,94 @@
+# -*- 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 .
+
+
+from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword
+
+from .pages.accounts_list import AccountsList, AccountHistory
+from .pages.login import LoginPage, BadLoginPage
+
+
+__all__ = ['Fortuneo']
+
+
+class Fortuneo(BaseBrowser):
+ DOMAIN_LOGIN = 'www.fortuneo.fr'
+ DOMAIN = 'www.fortuneo.fr'
+ PROTOCOL = 'https'
+ ENCODING = None # refer to the HTML encoding
+ PAGES = {
+ '.*identification.jsp.*': LoginPage,
+ #'https://www.fortuneo.fr/fr/identification.jsp': BadLoginPage,
+ '.*/prive/default.jsp.*': AccountsList,
+ '.*/prive/mes-comptes/livret/consulter-situation/consulter-solde.jsp.*': AccountHistory,
+ }
+
+ def __init__(self, *args, **kwargs):
+ BaseBrowser.__init__(self, *args, **kwargs)
+
+ def home(self):
+ self.location('https://' + self.DOMAIN_LOGIN + '/fr/identification.jsp')
+ #self.location('https://' + self.DOMAIN_LOGIN + '/fr/prive/default.jsp?ANav=1')
+ #self.location('https://' + self.DOMAIN_LOGIN + '/fr/prive/mes-comptes/synthese-tous-comptes.jsp')
+
+ 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.location('https://' + self.DOMAIN_LOGIN + '/fr/identification.jsp')
+
+ self.page.login(self.username, self.password)
+
+ if self.is_on_page(LoginPage) or \
+ self.is_on_page(BadLoginPage):
+ raise BrowserIncorrectPassword()
+
+ def get_accounts_list(self):
+ if not self.is_on_page(AccountsList):
+ self.location('/fr/prive/mes-comptes/synthese-globale/synthese-tous-comptes.jsp')
+ #self.location('')
+
+ return self.page.get_list()
+
+ def get_account(self, id):
+ assert isinstance(id, basestring)
+
+ #if not self.is_on_page(AccountsList):
+ # self.location('/fr/prive/default.jsp?ANav=1')
+
+ l = self.page.get_list()
+ for a in l:
+ if a.id == id:
+ return a
+
+ return None
+
+ def iter_history(self, url):
+ self.location(url)
+
+ if not self.is_on_page(AccountHistory):
+ # TODO: support other kind of accounts
+ return iter([])
+
+ return self.page.iter_transactions()
diff --git a/modules/fortuneo/pages/__init__.py b/modules/fortuneo/pages/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/fortuneo/pages/accounts_list.py b/modules/fortuneo/pages/accounts_list.py
new file mode 100644
index 00000000..a072ea7b
--- /dev/null
+++ b/modules/fortuneo/pages/accounts_list.py
@@ -0,0 +1,149 @@
+# -*- 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 .
+
+
+from urlparse import parse_qs, urlparse
+from lxml.etree import XML
+from cStringIO import StringIO
+from decimal import Decimal
+import re
+
+from weboob.capabilities.bank import Account
+from weboob.tools.capabilities.bank.transactions import FrenchTransaction
+from weboob.tools.browser import BasePage, BrokenPageError
+
+
+__all__ = ['AccountsList', 'AccountHistory']
+
+
+class AccountsList(BasePage):
+ def on_loaded(self):
+ pass
+
+ def get_list(self):
+ l = []
+ #for el in self.document.xpath('//table[@id="tableauComptesTitEtCotit"]/tbody/'):
+ #l.append('test')
+ #l.append('Livret +')
+ #l.append('20')
+ #l.append('https://www.fortuneo.fr/fr/prive/mes-comptes/livret/caracteristiques-mon-compte/?COMPTE_ACTIF=FT00654224521421145')
+ account.label = "test"
+ account.id = "Livret +"
+ account.balance = "20"
+ account._link_id = "https://www.fortuneo.fr/fr/prive/mes-comptes/livret/caracteristiques-mon-compte/?COMPTE_ACTIF=FT00654224521421145"
+ l.append(account)
+ #for tr in self.document.getiterator('tr'):
+ # if 'LGNTableRow' in tr.attrib.get('class', '').split():
+ # account = Account()
+ # for td in tr.getiterator('td'):
+ # if td.attrib.get('headers', '') == 'TypeCompte':
+ # a = td.find('a')
+ # account.label = unicode(a.find("span").text)
+ # account._link_id = a.get('href', '')
+
+ # elif td.attrib.get('headers', '') == 'NumeroCompte':
+ # id = td.text
+ # id = id.replace(u'\xa0','')
+ # account.id = id
+
+ # elif td.attrib.get('headers', '') == 'Libelle':
+ # pass
+
+ # elif td.attrib.get('headers', '') == 'Solde':
+ # balance = td.find('div').text
+ # if balance != None:
+ # balance = balance.replace(u'\xa0','').replace(',','.')
+ # account.balance = Decimal(balance)
+ # else:
+ # account.balance = Decimal(0)
+
+ # l.append(account)
+
+ return l
+
+class Transaction(FrenchTransaction):
+ print "DEBUG a implementer"
+ pass
+ #PATTERNS = [(re.compile(r'^CARTE \w+ RETRAIT DAB.* (?P
\d{2})/(?P\d{2}) (?P\d+)H(?P\d+) (?P.*)'),
+ # FrenchTransaction.TYPE_WITHDRAWAL),
+ # (re.compile(r'^(?PCARTE) \w+ (?P\d{2})/(?P\d{2}) (?P.*)'),
+ # FrenchTransaction.TYPE_CARD),
+ # (re.compile(r'^(?P(COTISATION|PRELEVEMENT|TELEREGLEMENT|TIP)) (?P.*)'),
+ # FrenchTransaction.TYPE_ORDER),
+ # (re.compile(r'^(?PVIR(EMEN)?T? \w+) (?P.*)'),
+ # FrenchTransaction.TYPE_TRANSFER),
+ # (re.compile(r'^(CHEQUE) (?P.*)'), FrenchTransaction.TYPE_CHECK),
+ # (re.compile(r'^(FRAIS) (?P.*)'), FrenchTransaction.TYPE_BANK),
+ # (re.compile(r'^(?PECHEANCEPRET)(?P.*)'),
+ # FrenchTransaction.TYPE_LOAN_PAYMENT),
+ # (re.compile(r'^(?PREMISE CHEQUES)(?P.*)'),
+ # FrenchTransaction.TYPE_DEPOSIT),
+ # ]
+
+class AccountHistory(BasePage):
+ def get_part_url(self):
+ print "DEBUG a implementer"
+ pass
+ #for script in self.document.getiterator('script'):
+ # if script.text is None:
+ # continue
+
+ # m = re.search('var listeEcrCavXmlUrl="(.*)";', script.text)
+ # if m:
+ # return m.group(1)
+
+ #raise BrokenPageError('Unable to find link to history part')
+
+ def iter_transactions(self):
+ print "DEBUG a implementer"
+ pass
+ #url = self.get_part_url()
+ #while 1:
+ # d = XML(self.browser.readurl(url))
+ # el = d.xpath('//dataBody')[0]
+ # s = StringIO(el.text)
+ # doc = self.browser.get_document(s)
+
+ # for tr in self._iter_transactions(doc):
+ # yield tr
+
+ # el = d.xpath('//dataHeader')[0]
+ # if int(el.find('suite').text) != 1:
+ # return
+
+ # url = urlparse(url)
+ # p = parse_qs(url.query)
+ # url = self.browser.buildurl(url.path, n10_nrowcolor=0,
+ # operationNumberPG=el.find('operationNumber').text,
+ # operationTypePG=el.find('operationType').text,
+ # pageNumberPG=el.find('pageNumber').text,
+ # sign=p['sign'][0],
+ # src=p['src'][0])
+
+
+ def _iter_transactions(self, doc):
+ print "DEBUG a implementer"
+ pass
+ #for i, tr in enumerate(self.parser.select(doc.getroot(), 'tr')):
+ # t = Transaction(i)
+ # t.parse(date=tr.xpath('./td[@headers="Date"]')[0].text,
+ # raw=tr.attrib['title'].strip())
+ # t.set_amount(*reversed([el.text for el in tr.xpath('./td[@class="right"]')]))
+ # t._coming = tr.xpath('./td[@headers="AVenir"]')[0].text
+ # yield t
diff --git a/modules/fortuneo/pages/login.py b/modules/fortuneo/pages/login.py
new file mode 100644
index 00000000..44542fcb
--- /dev/null
+++ b/modules/fortuneo/pages/login.py
@@ -0,0 +1,99 @@
+# -*- 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 .
+
+
+from logging import error
+
+from weboob.tools.browser import BasePage, BrowserUnavailable
+#from lxml import etree
+
+
+__all__ = ['LoginPage']
+
+def dump(obj):
+ for attr in dir(obj):
+ print "obj.%s = %s" % (attr, getattr(obj, attr))
+
+class LoginPage(BasePage):
+ def login(self, login, passwd):
+ #print "DEBUG BasePage=", BasePage.url
+ #dump(BasePage)
+ self.browser.select_form(nr=3)
+ #self.browser['locale'] = 'fr'
+ self.browser['login'] = login
+ self.browser['passwd'] = passwd
+ #self.browser['idDyn'] = 'false'
+ self.browser.submit()
+ #print "DEBUG ", self.page
+
+#class LoginPage(BasePage):
+# def on_loaded(self):
+# pass
+# #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):
+# DOMAIN_LOGIN = self.browser.DOMAIN_LOGIN
+# DOMAIN = self.browser.DOMAIN
+#
+# url_login = 'https://' + DOMAIN_LOGIN + '/index.html'
+#
+# base_url = 'https://' + DOMAIN
+# url = base_url + '/cvcsgenclavier?mode=jsom&estSession=0'
+# headers = {
+# 'Referer': url_login
+# }
+# request = self.browser.request_class(url, None, headers)
+# infos_data = self.browser.readurl(request)
+# infos_xml = etree.XML(infos_data)
+# infos = {}
+# for el in ("cryptogramme", "nblignes", "nbcolonnes"):
+# infos[el] = infos_xml.find(el).text
+#
+# infos["grille"] = ""
+# for g in infos_xml.findall("grille"):
+# infos["grille"] += g.text + ","
+# infos["keyCodes"] = infos["grille"].split(",")
+#
+# url = base_url + '/cvcsgenimage?modeClavier=0&cryptogramme=' + infos["cryptogramme"]
+# img = Captcha(self.browser.openurl(url), infos)
+#
+# try:
+# img.build_tiles()
+# except TileError, err:
+# error("Error: %s" % err)
+# if err.tile:
+# err.tile.display()
+#
+# self.browser.openurl(url_login)
+# self.browser.select_form('authentification')
+# self.browser.set_all_readonly(False)
+#
+# self.browser['codcli'] = login
+# self.browser['codsec'] = img.get_codes(password)
+# self.browser['cryptocvcs'] = infos["cryptogramme"]
+# self.browser.submit()
+
+
+class BadLoginPage(BasePage):
+ pass
diff --git a/modules/fortuneo/test.py b/modules/fortuneo/test.py
new file mode 100644
index 00000000..d4d243c3
--- /dev/null
+++ b/modules/fortuneo/test.py
@@ -0,0 +1,31 @@
+# -*- 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 .
+
+
+from weboob.tools.test import BackendTest
+
+class FortuneoTest(BackendTest):
+ BACKEND = 'fortuneo'
+
+ def test_fortuneo(self):
+ l = list(self.backend.iter_accounts())
+ self.assertTrue(len(l) > 0)
+ a = l[0]
+ list(self.backend.iter_coming(a))
+ list(self.backend.iter_history(a))