diff --git a/modules/ganassurances/__init__.py b/modules/ganassurances/__init__.py new file mode 100644 index 00000000..44dacc94 --- /dev/null +++ b/modules/ganassurances/__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 GanAssurancesBackend + +__all__ = ['GanAssurancesBackend'] diff --git a/modules/ganassurances/backend.py b/modules/ganassurances/backend.py new file mode 100644 index 00000000..6f098768 --- /dev/null +++ b/modules/ganassurances/backend.py @@ -0,0 +1,65 @@ +# -*- 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.capabilities.bank import ICapBank, AccountNotFound +from weboob.tools.backend import BaseBackend, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import GanAssurances + + +__all__ = ['GanAssurancesBackend'] + + +class GanAssurancesBackend(BaseBackend, ICapBank): + NAME = 'ganassurances' + MAINTAINER = u'Romain Bignon' + EMAIL = 'romain@weboob.org' + VERSION = '0.e' + DESCRIPTION = u'Groupama Assurances French bank website' + LICENSE = 'AGPLv3+' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), + ValueBackendPassword('password', label='Password', regexp='\d+')) + BROWSER = GanAssurances + + 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: + return self.browser.get_history(account) + + def iter_coming(self, account): + with self.browser: + return self.browser.get_coming(account) diff --git a/modules/ganassurances/browser.py b/modules/ganassurances/browser.py new file mode 100644 index 00000000..c6875897 --- /dev/null +++ b/modules/ganassurances/browser.py @@ -0,0 +1,96 @@ +# -*- 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.browser import BaseBrowser, BrowserIncorrectPassword + +from .pages import LoginPage, AccountsPage, TransactionsPage + + +__all__ = ['GanAssurances'] + + +class GanAssurances(BaseBrowser): + PROTOCOL = 'https' + DOMAIN = 'espaceclient.ganassurances.fr' + PAGES = {'https://espaceclient.ganassurances.fr/wps/portal/login.*': LoginPage, + 'https://espaceclient.ganassurances.fr/wps/myportal/TableauDeBord': AccountsPage, + 'https://espaceclient.ganassurances.fr/wps/myportal/!ut.*': TransactionsPage, + } + + def is_logged(self): + return self.page is not None and not self.is_on_page(LoginPage) + + def home(self): + self.location('https://espaceclient.ganassurances.fr/wps/myportal/TableauDeBord') + + 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 + + if not self.is_on_page(LoginPage): + self.home() + + self.page.login(self.username, self.password) + + if not self.is_logged(): + raise BrowserIncorrectPassword() + + def get_accounts_list(self): + if not self.is_on_page(AccountsPage): + self.location('/wps/myportal/TableauDeBord') + 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 get_history(self, account): + if account._link is None: + return iter([]) + + self.location(account._link) + assert self.is_on_page(TransactionsPage) + + return self.page.get_history() + + def get_coming(self, account): + if account._link is None: + return iter([]) + + self.location(account._link) + assert self.is_on_page(TransactionsPage) + + self.location(self.page.get_coming_link()) + assert self.is_on_page(TransactionsPage) + + return self.page.get_history() diff --git a/modules/ganassurances/favicon.png b/modules/ganassurances/favicon.png new file mode 100644 index 00000000..1f716f27 Binary files /dev/null and b/modules/ganassurances/favicon.png differ diff --git a/modules/ganassurances/pages.py b/modules/ganassurances/pages.py new file mode 100644 index 00000000..b427448d --- /dev/null +++ b/modules/ganassurances/pages.py @@ -0,0 +1,116 @@ +# -*- 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 weboob.tools.browser import BasePage +from weboob.capabilities.bank import Account +from weboob.tools.capabilities.bank.transactions import FrenchTransaction + + +__all__ = ['LoginPage', 'AccountsPage', 'TransactionsPage'] + + +class LoginPage(BasePage): + def login(self, login, passwd): + self.browser.select_form(name='loginForm') + self.browser.set_all_readonly(False) + self.browser['LoginPortletFormID'] = login.encode(self.browser.ENCODING) + self.browser['LoginPortletFormPassword1'] = passwd.encode(self.browser.ENCODING) + self.browser.submit(nologin=True) + +class AccountsPage(BasePage): + ACCOUNT_TYPES = {u'Solde des comptes bancaires - Groupama Banque': Account.TYPE_CHECKING, + u'Epargne bancaire constituée - Groupama Banque': Account.TYPE_SAVINGS, + } + + def get_list(self): + account_type = Account.TYPE_UNKNOWN + accounts = [] + + for tr in self.document.xpath('//table[@class="ecli"]/tr'): + if tr.attrib.get('class', '') == 'entete': + account_type = self.ACCOUNT_TYPES.get(tr.find('th').text.strip(), Account.TYPE_UNKNOWN) + continue + + tds = tr.findall('td') + + balance = tds[-1].text.strip() + if balance == '': + continue + + account = Account() + account.label = u' '.join([txt.strip() for txt in tds[0].itertext()]) + account.label = re.sub(u'[ \xa0\u2022\r\n\t]+', u' ', account.label).strip() + account.id = re.findall('(\d+)', account.label)[0] + account.balance = Decimal(FrenchTransaction.clean_amount(balance)) + account.currency = account.get_currency(balance) + account.type = account_type + m = re.search(r"javascript:submitForm\(([\w_]+),'([^']+)'\);", tds[0].find('a').attrib['onclick']) + if not m: + self.logger.warning('Unable to find link for %r' % account.label) + account._link = None + else: + account._link = m.group(2) + + accounts.append(account) + + return accounts + + +class Transaction(FrenchTransaction): + PATTERNS = [(re.compile('^Facture (?P
\d{2})/(?P\d{2})-(?P.*) carte .*'), + FrenchTransaction.TYPE_CARD), + (re.compile(u'^(Prlv( de)?|Ech(éance|\.)) (?P.*)'), + FrenchTransaction.TYPE_ORDER), + (re.compile('^(Vir|VIR)( de)? (?P.*)'), + FrenchTransaction.TYPE_TRANSFER), + (re.compile(u'^CHEQUE.*? (N° \w+)?$'), FrenchTransaction.TYPE_CHECK), + (re.compile('^Cotis(ation)? (?P.*)'), + FrenchTransaction.TYPE_BANK), + (re.compile('(?PInt .*)'), FrenchTransaction.TYPE_BANK), + ] + +class TransactionsPage(BasePage): + def get_history(self): + count = 0 + for tr in self.document.xpath('//table[@id="releve_operation"]/tr'): + tds = tr.findall('td') + + if len(tds) < 4: + continue + + t = Transaction(count) + + date = u''.join([txt.strip() for txt in tds[0].itertext()]) + raw = u' '.join([txt.strip() for txt in tds[1].itertext()]) + debit = u''.join([txt.strip() for txt in tds[-2].itertext()]) + credit = u''.join([txt.strip() for txt in tds[-1].itertext()]) + t.parse(date, re.sub(r'[ ]+', ' ', raw)) + t.set_amount(credit, debit) + + yield t + + count += 1 + + def get_coming_link(self): + a = self.document.getroot().cssselect('div#sous_nav ul li a.bt_sans_off')[0] + return re.sub('[ \t\r\n]+', '', a.attrib['href']) diff --git a/modules/ganassurances/test.py b/modules/ganassurances/test.py new file mode 100644 index 00000000..327cbe03 --- /dev/null +++ b/modules/ganassurances/test.py @@ -0,0 +1,30 @@ +# -*- 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 GanAssurancesTest(BackendTest): + BACKEND = 'ganassurances' + + def test_banquepop(self): + l = list(self.backend.iter_accounts()) + if len(l) > 0: + a = l[0] + list(self.backend.iter_history(a))