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))