diff --git a/modules/s2e/__init__.py b/modules/s2e/__init__.py
new file mode 100644
index 00000000..0be786f7
--- /dev/null
+++ b/modules/s2e/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2015 Christophe Lampin
+#
+# 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 .module import S2eModule
+
+__all__ = ['S2eModule']
diff --git a/modules/s2e/browser.py b/modules/s2e/browser.py
new file mode 100644
index 00000000..5675d5cc
--- /dev/null
+++ b/modules/s2e/browser.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2015 Christophe Lampin
+#
+# 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 datetime import datetime
+from decimal import Decimal
+
+from weboob.browser import LoginBrowser, URL, need_login
+from weboob.browser.profiles import Android
+from weboob.exceptions import BrowserIncorrectPassword
+from weboob.capabilities.bank import Account, Transaction
+
+from .pages import LoginPage, CalcPage, ProfilPage, AccountsPage, HistoryPage, I18nPage
+
+__all__ = ['Esalia']
+
+
+class S2eBrowser(LoginBrowser):
+
+ PROFILE = Android()
+ CTCC = ""
+ LANG = "FR"
+
+ sessionId = None
+
+ loginp = URL('/$', LoginPage)
+ calcp = URL('/s2e_services/restServices/calculetteService/grillemdp\?uuid=(?P)', CalcPage)
+ profilp = URL('/s2e_services/restServices/authentification/loginS', ProfilPage)
+ accountsp = URL('/s2e_services/restServices/situationCompte', AccountsPage)
+ historyp = URL('/s2e_services/restServices/listeOperation', HistoryPage)
+ i18np = URL('/(?P.*)/LANG/(?P.*).json', I18nPage)
+
+ def __init__(self, url, username, password, *args, **kwargs):
+ super(S2eBrowser, self).__init__(username, password, *args, **kwargs)
+ self.BASEURL = "https://" + url
+
+ def do_login(self):
+ self.logger.debug('call Browser.do_login')
+ self.loginp.stay_or_go()
+ self.page.login(self.username, self.password)
+ if self.sessionId is None:
+ raise BrowserIncorrectPassword()
+
+ @need_login
+ def get_accounts_list(self):
+ data = {'clang': self.LANG,
+ 'ctcc': self.CTCC,
+ 'login': self.username,
+ 'session': self.sessionId}
+
+ for k, fond in self.accountsp.open(data=data).get_list().items():
+ a = Account()
+ a.id = k
+ a.type = Account.TYPE_LOAN
+ a.balance = Decimal(fond["montantValeurEuro"]).quantize(Decimal('.01'))
+ a.label = fond["libelleSupport"]
+ a.currency = u"EUR" # Don't find any possbility to get that from configuration.
+ yield a
+
+ @need_login
+ def iter_history(self, account):
+ # Load i18n for type translation
+ self.i18np.open(lang1=self.LANG, lang2=self.LANG).load_i18n()
+
+ # For now detail for each account is not available. History is global for all accounts and very simplist
+ data = {'clang': self.LANG,
+ 'ctcc': self.CTCC,
+ 'login': self.username,
+ 'session': self.sessionId}
+
+ for trans in self.historyp.open(data=data).get_transactions():
+ t = Transaction()
+ t.id = trans["referenceOperationIndividuelle"]
+ t.date = datetime.strptime(trans["dateHeureSaisie"], "%d/%m/%Y")
+ t.rdate = datetime.strptime(trans["dateHeureSaisie"], "%d/%m/%Y")
+ t.type = Transaction.TYPE_DEPOSIT if trans["montantNetEuro"] > 0 else Transaction.TYPE_PAYBACK
+ t.raw = trans["typeOperation"]
+ t.label = self.i18n["OPERATION_TYPE_" + trans["casDeGestion"]]
+ t.amount = Decimal(trans["montantNetEuro"]).quantize(Decimal('.01'))
+ yield t
+
+
+class Esalia(S2eBrowser):
+ CTCC = "SG"
+ loginp = URL('/Esalia/$', LoginPage)
+ i18np = URL('/Esalia/SG/(?P.*)/LANG/(?P.*).json', I18nPage)
+
+
+class Capeasi(S2eBrowser):
+ CTCC = "AXA"
+ loginp = URL('/AXA/$', LoginPage)
+ i18np = URL('/AXA/(?P.*)/LANG/(?P.*).json', I18nPage)
+
+
+class EREHSBC(S2eBrowser):
+ CTCC = "HSBC"
+ loginp = URL('/ERE-HSBC/$', LoginPage)
+ i18np = URL('/ERE-HSBC/HSBC/(?P.*)/LANG/(?P.*).json', I18nPage)
+
+
+class CreditNord(S2eBrowser):
+ CTCC = "" # FIXME : Not Available Yet
+ loginp = URL('//$', LoginPage)
+ # Hack : Lang.json of BNPERE is only available in app. Get it from Esalia
+ i18np = URL('https://m.esalia.com/Esalia/SG/(?P.*)/LANG/(?P.*).json', I18nPage)
+
+
+class BNPPERE(S2eBrowser):
+ CTCC = "BNP"
+ loginp = URL('/$', LoginPage)
+ # Hack : Lang.json of BNPERE is only available in app. Get it from Esalia
+ i18np = URL('https://m.esalia.com/Esalia/SG/(?P.*)/LANG/(?P.*).json', I18nPage)
diff --git a/modules/s2e/module.py b/modules/s2e/module.py
new file mode 100644
index 00000000..0135add3
--- /dev/null
+++ b/modules/s2e/module.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2015 Christophe Lampin
+
+# 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.base import find_object
+from weboob.capabilities.bank import CapBank, AccountNotFound
+from weboob.tools.backend import Module, BackendConfig
+from weboob.tools.value import Value, ValueBackendPassword
+from weboob.tools.ordereddict import OrderedDict
+
+from .browser import Esalia, Capeasi, EREHSBC, BNPPERE
+
+
+__all__ = ['S2eModule']
+
+
+class S2eModule(Module, CapBank):
+ NAME = 's2e'
+ MAINTAINER = u'Christophe Lampin'
+ EMAIL = 'weboob@lampin.net'
+ VERSION = '1.1'
+ LICENSE = 'AGPLv3+'
+ DESCRIPTION = u'S2e module for Employee Savings Plans. Support for Esalia, Capeasi, "BNP Paribas Épargne & Retraite Entreprises" and "HSBC Epargne et Retraite en Entreprise"'
+
+ website_choices = OrderedDict([(k, u'%s (%s)' % (v, k)) for k, v in sorted({
+ 'm.esalia.com': u'Esalia', # Good Url. Tested
+ 'mobile.capeasi.com': u'Capeasi', # Good Url. Not fully tested
+ 'mobi.ere.hsbc.fr': u'ERE HSBC', # Good Url. Not fully tested
+ 'smartphone.s2e-net.com': u'BNPP ERE', # Url To Confirm. Not tested
+ # 'smartphone.s2e-net.com': u'Groupe Crédit du Nord', # Mobile version not available yet.
+ }.iteritems(), key=lambda k_v: (k_v[1], k_v[0]))])
+
+ BROWSERS = {
+ 'm.esalia.com': Esalia,
+ 'mobile.capeasi.com': Capeasi,
+ 'mobi.ere.hsbc.fr': EREHSBC,
+ 'smartphone.s2e-net.com': BNPPERE,
+ # 'smartphone.s2e-net.com': CreditNord, # Mobile version not available yet.
+ }
+
+ CONFIG = BackendConfig(Value('website', label='Banque', choices=website_choices, default='smartphone.s2e-net.com'),
+ ValueBackendPassword('login', label='Identifiant', masked=False),
+ ValueBackendPassword('password', label='Code secret', regexp='^(\d{6}|)$'))
+
+ def create_default_browser(self):
+ self.BROWSER = self.BROWSERS[self.config['website'].get()]
+ return self.create_browser(self.config['website'].get(),
+ self.config['login'].get(),
+ self.config['password'].get())
+
+ def iter_accounts(self):
+ return self.browser.get_accounts_list()
+
+ def get_account(self, _id):
+ return find_object(self.browser.get_accounts_list(), id=_id, error=AccountNotFound)
+
+ def iter_history(self, account):
+ return self.browser.iter_history(account)
\ No newline at end of file
diff --git a/modules/s2e/pages.py b/modules/s2e/pages.py
new file mode 100644
index 00000000..72d6bc45
--- /dev/null
+++ b/modules/s2e/pages.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2015 Christophe Lampiné
+#
+# 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 random
+
+from weboob.browser.pages import HTMLPage, LoggedPage, JsonPage
+
+
+class LoginPage(HTMLPage):
+ def generate_uuid(self):
+ chars = list('0123456789abcdef')
+ uuid = [None]*36
+ rnd = random.random
+ for i in (8, 13, 18, 23):
+ uuid[i] = '-'
+ uuid[14] = '4' # version 4
+
+ for i in range(36):
+ if uuid[i] is None:
+ r = 0 | int(rnd()*16)
+ idx = (((r & 0x3) | 0x8) if (i == 19) else (r & 0xf))
+ uuid[i] = chars[idx]
+ i += 1
+
+ return ''.join(uuid)
+
+ def login(self, login, password):
+ uuid = self.generate_uuid()
+ data = self.browser.calcp.open(uuid=uuid).get_data(login, password)
+ self.browser.profilp.open(data=data).store_sessionId()
+
+
+class CalcPage(JsonPage):
+ def get_data(self, login, password):
+ convert_data = {}
+ for num_data in self.doc['grilleMdp']:
+ convert_data[num_data["nom"]] = num_data["valeur"]
+
+ encrypt_pass = ""
+ for char in password:
+ encrypt_pass += (convert_data[int(char)] + ":")
+
+ data = {'clang': self.browser.LANG,
+ 'conversationId': self.doc["conversationId"],
+ 'ctcc': self.browser.CTCC,
+ 'login': login,
+ 'password': encrypt_pass}
+
+ return data
+
+
+class ProfilPage(JsonPage):
+ def store_sessionId(self):
+ self.browser.sessionId = self.doc['session']
+
+
+class AccountsPage(LoggedPage, JsonPage):
+ def get_list(self):
+ accounts = {}
+ for entreprise in self.doc["listeEntreprise"]:
+ for dispositif in entreprise["listeDispositf"]: # Ceci n'est pas une erreur de frappe ;)
+ for fonds in dispositif["listeFonds"]:
+ if fonds["montantValeurEuro"] == 0:
+ continue
+ fonds["codeLong"] = entreprise["codeEntreprise"] + dispositif["codeDispositif"] + fonds["codeSupport"]
+ if fonds["codeLong"] in accounts:
+ accounts[fonds["codeLong"]]["montantValeurEuro"] += fonds["montantValeurEuro"]
+ else:
+ accounts[fonds["codeLong"]] = fonds
+ return accounts
+
+
+class HistoryPage(LoggedPage, JsonPage):
+ def get_transactions(self):
+ for operation in self.doc["listeOperations"]:
+ yield operation
+
+
+class I18nPage(JsonPage):
+ def load_i18n(self):
+ self.browser.i18n = self.doc["i18n"]
diff --git a/modules/s2e/test.py b/modules/s2e/test.py
new file mode 100644
index 00000000..77ce451d
--- /dev/null
+++ b/modules/s2e/test.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2015 Christophe Lampin
+#
+# 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 S2eTest(BackendTest):
+ MODULE = 's2e'
+
+ def test_bank(self):
+ l = list(self.backend.iter_accounts())
+ if len(l) > 0:
+ a = l[0]
+ list(self.backend.iter_history(a))