diff --git a/modules/creditdunord/__init__.py b/modules/creditdunord/__init__.py
new file mode 100644
index 00000000..dabc8016
--- /dev/null
+++ b/modules/creditdunord/__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 CreditDuNordBackend
+
+__all__ = ['CreditDuNordBackend']
diff --git a/modules/creditdunord/backend.py b/modules/creditdunord/backend.py
new file mode 100644
index 00000000..cba160ce
--- /dev/null
+++ b/modules/creditdunord/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 CreditDuNordBrowser
+
+
+__all__ = ['CreditDuNordBackend']
+
+
+class CreditDuNordBackend(BaseBackend, ICapBank):
+ NAME = 'creditdunord'
+ MAINTAINER = u'Romain Bignon'
+ EMAIL = 'romain@weboob.org'
+ VERSION = '0.f'
+ DESCRIPTION = u'Crédit du Nord French bank website'
+ LICENSE = 'AGPLv3+'
+ CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False),
+ ValueBackendPassword('password', label='Password of account'))
+ BROWSER = CreditDuNordBrowser
+
+ 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/creditdunord/browser.py b/modules/creditdunord/browser.py
new file mode 100644
index 00000000..596949cb
--- /dev/null
+++ b/modules/creditdunord/browser.py
@@ -0,0 +1,123 @@
+# -*- 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, AccountsPage, TransactionsPage
+
+
+__all__ = ['CreditDuNordBrowser']
+
+
+class CreditDuNordBrowser(BaseBrowser):
+ PROTOCOL = 'https'
+ DOMAIN = 'www.credit-du-nord.fr'
+ #CERTHASH = 'b2f8a8a7a03c54d7bb918f10eb4e141c3fb51bebf0eb8371aefb33a997efc600'
+ ENCODING = 'UTF-8'
+ PAGES = {'https://www.credit-du-nord.fr/?': LoginPage,
+ 'https://www.credit-du-nord.fr/vos-comptes/particuliers(\?.*)?': AccountsPage,
+ 'https://www.credit-du-nord.fr/vos-comptes/.*/transac/.*': TransactionsPage,
+ }
+
+ def is_logged(self):
+ return self.page is not None and not self.is_on_page(LoginPage)
+
+ def home(self):
+ if self.is_logged():
+ self.location('https://www.credit-du-nord.fr/vos-comptes/particuliers')
+ else:
+ self.login()
+ return
+ return self.location('https://www.credit-du-nord.fr/vos-comptes/particuliers')
+
+ def login(self):
+ assert isinstance(self.username, basestring)
+ assert isinstance(self.password, basestring)
+
+ # not necessary (and very slow)
+ #self.location('https://www.credit-du-nord.fr/', no_login=True)
+
+ data = {'bank': 'credit-du-nord',
+ 'pagecible': 'vos-comptes',
+ 'password': self.password.encode(self.ENCODING),
+ 'pwAuth': 'Authentification+mot+de+passe',
+ 'username': self.username.encode(self.ENCODING),
+ }
+
+ self.location('https://www.credit-du-nord.fr/saga/authentification', urllib.urlencode(data), no_login=True)
+
+ if not self.is_logged():
+ raise BrowserIncorrectPassword()
+
+ def get_accounts_list(self):
+ if not self.is_on_page(AccountsPage):
+ self.location('https://www.credit-du-nord.fr/vos-comptes/particuliers')
+ 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, link, link_id, execution, is_coming=None):
+ event = 'clicDetailCompte'
+ while 1:
+ data = {'_eventId': event,
+ '_ipc_eventValue': '',
+ '_ipc_fireEvent': '',
+ 'deviseAffichee': 'DEVISE',
+ 'execution': execution,
+ 'idCompteClique': link_id,
+ }
+ self.location(link, urllib.urlencode(data))
+
+ assert self.is_on_page(TransactionsPage)
+
+ self.page.is_coming = is_coming
+
+ for tr in self.page.get_history():
+ yield tr
+
+ is_last = self.page.is_last()
+ if is_last:
+ return
+
+ event = 'clicChangerPageSuivant'
+ execution = self.page.get_execution()
+ is_coming = self.page.is_coming
+
+ def get_history(self, account):
+ for tr in self.iter_transactions(account._link, account._link_id, account._execution):
+ yield tr
+
+ for tr in self.get_card_operations(account):
+ yield tr
+
+ def get_card_operations(self, account):
+ for link_id in account._card_ids:
+ for tr in self.iter_transactions(account._link, link_id, account._execution, True):
+ yield tr
diff --git a/modules/creditdunord/favicon.png b/modules/creditdunord/favicon.png
new file mode 100644
index 00000000..28bbb53a
Binary files /dev/null and b/modules/creditdunord/favicon.png differ
diff --git a/modules/creditdunord/pages.py b/modules/creditdunord/pages.py
new file mode 100644
index 00000000..217d2393
--- /dev/null
+++ b/modules/creditdunord/pages.py
@@ -0,0 +1,163 @@
+# -*- 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 cStringIO import StringIO
+
+from weboob.tools.browser import BasePage, BrokenPageError
+from weboob.tools.json import json
+from weboob.capabilities.bank import Account
+from weboob.tools.capabilities.bank.transactions import FrenchTransaction
+
+
+__all__ = ['LoginPage', 'AccountsPage', 'TransactionsPage']
+
+
+class LoginPage(BasePage):
+ pass
+
+class CDNBasePage(BasePage):
+ def get_from_js(self, pattern, end):
+ """
+ find a pattern in any javascript text
+ """
+ for script in self.document.xpath('//script'):
+ txt = script.text
+ if txt is None:
+ continue
+
+ start = txt.find(pattern)
+ if start < 0:
+ continue
+
+ txt = txt[start+len(pattern):start+txt[start+len(pattern):].find(end)+len(pattern)]
+ return txt
+
+ def get_execution(self):
+ return self.get_from_js("name: 'execution', value: '", "'")
+
+class AccountsPage(CDNBasePage):
+ COL_ID = 4
+ COL_LABEL = 5
+ COL_BALANCE = -1
+
+ def get_history_link(self):
+ return self.parser.strip(self.get_from_js(",url: Ext.util.Format.htmlDecode('", "'"))
+
+ def get_list(self):
+ accounts = []
+
+ txt = self.get_from_js('_data = new Array(', ');')
+
+ if txt is None:
+ raise BrokenPageError('Unable to find accounts list in scripts')
+
+ data = json.loads('[%s]' % txt.replace("'", '"'))
+
+ for line in data:
+ a = Account()
+ a.id = line[self.COL_ID].replace(' ','')
+ a.label = self.parser.tocleanstring(self.parser.parse(StringIO(line[self.COL_LABEL])).xpath('//div[@class="libelleCompteTDB"]')[0])
+ a.balance = Decimal(FrenchTransaction.clean_amount(line[self.COL_BALANCE]))
+ a._link = self.get_history_link()
+ a._execution = self.get_execution()
+ a._link_id = line[self.COL_ID]
+
+ if a.id.endswith('_CarteVisaPremier'):
+ accounts[0]._card_ids.append(a._link_id)
+ if not accounts[0].coming:
+ accounts[0].coming = Decimal('0.0')
+ accounts[0].coming += a.balance
+ continue
+
+ a._card_ids = []
+ accounts.append(a)
+
+ return iter(accounts)
+
+class Transaction(FrenchTransaction):
+ PATTERNS = [(re.compile(r'^(?PRET DAB \w+ .*?) LE (?P\d{2})(?P\d{2})$'),
+ FrenchTransaction.TYPE_WITHDRAWAL),
+ (re.compile(r'^VIR(EMENT)?\.?(DE)? (?P.*)'),
+ FrenchTransaction.TYPE_TRANSFER),
+ (re.compile(r'^PRLV (DE )?(?P.*)'), FrenchTransaction.TYPE_ORDER),
+ (re.compile(r'^CB (?P.*) LE (?P\d{2})\.?(?P\d{2})$'),
+ FrenchTransaction.TYPE_CARD),
+ (re.compile(r'^CHEQUE.*'), FrenchTransaction.TYPE_CHECK),
+ (re.compile(r'^(CONVENTION \d+ )?COTISATION (?P.*)'),
+ FrenchTransaction.TYPE_BANK),
+ (re.compile(r'^REM(ISE)?\.?( CHQ\.)? .*'), FrenchTransaction.TYPE_DEPOSIT),
+ (re.compile(r'^(?P.*?)( \d{2}.*)? LE (?P\d{2})\.?(?P\d{2})$'),
+ FrenchTransaction.TYPE_CARD),
+ ]
+
+
+class TransactionsPage(CDNBasePage):
+ COL_ID = 0
+ COL_DATE = 1
+ COL_DEBIT_DATE = 2
+ COL_LABEL = 3
+ COL_VALUE = -1
+
+ is_coming = None
+
+ def is_last(self):
+ for script in self.document.xpath('//script'):
+ txt = script.text
+ if txt is None:
+ continue
+
+ if txt.find('clicChangerPageSuivant') >= 0:
+ return False
+
+ return True
+
+ def get_history(self):
+ txt = self.get_from_js('ListeMvts_data = new Array(', ');')
+
+ if txt is None:
+ raise BrokenPageError('Unable to find transactions list in scripts')
+
+ data = json.loads('[%s]' % txt.replace('"', '\\"').replace("'", '"'))
+
+ for line in data:
+ t = Transaction(line[self.COL_ID])
+
+ if self.is_coming is not None:
+ t.type = t.TYPE_CARD
+ date = self.parser.strip(line[self.COL_DEBIT_DATE])
+ else:
+ date = self.parser.strip(line[self.COL_DATE])
+ raw = self.parser.strip(line[self.COL_LABEL])
+
+ t.parse(date, raw)
+ t.set_amount(line[self.COL_VALUE])
+
+ if self.is_coming is True and raw.startswith('TOTAL DES') and t.amount > 0:
+ # ignore card credit and next transactions are already debited
+ self.is_coming = False
+ continue
+ if self.is_coming is None and raw.startswith('ACHATS CARTE'):
+ # Ignore card debit
+ continue
+
+ t._is_coming = bool(self.is_coming)
+ yield t
diff --git a/modules/creditdunord/test.py b/modules/creditdunord/test.py
new file mode 100644
index 00000000..4cf04dbd
--- /dev/null
+++ b/modules/creditdunord/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 CreditDuNordTest(BackendTest):
+ BACKEND = 'creditdunord'
+
+ def test_creditdunord(self):
+ l = list(self.backend.iter_accounts())
+
+ a = l[0]
+ list(self.backend.iter_history(a))
+ list(self.backend.iter_coming(a))