diff --git a/modules/creditcooperatif/backend.py b/modules/creditcooperatif/backend.py
index 6d539d5d..38e0dec6 100644
--- a/modules/creditcooperatif/backend.py
+++ b/modules/creditcooperatif/backend.py
@@ -20,9 +20,10 @@
from weboob.capabilities.bank import ICapBank, AccountNotFound
from weboob.tools.backend import BaseBackend, BackendConfig
-from weboob.tools.value import ValueBackendPassword
+from weboob.tools.value import ValueBackendPassword, Value
-from .browser import CreditCooperatif
+from .perso.browser import CreditCooperatif as CreditCooperatifPerso
+from .pro.browser import CreditCooperatif as CreditCooperatifPro
__all__ = ['CreditCooperatifBackend']
@@ -35,14 +36,23 @@ class CreditCooperatifBackend(BaseBackend, ICapBank):
VERSION = '0.e'
DESCRIPTION = u'Credit Cooperatif French bank website'
LICENSE = 'AGPLv3+'
- CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False),
+ auth_type = {'particular': "Interface Particuliers",
+ 'weak' : "Code confidentiel (pro)",
+ 'strong': "Sesame (pro)"}
+ CONFIG = BackendConfig(Value('auth_type', label='Authentication type', choices=auth_type, default="particular"),
+ ValueBackendPassword('login', label='Account ID', masked=False),
ValueBackendPassword('password', label='Password or one time pin'))
- BROWSER = CreditCooperatif
-
def create_default_browser(self):
- return self.create_browser(self.config['login'].get(),
- self.config['password'].get())
+ if self.config['auth_type'].get() == 'particular':
+ self.BROWSER = CreditCooperatifPerso
+ return self.create_browser(self.config['login'].get(),
+ self.config['password'].get())
+ else:
+ self.BROWSER = CreditCooperatifPro
+ return self.create_browser(self.config['login'].get(),
+ self.config['password'].get(),
+ strong_auth=self.config['auth_type'].get() == "strong")
def iter_accounts(self):
with self.browser:
diff --git a/modules/creditcooperatif/perso/__init__.py b/modules/creditcooperatif/perso/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/creditcooperatif/browser.py b/modules/creditcooperatif/perso/browser.py
similarity index 100%
rename from modules/creditcooperatif/browser.py
rename to modules/creditcooperatif/perso/browser.py
diff --git a/modules/creditcooperatif/pages.py b/modules/creditcooperatif/perso/pages.py
similarity index 99%
rename from modules/creditcooperatif/pages.py
rename to modules/creditcooperatif/perso/pages.py
index 02574ea0..2cbd9a65 100644
--- a/modules/creditcooperatif/pages.py
+++ b/modules/creditcooperatif/perso/pages.py
@@ -98,7 +98,7 @@ class TransactionsJSONPage(BasePage):
ROW_DEBIT = -2
def get_transactions(self):
- for tr in self.document['aaData']:
+ for tr in self.document['exportData'][1:]:
t = Transaction(0)
t.parse(tr[self.ROW_DATE], tr[self.ROW_TEXT])
t.set_amount(tr[self.ROW_CREDIT], tr[self.ROW_DEBIT])
diff --git a/modules/creditcooperatif/pro/__init__.py b/modules/creditcooperatif/pro/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/creditcooperatif/pro/browser.py b/modules/creditcooperatif/pro/browser.py
new file mode 100644
index 00000000..58723b6e
--- /dev/null
+++ b/modules/creditcooperatif/pro/browser.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2012 Kevin Pouget
+#
+# 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, AccountsPage, TransactionsPage, ComingTransactionsPage
+
+
+__all__ = ['CreditCooperatif']
+
+
+class CreditCooperatif(BaseBrowser):
+ PROTOCOL = 'https'
+ ENCODING = 'iso-8859-15'
+ DOMAIN = "www.coopanet.com"
+ PAGES = {'https://www.coopanet.com/banque/sso/.*': LoginPage,
+ 'https://www.coopanet.com/banque/cpt/incoopanetj2ee.do.*': AccountsPage,
+ 'https://www.coopanet.com/banque/cpt/cpt/situationcomptes.do\?lnkReleveAction=X&numeroExterne=.*': TransactionsPage,
+ 'https://www.coopanet.com/banque/cpt/cpt/relevecompte.do\?tri_page=.*': TransactionsPage,
+ 'https://www.coopanet.com/banque/cpt/cpt/situationcomptes.do\?lnkOpCB=X&numeroExterne=.*': ComingTransactionsPage
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.strong_auth = kwargs.pop('strong_auth', False)
+ BaseBrowser.__init__(self, *args, **kwargs)
+
+ def home(self):
+ self.location("/banque/sso/")
+
+ assert self.is_on_page(LoginPage)
+
+ def is_logged(self):
+ return not self.is_on_page(LoginPage)
+
+ 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)
+ assert isinstance(self.strong_auth, bool)
+
+ if self.is_logged():
+ return
+
+ if not self.is_on_page(LoginPage):
+ self.home()
+
+ self.page.login(self.username, self.password, self.strong_auth)
+
+ if not self.is_logged():
+ raise BrowserIncorrectPassword()
+
+ def get_accounts_list(self):
+ self.location(self.buildurl('/banque/cpt/incoopanetj2ee.do?ssomode=ok'))
+
+ return self.page.get_list()
+
+ def get_account(self, id):
+ assert isinstance(id, basestring)
+
+ for a in self.get_accounts_list():
+ if a.id == id:
+ return a
+
+ return None
+
+ def get_history(self, account):
+ self.location('/banque/cpt/cpt/situationcomptes.do?lnkReleveAction=X&numeroExterne='+ account.id)
+
+ while 1:
+ assert self.is_on_page(TransactionsPage)
+
+ for tr in self.page.get_history():
+ yield tr
+
+ next_url = self.page.get_next_url()
+ if next_url is None:
+ return
+
+ self.location(next_url)
+
+ def get_coming(self, account):
+ self.location('/banque/cpt/cpt/situationcomptes.do?lnkOpCB=X&numeroExterne='+ account.id)
+
+ assert self.is_on_page(ComingTransactionsPage)
+
+ for ctr in self.page.get_history():
+ yield ctr
diff --git a/modules/creditcooperatif/pro/pages.py b/modules/creditcooperatif/pro/pages.py
new file mode 100644
index 00000000..c6f4f772
--- /dev/null
+++ b/modules/creditcooperatif/pro/pages.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2012 Kevin Pouget
+#
+# 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
+import time
+
+from weboob.tools.browser import BasePage
+from weboob.capabilities.bank import Account
+from weboob.tools.capabilities.bank.transactions import FrenchTransaction
+
+
+__all__ = ['LoginPage', 'AccountsPage', 'TransactionsPage', 'ComingTransactionsPage']
+
+class LoginPage(BasePage):
+ def login(self, login, pin, strong_auth):
+ form_nb = 1 if strong_auth else 0
+ indentType = "RENFORCE" if strong_auth else "MDP"
+
+ self.browser.select_form(name='loginCoForm', nr=form_nb)
+ self.browser['codeUtil'] = login
+ self.browser['motPasse'] = pin
+
+ assert self.browser['identType'] == indentType
+ self.browser.submit(nologin=True)
+
+class AccountsPage(BasePage):
+ ACCOUNT_TYPES = {u'COMPTE NEF': Account.TYPE_CHECKING}
+
+ CPT_ROW_ID = 0
+ CPT_ROW_NAME = 1
+ CPT_ROW_NATURE = 2
+ CPT_ROW_BALANCE = 3
+ CPT_ROW_ENCOURS = 4
+
+ def is_error(self):
+ for par in self.document.xpath('//p[@class=acctxtnoirlien]'):
+ if par.text is not None and u"La page demandée ne peut pas être affichée." in par.text:
+ return True
+
+ return False
+
+ def get_list(self):
+ for trCompte in self.document.xpath('//table[@id="compte"]/tbody/tr'):
+ tds = trCompte.findall('td')
+
+ account = Account()
+
+ account.id = tds[self.CPT_ROW_ID].text.strip()
+ account.label = tds[self.CPT_ROW_NAME].text.strip()
+
+ account_type_str = "".join([td.text for td in tds[self.CPT_ROW_NATURE].xpath('.//td[@class="txt"]')]).strip()
+
+ account.type = self.ACCOUNT_TYPES.get(account_type_str, Account.TYPE_UNKNOWN)
+
+ account.balance = Decimal(FrenchTransaction.clean_amount(tds[self.CPT_ROW_BALANCE].find("a").text))
+ account.coming = Decimal(FrenchTransaction.clean_amount( tds[self.CPT_ROW_ENCOURS].find("a").text))
+ account.currency = account.get_currency(tds[self.CPT_ROW_BALANCE].find("a").text)
+ yield account
+
+ return
+
+class Transaction(FrenchTransaction):
+ PATTERNS = [(re.compile('^RETRAIT DAB (?P.*?).*'),
+ FrenchTransaction.TYPE_WITHDRAWAL),
+ (re.compile('^(?P.*) RETRAIT DU (?P\d{2})(?P\d{2})(?P\d{2}) .*'),
+ FrenchTransaction.TYPE_WITHDRAWAL),
+ (re.compile('^CARTE \d+ .*'), FrenchTransaction.TYPE_CARD),
+ (re.compile('^VIR(EMENT)? (?P.*)'), FrenchTransaction.TYPE_TRANSFER),
+ (re.compile('^PRLV (?P.*)'), FrenchTransaction.TYPE_ORDER),
+ (re.compile('^CHEQUE.*'), FrenchTransaction.TYPE_CHECK),
+ (re.compile('^(AGIOS /|FRAIS) (?P.*)'), FrenchTransaction.TYPE_BANK),
+ (re.compile('^ABONNEMENT (?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_next_url(self):
+ # can be 'Suivant' or ' Suivant'
+ next = self.document.xpath("//a[normalize-space(text()) = 'Suivant']")
+
+ if not next:
+ return None
+
+ return next[0].attrib["href"]
+
+ TR_DATE = 0
+ TR_TEXT = 2
+ TR_DEBIT = 3
+ TR_CREDIT = 4
+ def get_history(self):
+ for tr in self.document.xpath('//table[@id="operation"]/tbody/tr'):
+ tds = tr.findall('td')
+
+ def get_content(td):
+ ret = "".join([ttd.text if ttd.text else "" for ttd in td.xpath(".//td")])
+ return ret.replace(u"\xa0", " ").strip()
+
+ date = get_content(tds[self.TR_DATE])
+ raw = get_content(tds[self.TR_TEXT])
+
+ debit = get_content(tds[self.TR_DEBIT])
+ credit = get_content(tds[self.TR_CREDIT])
+
+ t = Transaction(date+""+raw)
+ t.parse(date, re.sub(r'[ ]+', ' ', raw))
+ t.set_amount(credit, debit)
+
+ yield t
+
+class ComingTransactionsPage(BasePage):
+ COM_TR_COMMENT = 0
+ COM_TR_DATE = 1
+ COM_TR_TEXT = 2
+ COM_TR_VALUE = 3
+
+ def get_history(self):
+ comment = None
+ for tr in self.document.xpath('//table[@id="operation"]/tbody/tr'):
+ tds = tr.findall('td')
+
+ def get_content(td):
+ ret = td.text
+ return ret.replace(u"\xa0", " ").strip()
+
+ raw = get_content(tds[self.COM_TR_TEXT])
+
+ if comment is None:
+ comment = get_content(tds[self.COM_TR_COMMENT])
+ raw = "%s (%s) " % (raw, comment)
+
+ debit = get_content(tds[self.COM_TR_VALUE])
+ date = get_content(tds[self.COM_TR_DATE])
+
+ if comment is not None:
+ #date is 'JJ/MM'. add '/YYYY'
+ date += comment[comment.rindex("/"):]
+ else:
+ date += "/%d" % time.localtime().tm_year
+
+
+ t = Transaction(date+""+raw)
+ t.parse(date, re.sub(r'[ ]+', ' ', raw))
+ t.set_amount("", debit)
+
+ yield t