Skip to content
Commits on Source (28)
# -*- coding: utf-8 -*-
# Copyright(C) 2013-2015 Christophe Lampin
# Copyright(C) 2019 Budget Insight
#
# 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 Affero General Public License as published by
# 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 Affero General Public License for more details.
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# 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/>.
from __future__ import unicode_literals
from .module import AmeliModule
......
# -*- coding: utf-8 -*-
# Copyright(C) 2013-2015 Christophe Lampin
# Copyright(C) 2019 Budget Insight
#
# 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 Affero General Public License as published by
# 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 Affero General Public License for more details.
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# 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/>.
from __future__ import unicode_literals
from weboob.browser import LoginBrowser, URL, need_login
from weboob.exceptions import BrowserIncorrectPassword, ActionNeeded
from .pages import (
LoginPage, HomePage, CguPage, AccountPage, LastPaymentsPage, PaymentsPage, PaymentDetailsPage, Raw, UnavailablePage,
)
from weboob.tools.compat import basestring
from datetime import date
from time import time
from dateutil.relativedelta import relativedelta
__all__ = ['AmeliBrowser']
from weboob.browser import LoginBrowser, URL, need_login
from .pages import ErrorPage, LoginPage, SubscriptionPage, DocumentsPage
class AmeliBrowser(LoginBrowser):
BASEURL = 'https://assure.ameli.fr'
loginp = URL(r'/PortailAS/appmanager/PortailAS/assure\?.*_pageLabel=as_login_page', LoginPage)
homep = URL(r'/PortailAS/appmanager/PortailAS/assure\?_nfpb=true&_pageLabel=as_accueil_page', HomePage)
cgup = URL(r'/PortailAS/appmanager/PortailAS/assure\?_nfpb=true&_pageLabel=as_conditions_generales_page', CguPage)
accountp = URL(r'/PortailAS/appmanager/PortailAS/assure\?_nfpb=true&_pageLabel=as_info_perso_page', AccountPage)
paymentsp = URL(r'/PortailAS/appmanager/PortailAS/assure\?_nfpb=true&_pageLabel=as_paiements_page', PaymentsPage)
paymentdetailsp = URL(r'/PortailAS/paiements.do\?actionEvt=chargerDetailPaiements.*', PaymentDetailsPage)
lastpaymentsp = URL(r'/PortailAS/paiements.do\?actionEvt=afficherPaiements.*', LastPaymentsPage)
pdf_page = URL(r'PortailAS/PDFServletReleveMensuel.dopdf\?PDF.moisRecherche=.*', Raw)
unavailablep = URL(r'/vu/INDISPO_COMPTE_ASSURES.html', UnavailablePage)
error_page = URL(r'/vu/INDISPO_COMPTE_ASSURES.html', ErrorPage)
login_page = URL(r'/PortailAS/appmanager/PortailAS/assure\?_nfpb=true&connexioncompte_2actionEvt=afficher.*', LoginPage)
subscription_page = URL(r'/PortailAS/appmanager/PortailAS/assure\?_nfpb=true&_pageLabel=as_info_perso_page.*', SubscriptionPage)
documents_page = URL(r'/PortailAS/paiements.do', DocumentsPage)
def do_login(self):
self.logger.debug('call Browser.do_login')
# set this cookie to get login form in response
self.session.cookies['hbc'] = ''
self.loginp.stay_or_go()
if self.homep.is_here():
return True
self.login_page.go()
self.page.login(self.username, self.password)
error = self.page.is_error()
if error:
raise BrowserIncorrectPassword(error)
self.page.locate_to_cgu_page()
if self.cgup.is_here():
raise ActionNeeded(self.page.get_cgu())
self.homep.stay_or_go() # Redirection not interpreted by browser. Manually redirect on homep
if not self.homep.is_here():
raise BrowserIncorrectPassword()
@need_login
def iter_subscription_list(self):
self.logger.debug('call Browser.iter_subscription_list')
self.accountp.stay_or_go()
return self.page.iter_subscription_list()
@need_login
def get_subscription(self, id):
self.logger.debug('call Browser.get_subscription')
assert isinstance(id, basestring)
for sub in self.iter_subscription_list():
if id == sub._id:
return sub
return None
@need_login
def iter_history(self, sub):
transactions = []
self.logger.debug('call Browser.iter_history')
self.paymentsp.stay_or_go()
payments_url = self.page.get_last_payments_url()
self.location(payments_url)
assert self.lastpaymentsp.is_here()
urls = self.page.iter_last_payments()
for url in urls:
self.location(url)
assert self.paymentdetailsp.is_here()
for payment in self.page.iter_payment_details(sub):
transactions.append(payment)
# go to a page with a "deconnexion" link so that logged property
# stays True and next call to do_login doesn't crash when using the
# blackbox
self.accountp.go()
return transactions
@need_login
def iter_documents(self, sub):
self.logger.debug('call Browser.iter_documents')
self.paymentsp.stay_or_go()
payments_url = self.page.get_last_payments_url()
self.location(payments_url)
assert self.lastpaymentsp.is_here()
for document in self.page.iter_documents(sub):
yield document
def iter_subscription(self):
self.subscription_page.go()
return self.page.iter_subscriptions()
@need_login
def get_document(self, id):
self.logger.debug('call Browser.get_document')
assert isinstance(id, basestring)
subs = self.iter_subscription_list()
for sub in subs:
for b in self.iter_documents(sub):
if id == b.id:
return b
return False
def iter_documents(self, subscription):
end_date = date.today()
start_date = end_date - relativedelta(years=1)
# FUN FACT, website tell us documents are available for 6 months
# let's suppose today is 28/05/19, website frontend limit DateDebut to 28/11/18 but we can get a little bit more
# by setting a previous date and get documents that are no longer available for simple user
params = {
'Beneficiaire': 'tout_selectionner',
'DateDebut': start_date.strftime('%d/%m/%Y'),
'DateFin': end_date.strftime('%d/%m/%Y'),
'actionEvt': 'afficherPaiementsComplementaires',
'afficherIJ': 'false',
'afficherInva': 'false',
'afficherPT': 'false',
'afficherRS': 'false',
'afficherReleves': 'false',
'afficherRentes': 'false',
'idNoCache': int(time()*1000)
}
# the second request is stateful
# first value of actionEvt is afficherPaiementsComplementaires to get all payments from last 6 months
# (start_date 6 months in the past is needed but not enough)
self.documents_page.go(params=params)
# then we set Rechercher to actionEvt to filter for this subscription, within last 6 months
# without first request we would have filter for this subscription but within last 2 months
params['actionEvt'] = 'Rechercher'
params['Beneficiaire'] = subscription._param
self.documents_page.go(params=params)
return self.page.iter_documents(subid=subscription.id)
# -*- coding: utf-8 -*-
# Copyright(C) 2013-2015 Christophe Lampin
# Copyright(C) 2019 Budget Insight
#
# 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 Affero General Public License as published by
# 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 Affero General Public License for more details.
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# 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/>.
from __future__ import unicode_literals
from weboob.capabilities.bill import DocumentTypes, CapDocument, SubscriptionNotFound, DocumentNotFound, Subscription, Bill
from weboob.capabilities.base import find_object
from weboob.tools.backend import Module, BackendConfig
from weboob.capabilities.bill import CapDocument, Document, DocumentTypes, SubscriptionNotFound, DocumentNotFound
from weboob.tools.value import ValueBackendPassword
from .browser import AmeliBrowser
__all__ = ['AmeliModule']
class AmeliModule(Module, CapDocument):
NAME = 'ameli'
DESCRIPTION = 'Ameli website: French Health Insurance'
MAINTAINER = 'Christophe Lampin'
EMAIL = 'weboob@lampin.net'
DESCRIPTION = "le site de l'Assurance Maladie en ligne"
MAINTAINER = 'Florian Duguet'
EMAIL = 'florian.duguet@budget-insight.com'
LICENSE = 'LGPLv3+'
VERSION = '1.6'
LICENSE = 'AGPLv3+'
BROWSER = AmeliBrowser
CONFIG = BackendConfig(ValueBackendPassword('login', label='Numero de SS', regexp=r'^\d{13}$', masked=False),
ValueBackendPassword('password', label='Password', masked=True))
CONFIG = BackendConfig(ValueBackendPassword('login', label='Mon numero de sécurité sociale', regexp=r'^\d{13}$', masked=False),
ValueBackendPassword('password', label='Mon code (4 à 13 chiffres)', regexp=r'^\d{4,13}', masked=True))
accepted_document_types = (DocumentTypes.BILL,)
def create_default_browser(self):
return self.create_browser(self.config['login'].get(),
self.config['password'].get())
return self.create_browser(self.config['login'].get(), self.config['password'].get())
def iter_subscription(self):
return self.browser.iter_subscription_list()
return self.browser.iter_subscription()
def get_subscription(self, _id):
subscription = self.browser.get_subscription(_id)
if not subscription:
raise SubscriptionNotFound()
else:
return subscription
def iter_documents_history(self, subscription):
if not isinstance(subscription, Subscription):
subscription = self.get_subscription(subscription)
return self.browser.iter_history(subscription)
return find_object(self.iter_subscription(), id=_id, error=SubscriptionNotFound)
def iter_documents(self, subscription):
if not isinstance(subscription, Subscription):
subscription = self.get_subscription(subscription)
return self.browser.iter_documents(subscription)
def get_document(self, id):
bill = self.browser.get_document(id)
if not bill:
raise DocumentNotFound()
else:
return bill
def download_document(self, bill):
if not isinstance(bill, Bill):
bill = self.get_document(bill)
response = self.browser.open(bill.url, stream=True)
if not response or response.headers['content-type'] != "application/pdf":
return None
return response.content
def get_document(self, _id):
subid = _id.rsplit('_', 1)[0]
subscription = self.get_subscription(subid)
return find_object(self.iter_documents(subscription), id=_id, error=DocumentNotFound)
def download_document(self, document):
if not isinstance(document, Document):
document = self.get_document(document)
return self.browser.open(document.url).content
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# Copyright(C) 2013-2015 Christophe Lampin
# Copyright(C) 2019 Budget Insight
#
# 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 Affero General Public License as published by
# 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 Affero General Public License for more details.
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# 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/>.
from __future__ import unicode_literals
from datetime import datetime
import re
from decimal import Decimal
from weboob.browser.filters.html import Attr, XPathNotFound
from weboob.browser.pages import HTMLPage, RawPage, LoggedPage
from weboob.capabilities.bill import DocumentTypes, Subscription, Detail, Bill
from weboob.browser.filters.standard import CleanText, Regexp
from weboob.browser.elements import method, ListElement, ItemElement
from weboob.browser.filters.html import Attr, Link
from weboob.browser.filters.standard import CleanText, Regexp, CleanDecimal, Currency, Field, Format, Env
from weboob.browser.pages import LoggedPage, HTMLPage, PartialHTMLPage
from weboob.capabilities.bill import Subscription, Bill
from weboob.exceptions import BrowserUnavailable
from weboob.tools.date import parse_french_date
# Ugly array to avoid the use of french locale
class LoginPage(HTMLPage):
def login(self, username, password):
form = self.get_form(id='connexioncompte_2connexionCompteForm')
form['connexioncompte_2numSecuriteSociale'] = username
form['connexioncompte_2codeConfidentiel'] = password
form.submit()
FRENCH_MONTHS = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre']
class ErrorPage(HTMLPage):
def on_load(self):
msg = CleanText('//div[@id="backgroundId"]//p')(self.doc)
raise BrowserUnavailable(msg)
class AmeliBasePage(HTMLPage):
@property
def logged(self):
if self.doc.xpath('//a[contains(text(), "Déconnexion")]'):
logged = True
else:
logged = False
self.logger.debug('logged: %s' % (logged))
return logged
def is_error(self):
errors = self.doc.xpath('//*[@id="r_errors"]')
if errors:
return errors[0].text_content()
class SubscriptionPage(LoggedPage, HTMLPage):
@method
class iter_subscriptions(ListElement):
item_xpath = '//div[@id="corps-de-la-page"]//div[@class="tableau"]/div'
errors = CleanText('//p[@class="msg_erreur"]', default='')(self.doc)
if errors:
return errors
class item(ItemElement):
klass = Subscription
errors = CleanText('//div[@class="zone-alerte"]/span')(self.doc)
if errors:
return errors
obj__labelid = Attr('.', 'aria-labelledby')
return False
def obj__birthdate(self):
return CleanText('//button[@id="%s"]//td[@class="dateNaissance"]' % Field('_labelid')(self))(self)
def obj_id(self):
# DON'T TAKE social security number for id because it's a very confidential data, take birth date instead
return ''.join(re.findall(r'\d+', Field('_birthdate')(self)))
class LoginPage(AmeliBasePage):
def login(self, login, password):
form = self.get_form('//form[@name="connexionCompteForm"]')
form['connexioncompte_2numSecuriteSociale'] = login.encode('utf8')
form['connexioncompte_2codeConfidentiel'] = password.encode('utf8')
form.submit()
def obj__param(self):
reversed_date = ''.join(reversed(re.findall(r'\d+', Field('_birthdate')(self))))
name = CleanText('//button[@id="%s"]//td[@class="nom"]' % Field('_labelid')(self))(self)
return '%s!-!%s!-!1' % (reversed_date, name)
def locate_to_cgu_page(self):
try:
# they've put a head tag inside body, yes i know...
url = Regexp(Attr('//div[@id="connexioncompte_2"]//meta', 'content'), r'url=(.*)')(self.doc)
except XPathNotFound:
# no cgu to validate
return
self.browser.location(url)
class CguPage(AmeliBasePage):
def get_cgu(self):
return CleanText('//div[@class="page_nouvelles_cgus"]/p[1]')(self.doc)
class HomePage(AmeliBasePage):
pass
class AccountPage(AmeliBasePage):
def iter_subscription_list(self):
names_list = self.doc.xpath('//span[@class="NomEtPrenomLabel"]')
fullname = CleanText(newlines=True).filter(names_list[0])
number = re.sub(r'[^\d]+', '', CleanText('//span[@class="blocNumSecu"]', replace=[(' ', '')])(self.doc))
sub = Subscription(number)
sub._id = number
sub.label = fullname
firstname = CleanText('//span[@class="prenom-titulaire"]')(self.doc)
sub.subscriber = firstname
yield sub
class PaymentsPage(AmeliBasePage):
def get_last_payments_url(self):
begin_date = self.doc.xpath('//input[@id="paiements_1dateDebut"]/@data-mindate')[0]
end_date = self.doc.xpath('//input[@id="paiements_1dateFin"]/@data-maxdate')[0]
url = ('/PortailAS/paiements.do?actionEvt=afficherPaiementsComplementaires&DateDebut='
+ begin_date + '&DateFin=' + end_date +
'&Beneficiaire=tout_selectionner&afficherReleves=false&afficherIJ=false&afficherInva=false'
'&afficherRentes=false&afficherRS=false&indexPaiement=&idNotif=')
return url
class LastPaymentsPage(LoggedPage, AmeliBasePage):
def iter_last_payments(self):
elts = self.doc.xpath('//li[@class="rowitem remboursement"]')
for elt in elts:
items = Regexp(CleanText('./@onclick'), r".*ajaxCallRemoteChargerDetailPaiement \('(\w+={0,2})', '(\w+)', '(\d+)', '(\d+)'\).*", '\\1,\\2,\\3,\\4')(elt).split(',')
yield "/PortailAS/paiements.do?actionEvt=chargerDetailPaiements&idPaiement=" + items[0] + "&naturePaiement=" + items[1] + "&indexGroupe=" + items[2] + "&indexPaiement=" + items[3]
def iter_documents(self, sub):
elts = self.doc.xpath('//li[@class="rowdate"]')
for elt in elts:
try:
elt.xpath('.//a[contains(@id,"lienPDFReleve")]')[0]
except IndexError:
continue
date_str = elt.xpath('.//span[contains(@id,"moisEnCours")]')[0].text
month_str = date_str.split()[0]
date = datetime.strptime(re.sub(month_str, str(FRENCH_MONTHS.index(month_str) + 1), date_str), "%m %Y").date()
bil = Bill()
bil.id = sub._id + "." + date.strftime("%Y%m")
bil.date = date
bil.format = 'pdf'
bil.type = DocumentTypes.BILL
bil.label = date.strftime("%Y%m%d")
bil.url = '/PortailAS/PDFServletReleveMensuel.dopdf?PDF.moisRecherche=' + date.strftime("%m%Y")
yield bil
def get_document(self, bill):
self.location(bill.url, params=bill._args)
class PaymentDetailsPage(AmeliBasePage):
def iter_payment_details(self, sub):
id_str = self.doc.xpath('//div[@class="entete container"]/h2')[0].text.strip()
m = re.match(r'.*le (.*) pour un montant de.*', id_str)
if m:
blocs_benes = self.doc.xpath('//span[contains(@id,"nomBeneficiaire")]')
blocs_prestas = self.doc.xpath('//table[@id="tableauPrestation"]')
i = 0
last_bloc = len(blocs_benes)
for i in range(0, last_bloc):
bene = blocs_benes[i].text
id_str = m.group(1)
id_date = datetime.strptime(id_str, '%d/%m/%Y').date()
id = sub._id + "." + datetime.strftime(id_date, "%Y%m%d")
table = blocs_prestas[i].xpath('.//tr')
line = 1
last_date = None
for tr in table:
tds = tr.xpath('.//td')
if len(tds) == 0:
continue
det = Detail()
# TO TEST : Indemnités journalières : Pas pu tester de cas de figure similaire dans la nouvelle mouture du site
if len(tds) == 4:
date_str = Regexp(pattern=r'.*<br/>(\d+/\d+/\d+)\).*').filter(tds[0].text)
det.id = id + "." + str(line)
det.label = tds[0].xpath('.//span')[0].text.strip()
jours = tds[1].text
if jours is None:
jours = '0'
montant = tds[2].text
if montant is None:
montant = '0'
price = tds[3].text
if price is None:
price = '0'
if date_str is None or date_str == '':
det.infos = ''
det.datetime = last_date
else:
det.infos = date_str + ' (' + re.sub(r'[^\d,-]+', '', jours) + 'j) * ' + re.sub(r'[^\d,-]+', '', montant) + ''
det.datetime = datetime.strptime(date_str.split(' ')[3], '%d/%m/%Y').date()
last_date = det.datetime
det.price = Decimal(re.sub(r'[^\d,-]+', '', price).replace(',', '.'))
if len(tds) == 5:
date_str = Regexp(pattern=r'\w*(\d{2})/(\d{2})/(\d{4}).*', template='\\1/\\2/\\3', default="").filter("".join(tds[0].itertext()))
det.id = id + "." + str(line)
det.label = bene + ' - ' + tds[0].xpath('.//span')[0].text.strip()
paye = tds[1].text
if paye is None:
paye = '0'
base = tds[2].text
if base is None:
base = '0'
tdtaux = tds[3].xpath('.//span')[0].text
if tdtaux is None:
taux = '0'
else:
taux = tdtaux.strip()
tdprice = tds[4].xpath('.//span')[0].text
if tdprice is None:
price = '0'
else:
price = tdprice.strip()
if date_str is None or date_str == '':
det.infos = ''
det.datetime = last_date
else:
det.infos = ' Payé ' + re.sub(r'[^\d,-]+', '', paye) + '€ / Base ' + re.sub(r'[^\d,-]+', '', base) + '€ / Taux ' + re.sub(r'[^\d,-]+', '', taux) + '%'
det.datetime = datetime.strptime(date_str, '%d/%m/%Y').date()
last_date = det.datetime
det.price = Decimal(re.sub(r'[^\d,-]+', '', price).replace(',', '.'))
line = line + 1
yield det
class Raw(LoggedPage, RawPage):
pass
class UnavailablePage(HTMLPage):
def on_load(self):
raise BrowserUnavailable(CleanText('//span[@class="texte-indispo"]')(self.doc))
obj_subscriber = CleanText('.//span[@class="NomEtPrenomLabel"]')
obj_label = obj_subscriber
class DocumentsPage(LoggedPage, PartialHTMLPage):
@method
class iter_documents(ListElement):
item_xpath = '//ul[@id="unordered_list"]//li[has-class("rowitem")]'
class item(ItemElement):
klass = Bill
obj_id = Format('%s_%s', Env('subid'), Regexp(Field('url'), r'idPaiement=(.*)'))
obj_label = CleanText('.//div[has-class("col-label")]')
obj_price = CleanDecimal.French('.//div[has-class("col-montant")]/span')
obj_currency = Currency('.//div[has-class("col-montant")]/span')
obj_url = Link('.//a[@class="downdetail"]')
obj_format = 'pdf'
def obj_date(self):
year = Regexp(CleanText('./preceding-sibling::li[@class="rowdate"]//span[@class="mois"]'), r'(\d+)')(self)
day_month = CleanText('.//div[has-class("col-date")]/span')(self)
return parse_french_date(day_month + ' ' + year)
# -*- coding: utf-8 -*-
# Copyright(C) 2013-2015 Christophe Lampin
#
# 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 Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from weboob.tools.test import BackendTest
class AmeliTest(BackendTest):
MODULE = 'ameli'
def test_ameli(self):
for subscription in self.backend.iter_subscription():
list(self.backend.iter_documents_history(subscription.id))
for bill in self.backend.iter_documents(subscription.id):
self.backend.download_document(bill.id)
......@@ -540,18 +540,20 @@ class AccountsPage(LoggedPage, MyHTMLPage):
u'Synthèse': None, # ignore this title
}
PATTERN = [(re.compile('.*Titres Pea.*'), Account.TYPE_PEA),
(re.compile(".*Plan D'epargne En Actions.*"), Account.TYPE_PEA),
(re.compile(".*Compte Especes Pea.*"), Account.TYPE_PEA),
(re.compile('.*Plan Epargne Retraite.*'), Account.TYPE_PERP),
(re.compile('.*Titres.*'), Account.TYPE_MARKET),
(re.compile('.*Selection Vie.*'),Account.TYPE_LIFE_INSURANCE),
(re.compile('^Fructi Pulse.*'), Account.TYPE_MARKET),
(re.compile('^(Quintessa|Solevia).*'), Account.TYPE_MARKET),
(re.compile('^Plan Epargne Enfant Mul.*'), Account.TYPE_MARKET),
(re.compile('^Alc Premium'), Account.TYPE_MARKET),
(re.compile('^Plan Epargne Enfant Msu.*'), Account.TYPE_LIFE_INSURANCE),
]
PATTERN = [
(re.compile(r'.*Titres Pea.*'), Account.TYPE_PEA),
(re.compile(r".*Plan D'epargne En Actions.*"), Account.TYPE_PEA),
(re.compile(r".*Compte Especes Pea.*"), Account.TYPE_PEA),
(re.compile(r'.*Plan Epargne Retraite.*'), Account.TYPE_PERP),
(re.compile(r'.*Titres.*'), Account.TYPE_MARKET),
(re.compile(r'.*Selection Vie.*'), Account.TYPE_LIFE_INSURANCE),
(re.compile(r'^Fructi Pulse.*'), Account.TYPE_MARKET),
(re.compile(r'^(Quintessa|Solevia).*'), Account.TYPE_MARKET),
(re.compile(r'^Plan Epargne Enfant Mul.*'), Account.TYPE_MARKET),
(re.compile(r'^Alc Premium'), Account.TYPE_MARKET),
(re.compile(r'^Plan Epargne Enfant Msu.*'), Account.TYPE_LIFE_INSURANCE),
(re.compile(r'^Parts Sociales.*'), Account.TYPE_MARKET),
]
def pop_up(self):
if self.doc.xpath('//span[contains(text(), "du navigateur Internet.")]'):
......
......@@ -109,26 +109,28 @@ def iter_accounts(self):
account.coming = self.page.get_balance()
yield account
if self.type == '2':
for rib in self.page.get_rib_list():
for company in self.page.get_companies():
self.accounts.stay_or_go()
self.page.expand(rib=rib)
accounts = list(self.page.iter_accounts(rib=rib))
ids = {}
prev_rib = None
for account in accounts:
if account.id in ids:
self.logger.warning('duplicate account %r', account.id)
account.id += '_%s' % ''.join(account.label.split())
if prev_rib != account._rib:
self.coming.go()
self.page.expand(rib=account._rib)
account.coming = self.page.get_balance(account)
prev_rib = account._rib
ids[account.id] = account
yield account
self.page.expand(company=company)
for rib in self.page.get_rib_list():
self.page.expand(rib=rib, company=company)
accounts = list(self.page.iter_accounts(rib=rib, company=company))
ids = {}
prev_rib = None
for account in accounts:
if account.id in ids:
self.logger.warning('duplicate account %r', account.id)
account.id += '_%s' % ''.join(account.label.split())
if prev_rib != account._rib:
self.coming.go()
self.page.expand(rib=account._rib, company=account._company)
account.coming = self.page.get_balance(account)
prev_rib = account._rib
ids[account.id] = account
yield account
# Could be the very same as non corporate but this shitty website seems
# completely bugged
......@@ -136,34 +138,34 @@ def get_ti_corporate_transactions(self, account):
if account.id not in self.transactions_dict:
self.transactions_dict[account.id] = []
self.ti_histo_go()
self.page.expand(self.page.get_periods()[0], account=account)
self.page.expand(self.page.get_periods()[0], account=account, company=account._company)
for tr in sorted_transactions(self.page.get_history()):
self.transactions_dict[account.id].append(tr)
return self.transactions_dict[account.id]
def get_ti_transactions(self, account):
self.ti_card_go()
self.page.expand(account=account)
self.page.expand(account=account, company=account._company)
for tr in sorted_transactions(self.page.get_history()):
yield tr
self.ti_histo_go()
self.page.expand(self.page.get_periods()[0], account=account)
self.page.expand(self.page.get_periods()[0], account=account, company=account._company)
for period in self.page.get_periods():
self.page.expand(period, account=account)
self.page.expand(period, account=account, company=account._company)
for tr in sorted_transactions(self.page.get_history()):
yield tr
def get_ge_transactions(self, account):
transactions = []
self.coming.go()
self.page.expand(rib=account._rib)
self.page.expand(account=account, rib=account._rib, company=account._company)
link = self.page.get_link(account)
if link:
self.location(link)
transactions += self.page.get_history()
self.history.go()
for period in self.page.get_periods():
self.page.expand(period, rib=account._rib)
self.page.expand(period, rib=account._rib, company=account._company, account=account)
link = self.page.get_link(account)
if link:
self.location(link)
......
......@@ -49,12 +49,18 @@ def login(self, type, username, password):
class ExpandablePage(LoggedPage, HTMLPage):
def expand(self, account=None, rib=None):
def expand(self, account=None, rib=None, company=None):
form = self.get_form()
if rib is not None:
form['ribSaisi'] = rib
if account is not None:
form['numCarteSaisi'] = account._nav_num
form['nomCarteSaisi'] = account.label # some forms use 'nomCarteSaisi' some use 'titulaireSaisie'
form['titulaireSaisie'] = account.label
if company is not None:
if 'entrepriseSaisie' in form.keys(): # some forms use 'entrepriseSaisie' some use 'entrepriseSaisi'
form['entrepriseSaisie'] = company
else:
form['entrepriseSaisi'] = company
# needed if coporate titulaire
form.url = form.url.replace('Appliquer', 'Afficher')
form.submit()
......@@ -80,13 +86,16 @@ def get_periods(self):
periods.append(period)
return periods
def expand(self, period, account=None, rib=None):
def expand(self, period, account=None, rib=None, company=None):
form = self.get_form(submit='//input[@value="Display"]')
if account is not None:
form['numCarteSaisi'] = account._nav_num
form['nomCarteSaisi'] = account.label
form['titulaireSaisi'] = account.label
form['periodeSaisie'] = period
if rib is not None:
form['ribSaisi'] = rib
if company is not None:
form['entrepriseSaisi'] = company
# needed if coporate titulaire
form.url = form.url.replace('Appliquer', 'Afficher')
form.submit()
......@@ -109,6 +118,7 @@ class item(ItemElement):
obj_label = CleanText('./td[1]')
obj_type = Account.TYPE_CARD
obj__rib = Env('rib')
obj__company = Env('company')
obj_currency = u'EUR'
obj_number = CleanText('./td[2]', replace=[(' ', '')])
obj_url = AbsoluteLink('./td[2]/a')
......@@ -118,6 +128,9 @@ class item(ItemElement):
def store(self, obj):
return obj
def get_companies(self):
return self.doc.xpath('//select[@name="entrepriseSaisie"]/option/@value')
class ComingPage(ExpandablePage):
def get_link(self, account):
......
......@@ -334,6 +334,7 @@ class item(ItemElement):
'Crédit immobilier': Account.TYPE_MORTGAGE,
'Réserve Provisio': Account.TYPE_REVOLVING_CREDIT,
'Prêt personnel': Account.TYPE_CONSUMER_CREDIT,
'Crédit Silo': Account.TYPE_REVOLVING_CREDIT,
}
klass = Account
......
......@@ -333,20 +333,18 @@ def get_history(self, account, coming=False):
return self.get_regular_transactions(account, coming)
def get_regular_transactions(self, account, coming):
# We look for 3 years of history.
params = {}
params['movementSearch[toDate]'] = (date.today() + relativedelta(days=40)).strftime('%d/%m/%Y')
params['movementSearch[fromDate]'] = (date.today() - relativedelta(years=3)).strftime('%d/%m/%Y')
params['movementSearch[selectedAccounts][]'] = account._webid
if not coming:
# We look for 3 years of history.
params = {}
params['movementSearch[toDate]'] = (date.today() + relativedelta(days=40)).strftime('%d/%m/%Y')
params['movementSearch[fromDate]'] = (date.today() - relativedelta(years=3)).strftime('%d/%m/%Y')
params['movementSearch[selectedAccounts][]'] = account._webid
self.location('%s/mouvements' % account.url.rstrip('/'), params=params)
for transaction in self.page.iter_history():
yield transaction
elif account.type == Account.TYPE_CHECKING:
self.location('%s/mouvements-a-venir' % account.url.rstrip('/'), params=params)
for transaction in self.page.iter_history(coming=True):
yield transaction
# Note: Checking accounts have a 'Mes prélèvements à venir' tab,
# but these transactions have no date anymore so we ignore them.
def get_card_transactions(self, account, coming):
# All card transactions can be found in the CSV (history and coming),
......
......@@ -460,7 +460,6 @@ def next_page(self):
class item(ItemElement):
klass = Transaction
obj_date = Date(Attr('.//time', 'datetime'))
obj_amount = CleanDecimal('.//div[has-class("list__movement__line--amount")]', replace_dots=True)
obj_category = CleanText('.//span[has-class("category")]')
obj__account_name = CleanText('.//span[contains(@class, "account__name-xs")]', default=None)
......
......@@ -617,7 +617,7 @@ def get_accounts_list(self):
self.location(self.accounts_url)
assert self.pro_accounts_list.is_here()
for account in self.page.get_accounts_list():
for account in self.page.iter_accounts():
ids.add(account.id)
accounts.append(account)
......@@ -625,7 +625,7 @@ def get_accounts_list(self):
self.location(self.accounts_and_loans_url)
assert self.pro_accounts_list.is_here()
for account in self.page.get_accounts_list():
for account in self.page.iter_accounts():
if account.id not in ids:
ids.add(account.id)
accounts.append(account)
......
......@@ -39,31 +39,26 @@
class Transaction(FrenchTransaction):
PATTERNS = [(re.compile(u'^(?P<category>CHEQUE)( N)? (?P<text>.*)'),
FrenchTransaction.TYPE_CHECK),
(re.compile(r'^(?P<category>ACHAT CB) (?P<text>.*) (?P<dd>\d{2})\.(?P<mm>\d{2}).(?P<yy>\d{2,4}).*'),
FrenchTransaction.TYPE_CARD),
(re.compile('^(?P<category>(PRELEVEMENT DE|TELEREGLEMENT|TIP)) (?P<text>.*)'),
FrenchTransaction.TYPE_ORDER),
(re.compile('^(?P<category>ECHEANCEPRET)(?P<text>.*)'),
FrenchTransaction.TYPE_LOAN_PAYMENT),
(re.compile(r'^CARTE \w+ (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{2,4}) A \d+H\d+ (?P<category>RETRAIT DAB) (?P<text>.*)'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^(?P<category>RETRAIT DAB) (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{2,4}) \d+H\d+ (?P<text>.*)'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^(?P<category>RETRAIT) (?P<text>.*) (?P<dd>\d{2})\.(?P<mm>\d{2})\.(?P<yy>\d{2,4})'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile('^(?P<category>VIR(EMEN)?T?) (DE |POUR )?(?P<text>.*)'),
FrenchTransaction.TYPE_TRANSFER),
(re.compile('^(?P<category>REMBOURST)(?P<text>.*)'), FrenchTransaction.TYPE_PAYBACK),
(re.compile('^(?P<category>COMMISSIONS)(?P<text>.*)'), FrenchTransaction.TYPE_BANK),
(re.compile('^(?P<category>FRAIS POUR)(?P<text>.*)'), FrenchTransaction.TYPE_BANK),
(re.compile('^(?P<text>(?P<category>REMUNERATION).*)'), FrenchTransaction.TYPE_BANK),
(re.compile('^(?P<category>REMISE DE CHEQUES?) (?P<text>.*)'), FrenchTransaction.TYPE_DEPOSIT),
(re.compile(u'^(?P<text>DEBIT CARTE BANCAIRE DIFFERE.*)'), FrenchTransaction.TYPE_CARD_SUMMARY),
(re.compile('^COTISATION TRIMESTRIELLE.*'), FrenchTransaction.TYPE_BANK),
(re.compile('^(?P<category>FRAIS (TRIMESTRIELS) DE TENUE DE COMPTE.*)'), FrenchTransaction.TYPE_BANK)
]
PATTERNS = [
(re.compile(r'^(?P<category>CHEQUE)( N)? (?P<text>.*)'), FrenchTransaction.TYPE_CHECK),
(re.compile(r'^(?P<category>ACHAT CB) (?P<text>.*) (?P<dd>\d{2})\.(?P<mm>\d{2}).(?P<yy>\d{2,4}).*'), FrenchTransaction.TYPE_CARD),
(re.compile(r'^(?P<category>(PRELEVEMENT DE|TELEREGLEMENT|TIP)) (?P<text>.*)'), FrenchTransaction.TYPE_ORDER),
(re.compile(r'^(?P<category>ECHEANCEPRET)(?P<text>.*)'), FrenchTransaction.TYPE_LOAN_PAYMENT),
(re.compile(r'^CARTE \w+ (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{2,4}) A \d+H\d+ (?P<category>RETRAIT DAB) (?P<text>.*)'), FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^(?P<category>RETRAIT DAB) (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{2,4}) \d+H\d+ (?P<text>.*)'), FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^(?P<category>RETRAIT) (?P<text>.*) (?P<dd>\d{2})\.(?P<mm>\d{2})\.(?P<yy>\d{2,4})'), FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile(r'^(?P<category>VIR(EMEN)?T?) (DE |POUR )?(?P<text>.*)'), FrenchTransaction.TYPE_TRANSFER),
(re.compile(r'^(?P<category>REMBOURST)(?P<text>.*)'), FrenchTransaction.TYPE_PAYBACK),
(re.compile(r'^(?P<category>COMMISSIONS)(?P<text>.*)'), FrenchTransaction.TYPE_BANK),
(re.compile(r'^(?P<category>FRAIS POUR)(?P<text>.*)'), FrenchTransaction.TYPE_BANK),
(re.compile(r'^(?P<text>(?P<category>REMUNERATION).*)'), FrenchTransaction.TYPE_BANK),
(re.compile(r'^(?P<category>REMISE DE CHEQUES?) (?P<text>.*)'), FrenchTransaction.TYPE_DEPOSIT),
(re.compile(r'^(?P<text>DEBIT CARTE BANCAIRE DIFFERE.*)'), FrenchTransaction.TYPE_CARD_SUMMARY),
(re.compile(r'^COTISATION TRIMESTRIELLE.*'), FrenchTransaction.TYPE_BANK),
(re.compile(r'^REMISE COMMERCIALE.*'), FrenchTransaction.TYPE_BANK),
(re.compile(r'^.*UTILISATION DU DECOUVERT$'), FrenchTransaction.TYPE_BANK),
(re.compile(r'^(?P<category>FRAIS (TRIMESTRIELS) DE TENUE DE COMPTE.*)'), FrenchTransaction.TYPE_BANK)
]
class AccountHistory(LoggedPage, MyHTMLPage):
......
......@@ -17,17 +17,15 @@
# 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/>.
import re
from decimal import Decimal
from __future__ import unicode_literals
from weboob.browser.elements import ListElement, ItemElement, method
from weboob.browser.filters.standard import CleanText, CleanDecimal, Date
from weboob.browser.filters.html import Link
from weboob.browser.filters.standard import CleanText, CleanDecimal, Coalesce, Currency, Date, Map, Field, Regexp
from weboob.browser.filters.html import AbsoluteLink, Link
from weboob.browser.pages import LoggedPage, pagination
from weboob.capabilities.bank import Account
from weboob.capabilities.profile import Company
from weboob.capabilities.base import NotAvailable
from weboob.tools.compat import urljoin, unicode
from weboob.exceptions import BrowserUnavailable
from .accounthistory import Transaction
......@@ -36,55 +34,55 @@
class RedirectPage(LoggedPage, MyHTMLPage):
def check_for_perso(self):
return self.doc.xpath(u'//p[contains(text(), "L\'identifiant utilisé est celui d\'un compte de Particuliers")]')
return self.doc.xpath('''//p[contains(text(), "L'identifiant utilisé est celui d'un compte de Particuliers")]''')
ACCOUNT_TYPES = {
'Comptes titres': Account.TYPE_MARKET,
'Comptes épargne': Account.TYPE_SAVINGS,
'Comptes courants': Account.TYPE_CHECKING,
}
class ProAccountsList(LoggedPage, MyHTMLPage):
# TODO Be careful about connections with personnalized account groups
# According to their presentation video (https://www.labanquepostale.fr/pmo/nouvel-espace-client-business.html),
# on the new website people are able to make personnalized groups of account instead of the usual drop-down categories on which to parse to find a match in ACCOUNT_TYPES
# If clients use the functionnality we might need to add entries new in ACCOUNT_TYPES
def on_load(self):
if self.doc.xpath('//div[@id="erreur_generale"]'):
raise BrowserUnavailable(CleanText(u'//div[@id="erreur_generale"]//p[contains(text(), "Le service est momentanément indisponible")]')(self.doc))
ACCOUNT_TYPES = {u'comptes titres': Account.TYPE_MARKET,
u'comptes épargne': Account.TYPE_SAVINGS,
# wtf? ^
u'comptes épargne': Account.TYPE_SAVINGS,
u'comptes courants': Account.TYPE_CHECKING,
}
def get_accounts_list(self):
for table in self.doc.xpath('//div[@class="comptestabl"]/table'):
try:
account_type = self.ACCOUNT_TYPES[table.get('summary').lower()]
if not account_type:
account_type = self.ACCOUNT_TYPES[table.xpath('./caption/text()')[0].strip().lower()]
except (IndexError,KeyError):
account_type = Account.TYPE_UNKNOWN
for tr in table.xpath('./tbody/tr'):
cols = tr.findall('td')
link = cols[0].find('a')
if link is None:
continue
a = Account()
a.type = account_type
a.id = unicode(re.search('([A-Z\d]{4}[A-Z\d\*]{3}[A-Z\d]{4})', link.attrib['title']).group(1))
a.label = unicode(link.attrib['title'].replace('%s ' % a.id, ''))
# We use '.text_content()' to avoid HTML comments like '<!-- ... -->'
tmp_balance = CleanText(None).filter(cols[1].text_content())
a.currency = a.get_currency(tmp_balance)
if not a.currency:
a.currency = u'EUR'
a.balance = Decimal(Transaction.clean_amount(tmp_balance))
a._has_cards = False
a.url = urljoin(self.url, link.attrib['href'])
yield a
raise BrowserUnavailable(CleanText('//div[@id="erreur_generale"]//p[contains(text(), "Le service est momentanément indisponible")]')(self.doc))
def is_here(self):
return CleanText('//h1[contains(text(), "Synthèse des comptes")]')(self.doc)
@method
class iter_accounts(ListElement):
item_xpath = '//div[@id="mainContent"]//div[h3/a]'
class item(ItemElement):
klass = Account
obj_id = Regexp(CleanText('./h3/a/@title'), r'([A-Z\d]{4}[A-Z\d\*]{3}[A-Z\d]{4})')
obj_balance = CleanDecimal.French('./span/text()[1]') # This website has the good taste of leaving hard coded HTML comments. This is the way to pin point to the righ text item.
obj_currency = Currency('./span')
obj_url = AbsoluteLink('./h3/a')
# account are grouped in /div based on their type, we must fetch the closest one relative to item_xpath
obj_type = Map(CleanText('./ancestor::div[1]/preceding-sibling::h2[1]/button/div[@class="title-accordion"]'), ACCOUNT_TYPES, Account.TYPE_UNKNOWN)
def obj_label(self):
""" Need to get rid of the id wherever we find it in account labels like "LIV A 0123456789N MR MOMO" (livret A) as well as "0123456789N MR MOMO" (checking account) """
return CleanText('./h3/a/@title')(self).replace('%s ' % Field('id')(self), '')
class ProAccountHistory(LoggedPage, MyHTMLPage):
@pagination
@method
class iter_history(ListElement):
item_xpath = u'//div[@id="tabReleve"]//tbody/tr'
item_xpath = '//div[@id="tabReleve"]//tbody/tr'
def next_page(self):
# The next page on the website can return pages already visited without logical mechanism
......@@ -96,12 +94,12 @@ def next_page(self):
next_page_link = Link(next_page_xpath)(self.el)
next_page = self.page.browser.location(next_page_link)
first_transaction = CleanText(tr_xpath)(next_page.page.doc)
count = 0 # avoid an infinite loop
count = 0 # avoid an infinite loop
while first_transaction in self.page.browser.first_transactions and count < 30:
next_page = self.page.browser.location(next_page_link)
next_page_link = Link(next_page_xpath)(next_page.page.doc)
first_transaction = CleanText(tr_xpath)(next_page.page.doc)
first_transaction = CleanText(tr_xpath)(next_page.page.doc)
count += 1
if count < 30:
......@@ -112,13 +110,15 @@ class item(ItemElement):
obj_date = Date(CleanText('.//td[@headers="date"]'), dayfirst=True)
obj_raw = Transaction.Raw('.//td[@headers="libelle"]')
obj_amount = CleanDecimal('.//td[@headers="debit" or @headers="credit"]',
replace_dots=True, default=NotAvailable)
obj_amount = Coalesce(
CleanDecimal.French('.//td[@headers="debit"]', default=NotAvailable),
CleanDecimal.French('.//td[@headers="credit"]', default=NotAvailable),
)
class DownloadRib(LoggedPage, MyHTMLPage):
def get_rib_value(self, acc_id):
opt = self.doc.xpath('//div[@class="rechform"]//option')
opt = self.doc.xpath('//select[@id="idxSelection"]/optgroup//option')
for o in opt:
if acc_id in o.text:
return o.xpath('./@value')[0]
......
......@@ -109,7 +109,7 @@ def get_content(self):
class ProSubscriptionPage(LoggedPage, HTMLPage):
@method
class iter_subscriptions(ListElement):
item_xpath = '//select[@id="numeroCompteRechercher"]/option'
item_xpath = '//select[@id="numeroCompteRechercher"]/option[not(@disabled)]'
class item(ItemElement):
klass = Subscription
......
......@@ -135,9 +135,6 @@ def get_loans_list(self):
def get_list(self):
self.accounts.go()
for acc in self.page.iter_accounts(accnum=self.accnum, current_univers=self.current_univers):
if acc.type == Account.TYPE_CHECKING:
self.iban.go(number=acc._number)
self.page.set_iban(account=acc)
yield acc
@need_login
......@@ -227,3 +224,9 @@ def get_profile(self):
self.page.set_email(profile=profile)
return profile
@need_login
def fill_account(self, account, fields):
if account.type == Account.TYPE_CHECKING and 'iban' in fields:
self.iban.go(number=account._number)
self.page.set_iban(account=account)
......@@ -18,7 +18,7 @@
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from weboob.capabilities.bank import CapBankWealth, AccountNotFound
from weboob.capabilities.bank import CapBankWealth, AccountNotFound, Account
from weboob.capabilities.base import find_object
from weboob.capabilities.profile import CapProfile
from weboob.tools.backend import Module, BackendConfig
......@@ -73,3 +73,13 @@ def iter_investment(self, account):
def get_profile(self):
return self.browser.get_profile()
def fill_account(self, account, fields):
if self.config['website'].get() != 'bred':
return
self.browser.fill_account(account, fields)
OBJECTS = {
Account: fill_account,
}
......@@ -162,35 +162,36 @@ class Transaction(FrenchTransaction):
]
class IndexPage(LoggedPage, HTMLPage):
ACCOUNT_TYPES = {u'Epargne liquide': Account.TYPE_SAVINGS,
u'Compte Courant': Account.TYPE_CHECKING,
u'COMPTE A VUE': Account.TYPE_CHECKING,
u'COMPTE CHEQUE': Account.TYPE_CHECKING,
u'Mes comptes': Account.TYPE_CHECKING,
u'CPT DEPOT PART.': Account.TYPE_CHECKING,
u'CPT DEPOT PROF.': Account.TYPE_CHECKING,
u'Mon épargne': Account.TYPE_SAVINGS,
u'Mes autres comptes': Account.TYPE_SAVINGS,
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,
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'Compte 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,
}
ACCOUNT_TYPES = {
'Epargne liquide': Account.TYPE_SAVINGS,
'Compte Courant': Account.TYPE_CHECKING,
'COMPTE A VUE': Account.TYPE_CHECKING,
'COMPTE CHEQUE': Account.TYPE_CHECKING,
'Mes comptes': Account.TYPE_CHECKING,
'CPT DEPOT PART.': Account.TYPE_CHECKING,
'CPT DEPOT PROF.': Account.TYPE_CHECKING,
'Mon épargne': Account.TYPE_SAVINGS,
'Mes autres comptes': Account.TYPE_SAVINGS,
'Compte Epargne et DAT': Account.TYPE_SAVINGS,
'Plan et Contrat d\'Epargne': Account.TYPE_SAVINGS,
'COMPTE SUR LIVRET': Account.TYPE_SAVINGS,
'LIVRET DEV.DURABLE': Account.TYPE_SAVINGS,
'LDD Solidaire': Account.TYPE_SAVINGS,
'LIVRET A': Account.TYPE_SAVINGS,
'LIVRET JEUNE': Account.TYPE_SAVINGS,
'LIVRET GRAND PRIX': Account.TYPE_SAVINGS,
'LEP': Account.TYPE_SAVINGS,
'LEL': Account.TYPE_SAVINGS,
'CPT PARTS SOCIALES': Account.TYPE_MARKET,
'PEL 16 2013': Account.TYPE_SAVINGS,
'Titres': Account.TYPE_MARKET,
'Compte titres': Account.TYPE_MARKET,
'Mes crédits immobiliers': Account.TYPE_LOAN,
'Mes crédits renouvelables': Account.TYPE_LOAN,
'Mes crédits consommation': Account.TYPE_LOAN,
'PEA NUMERAIRE': Account.TYPE_PEA,
'PEA': Account.TYPE_PEA,
}
def build_doc(self, content):
content = content.strip(b'\x00')
......
......@@ -273,6 +273,14 @@ def iter_accounts(self):
- Multiple perimeters: visit all perimeters one by one and return all accounts.
'''
accounts_list = []
# Sometimes the URL of the page after login has a session_value=None,
# so we must set it correctly otherwise the next requests will crash.
if not self.session_value:
m = re.search(r'sessionSAG=([^&]+)', self.url)
if m:
self.session_value = m.group(1)
if len(self.perimeters) == 1:
self.accounts.stay_or_go(session_value=self.session_value)
for account in self.iter_perimeter_accounts(iban=True, all_accounts=True):
......@@ -530,6 +538,9 @@ def iter_history(self, account, coming=False):
# we must skip the ongoing one but fetch the other ones
# even if they are in the future.
ongoing_coming = self.page.get_ongoing_coming()
if not ongoing_coming:
# This card has no available history or coming.
return
card_transactions = []
latest_date = None
......
......@@ -196,6 +196,7 @@ def get_iban(self):
'CCHQ': Account.TYPE_CHECKING,
'CCOU': Account.TYPE_CHECKING,
'AUTO ENTRP': Account.TYPE_CHECKING,
'AUTO ENTRS': Account.TYPE_CHECKING,
'DEVISE USD': Account.TYPE_CHECKING,
'EKO': Account.TYPE_CHECKING,
'DEVISE CHF': Account.TYPE_CHECKING,
......@@ -417,8 +418,10 @@ def get_ongoing_coming(self):
# the coming is positive, it will become 'Opérations créditées'
raw_date = Regexp(
CleanText('//table[@class="ca-table"]//tr[1]//b[contains(text(), "Opérations débitées") or contains(text(), "Opérations créditées")]'),
r'le (.*) :'
r'le (.*) :', default=None
)(self.doc)
if not raw_date:
return None
return parse_french_date(raw_date).date()
def get_card_transactions(self, latest_date, ongoing_coming):
......