pax_global_header 0000666 0000000 0000000 00000000064 12356015330 0014510 g ustar 00root root 0000000 0000000 52 comment=e025fb0b2040e76d68512fca33d3483aa63d925d
woob-e025fb0b2040e76d68512fca33d3483aa63d925d-modules-cic/ 0000775 0000000 0000000 00000000000 12356015330 0021562 5 ustar 00root root 0000000 0000000 woob-e025fb0b2040e76d68512fca33d3483aa63d925d-modules-cic/modules/ 0000775 0000000 0000000 00000000000 12356015330 0023232 5 ustar 00root root 0000000 0000000 woob-e025fb0b2040e76d68512fca33d3483aa63d925d-modules-cic/modules/cic/ 0000775 0000000 0000000 00000000000 12356015330 0023770 5 ustar 00root root 0000000 0000000 woob-e025fb0b2040e76d68512fca33d3483aa63d925d-modules-cic/modules/cic/__init__.py 0000664 0000000 0000000 00000001434 12356015330 0026103 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Julien Veyssier
#
# 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 CICBackend
__all__ = ['CICBackend']
woob-e025fb0b2040e76d68512fca33d3483aa63d925d-modules-cic/modules/cic/backend.py 0000664 0000000 0000000 00000005733 12356015330 0025741 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Julien Veyssier
#
# 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 string
from weboob.capabilities.bank import CapBank, AccountNotFound, Recipient, Account
from weboob.tools.backend import BaseBackend, BackendConfig
from weboob.tools.value import ValueBackendPassword
from .browser import CICBrowser
__all__ = ['CICBackend']
class CICBackend(BaseBackend, CapBank):
NAME = 'cic'
MAINTAINER = u'Romain Bignon'
EMAIL = 'romain@weboob.org'
VERSION = '0.j'
DESCRIPTION = u'CIC'
LICENSE = 'AGPLv3+'
CONFIG = BackendConfig(ValueBackendPassword('login', label='Identifiant', regexp='^\d{1,13}\w$', masked=False),
ValueBackendPassword('password', label='Mot de passe'))
BROWSER = CICBrowser
def create_default_browser(self):
return self.create_browser(self.config['login'].get(), self.config['password'].get())
def iter_accounts(self):
for account in self.browser.get_accounts_list():
yield account
def get_account(self, _id):
account = self.browser.get_account(_id)
if account:
return account
else:
raise AccountNotFound()
def iter_coming(self, account):
with self.browser:
for tr in self.browser.get_history(account):
if tr._is_coming:
yield tr
def iter_history(self, account):
with self.browser:
for tr in self.browser.get_history(account):
if not tr._is_coming:
yield tr
def iter_transfer_recipients(self, ignored):
for account in self.browser.get_accounts_list():
recipient = Recipient()
recipient.id = account.id
recipient.label = account.label
yield recipient
def transfer(self, account, to, amount, reason=None):
if isinstance(account, Account):
account = account.id
account = str(account).strip(string.letters)
to = str(to).strip(string.letters)
try:
assert account.isdigit()
assert to.isdigit()
amount = Decimal(amount)
except (AssertionError, ValueError):
raise AccountNotFound()
with self.browser:
return self.browser.transfer(account, to, amount, reason)
woob-e025fb0b2040e76d68512fca33d3483aa63d925d-modules-cic/modules/cic/browser.py 0000664 0000000 0000000 00000017767 12356015330 0026047 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Julien Veyssier
#
# 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, parse_qsl, urlparse
from datetime import datetime, timedelta
from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword
from weboob.capabilities.bank import Transfer, TransferError
from .pages import LoginPage, LoginErrorPage, AccountsPage, UserSpacePage, EmptyPage, \
OperationsPage, CardPage, ComingPage, NoOperationsPage, InfoPage, \
TransfertPage, ChangePasswordPage, VerifCodePage
__all__ = ['CICBrowser']
# Browser
class CICBrowser(BaseBrowser):
PROTOCOL = 'https'
DOMAIN = 'www.cic.fr'
CERTHASH = 'b7f681798d4f414fb5cb4032a0b6e8e0d61eeea564a1fb2c1c5a6fc351c70c50'
ENCODING = 'iso-8859-1'
USER_AGENT = BaseBrowser.USER_AGENTS['wget']
PAGES = {'https://www.cic.fr/.*/fr/banques/particuliers/index.html': LoginPage,
'https://www.cic.fr/.*/fr/identification/default.cgi': LoginErrorPage,
'https://www.cic.fr/.*/fr/banque/situation_financiere.cgi': AccountsPage,
'https://www.cic.fr/.*/fr/banque/espace_personnel.aspx': UserSpacePage,
'https://www.cic.fr/.*/fr/banque/mouvements.cgi.*': OperationsPage,
'https://www.cic.fr/.*/fr/banque/mvts_instance.cgi.*': ComingPage,
'https://www.cic.fr/.*/fr/banque/nr/nr_devbooster.aspx.*': OperationsPage,
'https://www.cic.fr/.*/fr/banque/operations_carte\.cgi.*': CardPage,
'https://www.cic.fr/.*/fr/banque/CR/arrivee\.asp.*': NoOperationsPage,
'https://www.cic.fr/.*/fr/banque/BAD.*': InfoPage,
'https://www.cic.fr/.*/fr/banque/.*Vir.*': TransfertPage,
'https://www.cic.fr/.*/fr/validation/change_password.cgi': ChangePasswordPage,
'https://www.cic.fr/.*/fr/validation/verif_code.cgi.*': VerifCodePage,
'https://www.cic.fr/.*/fr/': EmptyPage,
'https://www.cic.fr/.*/fr/banques/index.html': EmptyPage,
'https://www.cic.fr/.*/fr/banque/paci_beware_of_phishing.html.*': EmptyPage,
'https://www.cic.fr/.*/fr/validation/(?!change_password|verif_code).*': EmptyPage,
}
currentSubBank = None
def is_logged(self):
return not self.is_on_page(LoginPage) and not self.is_on_page(LoginErrorPage)
def home(self):
return self.location('https://www.cic.fr/sb/fr/banques/particuliers/index.html')
def login(self):
assert isinstance(self.username, basestring)
assert isinstance(self.password, basestring)
if not self.is_on_page(LoginPage):
self.location('https://www.cic.fr/', no_login=True)
self.page.login(self.username, self.password)
if not self.is_logged() or self.is_on_page(LoginErrorPage):
raise BrowserIncorrectPassword()
self.getCurrentSubBank()
def get_accounts_list(self):
if not self.is_on_page(AccountsPage):
self.location('https://www.cic.fr/%s/fr/banque/situation_financiere.cgi' % self.currentSubBank)
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 getCurrentSubBank(self):
# the account list and history urls depend on the sub bank of the user
url = urlparse(self.geturl())
self.currentSubBank = url.path.lstrip('/').split('/')[0]
def list_operations(self, page_url):
if page_url.startswith('/'):
self.location(page_url)
else:
self.location('https://%s/%s/fr/banque/%s' % (self.DOMAIN, self.currentSubBank, page_url))
go_next = True
while go_next:
if not self.is_on_page(OperationsPage):
return
for op in self.page.get_history():
yield op
go_next = self.page.go_next()
def get_history(self, account):
transactions = []
last_debit = None
for tr in self.list_operations(account._link_id):
# to prevent redundancy with card transactions, we do not
# store 'RELEVE CARTE' transaction.
if tr.raw != 'RELEVE CARTE':
transactions.append(tr)
elif last_debit is None:
last_debit = (tr.date - timedelta(days=10)).month
coming_link = self.page.get_coming_link() if self.is_on_page(OperationsPage) else None
if coming_link is not None:
for tr in self.list_operations(coming_link):
transactions.append(tr)
month = 0
for card_link in account._card_links:
v = urlsplit(card_link)
args = dict(parse_qsl(v.query))
# useful with 12 -> 1
if int(args['mois']) < month:
month = month + 1
else:
month = int(args['mois'])
for tr in self.list_operations(card_link):
if month > last_debit:
tr._is_coming = True
transactions.append(tr)
transactions.sort(key=lambda tr: tr.rdate, reverse=True)
return transactions
def transfer(self, account, to, amount, reason=None):
# access the transfer page
transfert_url = 'WI_VPLV_VirUniSaiCpt.asp?RAZ=ALL&Cat=6&PERM=N&CHX=A'
self.location('https://%s/%s/fr/banque/%s' % (self.DOMAIN, self.currentSubBank, transfert_url))
# fill the form
self.select_form(name='FormVirUniSaiCpt')
self['IDB'] = [account[-1]]
self['ICR'] = [to[-1]]
self['MTTVIR'] = '%s' % str(amount).replace('.', ',')
if reason is not None:
self['LIBDBT'] = reason
self['LIBCRT'] = reason
self.submit()
# look for known errors
content = unicode(self.response().get_data(), self.ENCODING)
insufficient_amount_message = u'Montant insuffisant.'
maximum_allowed_balance_message = u'Solde maximum autorisé dépassé.'
if content.find(insufficient_amount_message) != -1:
raise TransferError('The amount you tried to transfer is too low.')
if content.find(maximum_allowed_balance_message) != -1:
raise TransferError('The maximum allowed balance for the target account has been / would be reached.')
# look for the known "all right" message
ready_for_transfer_message = u'Confirmez un virement entre vos comptes'
if not content.find(ready_for_transfer_message):
raise TransferError('The expected message "%s" was not found.' % ready_for_transfer_message)
# submit the confirmation form
self.select_form(name='FormVirUniCnf')
submit_date = datetime.now()
self.submit()
# look for the known "everything went well" message
content = unicode(self.response().get_data(), self.ENCODING)
transfer_ok_message = u'Votre virement a été exécuté ce jour'
if not content.find(transfer_ok_message):
raise TransferError('The expected message "%s" was not found.' % transfer_ok_message)
# We now have to return a Transfer object
transfer = Transfer(submit_date.strftime('%Y%m%d%H%M%S'))
transfer.amount = amount
transfer.origin = account
transfer.recipient = to
transfer.date = submit_date
return transfer
woob-e025fb0b2040e76d68512fca33d3483aa63d925d-modules-cic/modules/cic/favicon.png 0000664 0000000 0000000 00000003156 12356015330 0026130 0 ustar 00root root 0000000 0000000 PNG
IHDR @ @ iq bKGD _ _ _4ƍ pHYs tIMEpv tEXtComment Created with GIMPW IDATx{lSU?]u+kvɺ! &T01Ѩ5?ƨ5
0,"21Fۭtt}{1n]I&{~{:::::::::::::?|.4`Mm%%!>? ro|+LR5*?7Z@S##xCSlr:e&E%& B(|B0Lр!DŽ1wӢiE\BVl.-("sf:' 5viSS0"UV@8p6oM2~ Ÿ(TT6VS ;4RW㼥 ǏF}v.3sI}I۟}[i)`gc^1'g`m}Y
$ Pa̹N]UnW~îi*RY}F @ɩ(>$V9Z5k[\q>;(rýEGzp[XT$'eIJdP!?HaxbU%c0RQagm]elllLSTUEQUdEz441Y$D8g8ߗ4$okB"!aEF\ĹU3XʱsK#_|{"#O}qYgL8r"b8 '?uTcS⬦n&*+ ضɓ 'M.O΄cw4o0h`o[i<c6DcrZVV!TU3ޱTX'_*GVkf1#ɢG''h(jϪ 3Ḧme3&ZhnU eNhr̢4Ϫ=E7 -KO
-U8-LF[ hv44.-v{wOZCM{ܨ,$M{ݱ40D6oXɻ<^yh5Y<{~m˒Lϕk=璄G}mT
rк&2wy[|wtd80]sK\loh8)
-մ]A QLO$N2^Y?iQxh#{`*_@`Fv41<ғg{|եes
# PVTĞ{V) 輅lYryKoߞȪ|GPip`m9iY(H(M懓Q;}~qn?x0ͥeoޤ[@UUE%4ct9ȱEYyom#%Oώ]?{np 67bs?JgF֔Ъf%zxf1]n6TPZXHBQ
Dh)$NsrdWFttttttttttttttttto
Z$gc IENDB` woob-e025fb0b2040e76d68512fca33d3483aa63d925d-modules-cic/modules/cic/pages.py 0000664 0000000 0000000 00000025253 12356015330 0025450 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Julien Veyssier
#
# 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 urlparse import urlparse, parse_qs
from decimal import Decimal
import re
from dateutil.relativedelta import relativedelta
from weboob.tools.browser import BasePage, BrowserIncorrectPassword, BrokenPageError
from weboob.tools.ordereddict import OrderedDict
from weboob.capabilities.bank import Account
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from weboob.tools.date import parse_french_date
class LoginPage(BasePage):
def login(self, login, passwd):
self.browser.select_form(name='ident')
self.browser['_cm_user'] = login.encode(self.browser.ENCODING)
self.browser['_cm_pwd'] = passwd.encode(self.browser.ENCODING)
self.browser.submit(nologin=True)
class LoginErrorPage(BasePage):
pass
class ChangePasswordPage(BasePage):
def on_loaded(self):
raise BrowserIncorrectPassword('Please change your password')
class VerifCodePage(BasePage):
def on_loaded(self):
raise BrowserIncorrectPassword('Unable to login: website asks a code from a card')
class InfoPage(BasePage):
pass
class EmptyPage(BasePage):
pass
class TransfertPage(BasePage):
pass
class UserSpacePage(BasePage):
pass
class AccountsPage(BasePage):
TYPES = {'C/C': Account.TYPE_CHECKING,
'Livret': Account.TYPE_SAVINGS,
'Pret': Account.TYPE_LOAN,
'Compte Courant': Account.TYPE_CHECKING,
'Compte Cheque': Account.TYPE_CHECKING,
'Compte Epargne': Account.TYPE_SAVINGS,
}
def get_list(self):
accounts = OrderedDict()
for tr in self.document.getiterator('tr'):
first_td = tr.getchildren()[0]
if (first_td.attrib.get('class', '') == 'i g' or first_td.attrib.get('class', '') == 'p g') \
and first_td.find('a') is not None:
a = first_td.find('a')
link = a.get('href', '')
if link.startswith('POR_SyntheseLst'):
continue
url = urlparse(link)
p = parse_qs(url.query)
if not 'rib' in p:
continue
for i in (2,1):
balance = FrenchTransaction.clean_amount(tr.getchildren()[i].text)
currency = Account.get_currency(tr.getchildren()[i].text)
if len(balance) > 0:
break
balance = Decimal(balance)
id = p['rib'][0]
if id in accounts:
account = accounts[id]
if not account.coming:
account.coming = Decimal('0.0')
account.coming += balance
account._card_links.append(link)
continue
account = Account()
account.id = id
account.label = unicode(a.text).strip().lstrip(' 0123456789').title()
for pattern, actype in self.TYPES.iteritems():
if account.label.startswith(pattern):
account.type = actype
account._link_id = link
account._card_links = []
# Find accounting amount
page = self.browser.get_document(self.browser.openurl(link))
coming = self.find_amount(page, u"Opérations à venir")
accounting = self.find_amount(page, u"Solde comptable")
if accounting is not None and accounting + (coming or Decimal('0')) != balance:
self.logger.warning('%s + %s != %s' % (accounting, coming, balance))
if accounting is not None:
balance = accounting
if coming is not None:
account.coming = coming
account.balance = balance
account.currency = currency
accounts[account.id] = account
return accounts.itervalues()
def find_amount(self, page, title):
try:
td = page.xpath(u'//th[contains(text(), "%s")]/../td' % title)[0]
except IndexError:
return None
else:
return Decimal(FrenchTransaction.clean_amount(td.text))
class Transaction(FrenchTransaction):
PATTERNS = [(re.compile('^VIR(EMENT)? (?P.*)'), FrenchTransaction.TYPE_TRANSFER),
(re.compile('^PRLV (?P.*)'), FrenchTransaction.TYPE_ORDER),
(re.compile('^(?P.*) CARTE \d+ PAIEMENT CB\s+(?P\d{2})(?P\d{2}) ?(.*)$'),
FrenchTransaction.TYPE_CARD),
(re.compile('^RETRAIT DAB (?P\d{2})(?P\d{2}) (?P.*) CARTE [\*\d]+'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile('^CHEQUE( (?P.*))?$'), FrenchTransaction.TYPE_CHECK),
(re.compile('^(F )?COTIS\.? (?P.*)'),FrenchTransaction.TYPE_BANK),
(re.compile('^(REMISE|REM CHQ) (?P.*)'),FrenchTransaction.TYPE_DEPOSIT),
]
_is_coming = False
class OperationsPage(BasePage):
def get_history(self):
index = 0
for tr in self.document.getiterator('tr'):
# columns can be:
# - date | value | operation | debit | credit | contre-valeur
# - date | value | operation | debit | credit
# - date | operation | debit | credit
# That's why we skip any extra columns, and take operation, debit
# and credit from last instead of first indexes.
tds = tr.getchildren()[:5]
if len(tds) < 4:
continue
if tds[0].attrib.get('class', '') == 'i g' or \
tds[0].attrib.get('class', '') == 'p g' or \
tds[0].attrib.get('class', '').endswith('_c1 c _c1'):
operation = Transaction(index)
index += 1
parts = [txt.strip() for txt in tds[-3].itertext() if len(txt.strip()) > 0]
# To simplify categorization of CB, reverse order of parts to separate
# location and institution.
if parts[0].startswith('PAIEMENT CB'):
parts.reverse()
date = tds[0].text
vdate = tds[1].text if len(tds) >= 5 else None
raw = u' '.join(parts)
operation.parse(date=date, vdate=vdate, raw=raw)
credit = self.parser.tocleanstring(tds[-1])
debit = self.parser.tocleanstring(tds[-2])
operation.set_amount(credit, debit)
yield operation
def go_next(self):
form = self.document.xpath('//form[@id="paginationForm"]')
if len(form) == 0:
return False
form = form[0]
text = self.parser.tocleanstring(form)
m = re.search(u'(\d+) / (\d+)', text or '', flags=re.MULTILINE)
if not m:
return False
cur = int(m.group(1))
last = int(m.group(2))
if cur == last:
return False
inputs = {}
for elm in form.xpath('.//input[@type="input"]'):
key = elm.attrib['name']
value = elm.attrib['value']
inputs[key] = value
inputs['page'] = str(cur + 1)
self.browser.location(form.attrib['action'], urllib.urlencode(inputs))
return True
def get_coming_link(self):
try:
a = self.parser.select(self.document, u'//a[contains(text(), "Opérations à venir")]', 1, 'xpath')
except BrokenPageError:
return None
else:
return a.attrib['href']
class ComingPage(OperationsPage):
def get_history(self):
index = 0
for tr in self.document.xpath('//table[@class="liste"]/tbody/tr'):
tds = tr.findall('td')
if len(tds) < 3:
continue
tr = Transaction(index)
date = self.parser.tocleanstring(tds[0])
raw = self.parser.tocleanstring(tds[1])
amount = self.parser.tocleanstring(tds[-1])
tr.parse(date=date, raw=raw)
tr.set_amount(amount)
tr._is_coming = True
yield tr
class CardPage(OperationsPage):
def get_history(self):
index = 0
# Check if this is a multi-cards page
pages = []
for a in self.document.xpath('//table[@class="liste"]/tbody/tr/td/a'):
card_link = a.get('href')
history_url = 'https://%s/%s/fr/banque/%s' % (self.browser.DOMAIN, self.browser.currentSubBank, card_link)
page = self.browser.get_document(self.browser.openurl(history_url))
pages.append(page)
if len(pages) == 0:
# If not, add this page as transactions list
pages.append(self.document)
for page in pages:
label = self.parser.tocleanstring(self.parser.select(page.getroot(), 'div.lister p.c', 1))
label = re.findall('(\d+ [^ ]+ \d+)', label)[-1]
# use the trick of relativedelta to get the last day of month.
debit_date = parse_french_date(label) + relativedelta(day=31)
for tr in page.xpath('//table[@class="liste"]/tbody/tr'):
tds = tr.findall('td')[:4]
if len(tds) < 4:
continue
tr = Transaction(index)
parts = [txt.strip() for txt in list(tds[-3].itertext()) + list(tds[-2].itertext()) if len(txt.strip()) > 0]
tr.parse(date=tds[0].text.strip(' \xa0'),
raw=u' '.join(parts))
tr.date = debit_date
tr.type = tr.TYPE_CARD
# Don't take all of the content (with tocleanstring for example),
# because there is a span.aide.
tr.set_amount(tds[-1].text)
yield tr
class NoOperationsPage(OperationsPage):
def get_history(self):
return iter([])
woob-e025fb0b2040e76d68512fca33d3483aa63d925d-modules-cic/modules/cic/test.py 0000664 0000000 0000000 00000001741 12356015330 0025324 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Julien Veyssier
#
# 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 CICTest(BackendTest):
BACKEND = 'cic'
def test_cic(self):
l = list(self.backend.iter_accounts())
if len(l) > 0:
a = l[0]
list(self.backend.iter_history(a))