# -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Jocelyn Jaubert
#
# 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
import datetime
from urlparse import parse_qs, urlparse
from lxml.etree import XML
from lxml.html import fromstring
from decimal import Decimal, InvalidOperation
import re
from weboob.capabilities.base import empty, NotAvailable
from weboob.capabilities.bank import Account, Investment
from weboob.capabilities.contact import Advisor
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from weboob.browser.elements import DictElement, ItemElement, method
from weboob.browser.filters.json import Dict
from weboob.browser.filters.standard import CleanText, CleanDecimal, Regexp
from weboob.browser.pages import JsonPage, LoggedPage
from .base import BasePage
def MyDecimal(*args, **kwargs):
kwargs.update(replace_dots=True, default=NotAvailable)
return CleanDecimal(*args, **kwargs)
class AccountsList(LoggedPage, BasePage):
LINKID_REGEXP = re.compile(".*ch4=(\w+).*")
TYPES = {u'Compte Bancaire': Account.TYPE_CHECKING,
u'Compte Epargne': Account.TYPE_SAVINGS,
u'Compte Sur Livret': Account.TYPE_SAVINGS,
u'Compte Titres': Account.TYPE_MARKET,
u'Crédit': Account.TYPE_LOAN,
u'Ldd': Account.TYPE_SAVINGS,
u'Livret': Account.TYPE_SAVINGS,
u'PEA': Account.TYPE_SAVINGS,
u'PEL': Account.TYPE_SAVINGS,
u'Plan Epargne': Account.TYPE_SAVINGS,
u'Prêt': Account.TYPE_LOAN,
}
def get_list(self):
def check_valid_url(url):
pattern = ['/restitution/cns_detailAVPAT.html',
'/restitution/cns_detailPea.html',
'/restitution/cns_detailAlterna.html',
]
for p in pattern:
if url.startswith(p):
return False
return True
for tr in self.doc.getiterator('tr'):
if 'LGNTableRow' not in tr.attrib.get('class', '').split():
continue
account = Account()
for td in tr.getiterator('td'):
if td.attrib.get('headers', '') == 'TypeCompte':
a = td.find('a')
if a is None:
break
account.label = CleanText('.')(a)
account._link_id = a.get('href', '')
for pattern, actype in self.TYPES.iteritems():
if account.label.startswith(pattern):
account.type = actype
break
else:
if account._link_id.startswith('/asv/asvcns10.html'):
account.type = Account.TYPE_LIFE_INSURANCE
# Website crashes when going on theses URLs
if not check_valid_url(account._link_id):
account._link_id = None
elif td.attrib.get('headers', '') == 'NumeroCompte':
account.id = CleanText(u'.', replace=[(' ', '')])(td)
elif td.attrib.get('headers', '') == 'Libelle':
text = CleanText('.')(td)
if text != '':
account.label = text
elif td.attrib.get('headers', '') == 'Solde':
div = td.xpath('./div[@class="Solde"]')
if len(div) > 0:
balance = CleanText('.')(div[0])
if len(balance) > 0 and balance not in ('ANNULEE', 'OPPOSITION'):
try:
account.balance = Decimal(FrenchTransaction.clean_amount(balance))
except InvalidOperation:
self.logger.error('Unable to parse balance %r' % balance)
continue
account.currency = account.get_currency(balance)
else:
account.balance = NotAvailable
if not account.label or empty(account.balance):
continue
if account._link_id and 'CARTE_' in account._link_id:
account.type = account.TYPE_CARD
if account.type == Account.TYPE_UNKNOWN:
self.logger.debug('Unknown account type: %s', account.label)
yield account
class CardsList(LoggedPage, BasePage):
def iter_cards(self):
for tr in self.doc.getiterator('tr'):
tds = tr.findall('td')
if len(tds) < 4 or tds[0].attrib.get('class', '') != 'tableauIFrameEcriture1':
continue
yield tr.xpath('.//a')[0].attrib['href']
class Transaction(FrenchTransaction):
PATTERNS = [(re.compile(r'^CARTE \w+ RETRAIT DAB.*? (?P
\d{2})/(?P\d{2})( (?P\d+)H(?P\d+))? (?P.*)'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^CARTE \w+ (?P\d{2})/(?P\d{2})( A (?P\d+)H(?P\d+))? RETRAIT DAB (?P.*)'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^CARTE \w+ REMBT (?P\d{2})/(?P\d{2})( A (?P\d+)H(?P\d+))? (?P.*)'),
FrenchTransaction.TYPE_PAYBACK),
(re.compile(r'^(?PCARTE) \w+ (?P\d{2})/(?P\d{2}) (?P.*)'),
FrenchTransaction.TYPE_CARD),
(re.compile(r'^(?P\d{2})(?P\d{2})/(?P.*?)/?(-[\d,]+)?$'),
FrenchTransaction.TYPE_CARD),
(re.compile(r'^(?P(COTISATION|PRELEVEMENT|TELEREGLEMENT|TIP)) (?P.*)'),
FrenchTransaction.TYPE_ORDER),
(re.compile(r'^(\d+ )?VIR (PERM )?POUR: (.*?) (REF: \d+ )?MOTIF: (?P.*)'),
FrenchTransaction.TYPE_TRANSFER),
(re.compile(r'^(?PVIR(EMEN)?T? \w+) (?P.*)'),
FrenchTransaction.TYPE_TRANSFER),
(re.compile(r'^(CHEQUE) (?P.*)'), FrenchTransaction.TYPE_CHECK),
(re.compile(r'^(FRAIS) (?P.*)'), FrenchTransaction.TYPE_BANK),
(re.compile(r'^(?PECHEANCEPRET)(?P.*)'),
FrenchTransaction.TYPE_LOAN_PAYMENT),
(re.compile(r'^(?PREMISE CHEQUES)(?P.*)'),
FrenchTransaction.TYPE_DEPOSIT),
(re.compile(r'^CARTE RETRAIT (?P.*)'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^TOTAL DES FACTURES (?P.*)'),
FrenchTransaction.TYPE_CARD_SUMMARY),
(re.compile(r'^DEBIT MENSUEL CARTE (?P.*)'),
FrenchTransaction.TYPE_CARD_SUMMARY),
(re.compile(r'^CREDIT MENSUEL CARTE (?P.*)'),
FrenchTransaction.TYPE_CARD_SUMMARY),
]
class AccountHistory(LoggedPage, BasePage):
def is_here(self):
return not CleanText('//h1[contains(text(), "Effectuer un virement")]')(self.doc)
debit_date = None
def get_part_url(self):
for script in self.doc.getiterator('script'):
if script.text is None:
continue
m = re.search('var listeEcrCavXmlUrl="(.*)";', script.text)
if m:
return m.group(1)
return None
def iter_transactions(self):
url = self.get_part_url()
if url is None:
# There are no transactions in this kind of account
return
is_deferred_card = bool(self.doc.xpath(u'//div[contains(text(), "Différé")]'))
while True:
d = XML(self.browser.open(url).content)
el = d.xpath('//dataBody')
if not el:
return
el = el[0]
s = unicode(el.text).encode('iso-8859-1')
doc = fromstring(s)
for tr in self._iter_transactions(doc):
if is_deferred_card and tr.type is Transaction.TYPE_CARD:
tr.type = Transaction.TYPE_DEFERRED_CARD
yield tr
el = d.xpath('//dataHeader')[0]
if int(el.find('suite').text) != 1:
return
url = urlparse(url)
p = parse_qs(url.query)
args = {}
args['n10_nrowcolor'] = 0
args['operationNumberPG'] = el.find('operationNumber').text
args['operationTypePG'] = el.find('operationType').text
args['pageNumberPG'] = el.find('pageNumber').text
args['idecrit'] = el.find('idecrit').text or ''
args['sign'] = p['sign'][0]
args['src'] = p['src'][0]
url = '%s?%s' % (url.path, urllib.urlencode(args))
def _iter_transactions(self, doc):
t = None
for i, tr in enumerate(doc.xpath('//tr')):
try:
raw = tr.attrib['title'].strip()
except KeyError:
raw = CleanText('./td[@headers="Libelle"]//text()')(tr)
date = CleanText('./td[@headers="Date"]')(tr)
if date == '':
m = re.search(r'(\d+)/(\d+)', raw)
if not m:
continue
old_debit_date = self.debit_date
self.debit_date = t.date if t else datetime.date.today()
self.debit_date = self.debit_date.replace(day=int(m.group(1)), month=int(m.group(2)))
if old_debit_date is not None:
while self.debit_date > old_debit_date:
self.debit_date = self.debit_date.replace(year=self.debit_date.year - 1)
self.logger.error('adjusting debit date to %s', self.debit_date)
if not t:
continue
t = Transaction()
if 'EnTraitement' in tr.get('class', ''):
t._coming = True
else:
t._coming = False
t.set_amount(*reversed([el.text for el in tr.xpath('./td[@class="right"]')]))
if date == '':
# Credit from main account.
t.amount = -t.amount
date = self.debit_date
t.rdate = t.parse_date(date)
t.parse(raw=raw, date=(self.debit_date or date), vdate=(date or None))
yield t
def get_iban(self):
return CleanText().filter(self.doc.xpath("//font[contains(text(),'IBAN')]/b[1]")[0]).replace(' ', '')
class Invest(object):
def create_investement(self, cells):
inv = Investment()
inv.quantity = MyDecimal('.', replace_dots=True, default=NotAvailable)(cells[self.COL_QUANTITY])
inv.unitvalue = MyDecimal('.', replace_dots=True, default=NotAvailable)(cells[self.COL_UNITVALUE])
inv.unitprice = NotAvailable
inv.valuation = MyDecimal('.', replace_dots=True, default=NotAvailable)(cells[self.COL_VALUATION])
inv.diff = NotAvailable
link = cells[self.COL_LABEL].xpath('a[contains(@href, "CDCVAL=")]')[0]
m = re.search('CDCVAL=([^&]+)', link.attrib['href'])
if m:
inv.code = m.group(1)
else:
inv.code = NotAvailable
return inv
class Market(LoggedPage, BasePage, Invest):
COL_LABEL = 0
COL_QUANTITY = 1
COL_UNITPRICE = 2
COL_VALUATION = 3
COL_DIFF = 4
def iter_investment(self):
doc = self.browser.open('/brs/fisc/fisca10a.html').page.doc
num_page = None
try:
num_page = int(CleanText('.')(doc.xpath(u'.//tr[contains(td[1], "Relevé des plus ou moins values latentes")]/td[2]')[0]).split('/')[1])
except IndexError:
pass
docs = [doc]
if num_page:
for n in range(2, num_page + 1):
docs.append(self.browser.open('%s%s' % ('/brs/fisc/fisca10a.html?action=12&numPage=', str(n))).page.doc)
for doc in docs:
# There are two different tables possible depending on the market account type.
is_detailed = bool(doc.xpath(u'//span[contains(text(), "Années d\'acquisition")]'))
tr_xpath = '//tr[@height and td[@colspan="6"]]' if is_detailed else '//tr[count(td)>5]'
for tr in doc.xpath(tr_xpath):
cells = tr.findall('td')
inv = Investment()
inv.label = unicode(cells[self.COL_LABEL].xpath('.//span')[0].attrib['title'].split(' - ')[0])
inv.code = unicode(cells[self.COL_LABEL].xpath('.//span')[0].attrib['title'].split(' - ')[1])
if is_detailed:
inv.quantity = MyDecimal('.')(tr.xpath('./following-sibling::tr/td[2]')[0])
inv.unitprice = MyDecimal('.', replace_dots=True)(tr.xpath('./following-sibling::tr/td[3]')[1])
inv.unitvalue = MyDecimal('.', replace_dots=True)(tr.xpath('./following-sibling::tr/td[3]')[0])
inv.valuation = MyDecimal('.')(tr.xpath('./following-sibling::tr/td[4]')[0])
inv.diff = MyDecimal('.')(tr.xpath('./following-sibling::tr/td[5]')[0])
else:
inv.quantity = MyDecimal('.')(cells[self.COL_QUANTITY])
inv.diff = MyDecimal('.')(cells[self.COL_DIFF])
inv.unitprice = MyDecimal('.')(cells[self.COL_UNITPRICE].xpath('.//tr[1]/td[2]')[0])
inv.unitvalue = MyDecimal('.')(cells[self.COL_VALUATION].xpath('.//tr[1]/td[2]')[0])
inv.valuation = MyDecimal('.')(cells[self.COL_VALUATION].xpath('.//tr[2]/td[2]')[0])
yield inv
class LifeInsurance(LoggedPage, BasePage):
def get_error(self):
try:
return self.doc.xpath("//div[@class='net2g_asv_error_full_page']")[0].text.strip()
except IndexError:
return super(LifeInsurance, self).get_error()
class LifeInsuranceInvest(LifeInsurance, Invest):
COL_LABEL = 0
COL_QUANTITY = 1
COL_UNITVALUE = 2
COL_VALUATION = 3
def iter_investment(self):
for tr in self.doc.xpath("//table/tbody/tr[starts-with(@class, 'net2g_asv_tableau_ligne_')]"):
cells = tr.findall('td')
inv = self.create_investement(cells)
inv.label = cells[self.COL_LABEL].xpath('a/span')[0].text.strip()
inv.description = cells[self.COL_LABEL].xpath('a//div/b[last()]')[0].tail
yield inv
class LifeInsuranceHistory(LifeInsurance):
COL_DATE = 0
COL_LABEL = 1
COL_AMOUNT = 2
COL_STATUS = 3
def iter_transactions(self):
for tr in self.doc.xpath("//table/tbody/tr[starts-with(@class, 'net2g_asv_tableau_ligne_')]"):
cells = tr.findall('td')
link = cells[self.COL_LABEL].xpath('a')[0]
# javascript:detailOperation('operationForm', '2');
m = re.search(", '([0-9]+)'", link.attrib['href'])
if m:
id_trans = m.group(1)
else:
id_trans = ''
trans = Transaction()
trans._temp_id = id_trans
trans.parse(raw=link.attrib['title'], date=cells[self.COL_DATE].text)
trans.set_amount(cells[self.COL_AMOUNT].text)
# search for 'Réalisé'
trans._coming = 'alis' not in cells[self.COL_STATUS].text.strip()
if not self.set_date(trans):
continue
if u'Annulé' in cells[self.COL_STATUS].text.strip():
continue
yield trans
def set_date(self, trans):
"""fetch date and vdate from another page"""
# go to the page containing the dates
form = self.get_form(id='operationForm')
form['a100_asv_action'] = 'detail'
form['a100_asv_indexOp'] = trans._temp_id
form.url = '/asv/AVI/asvcns21c.html'
# but the page sometimes fail
for i in xrange(3, -1, -1):
page = form.submit().page
doc = page.doc
if not page.get_error():
break
self.logger.warning('Life insurance history error (%s), retrying %d more times', page.get_error(), i)
else:
self.logger.warning('Life insurance history error (%s), failed', page.get_error())
return False
# process the data
date_xpath = '//td[@class="net2g_asv_suiviOperation_element1"]/following-sibling::td'
vdate_xpath = '//td[@class="net2g_asv_tableau_cell_date"]'
trans.date = self.parse_date(doc, trans, date_xpath, 1)
trans.rdate = trans.date
trans.vdate = self.parse_date(doc, trans, vdate_xpath, 0)
return True
@staticmethod
def parse_date(doc, trans, xpath, index):
elem = doc.xpath(xpath)[index]
if elem.text:
return trans.parse_date(elem.text.strip())
else:
return NotAvailable
class ListRibPage(LoggedPage, BasePage):
def get_rib_url(self, account):
for div in self.doc.xpath('//table//td[@class="fond_cellule"]//div[@class="tableauBodyEcriture1"]//table//tr'):
if account.id == CleanText().filter(div.xpath('./td[2]//div/div')).replace(' ', ''):
href = CleanText().filter(div.xpath('./td[4]//a/@href'))
m = re.search("javascript:windowOpenerRib\('(.*?)'(.*)\)", href)
if m:
return m.group(1)
class AdvisorPage(BasePage):
def get_advisor(self):
fax = CleanText('//div[contains(text(), "Fax")]/following-sibling::div[1]', replace=[(' ', '')])(self.doc)
agency = CleanText('//div[contains(@class, "agence")]/div[last()]')(self.doc)
address = CleanText('//div[contains(text(), "Adresse")]/following-sibling::div[1]')(self.doc)
for div in self.doc.xpath('//div[div[text()="Contacter mon conseiller"]]'):
a = Advisor()
a.name = CleanText('./div[2]')(div)
a.phone = Regexp(CleanText(u'./following-sibling::div[div[contains(text(), "Téléphone")]][1]/div[last()]', replace=[(' ', '')]), '([+\d]+)')(div)
a.fax = fax
a.agency = agency
a.address = address
a.mobile = a.email = NotAvailable
a.role = u"wealth" if "patrimoine" in CleanText('./div[1]')(div) else u"bank"
yield a
class LoansPage(LoggedPage, JsonPage):
@method
class iter_accounts(DictElement):
item_xpath = 'donnees/tabPrestations'
class item(ItemElement):
klass = Account
obj_id = Dict('idPrestation')
obj_type = Account.TYPE_LOAN
obj_label = Dict('libelle')
obj_currency = Dict('capitalRestantDu/devise', default=NotAvailable)
obj__link_id = None
def obj_balance(self):
val = Dict('capitalRestantDu/valeur', default=NotAvailable)(self)
if val is NotAvailable:
return val
val = Decimal(val)
point = Decimal(Dict('capitalRestantDu/posDecimale')(self))
assert point >= 0
return val.scaleb(-point)
def validate(self, obj):
assert obj.id
assert obj.label
if obj.balance is NotAvailable:
# ... but the account may be in the main AccountsList anyway
self.logger.debug('skipping account %r %r due to missing balance', obj.id, obj.label)
return False
return True