diff --git a/modules/carrefourbanque/__init__.py b/modules/carrefourbanque/__init__.py new file mode 100644 index 00000000..c00a085f --- /dev/null +++ b/modules/carrefourbanque/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 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 CarrefourBanqueBackend + +__all__ = ['CarrefourBanqueBackend'] diff --git a/modules/carrefourbanque/backend.py b/modules/carrefourbanque/backend.py new file mode 100644 index 00000000..6b50ada4 --- /dev/null +++ b/modules/carrefourbanque/backend.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 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.capabilities.bank import ICapBank, AccountNotFound +from weboob.tools.backend import BaseBackend, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import CarrefourBanque + + +__all__ = ['CarrefourBanqueBackend'] + + +class CarrefourBanqueBackend(BaseBackend, ICapBank): + NAME = 'carrefourbanque' + MAINTAINER = u'Romain Bignon' + EMAIL = 'romain@weboob.org' + VERSION = '0.e' + DESCRIPTION = u'Carrefour Banque French bank website' + LICENSE = 'AGPLv3+' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', regexp='\d+', masked=False), + ValueBackendPassword('password', label='Password', regexp='\d+')) + BROWSER = CarrefourBanque + + def create_default_browser(self): + return self.create_browser(self.config['login'].get(), + self.config['password'].get()) + + def iter_accounts(self): + with self.browser: + return self.browser.get_accounts_list() + + 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: + for tr in self.browser.iter_history(account): + if not tr._coming: + yield tr + + def iter_coming(self, account): + with self.browser: + for tr in self.browser.iter_history(account): + if tr._coming: + yield tr diff --git a/modules/carrefourbanque/browser.py b/modules/carrefourbanque/browser.py new file mode 100644 index 00000000..8968aefe --- /dev/null +++ b/modules/carrefourbanque/browser.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 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 + +from .pages import LoginPage, HomePage, AccountsPage, TransactionsPage + + +__all__ = ['CarrefourBanque'] + + +class CarrefourBanque(BaseBrowser): + PROTOCOL = 'https' + DOMAIN = 'services.carrefour-banque.fr' + ENCODING = 'iso-8859-15' + PAGES = {'https?://services.carrefour-banque.fr/s2pnet/publ/identification.do': LoginPage, + 'https?://services.carrefour-banque.fr/stscripts/run.stn/s2p/SommairePS2P': HomePage, + 'https?://services.carrefour-banque.fr/s2pnet/priv/disponible.do': AccountsPage, + 'https?://services.carrefour-banque.fr/s2pnet/priv/consult.do\?btnTelechargement': (TransactionsPage, 'raw'), + } + + 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('/s2pnet/priv/disponible.do') + else: + self.login() + + 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) + + if self.is_logged(): + return + + args = {'login': '', + 'loginCBPASS': self.username.encode(self.ENCODING), + 'motPasse': self.password.encode(self.ENCODING), + 'testJS': 'true', + 'x': 8, + 'y': 4, + } + + self.location('https://services.carrefour-banque.fr/s2pnet/publ/identification.do', urllib.urlencode(args), no_login=True) + + assert self.is_on_page(LoginPage) + + # raises BrowserIncorrectPassword if no redirect form is found + self.page.redirect() + + def get_accounts_list(self): + if not self.is_on_page(AccountsPage): + self.location('/s2pnet/priv/disponible.do') + 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_history(self, account): + self.location('/s2pnet/priv/consult.do?btnTelechargement') + + assert self.is_on_page(TransactionsPage) + return self.page.get_history(account) diff --git a/modules/carrefourbanque/favicon.png b/modules/carrefourbanque/favicon.png new file mode 100644 index 00000000..bcfc3b10 Binary files /dev/null and b/modules/carrefourbanque/favicon.png differ diff --git a/modules/carrefourbanque/pages.py b/modules/carrefourbanque/pages.py new file mode 100644 index 00000000..74b2c6a1 --- /dev/null +++ b/modules/carrefourbanque/pages.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 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 datetime +from decimal import Decimal +import re +from mechanize import FormNotFoundError + +from weboob.tools.browser import BasePage, BrowserIncorrectPassword +from weboob.capabilities.bank import Account +from weboob.tools.capabilities.bank.transactions import FrenchTransaction + + +__all__ = ['LoginPage', 'AccountsPage', 'TransactionsPage'] + + +class LoginPage(BasePage): + def redirect(self): + try: + self.browser.select_form(name='redirectEpargne') + self.browser.submit(nologin=True) + except FormNotFoundError: + raise BrowserIncorrectPassword() + +class HomePage(BasePage): + pass + +class AccountsPage(BasePage): + def get_list(self): + div = self.document.xpath('//div[@id="descriptifdroite"]')[0] + + account = Account() + + account.id = re.search(u'(\d+)', div.xpath('.//div[@class="credithauttexte"]')[0].text).group(1) + account.label = u'Carte PASS' + account.balance = Decimal('0') + + for tr in div.xpath('.//table/tr'): + tds = tr.findall('td') + + if len(tds) < 3: + continue + + label = u''.join([txt.strip() for txt in tds[1].itertext()]) + value = u''.join([txt.strip() for txt in tds[2].itertext()]) + + if 'encours depuis le dernier' in label.lower(): + coming = u'-' + value + account.coming = Decimal(FrenchTransaction.clean_amount(coming)) + account.currency = account.get_currency(coming) + elif u'arrĂȘtĂ© de compte' in label.lower(): + m = re.search(u'(\d+)/(\d+)/(\d+)', label) + if m: + account._outstanding_date = datetime.date(*reversed(map(int, m.groups()))) + break + + yield account + +class TransactionsPage(BasePage): + COL_DATE = 0 + COL_TEXT = 1 + COL_AMOUNT = 2 + + def get_history(self, account): + transactions = [] + last_order = None + + for tr in self.document.split('\n')[1:]: + cols = tr.split(';') + + if len(cols) < 4: + continue + + t = FrenchTransaction(0) + date = cols[self.COL_DATE] + raw = cols[self.COL_TEXT] + amount = cols[self.COL_AMOUNT] + + t.parse(date, re.sub(r'[ ]+', ' ', raw)) + + if t.raw.startswith('PRELEVEMENT ACHATS DIFFERES'): + t._coming = False + if last_order is None: + last_order = t.date + else: + t._coming = True + + t.set_amount(amount) + transactions.append(t) + + # go to the previous stop date before the last order + while last_order is not None and last_order.day != account._outstanding_date.day: + last_order = last_order - datetime.timedelta(days=1) + + for t in transactions: + if t.date <= last_order: + t._coming = False + + return iter(transactions) diff --git a/modules/carrefourbanque/test.py b/modules/carrefourbanque/test.py new file mode 100644 index 00000000..2fd36ba6 --- /dev/null +++ b/modules/carrefourbanque/test.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 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 CarrefourBanqueTest(BackendTest): + BACKEND = 'carrefourbanque' + + def test_carrefourbanque(self): + l = list(self.backend.iter_accounts()) + if len(l) > 0: + a = l[0] + list(self.backend.iter_history(a))