From fa3b0ee1746bad40ef66883f378301ace2539d47 Mon Sep 17 00:00:00 2001 From: Romain Bignon Date: Tue, 31 Jul 2012 17:01:19 +0200 Subject: [PATCH] add module caissedepargne (closes #787) --- modules/caissedepargne/__init__.py | 23 +++++ modules/caissedepargne/backend.py | 61 ++++++++++++++ modules/caissedepargne/browser.py | 91 ++++++++++++++++++++ modules/caissedepargne/favicon.png | Bin 0 -> 1577 bytes modules/caissedepargne/pages.py | 130 +++++++++++++++++++++++++++++ modules/caissedepargne/test.py | 30 +++++++ 6 files changed, 335 insertions(+) create mode 100644 modules/caissedepargne/__init__.py create mode 100644 modules/caissedepargne/backend.py create mode 100644 modules/caissedepargne/browser.py create mode 100644 modules/caissedepargne/favicon.png create mode 100644 modules/caissedepargne/pages.py create mode 100644 modules/caissedepargne/test.py diff --git a/modules/caissedepargne/__init__.py b/modules/caissedepargne/__init__.py new file mode 100644 index 00000000..60e83a4a --- /dev/null +++ b/modules/caissedepargne/__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 CaisseEpargneBackend + +__all__ = ['CaisseEpargneBackend'] diff --git a/modules/caissedepargne/backend.py b/modules/caissedepargne/backend.py new file mode 100644 index 00000000..aec1ed35 --- /dev/null +++ b/modules/caissedepargne/backend.py @@ -0,0 +1,61 @@ +# -*- 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 CaisseEpargne + + +__all__ = ['CaisseEpargneBackend'] + + +class CaisseEpargneBackend(BaseBackend, ICapBank): + NAME = 'caissedepargne' + MAINTAINER = 'Romain Bignon' + EMAIL = 'romain@weboob.org' + VERSION = '0.d' + DESCRIPTION = u'Caisse d\'Épargne French bank website' + LICENSE = 'AGPLv3+' + CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), + ValueBackendPassword('password', label='Password', regexp='\d+')) + BROWSER = CaisseEpargne + + 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) diff --git a/modules/caissedepargne/browser.py b/modules/caissedepargne/browser.py new file mode 100644 index 00000000..50990ba8 --- /dev/null +++ b/modules/caissedepargne/browser.py @@ -0,0 +1,91 @@ +# -*- 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 urlparse import urlsplit + +from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword + +from .pages import LoginPage, IndexPage + + +__all__ = ['CaisseEpargne'] + + +class CaisseEpargne(BaseBrowser): + DOMAIN = 'caisse-epargne.fr' + PROTOCOL = 'https' + PAGES = {'https://[^/]+.caisse-epargne.fr/particuliers/ind_pauthpopup.aspx.*': LoginPage, + 'https://[^/]+.caisse-epargne.fr/Portail.aspx': IndexPage, + } + + def is_logged(self): + return self.page and not self.is_on_page(LoginPage) + + def home(self): + self.location('https://www.caisse-epargne.fr/particuliers/ind_pauthpopup.aspx?mar=101®=&fctpopup=auth&cv=0') + + 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.page.login2() + self.page.login3(self.password) + + if not self.is_logged(): + raise BrowserIncorrectPassword() + + v = urlsplit(self.page.url) + self.DOMAIN = v.netloc + + def get_accounts_list(self): + self.location(self.buildurl('/Portail.aspx')) + + 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 not self.is_on_page(IndexPage): + self.location(self.buildurl('/Portail.aspx')) + + self.page.go_history(account.id) + + assert self.is_on_page(IndexPage) + + return self.page.get_history() diff --git a/modules/caissedepargne/favicon.png b/modules/caissedepargne/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..86c21e88b7cd2544186f27d5847481a265411bd9 GIT binary patch literal 1577 zcmV+^2G;qBP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00ogrL_t(|+U;9QXcJKsy{#Xx z6a|suXazyt2%=QC!hq173n_{l=_<@NVB1|vT)1lxS6W<{k}eb#OR7{9Y!M9pief`e z&>~VzH0dWT)zp~uxOj|jzD;H(pNY+dM~1xl-Sh7MzG(sxXaiF-ZE$J;&;XzTKm&jV z02`S$|2Le7B2jlYJ02P$nwTi=-|Qi&wclfjiI4B2S5uV1_XdV8zRboBrT28|hyL@KJ+wFjWTANcl7&;019>%wZPh#xN$)Qm4J)kMFY z0kC&3(A>;j*1`gC{W@^?FtBy2{`~H4{fe)xF+f}*?$!X1nSc0@Gn7v2&)c>QxN=3$ zd?LXNpvyDM^;KYSQ17mk^SgJm)YHhVjf}Wf>`Mbc6xB@S^FU7z(9r=LKMtHeZOpuj ze7rLNZr-NJ6>5KT3sdj@PTO09-;#W z*#DE0L?=$vbuHB#fS*5!T3Xh>dS-@bWP~UlCwlO}UPY$XWGWuLy-a7d71uho_Vz#{CfxCBsY_@bTTpj`1%LSCKF`Z_=@whoKA3xfAF-c+|rqe)QUkw1*v%gLg~|qmF#v6CfX@d+qf7+n=Z$Oi%4?J>H$QKkd2Q}7H>bZxBEduRG8I^?8Nt`D z`3vT9z`lKaE|=qTw{BT~8D(OX&vWJ_37DK@zmmj3kZEAR*oY<)Ok#>ii8TNQ2Ka+# zXMvqNf!0aHGbpw@AarBlfaD| z)^sx()n|-GdaukR`}*`{H?u^otP-G1gfbcDMo5xa*-WNz*w`~D4PI9lkV+X#fKo!u z6Ez0_2m&*3GF`sxjCtiM=5ma=V%LSk#&rs>SFIZHc!jFMSFiGieEn*_MW!^GKxxbq z0k?1SYa$VD7Zw-!|JiKG^+@Xgbatw(wVfRQ*|Xx7yqM7(A7{s+$YKte78fnKVCw(~ z0&wr1{VGo&pw@e3r02BXXe0Qt*BB9mV#a3eUsXt}_xZfK6KvOzN|Iydoppw^=&N;3 z4}f;%Q>oJRUK^z<-7x@6OxRAnowdf4l+7w%LJfd6cyY4wK3`cY&CUSOc7La*fn&$? z=S3pw?y#Mem&e!`m!vy)9LFNgo1wW-yKJ%)xoap@50JOESzQgdaF$mc# z@a0RzJIe0ZEiUgaQx;pQw5Py*pUj9N`xK^1yGoi|`SG+M5FI(fj;E$7+IDv*0FCbk bt4DtUrylT#Z7<4i00000NkvXXu0mjf2-D(% literal 0 HcmV?d00001 diff --git a/modules/caissedepargne/pages.py b/modules/caissedepargne/pages.py new file mode 100644 index 00000000..2d58e000 --- /dev/null +++ b/modules/caissedepargne/pages.py @@ -0,0 +1,130 @@ +# -*- 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.mech import ClientForm +from weboob.tools.browser import BasePage, BrokenPageError +from weboob.capabilities.bank import Account +from weboob.tools.capabilities.bank.transactions import FrenchTransaction + + +__all__ = ['LoginPage', 'IndexPage'] + + +class LoginPage(BasePage): + def login(self, login): + self.browser.select_form(name='Main') + self.browser.set_all_readonly(False) + self.browser['ctl01$CC_ind_pauthpopup$ctl01$CC_ind_ident$ctl01$CC_ind_inputuserid_sup$txnuabbd'] = login + self.browser['__EVENTTARGET'] = 'ctl01$CC_ind_pauthpopup$ctl01$CC_ind_ident$ctl01$CC_ind_inputuserid_sup$btnValider' + self.browser.submit(nologin=True) + + def login2(self): + self.browser.select_form(name='Main') + self.browser.set_all_readonly(False) + self.browser['__EVENTARGUMENT'] = 'idsrv=WE' + self.browser.submit(nologin=True) + + def login3(self, passwd): + self.browser.select_form(name='Main') + self.browser['codconf'] = passwd + a = self.document.xpath('//a[@title="Valider"]')[0] + m = re.match("javascript:RedirectToDeiPart\('([^']+)'\);", a.attrib['href']) + if not m: + raise BrokenPageError('Unable to find validate URL') + self.browser.form.action = m.group(1) + self.browser.submit(nologin=True) + +class Transaction(FrenchTransaction): + PATTERNS = [(re.compile('^RET DAB (?P.*?) RETRAIT DU (?P
\d{2})(?P\d{2})(?P\d{2}).*'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('^RET DAB (?P.*?) CARTE ?:.*'), + FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile('(\w+) (?P
\d{2})(?P\d{2})(?P\d{2}) CB:[^ ]+ (?P.*)'), + 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('^(CONVENTION \d+ )?COTIS(ATION)? (?P.*)'), + FrenchTransaction.TYPE_BANK), + (re.compile(r'^\* (?P.*)'), FrenchTransaction.TYPE_BANK), + (re.compile('^REMISE (?P.*)'), FrenchTransaction.TYPE_DEPOSIT), + (re.compile('^(?P.*)( \d+)? QUITTANCE .*'), + FrenchTransaction.TYPE_ORDER), + ] + +class IndexPage(BasePage): + ACCOUNT_TYPES = {u'Epargne liquide': Account.TYPE_SAVINGS, + u'Compte Courant': Account.TYPE_CHECKING, + } + + def get_list(self): + for table in self.document.xpath('//table[@cellpadding="1"]'): + account_type = Account.TYPE_UNKNOWN + for tr in table.xpath('./tr'): + tds = tr.findall('td') + if tr.attrib.get('class', '') == 'DataGridHeader': + account_type = self.ACCOUNT_TYPES.get(tds[1].text.strip(), Account.TYPE_UNKNOWN) + else: + a = tds[1].find('a') + m = re.match("^javascript:__doPostBack\('.*','HISTORIQUE_COMPTE&(\d+)'\)", a.attrib['href']) + + account = Account() + account.id = m.group(1) + account.label = unicode(a.text.strip()) + account.type = account_type + account.balance = Decimal(FrenchTransaction.clean_amount(tds[-1].find('a').text.rstrip(' EUR'))) + yield account + + def go_history(self, id): + self.browser.select_form(name='main') + self.browser.set_all_readonly(False) + self.browser['__EVENTTARGET'] = 'MM$SYNTHESE' + self.browser['__EVENTARGUMENT'] = 'HISTORIQUE_COMPTE&%s' % id + self.browser['MM$m_CH$IsMsgInit'] = '0' + self.browser.controls.append(ClientForm.TextControl('text', 'm_ScriptManager', {'value': ''})) + self.browser['m_ScriptManager'] = 'MM$m_UpdatePanel|MM$SYNTHESE' + self.browser.controls.remove(self.browser.find_control(name='Cartridge$imgbtnMessagerie', type='image')) + self.browser.controls.remove(self.browser.find_control(name='MM$m_CH$ButtonImageFondMessagerie', type='image')) + self.browser.controls.remove(self.browser.find_control(name='MM$m_CH$ButtonImageMessagerie', type='image')) + self.browser.submit() + + def get_history(self): + i = 0 + for tr in self.document.xpath('//table[@cellpadding="1"]/tr'): + if tr.attrib.get('class', '') == 'DataGridHeader': + continue + + tds = tr.findall('td') + + t = Transaction(i) + + date = u''.join([txt.strip() for txt in tds[1].itertext()]) + raw = u' '.join([txt.strip() for txt in tds[2].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, raw) + t.set_amount(credit, debit) + yield t + + i += 1 diff --git a/modules/caissedepargne/test.py b/modules/caissedepargne/test.py new file mode 100644 index 00000000..41beac04 --- /dev/null +++ b/modules/caissedepargne/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 CaisseEpargneTest(BackendTest): + BACKEND = 'caissedepargne' + + def test_caisse_epargne(self): + l = list(self.backend.iter_accounts()) + if len(l) > 0: + a = l[0] + list(self.backend.iter_history(a))