Newer
Older
# -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of a weboob module.
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module 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 Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
Sylvie Ye
committed
from __future__ import unicode_literals
from weboob.browser.pages import LoggedPage, HTMLPage, JsonPage, pagination, FormNotFound
from weboob.browser.elements import ItemElement, method, ListElement, TableElement, SkipItem, DictElement
from weboob.browser.filters.standard import (
Date, CleanDecimal, Regexp, CleanText, Env, Upper,
Field, Eval, Format, Currency, Coalesce,
)
from weboob.browser.filters.html import Link, Attr, TableCell
from weboob.capabilities import NotAvailable
from weboob.capabilities.bank import (
Account, Investment, Recipient, TransferBankError, Transfer,
AddRecipientBankError, Loan, RecipientInvalidOTP,
from weboob.capabilities.bill import DocumentTypes, Subscription, Document
from weboob.tools.capabilities.bank.investments import is_isin_valid
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from weboob.tools.capabilities.bank.iban import is_rib_valid, rib2iban, is_iban_valid
from weboob.tools.captcha.virtkeyboard import GridVirtKeyboard
from weboob.tools.compat import unicode
from weboob.exceptions import NoAccountsException, BrowserUnavailable, ActionNeeded
from weboob.browser.filters.json import Dict
def MyDecimal(*args, **kwargs):
kwargs.update(replace_dots=True)
return CleanDecimal(*args, **kwargs)
class MyTableCell(TableCell):
def __init__(self, *names, **kwargs):
super(MyTableCell, self).__init__(*names, **kwargs)
self.td = './tr[%s]/td'
keys = ['MM$HISTORIQUE_COMPTE$btnCumul', 'Cartridge$imgbtnMessagerie', 'MM$m_CH$ButtonImageFondMessagerie',
'MM$m_CH$ButtonImageMessagerie']
for name in keys:
form.pop(name, None)
def float_to_decimal(f):
return Decimal(str(f))
def on_load(self):
error_msg = self.doc.get('error')
if error_msg and 'Le service est momentanément indisponible' in error_msg:
raise BrowserUnavailable(error_msg)
def get_response(self):
return self.doc
class CaissedepargneKeyboard(GridVirtKeyboard):
color = (255, 255, 255)
margin = 3, 3
symbols = {'0': 'ef8d775a73b751c5fbee06e2d537785c',
'1': 'bf51842846c3045f76355de32e4689c7',
'2': 'e4c057317b7ceb17241a0ae4c26844c4',
'3': 'c28c0c109a63f034d0f7c0f7ffdb364c',
'4': '6ea6a5152efb1d12c33f9cbf9476caec',
'5': '7ec4b424b5db7e7b2a54e6300fdb7515',
'6': 'a1fa95fc856804f978f20ad42c60f6d7',
'7': '64646adaa5a0b2506880970d8e928156',
'8': '4abcc6b24fa77f3756b96257962615eb',
'9': '3f41daf8ca5f250be5df91fe24079735'}
def __init__(self, image, symbols):
image = BytesIO(b64decode(image.encode('ascii')))
super(CaissedepargneKeyboard, self).__init__(symbols, 5, 3, image, self.color, convert='RGB')
def check_color(self, pixel):
for c in pixel:
if c < 250:
return True
class GarbagePage(LoggedPage, HTMLPage):
def on_load(self):
Jean Walrave
committed
go_back_link = Link('//a[@class="btn"]', default=NotAvailable)(self.doc)
if go_back_link is not NotAvailable:
assert len(go_back_link) != 1
go_back_link = re.search('\(~deibaseurl\)(.*)$', go_back_link).group(1)
self.browser.location('%s%s' % (self.browser.BASEURL, go_back_link))
def get_message(self):
return CleanText('//form[contains(@name, "leForm")]//span')(self.doc)
form.submit()
class _LogoutPage(HTMLPage):
def on_load(self):
raise BrowserUnavailable(CleanText('//*[@class="messErreur"]')(self.doc))
class Transaction(FrenchTransaction):
PATTERNS = [
(re.compile(r'^CB (?P<text>.*?) FACT (?P<dd>\d{2})(?P<mm>\d{2})(?P<yy>\d{2})\b', re.IGNORECASE), FrenchTransaction.TYPE_CARD),
(re.compile(r'^RET(RAIT)? DAB (?P<dd>\d+)-(?P<mm>\d+)-.*', re.IGNORECASE), FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^RET(RAIT)? DAB (?P<text>.*?) (?P<dd>\d{2})(?P<mm>\d{2})(?P<yy>\d{2}) (?P<HH>\d{2})H(?P<MM>\d{2})\b', re.IGNORECASE), FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^VIR(EMENT)?(\.PERIODIQUE)? (?P<text>.*)', re.IGNORECASE), FrenchTransaction.TYPE_TRANSFER),
(re.compile(r'^PRLV (?P<text>.*)', re.IGNORECASE), FrenchTransaction.TYPE_ORDER),
(re.compile(r'^CHEQUE.*', re.IGNORECASE), FrenchTransaction.TYPE_CHECK),
(re.compile(r'^(CONVENTION \d+ )?COTIS(ATION)? (?P<text>.*)', re.IGNORECASE), FrenchTransaction.TYPE_BANK),
(re.compile(r'^\* (?P<text>.*)', re.IGNORECASE), FrenchTransaction.TYPE_BANK),
(re.compile(r'^REMISE (?P<text>.*)', re.IGNORECASE), FrenchTransaction.TYPE_DEPOSIT),
(re.compile(r'^(?P<text>.*)( \d+)? QUITTANCE .*', re.IGNORECASE), FrenchTransaction.TYPE_ORDER),
(re.compile(r'^CB [\d\*]+ TOT DIF .*', re.IGNORECASE), FrenchTransaction.TYPE_CARD_SUMMARY),
(re.compile(r'^CB [\d\*]+ (?P<text>.*)', re.IGNORECASE), FrenchTransaction.TYPE_CARD),
(re.compile(r'^CB (?P<text>.*?) (?P<dd>\d{2})(?P<mm>\d{2})(?P<yy>\d{2})\b', re.IGNORECASE), FrenchTransaction.TYPE_CARD),
(re.compile(r'\*CB (?P<text>.*?) (?P<dd>\d{2})(?P<mm>\d{2})(?P<yy>\d{2})\b', re.IGNORECASE), FrenchTransaction.TYPE_CARD),
(re.compile(r'^FAC CB (?P<text>.*?) (?P<dd>\d{2})/(?P<mm>\d{2})\b', re.IGNORECASE), FrenchTransaction.TYPE_CARD),
(re.compile(r'^\*?CB (?P<text>.*)', re.IGNORECASE), FrenchTransaction.TYPE_CARD),
# For life insurances and capitalisation contracts
(re.compile(r'^VERSEMENT', re.IGNORECASE), FrenchTransaction.TYPE_DEPOSIT),
(re.compile(r'^Réinvestissement', re.IGNORECASE), FrenchTransaction.TYPE_DEPOSIT),
(re.compile(r'^REVALORISATION', re.IGNORECASE), FrenchTransaction.TYPE_BANK),
(re.compile(r'^ARBITRAGE', re.IGNORECASE), FrenchTransaction.TYPE_BANK),
(re.compile(r'^RACHAT PARTIEL', re.IGNORECASE), FrenchTransaction.TYPE_BANK),
]
ACCOUNT_TYPES = {u'Epargne liquide': Account.TYPE_SAVINGS,
u'Compte Courant': Account.TYPE_CHECKING,
u'COMPTE CHEQUE': Account.TYPE_CHECKING,
Jean Walrave
committed
u'CPT DEPOT PART.': Account.TYPE_CHECKING,
u'CPT DEPOT PROF.': Account.TYPE_CHECKING,
u'Compte Epargne et DAT': Account.TYPE_SAVINGS,
u'Plan et Contrat d\'Epargne': Account.TYPE_SAVINGS,
u'COMPTE SUR LIVRET': Account.TYPE_SAVINGS,
u'LIVRET DEV.DURABLE': Account.TYPE_SAVINGS,
Jean Walrave
committed
u'LDD Solidaire': Account.TYPE_SAVINGS,
u'LIVRET A': Account.TYPE_SAVINGS,
u'LIVRET JEUNE': Account.TYPE_SAVINGS,
u'LIVRET GRAND PRIX': Account.TYPE_SAVINGS,
u'LEP': Account.TYPE_SAVINGS,
u'LEL': Account.TYPE_SAVINGS,
u'CPT PARTS SOCIALES': Account.TYPE_SAVINGS,
u'PEL 16 2013': Account.TYPE_SAVINGS,
u'Titres': Account.TYPE_MARKET,
u'Mes crédits immobiliers': Account.TYPE_LOAN,
u'Mes crédits renouvelables': Account.TYPE_LOAN,
u'Mes crédits consommation': Account.TYPE_LOAN,
u'PEA NUMERAIRE': Account.TYPE_PEA,
u'PEA': Account.TYPE_PEA,
content = content.strip(b'\x00')
return super(IndexPage, self).build_doc(content)
# For now, we have to handle this because after this warning message,
# the user is disconnected (even if all others account are reachable)
if 'OIC_QCF' in self.browser.url:
# QCF is a mandatory test to make sure you know the basics about financials products
# however, you can still choose to postpone it. hence the continue link
link = Link('//span[@id="lea-prdvel-lien"]//b/a[contains(text(), "Continuer")]')(self.doc)
if link:
self.logger.warning("By-passing QCF")
self.browser.location(link)
else:
message = CleanText(self.doc.xpath('//span[contains(@id, "OIC_QCF")]/p'))(self)
if message and "investissement financier (QCF) n’est plus valide à ce jour ou que vous avez refusé d’y répondre" in message:
raise ActionNeeded(message)
mess = CleanText('//body/div[@class="content"]//p[contains(text(), "indisponible pour cause de maintenance")]')(self.doc)
if mess:
raise BrowserUnavailable(mess)
# This page is sometimes an useless step to the market website.
bourse_link = Link(u'//div[@id="MM_COMPTE_TITRE_pnlbourseoic"]//a[contains(text(), "Accédez à la consultation")]', default=None)(self.doc)
if bourse_link:
Jean Walrave
committed
self.browser.location(bourse_link)
def need_auth(self):
return bool(CleanText(u'//span[contains(text(), "Authentification non rejouable")]')(self.doc))
Jean Walrave
committed
def check_no_loans(self):
return not bool(CleanText(u'//table[@class="menu"]//div[contains(., "Crédits")]')(self.doc)) and \
not bool(CleanText(u'//table[@class="header-navigation_main"]//a[contains(., "Crédits")]')(self.doc))
Jean Walrave
committed
def check_measure_accounts(self):
return not CleanText(u'//div[@class="MessageErreur"]/ul/li[contains(text(), "Aucun compte disponible")]')(self.doc)
no_account_message = CleanText(u'//span[@id="MM_LblMessagePopinError"]/p[contains(text(), "Aucun compte disponible")]')(self.doc)
if no_account_message:
raise NoAccountsException(no_account_message)
def find_and_replace(self, info, acc_id):
# The site might be broken: id in js: 4097800039137N418S00197, id in title: 1379418S001 (N instead of 9)
# So we seek for a 1 letter difference and replace if found .... (so sad)
for i in range(len(info['id']) - len(acc_id) + 1):
sub_part = info['id'][i:i+len(acc_id)]
z = zip(sub_part, acc_id)
if len([tuple_letter for tuple_letter in z if len(set(tuple_letter)) > 1]) == 1:
info['link'] = info['link'].replace(sub_part, acc_id)
info['id'] = info['id'].replace(sub_part, acc_id)
return
def _get_account_info(self, a, accounts):
m = re.search("PostBack(Options)?\([\"'][^\"']+[\"'],\s*['\"]([HISTORIQUE_\w|SYNTHESE_ASSURANCE_CNP|BOURSE|COMPTE_TITRE][\d\w&]+)?['\"]", a.attrib.get('href', ''))
if m is None:
return None
else:
# it is in form CB&12345[&2]. the last part is only for new website
# and is necessary for navigation.
link = m.group(2)
parts = link.split('&')
info = {}
id = re.search("([\d]+)", a.attrib.get('title', ''))
if len(parts) > 1:
info['type'] = parts[0]
info['id'] = info['_id'] = parts[1]
if id or info['id'] in [acc._info['_id'] for acc in accounts.values()]:
_id = id.group(1) if id else next(iter({k for k, v in accounts.items() if info['id'] == v._info['_id']}))
self.find_and_replace(info, _id)
else:
info['type'] = link
info['id'] = info['_id'] = id.group(1)
if info['type'] in ('SYNTHESE_ASSURANCE_CNP', 'SYNTHESE_EPARGNE', 'ASSURANCE_VIE'):
info['acc_type'] = Account.TYPE_LIFE_INSURANCE
info['acc_type'] = Account.TYPE_MARKET
return info
def is_account_inactive(self, account_id):
return self.doc.xpath('//tr[td[contains(text(), $id)]][@class="Inactive"]', id=account_id)
def _add_account(self, accounts, link, label, account_type, balance, number=None):
info = self._get_account_info(link, accounts)
self.logger.warning('Unable to parse account %r: %r' % (label, link))
account._card_links = None
if is_rib_valid(info['id']):
account.iban = rib2iban(info['id'])
account.number = number
account.type = self.ACCOUNT_TYPES.get(label, info['acc_type'] if 'acc_type' in info else account_type)
if 'PERP' in account.label:
account.type = Account.TYPE_PERP
Quentin Defenouillere
committed
if 'NUANCES CAPITALISATI' in account.label:
account.type = Account.TYPE_CAPITALISATION
balance = balance or self.get_balance(account)
account.balance = Decimal(FrenchTransaction.clean_amount(balance)) if balance and balance is not NotAvailable else NotAvailable
account.currency = account.get_currency(balance) if balance and balance is not NotAvailable else NotAvailable
# Set coming history link to the parent account. At this point, we don't have card account yet.
if account._info['type'] == 'HISTORIQUE_CB' and account.id in accounts:
a.coming = Decimal('0.0')
a._card_links = account._info
return
accounts[account.id] = account
return account
def get_balance(self, account):
Quentin Defenouillere
committed
if account.type not in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_PERP, Account.TYPE_CAPITALISATION):
page = self.go_history(account._info).page
Quentin Defenouillere
committed
balance = page.doc.xpath('.//tr[td[contains(@id,"NumContrat")]]/td[@class="somme"]/a[contains(@href, $id)]', id=account.id)
if len(balance) > 0:
balance = balance if balance != u'' else NotAvailable
else:
# Specific xpath for some Life Insurances:
balance = page.doc.xpath('//tr[td[contains(text(), $id)]]/td/div[contains(@id, "Solde")]', id=account.id)
if len(balance) > 0:
balance = CleanText('.')(balance[0])
balance = balance if balance != u'' else NotAvailable
else:
# sometimes the accounts are attached but no info is available
balance = NotAvailable
self.go_list()
def get_measure_balance(self, account):
for tr in self.doc.xpath('//table[@cellpadding="1"]/tr[not(@class)]'):
account_number = CleanText('./td/a[contains(@class, "NumeroDeCompte")]')(tr)
if re.search(r'[A-Z]*\d{3,}', account_number).group() in account.id:
# The regex '\s\d{1,3}(?:[\s.,]\d{3})*(?:[\s.,]\d{2})' matches for example '106 100,64'
return re.search(r'\s\d{1,3}(?:[\s.,]\d{3})*(?:[\s.,]\d{2})', account_number).group()
return NotAvailable
def get_measure_ids(self):
accounts_id = []
for a in self.doc.xpath('//table[@cellpadding="1"]/tr/td[2]/a'):
accounts_id.append(re.search("(\d{6,})", Attr('.', 'href')(a)).group(1))
return accounts_id
accounts = OrderedDict()
Romain Bignon
committed
self.browser.new_website = False
for table in self.doc.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()) or\
self.ACCOUNT_TYPES.get(CleanText('.')(tds[2])) or\
self.ACCOUNT_TYPES.get(CleanText('.')(tds[3]), Account.TYPE_UNKNOWN)
# On the same row, there could have many accounts (check account and a card one).
# For the card line, the number will be the same than the checking account, so we skip it.
if len(tds) > 4:
for i, a in enumerate(tds[2].xpath('./a')):
label = CleanText('.')(a)
balance = CleanText('.')(tds[-2].xpath('./a')[i])
number = None
# if i > 0, that mean it's a card account. The number will be the same than it's
# checking parent account, we have to skip it.
if i == 0:
number = CleanText('.')(tds[-4].xpath('./a')[0])
self._add_account(accounts, a, label, account_type, balance, number)
# Only 4 tds on "banque de la reunion" website.
elif len(tds) == 4:
for i, a in enumerate(tds[1].xpath('./a')):
label = CleanText('.')(a)
balance = CleanText('.')(tds[-1].xpath('./a')[i])
self._add_account(accounts, a, label, account_type, balance)
self.logger.debug('we are on the %s website', 'old' if accounts else 'new')
self.browser.new_website = True
for table in self.doc.xpath('//div[@class="panel"]'):
title = table.getprevious()
if title is None:
continue
account_type = self.ACCOUNT_TYPES.get(CleanText('.')(title), Account.TYPE_UNKNOWN)
for tr in table.xpath('.//tr'):
tds = tr.findall('td')
a = tds[i].find('a')
if a is not None:
break
if a is None:
continue
# sometimes there's a tooltip span to ignore next to <strong>
# (perhaps only on creditcooperatif)
label = CleanText('./strong')(tds[0])
account = self._add_account(accounts, a, label, account_type, balance)
if account:
account.number = CleanText('.')(tds[1])
def is_access_error(self):
error_message = u"Vous n'êtes pas autorisé à accéder à cette fonction"
if error_message in CleanText('//div[@class="MessageErreur"]')(self.doc):
def go_loans_conso(self, tr):
link = tr.xpath('./td/a[contains(@id, "IdaCreditPerm")]')
m = re.search('CREDITCONSO&(\w+)', link[0].attrib['href'])
if m:
account = m.group(1)
form['__EVENTTARGET'] = 'MM$SYNTHESE_CREDITS'
form['__EVENTARGUMENT'] = 'ACTIVDESACT_CREDITCONSO&%s' % account
form['m_ScriptManager'] = 'MM$m_UpdatePanel|MM$SYNTHESE_CREDITS'
form.submit()
def get_loan_list(self):
accounts = OrderedDict()
# Old website
for tr in self.doc.xpath('//table[@cellpadding="1"]/tr[not(@class) and td[a]]'):
tds = tr.findall('td')
account = Account()
account._card_links = None
account.id = CleanText('./a')(tds[2]).split('-')[0].strip()
account.label = CleanText('./a')(tds[2]).split('-')[-1].strip()
account.type = Account.TYPE_LOAN
account.balance = -CleanDecimal('./a', replace_dots=True)(tds[4])
account.currency = account.get_currency(CleanText('./a')(tds[4]))
accounts[account.id] = account
self.logger.debug('we are on the %s website', 'old' if accounts else 'new')
if len(accounts) == 0:
# New website
for table in self.doc.xpath('//div[@class="panel"]'):
title = table.getprevious()
if title is None:
if "immobiliers" not in CleanText('.')(title):
account_type = self.ACCOUNT_TYPES.get(CleanText('.')(title), Account.TYPE_UNKNOWN)
for tr in table.xpath('./table/tbody/tr[contains(@id,"MM_SYNTHESE_CREDITS") and contains(@id,"IdTrGlobal")]'):
tds = tr.findall('td')
if len(tds) == 0 :
continue
for i in tds[0].xpath('.//a/strong'):
label = i.text.strip()
break
if len(tds) == 3 and Decimal(FrenchTransaction.clean_amount(CleanText('.')(tds[-2]))) and any(cls in Attr('.', 'id')(tr) for cls in ['dgImmo', 'dgConso']) == False:
# in case of Consumer credit or revolving credit, we substract avalaible amount with max amout
# to get what was spend
balance = Decimal(FrenchTransaction.clean_amount(CleanText('.')(tds[-2]))) - Decimal(FrenchTransaction.clean_amount(CleanText('.')(tds[-1])))
else:
balance = Decimal(FrenchTransaction.clean_amount(CleanText('.')(tds[-1])))
account = Loan()
account.id = label.split(' ')[-1]
account.label = unicode(label)
account.type = account_type
account.balance = -abs(balance)
account.currency = account.get_currency(CleanText('.')(tds[-1]))
account._card_links = []
if "renouvelables" in CleanText('.')(title):
if 'JSESSIONID' in self.browser.session.cookies:
# Need to delete this to access the consumer loans space (a new one will be created)
del self.browser.session.cookies['JSESSIONID']
self.go_loans_conso(tr)
d = self.browser.loans_conso()
if d:
account.total_amount = float_to_decimal(d['contrat']['creditMaxAutorise'])
account.available_amount = float_to_decimal(d['situationCredit']['disponible'])
account.next_payment_amount = float_to_decimal(d['situationCredit']['mensualiteEnCours'])
accounts[account.id] = account
@method
class get_real_estate_loans(ListElement):
# beware the html response is slightly different from what can be seen with the browser
# because of some JS most likely: use the native HTML response to build the xpath
item_xpath = '//h3[contains(text(), "immobiliers")]//following-sibling::div[@class="panel"][1]//div[@id[starts-with(.,"MM_SYNTHESE_CREDITS")] and contains(@id, "IdDivDetail")]'
class iter_account(TableElement):
item_xpath = './table[@class="static"][1]/tbody'
head_xpath = './table[@class="static"][1]/tbody/tr/th'
col_total_amount = u'Capital Emprunté'
col_rate = u'Taux d’intérêt nominal'
col_balance = u'Capital Restant Dû'
col_last_payment_date = u'Dernière échéance'
col_next_payment_amount = u'Montant prochaine échéance'
col_next_payment_date = u'Prochaine échéance'
def parse(self, el):
self.env['id'] = CleanText("./h2")(el).split()[-1]
self.env['label'] = CleanText("./h2")(el)
class item(ItemElement):
klass = Loan
obj_id = Env('id')
obj_label = Env('label')
obj_type = Loan.TYPE_LOAN
obj_total_amount = MyDecimal(MyTableCell("total_amount"))
obj_rate = Eval(lambda x: x / 100, MyDecimal(MyTableCell("rate", default=NotAvailable), default=NotAvailable))
obj_balance = MyDecimal(MyTableCell("balance"), sign=lambda x: -1)
obj_currency = Currency(MyTableCell("balance"))
obj_last_payment_date = Date(CleanText(MyTableCell("last_payment_date")))
obj_next_payment_amount = MyDecimal(MyTableCell("next_payment_amount"))
obj_next_payment_date = Date(CleanText(MyTableCell("next_payment_date", default=''), default=NotAvailable), default=NotAvailable)
def submit_form(self, form, eventargument, eventtarget, scriptmanager):
form['__EVENTARGUMENT'] = eventargument
form['__EVENTTARGET'] = eventtarget
form['m_ScriptManager'] = scriptmanager
fix_form(form)
form.submit()
eventargument = "CPTSYNT0"
if "MM$m_CH$IsMsgInit" in form:
# Old website
eventtarget = "Menu_AJAX"
scriptmanager = "m_ScriptManager|Menu_AJAX"
else:
# New website
eventtarget = "MM$m_PostBack"
scriptmanager = "MM$m_UpdatePanel|MM$m_PostBack"
self.submit_form(form, eventargument, eventtarget, scriptmanager)
def go_cards(self):
# Do not try to go the card summary if we have no card, it breaks the session
if self.browser.new_website and not CleanText('//form[@id="main"]//a/span[text()="Mes cartes bancaires"]')(self.doc):
self.logger.info("Do not try to go the CardsPage, there is not link on the main page")
return
form = self.get_form(id='main')
eventargument = ""
if "MM$m_CH$IsMsgInit" in form:
# Old website
eventtarget = "Menu_AJAX"
eventargument = "HISENCB0"
scriptmanager = "m_ScriptManager|Menu_AJAX"
eventtarget = "MM$SYNTHESE$btnSyntheseCarte"
scriptmanager = "MM$m_UpdatePanel|MM$SYNTHESE$btnSyntheseCarte"
self.submit_form(form, eventargument, eventtarget, scriptmanager)
# only for old website
def go_card_coming(self, eventargument):
form = self.get_form(id='main')
eventtarget = "MM$HISTORIQUE_CB"
scriptmanager = "m_ScriptManager|Menu_AJAX"
self.submit_form(form, eventargument, eventtarget, scriptmanager)
# only for new website
def go_coming(self, eventargument):
form = self.get_form(id='main')
eventtarget = "MM$HISTORIQUE_CB"
scriptmanager = "MM$m_UpdatePanel|MM$HISTORIQUE_CB"
self.submit_form(form, eventargument, eventtarget, scriptmanager)
# On some pages, navigate to indexPage does not lead to the list of measures, so we need this form ...
def go_measure_list(self):
form['__EVENTARGUMENT'] = "MESLIST0"
form['__EVENTTARGET'] = 'Menu_AJAX'
form['m_ScriptManager'] = 'm_ScriptManager|Menu_AJAX'
form.submit()
# This function goes to the accounts page of one measure giving its id
def go_measure_accounts_list(self, measure_id):
form['__EVENTARGUMENT'] = "CPTSYNT0"
if "MM$m_CH$IsMsgInit" in form:
# Old website
form['__EVENTTARGET'] = "MM$SYNTHESE_MESURES"
form['m_ScriptManager'] = "MM$m_UpdatePanel|MM$SYNTHESE_MESURES"
form['__EVENTARGUMENT'] = measure_id
else:
# New website
form['__EVENTTARGET'] = "MM$m_PostBack"
form['m_ScriptManager'] = "MM$m_UpdatePanel|MM$m_PostBack"
form.submit()
form['__EVENTARGUMENT'] = "CRESYNT0"
if "MM$m_CH$IsMsgInit" in form:
# Old website
else:
# New website
form['__EVENTTARGET'] = "MM$m_PostBack"
form['m_ScriptManager'] = "MM$m_UpdatePanel|MM$m_PostBack"
Jerome Berthier
committed
def is_history_of(self, account_id):
"""
Jerome Berthier
committed
Check whether the displayed history is for the correct account.
If we do not find the select box we consider we are on the expected account (like it was before this check)
Jerome Berthier
committed
"""
Jerome Berthier
committed
if self.doc.xpath('//select[@id="MM_HISTORIQUE_COMPTE_m_ExDropDownList"]'):
return bool(self.doc.xpath('//option[@value="%s" and @selected]' % account_id))
return True
Jerome Berthier
committed
def go_history(self, info, is_cbtab=False):
form['__EVENTTARGET'] = 'MM$%s' % (info['type'] if is_cbtab else 'SYNTHESE')
form['__EVENTARGUMENT'] = info['link']
if "MM$m_CH$IsMsgInit" in form and (form['MM$m_CH$IsMsgInit'] == "0" or info['type'] == 'ASSURANCE_VIE'):
form['m_ScriptManager'] = "MM$m_UpdatePanel|MM$SYNTHESE"
return form.submit()
Jerome Berthier
committed
def go_history_netpro(self, info, ):
"""
On the netpro website the go_history() does not work.
Even from a web browser the site does not work, and display the history of the first account
We use a different post to go through and display the history we need
"""
form = self.get_form(id='main')
form['m_ScriptManager'] = 'MM$m_UpdatePanel|MM$HISTORIQUE_COMPTE$m_ExDropDownList'
form['MM$HISTORIQUE_COMPTE$m_ExDropDownList'] = info['id']
form['__EVENTTARGET'] = 'MM$HISTORIQUE_COMPTE$m_ExDropDownList'
fix_form(form)
return form.submit()
def get_form_to_detail(self, transaction):
m = re.match('.*\("(.*)", "(DETAIL_OP&[\d]+).*\)\)', transaction._link)
# go to detailcard page
form['__EVENTARGUMENT'] = m.group(2)
return form
def get_history(self):
i = 0
for tr in self.doc.xpath('//table[@cellpadding="1"]/tr') + self.doc.xpath('//tr[@class="rowClick" or @class="rowHover"]'):
# if there are more than 4 columns, ignore the first one.
i = min(len(tds) - 4, 1)
if tr.attrib.get('class', '') == 'DataGridHeader':
if tds[2].text == u'Titulaire':
ignore = True
else:
ignore = False
# Remove useless details
detail = tr.cssselect('div.detail')
if len(detail) > 0:
detail[0].drop_tree()
date = u''.join([txt.strip() for txt in tds[i+0].itertext()])
raw = u' '.join([txt.strip() for txt in tds[i+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))
card_debit_date = self.doc.xpath(u'//span[@id="MM_HISTORIQUE_CB_m_TableTitle3_lblTitle"] | //label[contains(text(), "débiter le")]')
t.rdate = t.bdate = Date(dayfirst=True).filter(date)
m = re.search(r'\b(\d{2}/\d{2}/\d{4})\b', card_debit_date[0].text)
assert m
t.date = Date(dayfirst=True).filter(m.group(1))
if t.date is NotAvailable:
continue
if 'tot dif' in t.raw.lower():
t._link = Link(tr.xpath('./td/a'))(self.doc)
# "Cb" for new site, "CB" for old one
mtc = re.match(r'(Cb|CB) (\d{4}\*+\d{6}) ', raw)
if mtc is not None:
t.card = mtc.group(2)
t.set_amount(credit, debit)
yield t
i += 1
# <a id="MM_HISTORIQUE_CB_lnkSuivante" class="next" href="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("MM$HISTORIQUE_CB$lnkSuivante", "", true, "", "", false, true))">Suivant<span class="arrow">></span></a>
link = self.doc.xpath('//a[contains(@id, "lnkSuivante")]')
if len(link) == 0 or 'disabled' in link[0].attrib or link[0].attrib.get('class') == 'aspNetDisabled':
account_type = 'COMPTE'
m = re.search('HISTORIQUE_(\w+)', link[0].attrib['href'])
if m:
account_type = m.group(1)
form['__EVENTTARGET'] = "MM$HISTORIQUE_%s$lnkSuivante" % account_type
form['__EVENTARGUMENT'] = ''
if "MM$m_CH$IsMsgInit" in form and form['MM$m_CH$IsMsgInit'] == "N":
form['m_ScriptManager'] = "MM$m_UpdatePanel|MM$HISTORIQUE_COMPTE$lnkSuivante"
def go_life_insurance(self, account):
# The site shows nothing about life insurance accounts except balance, links are disabled
if 'measure_id' in account._info:
return
link = self.doc.xpath('//tr[td[contains(., ' + account.id + ') ]]//a')[0]
m = re.search("PostBackOptions?\([\"']([^\"']+)[\"'],\s*['\"]((REDIR_ASS_VIE)?[\d\w&]+)?['\"]", link.attrib.get('href', ''))
if m is not None:
form['__EVENTTARGET'] = m.group(1)
form['__EVENTARGUMENT'] = m.group(2)
if "MM$m_CH$IsMsgInit" not in form:
# Not available on new website
pass
form['MM$m_CH$IsMsgInit'] = "0"
form['m_ScriptManager'] = "MM$m_UpdatePanel|MM$SYNTHESE"
def transfer_link(self):
return self.doc.xpath(u'//a[span[contains(text(), "Effectuer un virement")]] | //a[contains(text(), "Réaliser un virement")]')
def go_transfer_via_history(self, account):
self.go_history(account._info)
# check that transfer is available for the connection before try to go on transfer page
# otherwise website will continually crash
if self.transfer_link():
self.browser.page.go_transfer(account)
def go_transfer(self, account):
link = self.transfer_link()
if len(link) == 0:
return self.go_transfer_via_history(account)
else:
link = link[0]
m = re.search("PostBackOptions?\([\"']([^\"']+)[\"'],\s*['\"]([^\"']+)?['\"]", link.attrib.get('href', ''))
if 'MM$HISTORIQUE_COMPTE$btnCumul' in form:
del form['MM$HISTORIQUE_COMPTE$btnCumul']
form['__EVENTTARGET'] = m.group(1)
form['__EVENTARGUMENT'] = m.group(2)
form.submit()
def transfer_unavailable(self):
return CleanText(u'//li[contains(text(), "Pour accéder à cette fonctionnalité, vous devez disposer d’un moyen d’authentification renforcée")]')(self.doc)
def loan_unavailable_msg(self):
msg = CleanText('//span[@id="MM_LblMessagePopinError"] | //p[@id="MM_ERREUR_PAGE_BLANCHE_pAlert"]')(self.doc)
if msg:
return msg
def go_subscription(self):
form['m_ScriptManager'] = 'MM$m_UpdatePanel|MM$Menu_Ajax'
form['__EVENTTARGET'] = 'MM$Menu_Ajax'
link = Link('//a[contains(@title, "e-Documents") or contains(@title, "Relevés en ligne")]')(self.doc)
form['__EVENTARGUMENT'] = re.search(r'Ajax", "(.*)", true', link).group(1)
form.submit()
Florian Duguet
committed
def is_subscription_unauthorized(self):
return 'non autorisée' in CleanText('//div[@id="MM_ContentMain"]')(self.doc)
def go_pro_transfer_availability(self):
form = self.get_form(id='main')
form['__EVENTTARGET'] = 'Menu_AJAX'
form['__EVENTARGUMENT'] = 'VIRLSRM0'
form['m_ScriptManager'] = 'm_ScriptManager|Menu_AJAX'
form.submit()
def is_transfer_allowed(self):
return not self.doc.xpath('//ul/li[contains(text(), "Aucun compte tiers n\'est disponible")]')
class CardsPage(IndexPage):
def is_here(self):
return CleanText('//h3[normalize-space(text())="Mes cartes (cartes dont je suis le titulaire)"]')(self.doc)
@method
class iter_cards(TableElement):
head_xpath = '//table[@class="cartes"]/tbody/tr/th'
col_label = 'Carte'
col_number = 'N°'
col_parent = 'Compte dépot associé'
col_coming = 'Encours'
item_xpath = '//table[@class="cartes"]/tbody/tr[not(th)]'
class item(ItemElement):
klass = Account
obj_type = Account.TYPE_CARD
obj_label = Format('%s %s', CleanText(TableCell('label')), Field('id'))
obj_number = CleanText(TableCell('number'))
obj_id = CleanText(TableCell('number'), replace=[('*', 'X')])
obj__parent_id = CleanText(TableCell('parent'))
obj_balance = 0
obj_currency = Currency(TableCell('coming'))
obj__card_links = None
def obj_coming(self):
if CleanText(TableCell('coming'))(self) == '-':
raise SkipItem('immediate debit card?')
return CleanDecimal.French(TableCell('coming'), sign=lambda x: -1)(self)
Maxime Pommier
committed
def condition(self):
immediate_str = ''
# There are some card without any information. To exclude them, we keep only account
# with extra "option" (ex: coming transaction link, block bank card...)
if 'Faire opposition' in CleanText("./td[5]")(self):
# Only deferred card have this option to see coming transaction, even when
# there is 0 coming (Table element have no thead for the 5th column).
if 'Consulter mon encours carte' in CleanText("./td[5]")(self):
return True
# Card without 'Consulter mon encours carte' are immediate card. There are logged
# for now to make the debug easier
immediate_str = '[Immediate card]'
self.logger.warning('Skip card %s (no history/coming information) %s', Field('number')(self), immediate_str)
return False
class CardsComingPage(IndexPage):
def is_here(self):
return CleanText('//h2[text()="Encours de carte à débit différé"]')(self.doc)
@method
class iter_cards(ListElement):
item_xpath = '//table[contains(@class, "compte") and position() = 1]//tr[contains(@id, "MM_HISTORIQUE_CB") and position() < last()]'
class item(ItemElement):
klass = Account
def obj_id(self):
# We must handle two kinds of Regexp because the 'X' are not
# located at the same level for sub-modules such as palatine
return Coalesce(
Regexp(CleanText(Field('label'), replace=[('*', 'X')]), r'(\d{6}\X{6}\d{4})', default=NotAvailable),
Regexp(CleanText(Field('label'), replace=[('*', 'X')]), r'(\d{4}\X{6}\d{6})', default=NotAvailable),
)(self)
def obj_number(self):
return Coalesce(
Regexp(CleanText(Field('label')), r'(\d{6}\*{6}\d{4})', default=NotAvailable),
Regexp(CleanText(Field('label')), r'(\d{4}\*{6}\d{6})', default=NotAvailable),
)(self)
obj_type = Account.TYPE_CARD
obj_label = CleanText('./td[1]')
obj_balance = Decimal(0)
obj_coming = CleanDecimal.French('./td[2]')
obj_currency = Currency('./td[2]')
obj__card_links = None
def get_card_coming_info(self, number, info):
# If the xpath match, that mean there are only one card
# We have enough information in `info` to get its coming transaction
if CleanText('//tr[@id="MM_HISTORIQUE_CB_rptMois0_ctl01_trItem"]')(self.doc):
return info
# If the xpath match, that mean there are at least 2 cards
xpath = '//tr[@id="MM_HISTORIQUE_CB_rptMois0_trItem_0"]'
# In case of multiple card, first card coming's transactions are reachable
# with information in `info`.
if Regexp(CleanText(xpath), r'(\d{6}\*{6}\d{4})')(self.doc) == number:
return info
# Some cards redirect to a checking account where we cannot found them. Since we have no details or history,
# we return None and skip them in the browser.
if CleanText('//a[contains(text(),"%s")]' % number)(self.doc):
# For all cards except the first one for the same check account, we have to get info through their href info
link = CleanText(Link('//a[contains(text(),"%s")]' % number))(self.doc)
infos = re.match(r'.*(DETAIL_OP_M0&[^\"]+).*', link)
info['link'] = infos.group(1)
return info
return None
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
class CardsOldWebsitePage(IndexPage):
def is_here(self):
return CleanText('//span[@id="MM_m_CH_lblTitle" and contains(text(), "Historique de vos encours CB")]')(self.doc)
def get_account(self):
infos = CleanText('.//span[@id="MM_HISTORIQUE_CB"]/table[position()=1]//td')(self.doc)
result = re.search(r'.*(\d{11}).*', infos)
return result.group(1)
def get_date(self):
title = CleanText('//span[@id="MM_HISTORIQUE_CB_m_TableTitle3_lblTitle"]')(self.doc)
title_date = re.match('.*le (.*) sur .*', title)
return Date(dayfirst=True).filter(title_date.group(1))
@method
class iter_cards(TableElement):
head_xpath = '//table[@id="MM_HISTORIQUE_CB_m_ExDGOpeM0"]//tr[@class="DataGridHeader"]/td'
item_xpath = '//table[@id="MM_HISTORIQUE_CB_m_ExDGOpeM0"]//tr[not(contains(@class, "DataGridHeader")) and position() < last()]'
col_label = 'Libellé'
col_coming = 'Solde'
class item(ItemElement):
klass = Account
obj_type = Account.TYPE_CARD
obj_label = Format('%s %s', CleanText(TableCell('label')), CleanText(Field('number')))
obj_balance = 0
obj_coming = CleanDecimal.French(TableCell('coming'))
obj_currency = Currency(TableCell('coming'))
obj__card_links = None
def obj__parent_id(self):
return self.page.get_account()
def obj_number(self):
return CleanText(TableCell('number'))(self).replace('*', 'X')
def obj_id(self):
number = Field('number')(self).replace('X', '')
account_id = '%s-%s' % (self.obj__parent_id(), number)
return account_id
def obj__coming_eventargument(self):
url = Attr('.//a', 'href')(self)
res = re.match(r'.*(DETAIL_OP_M0\&.*;\d{8})", .*', url)
return res.group(1)