# -*- coding: utf-8 -*-
# Copyright(C) 2013-2015 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 __future__ import unicode_literals
from datetime import date as ddate, datetime
from decimal import Decimal
import re
from weboob.browser.pages import HTMLPage, FormNotFound
from weboob.capabilities import NotAvailable
from weboob.capabilities.base import Currency
from weboob.capabilities.bank import (
Account, Investment, Recipient, Transfer, TransferError, TransferBankError,
AddRecipientError, Loan
)
from weboob.capabilities.contact import Advisor
from weboob.capabilities.profile import Profile
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded
from weboob.tools.capabilities.bank.transactions import FrenchTransaction as Transaction
from weboob.tools.date import parse_french_date, LinearDateGuesser
from weboob.tools.compat import urlparse, urljoin, unicode
from weboob.browser.elements import TableElement, ItemElement, method
from weboob.browser.filters.standard import Date, CleanText, CleanDecimal, Currency as CleanCurrency, \
Regexp, Format, Field
from weboob.browser.filters.html import Link, TableCell
def MyDecimal(*args, **kwargs):
kwargs.update(replace_dots=True)
return CleanDecimal(*args, **kwargs)
def MyDate(*args, **kwargs):
kwargs.update(dayfirst=True)
return Date(*args, **kwargs)
class MyLoggedPage(object):
pass
class BasePage(HTMLPage):
def on_load(self):
self.get_current()
def get_current(self):
try:
current_elem = self.doc.xpath('//div[@id="libPerimetre_2"]/span[@class="textePerimetre_2"]')[0]
except IndexError:
self.logger.debug('Can\'t update current perimeter on this page (%s).', type(self).__name__)
return False
self.browser.current_perimeter = CleanText().filter(current_elem).lower()
return True
def get_error(self):
error = CleanText('//h1[@class="h1-erreur"]')(self.doc)
if error:
self.logger.error('Error detected: %s', error)
return error
@property
def logged(self):
if not isinstance(self, MyLoggedPage):
return False
return self.get_error() is None
class CollectePageMixin(object):
"""
Multiple pages have the same url pattern: "/stb/collecteNI?fwkaid=...&fwkpid=...".
Use some page text to determine which page it is.
"""
IS_HERE_TEXT = None
def is_here(self):
for el in self.doc.xpath('//div[@class="boutons-act"]//h1'):
labels = self.IS_HERE_TEXT
if not isinstance(labels, (list, tuple)):
labels = [labels]
for label in labels:
if label in CleanText('.')(el):
return True
return False
class HomePage(BasePage):
ENCODING = 'iso8859-15'
def get_post_url(self):
for script in self.doc.xpath('//script'):
text = script.text
if text is None:
continue
m = re.search(r'var chemin = "([^"]+)"', text, re.MULTILINE)
if m:
return m.group(1)
return None
def go_to_auth(self):
form = self.get_form(name='bamaccess')
form.submit()
def get_publickey(self):
return Regexp(CleanText('.'), r'public_key.+?(\w+)')(self.doc)
class LoginPage(BasePage):
def on_load(self):
if self.doc.xpath('//font[@class="taille2"]'):
raise BrowserIncorrectPassword()
def login(self, username, password):
password = password[:6]
imgmap = {}
for td in self.doc.xpath('//table[@id="pave-saisie-code"]/tr/td'):
a = td.find('a')
num = a.text.strip()
if num.isdigit():
imgmap[num] = int(a.attrib['tabindex']) - 1
try:
form = self.get_form(name='formulaire')
except FormNotFound:
raise BrowserIncorrectPassword()
if self.browser.new_login:
form['CCPTE'] = username
form['CCCRYC'] = ','.join(['%02d' % imgmap[c] for c in password])
form['CCCRYC2'] = '0' * len(password)
form.submit()
def get_result_url(self):
return self.response.text.strip()
def get_accounts_url(self):
for script in self.doc.xpath('//script'):
text = script.text
if text is None:
continue
m = re.search(r'idSessionSag = "([^"]+)"', script.text)
if m:
idSessionSag = m.group(1)
return '%s%s%s%s' % (self.url, '?sessionSAG=', idSessionSag, '&stbpg=pagePU&actCrt=Synthcomptes&stbzn=btn&act=Synthcomptes')
class UselessPage(MyLoggedPage, BasePage):
pass
class LoginErrorPage(BasePage):
def on_load(self):
if CleanText(u'//p[contains(text(), "momentanément indisponible")]', default=None)(self.doc):
raise BrowserUnavailable()
class FirstVisitPage(BasePage):
def on_load(self):
raise ActionNeeded(u'Veuillez vous connecter au site du Crédit Agricole pour valider vos données personnelles, et réessayer ensuite.')
class _AccountsPage(MyLoggedPage, BasePage):
COL_LABEL = 0
COL_ID = 2
COL_VALUE = 4
COL_CURRENCY = 5
NB_COLS = 7
TYPES = {u'CCHQ': Account.TYPE_CHECKING, # par
u'CCOU': Account.TYPE_CHECKING, # pro
u'DAV PEA': Account.TYPE_SAVINGS,
u'LIV A': Account.TYPE_SAVINGS,
u'LDD': Account.TYPE_SAVINGS,
u'PEL': Account.TYPE_SAVINGS,
u'CEL': Account.TYPE_SAVINGS,
u'CODEBIS': Account.TYPE_SAVINGS,
u'LJMO': Account.TYPE_SAVINGS,
u'CSL': Account.TYPE_SAVINGS,
u'LEP': Account.TYPE_SAVINGS,
u'LEF': Account.TYPE_SAVINGS,
u'TIWI': Account.TYPE_SAVINGS,
u'CSL LSO': Account.TYPE_SAVINGS,
u'CSL CSP': Account.TYPE_SAVINGS,
u'ESPE INTEG': Account.TYPE_SAVINGS,
u'DAV TIGERE': Account.TYPE_SAVINGS,
u'CPTEXCPRO': Account.TYPE_SAVINGS,
u'CPTEXCENT': Account.TYPE_SAVINGS,
u'PRET PERSO': Account.TYPE_LOAN,
u'P. HABITAT': Account.TYPE_LOAN,
u'PRET 0%': Account.TYPE_LOAN,
u'INV PRO': Account.TYPE_LOAN,
u'TRES. PRO': Account.TYPE_LOAN,
u'PEA': Account.TYPE_PEA,
u'CPS': Account.TYPE_MARKET,
u'TITR': Account.TYPE_MARKET,
u'TITR CTD': Account.TYPE_MARKET,
u'réserves de crédit': Account.TYPE_CHECKING,
u'prêts personnels': Account.TYPE_LOAN,
u'crédits immobiliers': Account.TYPE_LOAN,
u'épargne disponible': Account.TYPE_SAVINGS,
u'épargne à terme': Account.TYPE_DEPOSIT,
u'épargne boursière': Account.TYPE_MARKET,
u'assurance vie et capitalisation': Account.TYPE_LIFE_INSURANCE,
}
def get_list(self, use_links=True):
# use_links: some info needs to be fetched on a dedicated account page
# but sometimes the page url/form works only once, so we may want to keep it for later
account_type = Account.TYPE_UNKNOWN
for tr in self.doc.xpath('//table[@class="ca-table"]/tr'):
try:
title = tr.xpath('.//h3/text()')[0].lower().strip()
except IndexError:
pass
else:
account_type = self.TYPES.get(title, Account.TYPE_UNKNOWN)
if not tr.attrib.get('class', '').startswith('colcelligne'):
continue
cols = tr.findall('td')
if not cols or len(cols) < self.NB_COLS:
continue
cleaner = CleanText().filter
label = cleaner(cols[self.COL_LABEL])
type = self.TYPES.get(label, Account.TYPE_UNKNOWN) or account_type
url = Link('.//a', default=None)(tr)
if type == Account.TYPE_LOAN and url is not None:
details = self.browser.open(url)
if not details.page.get_error():
account = details.page.item_loan()
else:
account = Loan()
account.total_amount = MyDecimal().filter(cols[self.COL_INITIAL_AMOUNT])
account.next_payment_amount = account.last_payment_amount = MyDecimal().filter(cols[self.COL_MONTHLY_PAYMENT])
else:
account = Account()
account.id = cleaner(cols[self.COL_ID])
account.label = cleaner(cols[self.COL_LABEL])
account.type = self.TYPES.get(account.label, Account.TYPE_UNKNOWN) or account_type
balance = cleaner(cols[self.COL_VALUE])
# we have to ignore those accounts, because using NotAvailable
# makes boobank and probably many others crash
# we should consider market accounts without balance and update them after
if balance in ('indisponible', '') and account.type is not Account.TYPE_MARKET:
continue
elif balance:
account.balance = Decimal(Transaction.clean_amount(balance))
account.currency = account.get_currency(cleaner(cols[self.COL_CURRENCY]))
account.url = None
self.set_link(account, cols, use_links)
account._perimeter = self.browser.current_perimeter
yield account
# Checking pagination
next_link = self.doc.xpath('//a[@class="btnsuiteliste"]/@href')
if next_link:
self.browser.location(next_link[0])
for account in self.browser.page.get_list():
yield account
def set_link(self, account, cols, use_link):
raise NotImplementedError()
def _iter_idelcos_ids(self):
for line in self.doc.xpath('//table[@class="ca-table"]/tr[@class="ligne-connexe"]'):
# ignore line if preceding line is also a link to deferred card
if line.xpath('./preceding-sibling::tr')[-1].attrib.get('class') == 'ligne-connexe':
continue
try:
link = line.xpath('.//a/@href')[0]
except IndexError:
continue
yield link
def iter_idelcos(self):
# Use a set because it is possible to see several times the same link.
idelcos = set()
for link in self._iter_idelcos_ids():
if link.startswith('javascript:'):
m = re.search(r"javascript:fwkPUAvancerForm\('Cartes','(\w+)'\)", link)
if m:
idelcos.add(m.group(1))
else:
m = re.search('IDELCO=(\d+)&', link)
if m:
idelcos.add(m.group(1))
return idelcos
def get_idelco(self, account_idelco):
for link in self._iter_idelcos_ids():
if link.startswith('javascript:'):
# no need to fetch a "full" link
return self.get_form(name=account_idelco)
elif 'IDELCO=%s&' % account_idelco in link:
return link
def submit_card(self, name):
form = name
form['fwkaction'] = 'Cartes'
form['fwkcodeaction'] = 'Executer'
form.submit()
def check_perimeters(self):
return len(self.doc.xpath('//a[@title="Espace Autres Comptes"]'))
class PerimeterPage(MyLoggedPage, BasePage):
def check_multiple_perimeters(self):
self.browser.perimeters = list()
self.get_current()
if self.browser.current_perimeter is None:
return
self.browser.perimeters.append(self.browser.current_perimeter)
multiple = self.doc.xpath(u'//p[span/a[contains(text(), "Accès")]]')
if not multiple:
if not len(self.doc.xpath(u'//div[contains(text(), "Périmètre en cours de chargement. Merci de patienter quelques secondes.")]')):
self.logger.debug('Possible error on this page.')
# We change perimeter in this case to add the second one.
self.browser.location(self.browser.chg_perimeter_url.format(self.browser.sag))
if self.browser.page.get_error() is not None:
self.browser.broken_perimeters.append('the other perimeter is broken')
self.browser.do_login()
else:
for table in self.doc.xpath('//table[@class]'):
space = CleanText().filter(table.find('caption').text.lower())
for perim in table.xpath('.//label'):
perim = CleanText().filter(perim.text.lower())
self.browser.perimeters.append(u'%s : %s' % (space, perim))
def get_perimeter_link(self, perimeter):
caption = perimeter.split(' : ')[0].title()
perim = perimeter.split(' : ')[1]
for table in self.doc.xpath('//table[@class and caption[contains(text(), $caption)]]', caption=caption):
for p in table.xpath(u'.//p[span/a[contains(text(), "Accès")]]'):
if perim in CleanText().filter(p.find('label').text.lower()):
link = p.xpath('./span/a')[0].attrib['href']
return link
class ChgPerimeterPage(PerimeterPage):
def on_load(self):
if self.get_error() is not None:
self.logger.debug('Error on ChgPerimeterPage')
return
self.get_current()
# sometimes the perimeter use " & " and sometimes " et "
if not (self.browser.current_perimeter in self.browser.perimeters or
self.browser.current_perimeter.replace(' et ', ' & ') in self.browser.perimeters):
assert len(self.browser.perimeters) == 1
self.browser.perimeters.append(self.browser.current_perimeter)
class CardsPage(MyLoggedPage, BasePage):
# cragr sends us this shit:
# Msft *
def build_doc(self, content):
content = re.sub(br'\* tag in columns.
if col_text.find('font') is not None:
col_text = col_text.find('font')
t.category = unicode(col_text.text.strip())
t.label = re.sub('(.*) (.*)', r'\2', t.category).strip()
br = col_text.find('br')
if br is not None:
sub_label = br.tail
if br is not None and sub_label is not None:
junk_ratio = len(re.findall('[^\w\s]', sub_label)) / float(len(sub_label))
nums_ratio = len(re.findall('\d', t.label)) / float(len(t.label))
if len(t.label) < 3 or t.label == t.category or junk_ratio < nums_ratio:
t.label = unicode(sub_label.strip())
# Sometimes, the category contains the label, even if there is another line with it again.
t.category = re.sub('(.*) .*', r'\1', t.category).strip()
t.type = self.TYPES.get(t.category, t.TYPE_UNKNOWN)
# Parse operation date in label (for card transactions for example)
m = re.match('(?P.*) (?P[0-3]\d)/(?P[0-1]\d)$', t.label)
if not m:
m = re.match('^(?P[0-3]\d)/(?P[0-1]\d) (?P.*)$', t.label)
if m:
if t.type in (t.TYPE_CARD, t.TYPE_WITHDRAWAL):
t.rdate = date_guesser.guess_date(int(m.groupdict()['dd']), int(m.groupdict()['mm']), change_current_date=False)
t.label = m.groupdict()['text'].strip()
# Strip city or other useless information from label.
t.label = re.sub('(.*) .*', r'\1', t.label).strip()
t.set_amount(credit, debit)
yield t
class HistoryPostPage(CollectePageMixin, TransactionsPage):
IS_HERE_TEXT = ('Consultation des comptes', 'Relevé')
class UnavailablePage(CollectePageMixin, BasePage):
def is_here(self):
return bool(self.get_error())
class AutoEncodingMixin(object):
def build_doc(self, data):
try:
data.decode('utf-8')
self.forced_encoding = 'utf-8'
except UnicodeDecodeError:
self.forced_encoding = 'iso8859-15'
return super(AutoEncodingMixin, self).build_doc(data)
class MarketPage(MyLoggedPage, AutoEncodingMixin, BasePage):
COL_ID = 1
COL_QUANTITY = 2
COL_UNITVALUE = 3
COL_VALUATION = 4
COL_UNITPRICE = 5
COL_DIFF = 6
def iter_investment(self):
for line in self.doc.xpath('//table[contains(@class, "ca-data-table")]/descendant::tr[count(td)>=7]'):
for sub in line.xpath('./td[@class="info-produit"]'):
sub.drop_tree()
cells = line.findall('td')
if cells[self.COL_ID].find('div/a') is None:
continue
inv = Investment()
inv.label = unicode(cells[self.COL_ID].find('div/a').text.strip())
inv.code = cells[self.COL_ID].find('div/br').tail.strip().split(' ')[0].split(u'\xa0')[0].split(u'\xc2\xa0')[0]
inv.quantity = self.parse_decimal(cells[self.COL_QUANTITY].find('span').text)
inv.valuation = self.parse_decimal(cells[self.COL_VALUATION].text)
inv.diff = self.parse_decimal(cells[self.COL_DIFF].text_content())
if "%" in cells[self.COL_UNITPRICE].text and "%" in cells[self.COL_UNITVALUE].text:
inv.unitvalue = inv.valuation / inv.quantity
inv.unitprice = (inv.valuation - inv.diff) / inv.quantity
else:
inv.unitprice = self.parse_decimal(re.search('([^(]+)', cells[self.COL_UNITPRICE].text).group(1))
inv.unitvalue = self.parse_decimal(cells[self.COL_UNITVALUE].text)
date = cells[self.COL_UNITVALUE].find('span').text
if ':' in date:
inv.vdate = ddate.today()
else:
day, month = [int(x) for x in date.split('/')][:2]
date_guesser = LinearDateGuesser()
inv.vdate = date_guesser.guess_date(day, month)
yield inv
def parse_decimal(self, value):
v = value.strip()
if v == '-' or v == '' or v == '_':
return NotAvailable
return Decimal(Transaction.clean_amount(value))
class MarketHomePage(MarketPage):
COL_ID_LABEL = 1
COL_VALUATION = 5
@method
class get_list(TableElement):
item_xpath = '//table[has-class("tableau_comptes_details")]//tr[td[2]]'
head_xpath = '//table[has-class("tableau_comptes_details")]//tr/th'
col_label = u'Comptes'
col_balance = re.compile(u'Valorisation')
class item(ItemElement):
klass = Account
condition = lambda self: Field('id')(self)
def obj_id(self):
return CleanText(default=None).filter(TableCell('label')(self)[0].xpath('./div[2]'))
def obj_label(self):
return CleanText(default=None).filter(TableCell('label')(self)[0].xpath('./div/b'))
obj_balance = MyDecimal(TableCell('balance'))
class LifeInsurancePage(MarketPage):
COL_ID = 0
COL_QUANTITY = 3
COL_UNITVALUE = 1
COL_VALUATION = 4
def go_on_detail(self, account_id):
# Sometimes this page is a synthesis, so we need to go on detail.
if len(self.doc.xpath(u'//h1[contains(text(), "Synthèse de vos contrats d\'assurance vie, de capitalisation et de prévoyance")]')) == 1:
form = self.get_form(name='frm_fwk')
form['ID_CNT_CAR'] = account_id
form['fwkaction'] = 'Enchainer'
form['fwkcodeaction'] = 'Executer'
form['puCible'] = 'SEPPU'
form.submit()
self.browser.location('https://assurance-personnes.credit-agricole.fr/filiale/entreeBam?sessionSAG=%s&stbpg=pagePU&act=SEPPU&stbzn=bnt&actCrt=SEPPU' % self.browser.sag)
def iter_investment(self):
for line in self.doc.xpath('//table[@summary and count(descendant::td) > 1]/tbody/tr'):
cells = line.findall('td')
if len(cells) < 5:
continue
inv = Investment()
inv.label = unicode(cells[self.COL_ID].text_content().strip())
a = cells[self.COL_ID].find('a')
if a is not None:
try:
inv.code = a.attrib['id'].split(' ')[0].split(u'\xa0')[0].split(u'\xc2\xa0')[0]
except KeyError:
#For "Mandat d'arbitrage" which is a recapitulatif of more investement
continue
else:
inv.code = NotAvailable
inv.quantity = self.parse_decimal(cells[self.COL_QUANTITY].text_content())
inv.unitvalue = self.parse_decimal(cells[self.COL_UNITVALUE].text_content())
inv.valuation = self.parse_decimal(cells[self.COL_VALUATION].text_content())
inv.unitprice = NotAvailable
inv.diff = NotAvailable
yield inv
class AdvisorPage(MyLoggedPage, BasePage):
def get_codeperimetre(self):
script = CleanText('//script[contains(text(), "codePerimetre")]', default=None)(self.doc)
if script:
return Regexp(pattern=r'codePerimetre.+?(\d+).+?codeAgence.+?(\d+)', template='\\1-\\2').filter(script)
@method
class get_advisor(ItemElement):
klass = Advisor
obj_name = CleanText('//span[@class="c1"]')
obj_email = CleanText('//span[@id="emailCons"]')
obj_phone = Regexp(CleanText('//span[@class="c2"]', symbols=' ', default=""), '(\d+)', default=NotAvailable)
obj_agency = CleanText('//span[@class="a1"]')
def iter_numbers(self):
for p in self.doc.xpath(u'//fieldset/div/p[not(contains(text(), "TTC"))]'):
phone = None
if p.find('b') is not None:
phone = ''.join(p.find('b').text_content().split('.'))
else:
m = re.search('(?=\d)([\d\s.]+)', p.text_content().strip())
if m:
phone = ''.join(m.group(1).split())
if not phone or len(phone) != 10:
continue
adv = Advisor()
adv.name = unicode(re.search('([^:]+)', p.find('span').text).group(1).strip())
if adv.name.startswith('Pour toute '):
adv.name = u"Fil général"
adv.phone = unicode(phone)
if "bourse" in adv.name:
adv.role = u"wealth"
[setattr(adv, k, NotAvailable) for k in ['email', 'mobile', 'fax', 'agency', 'address']]
yield adv
class ProfilePage(MyLoggedPage, BasePage):
@method
class get_profile(ItemElement):
klass = Profile
obj_email = Regexp(CleanText('//font/b/script[contains(text(), "Mail")]', default=""), '\'([^\']+)', default=NotAvailable)
def obj_address(self):
address = ""
for tr in self.page.doc.xpath('//tr[td[contains(text(), "Adresse")]]/following-sibling::tr[position() < 4]'):
address += " " + CleanText('./td[last()]')(tr).strip()
return ' '.join(address.split()) or NotAvailable
def obj_name(self):
name = CleanText('//span[contains(text(), "Espace Titulaire")]', default=None)(self)
if name and not "particuliers" in name:
return ''.join(name.split(':')[1:]).strip()
pattern = u'//td[contains(text(), "%s")]/following-sibling::td[1]'
return Format('%s %s', CleanText(pattern % "Nom"), CleanText(pattern % "Prénom"))(self)
class BGPIPage(MarketPage):
COL_ID = 0
COL_QUANTITY = 1
COL_UNITPRICE = 2
COL_UNITVALUE = 3
COL_VALUATION = 4
COL_PORTFOLIO = 5
COL_DIFF = 6
def iter_investment(self):
for line in self.doc.xpath('//table[contains(@class, "PuTableauLarge")]/tr[contains(@class, "PuLigne")]'):
cells = line.findall('td')
inv = Investment()
inv.label = unicode(cells[self.COL_ID].find('span').text_content().strip())
a = cells[self.COL_ID].find('a')
if a is not None:
inv.code = unicode(a.text_content().strip())
else:
inv.code = NotAvailable
inv.quantity = self.parse_decimal(cells[self.COL_QUANTITY].text_content())
if len(cells) == 5:
inv.unitvalue = self.parse_decimal(cells[2].text_content())
inv.valuation = self.parse_decimal(cells[3].text_content())
inv.portfolio_share = self.parse_decimal(cells[4].text_content())/100
else:
inv.unitvalue = self.parse_decimal(cells[self.COL_UNITVALUE].text_content())
inv.valuation = self.parse_decimal(cells[self.COL_VALUATION].text_content())
inv.unitprice = self.parse_decimal(cells[self.COL_UNITPRICE].text_content())
inv.diff = self.parse_decimal(cells[self.COL_DIFF].text_content())
inv.portfolio_share = self.parse_decimal(cells[self.COL_PORTFOLIO].text_content())/100
yield inv
def go_on(self, link):
origin = urlparse(self.url)
self.browser.location('https://%s%s' % (origin.netloc, link))
return True
def go_detail(self):
link = self.doc.xpath(u'.//a[contains(text(), "Détail")]')
return self.go_on(link[0].attrib['href']) if link else False
def go_back(self):
self.go_on(self.doc.xpath(u'.//a[contains(text(), "Retour à mes comptes")]')[0].attrib['href'])
form = self.browser.page.get_form(name='formulaire')
form.submit()
def cgu_needed(self):
return bool(CleanText(u'//h1[contains(text(), "Conditions Générales d\'utilisation des Services en Ligne")]')(self.doc))
class TransferInit(MyLoggedPage, BasePage):
def iter_emitters(self):
items = self.doc.xpath('//select[@name="VIR_VIR1_FR3_LE"]/option')
return self.parse_recipients(items, assume_internal=True)
def iter_recipients(self):
items = self.doc.xpath('//select[@name="VIR_VIR1_FR3_LB"]/option')
return self.parse_recipients(items)
def parse_recipients(self, items, assume_internal=False):
for opt in items:
lines = get_text_lines(opt)
if opt.attrib['value'].startswith('I') or assume_internal:
for n, line in enumerate(lines):
if line.strip().startswith('n°'):
rcpt = Recipient()
rcpt._index = opt.attrib['value']
rcpt._raw_label = ' '.join(lines)
rcpt.category = 'Interne'
rcpt.id = CleanText().filter(line[2:].strip())
# we don't have iban here, use account number
rcpt.label = ' '.join(lines[:n])
rcpt.currency = Currency.get_currency(lines[-1])
rcpt.enabled_at = datetime.now().replace(microsecond=0)
yield rcpt
break
elif opt.attrib['value'].startswith('E'):
rcpt = Recipient()
rcpt._index = opt.attrib['value']
rcpt._raw_label = ' '.join(lines)
rcpt.category = 'Externe'
rcpt.label = lines[0]
rcpt.iban = lines[1].upper()
rcpt.id = rcpt.iban
rcpt.enabled_at = datetime.now().replace(microsecond=0)
yield rcpt
def submit_accounts(self, account_id, recipient_id, amount, currency):
emitters = [rcpt for rcpt in self.iter_emitters() if rcpt.id == account_id and not rcpt.iban]
if len(emitters) != 1:
raise TransferError('Could not find emitter %r' % account_id)
recipients = [rcpt for rcpt in self.iter_recipients() if rcpt.id and rcpt.id == recipient_id]
if len(recipients) != 1:
raise TransferError('Could not find recipient %r' % recipient_id)
form = self.get_form(name='frm_fwk')
assert amount > 0
amount = str(amount.quantize(Decimal('0.00')))
form['T3SEF_MTT_EURO'], form['T3SEF_MTT_CENT'] = amount.split('.')
form['VIR_VIR1_FR3_LE'] = emitters[0]._index
form['VIR_VIR1_FR3_LB'] = recipients[0]._index
form['DEVISE'] = currency or emitters[0].currency
form['VIR_VIR1_FR3_LE_HID'] = emitters[0]._raw_label
form['VIR_VIR1_FR3_LB_HID'] = recipients[0]._raw_label
form['fwkaction'] = 'Confirmer' # mandatory
form['fwkcodeaction'] = 'Executer'
form.submit()
def url_list_recipients(self):
return CleanText(u'(//a[contains(text(),"Liste des bénéficiaires")])[1]/@href')(self.doc)
class RecipientListPage(MyLoggedPage, BasePage):
def url_add_recipient(self):
return CleanText(u'//a[contains(text(),"Ajouter un compte destinataire")]/@href')(self.doc)
class TransferPage(CollectePageMixin, MyLoggedPage, BasePage):
IS_HERE_TEXT = 'Virement'
### for transfers
def get_step(self):
return CleanText('//div[@id="etapes"]//li[has-class("encours")]')(self.doc)
def is_sent(self):
return self.get_step().startswith('Récapitulatif')
def is_confirm(self):
return self.get_step().startswith('Confirmation')
def is_reason(self):
return self.get_step().startswith('Informations complémentaires')
def get_transfer(self):
transfer = Transfer()
# FIXME all will probably fail if an account has a user-chosen label with "IBAN :" or "n°"
amount_xpath = '//fieldset//p[has-class("montant")]'
transfer.amount = MyDecimal(amount_xpath)(self.doc)
transfer.currency = CleanCurrency(amount_xpath)(self.doc)
if self.is_sent():
transfer.account_id = Regexp(CleanText('//p[@class="nomarge"][span[contains(text(),'
'"Compte émetteur")]]/text()'),
r'n°(\d+)')(self.doc)
base = CleanText('//fieldset//table[.//span[contains(text(), "Compte bénéficiaire")]]'
'//td[contains(text(),"n°") or contains(text(),"IBAN :")]//text()', newlines=False)(self.doc)
transfer.recipient_id = Regexp(None, r'IBAN : ([^\n]+)|n°(\d+)').filter(base)
transfer.recipient_id = transfer.recipient_id.replace(' ', '')
if 'IBAN' in base:
transfer.recipient_iban = transfer.recipient_id
transfer.exec_date = MyDate(CleanText('//p[@class="nomarge"][span[contains(text(), "Date de l\'ordre")]]/text()'))(self.doc)
else:
transfer.account_id = Regexp(CleanText('//fieldset[.//h3[contains(text(), "Compte émetteur")]]//p'),
r'n°(\d+)')(self.doc)
base = CleanText('//fieldset[.//h3[contains(text(), "Compte bénéficiaire")]]//text()',
newlines=False)(self.doc)
transfer.recipient_id = Regexp(None, r'IBAN : ([^\n]+)|n°(\d+)').filter(base)
transfer.recipient_id = transfer.recipient_id.replace(' ', '')
if 'IBAN' in base:
transfer.recipient_iban = transfer.recipient_id
transfer.exec_date = MyDate(CleanText('//fieldset//p[span[contains(text(), "Virement unique le :")]]/text()'))(self.doc)
transfer.label = CleanText('//fieldset//p[span[contains(text(), "Référence opération")]]')(self.doc)
transfer.label = re.sub(r'^Référence opération(?:\s*):', '', transfer.label).strip()
return transfer
def submit_more(self, label, date=None):
if date is None:
date = ddate.today()
form = self.get_form(name='frm_fwk')
form['VICrt_CDDOOR'] = label
form['VICrtU_DATEVRT_JJ'] = date.strftime('%d')
form['VICrtU_DATEVRT_MM'] = date.strftime('%m')
form['VICrtU_DATEVRT_AAAA'] = date.strftime('%Y')
form['DATEC'] = date.strftime('%d/%m/%Y')
form['PERIODE'] = 'U'
form['fwkaction'] = 'Confirmer'
form['fwkcodeaction'] = 'Executer'
form.submit()
def submit_confirm(self):
form = self.get_form(name='frm_fwk')
form['fwkaction'] = 'Confirmer'
form['fwkcodeaction'] = 'Executer'
form.submit()
def on_load(self):
super(TransferPage, self).on_load()
# warning: the "service indisponible" message (not catched here) is not a real BrowserUnavailable
err = CleanText('//div[has-class("blc-choix-erreur")]//p', default='')(self.doc)
if err:
raise TransferBankError(message=err)
class RecipientMiscPage(CollectePageMixin, MyLoggedPage, BasePage):
IS_HERE_TEXT = 'Liste des comptes bénéficiaires'
### for adding recipients
def send_sms(self):
form = self.get_form(name='frm_fwk')
assert 'code' not in form
form['fwkaction'] = 'DemandeCodeSMSVerifID'
form['fwkcodeaction'] = 'Executer'
form.submit()
def get_sms_error(self):
return CleanText('//div[@class="blc-choix-wrap-erreur"]')(self.doc)
def submit_recipient(self, label, iban):
try:
form = self.get_form(name='frm_fwk')
except FormNotFound:
raise AddRecipientError('An error occurred before sending recipient')
form['NOM_BENEF'] = label
for i in range(9):
form['CIBAN%d' % (i + 1)] = iban[i * 4:(i + 1) * 4]
form['fwkaction'] = 'VerifCodeIBAN'
form['fwkcodeaction'] = 'Executer'
form.submit()
def confirm_recipient(self):
try:
form = self.get_form(name='frm_fwk')
except FormNotFound:
raise AddRecipientError('An error occurred before finishing adding recipient')
form['fwkaction'] = 'ConfirmerAjout'
form['fwkcodeaction'] = 'Executer'
form.submit()
def check_recipient_error(self):
msg = CleanText('//tr[@bgcolor="#C74545"]', default='')(self.doc) # there is no id, class or anything...
if msg:
raise AddRecipientError(message=msg)
def find_recipient(self, iban):
iban = iban.upper()
for tr in self.doc.xpath('//table[starts-with(@summary,"Nom et IBAN")]/tbody/tr'):
iban_text = re.sub(r'\s', '', CleanText('./td[3]')(tr))
if iban_text.upper() == 'IBAN:%s' % iban:
res = Recipient()
res.iban = iban
res.label = CleanText('./td[2]')(tr)
return res
class RecipientPage(MyLoggedPage, BasePage):
def can_send_code(self):
form = self.get_form(name='frm_fwk')
return 'code' in form
def send_sms(self):
form = self.get_form(name='frm_fwk')
if 'code' in form:
# a code is still pending, ask a new one
form['fwkaction'] = 'NouvelleDemandeCodeSMS'
form['fwkcodeaction'] = 'Executer'
new_page = form.submit().page
assert isinstance(new_page, TransferPage)
return new_page.send_sms()
else:
form['fwkaction'] = 'DemandeCodeSMSVerifID'
form['fwkcodeaction'] = 'Executer'
form.submit()
def submit_code(self, code):
form = self.get_form(name='frm_fwk')
form['fwkaction'] = 'Confirmer'
form['fwkcodeaction'] = 'Executer'
form['code'] = code
form.submit()
def get_text_lines(el):
lines = [re.sub(r'\s+', ' ', line).strip() for line in el.text_content().split('\n')]
return [l for l in lines if l]
class DeferredCardsPage(CollectePageMixin, CardsPage):
IS_HERE_TEXT = (u'Cartes - détail', 'Cartes')
|