Romain Bignon
Builds for 1 pipeline failed in 10 minutes 43 seconds

backport master modules fixes

Showing 54 changed files with 945 additions and 436 deletions
......@@ -21,8 +21,11 @@ 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 .pages import LoginPage, HomePage, CguPage, AccountPage, LastPaymentsPage, PaymentsPage, PaymentDetailsPage, Raw
__all__ = ['AmeliBrowser']
......@@ -38,6 +41,7 @@ class AmeliBrowser(LoginBrowser):
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)
def do_login(self):
self.logger.debug('call Browser.do_login')
......
......@@ -35,13 +35,8 @@ class AmeliModule(Module, CapDocument):
VERSION = '1.3'
LICENSE = 'AGPLv3+'
BROWSER = AmeliBrowser
CONFIG = BackendConfig(ValueBackendPassword('login',
label='Numero de SS',
masked=False),
ValueBackendPassword('password',
label='Password',
masked=True)
)
CONFIG = BackendConfig(ValueBackendPassword('login', label='Numero de SS', regexp=r'^\d{13}$', masked=False),
ValueBackendPassword('password', label='Password', masked=True))
def create_default_browser(self):
return self.create_browser(self.config['login'].get(),
......
......@@ -27,9 +27,11 @@ from weboob.browser.filters.html import Attr, XPathNotFound
from weboob.browser.pages import HTMLPage, RawPage, LoggedPage
from weboob.capabilities.bill import Subscription, Detail, Bill
from weboob.browser.filters.standard import CleanText, Regexp
from weboob.exceptions import BrowserUnavailable
# Ugly array to avoid the use of french locale
FRENCH_MONTHS = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre']
......@@ -52,6 +54,10 @@ class AmeliBasePage(HTMLPage):
if errors:
return errors
errors = CleanText('//div[@class="zone-alerte"]/span')(self.doc)
if errors:
return errors
return False
......@@ -225,3 +231,8 @@ class PaymentDetailsPage(AmeliBasePage):
class Raw(LoggedPage, RawPage):
pass
class UnavailablePage(HTMLPage):
def on_load(self):
raise BrowserUnavailable(CleanText('//span[@class="texte-indispo"]')(self.doc))
......
......@@ -60,7 +60,9 @@ class AXABrowser(LoginBrowser):
denied = URL('https://connect.axa.fr/Account/AccessDenied', DeniedPage)
account_space_login = URL('https://connect.axa.fr/api/accountspace', AccountSpaceLogin)
errors = URL('https://espaceclient.axa.fr/content/ecc-public/accueil-axa-connect/_jcr_content/par/text.html', ErrorPage)
errors = URL('https://espaceclient.axa.fr/content/ecc-public/accueil-axa-connect/_jcr_content/par/text.html',
'https://espaceclient.axa.fr/content/ecc-public/errors/500.html',
ErrorPage)
def do_login(self):
# due to the website change, login changed too, this is for don't try to login with the wrong login
......@@ -109,7 +111,8 @@ class AXABanque(AXABrowser, StatesMixin):
'webapp/axabanque/jsp/.*/detail.*.faces', TransactionsPage)
unavailable = URL('login_errors/indisponibilite.*',
'.*page-indisponible.html.*',
'.*erreur/erreurBanque.faces', UnavailablePage)
'.*erreur/erreurBanque.faces',
'http://www.axabanque.fr/message/maintenance.htm', UnavailablePage)
# Wealth
wealth_accounts = URL('https://espaceclient.axa.fr/$',
'https://espaceclient.axa.fr/accueil.html',
......
......@@ -19,7 +19,7 @@
from .compat.weboob_capabilities_bank import CapBankWealth, CapBankTransferAddRecipient, AccountNotFound, RecipientNotFound
from weboob.capabilities.base import find_object, NotAvailable
from weboob.capabilities.base import find_object, NotAvailable, empty
from weboob.capabilities.bank import Account, TransferInvalidLabel
from weboob.capabilities.profile import CapProfile
from weboob.capabilities.bill import CapDocument, Subscription, Document, DocumentNotFound, SubscriptionNotFound
......@@ -112,7 +112,7 @@ class AXABanqueModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapDoc
def transfer_check_account_iban(self, old, new):
# Skip origin account iban check and force origin account iban
if new is NotAvailable:
if empty(new) or empty(old):
self.logger.warning(
'Origin account iban check (%s) is not possible because iban is currently not available',
old,
......
......@@ -121,8 +121,10 @@ class ErrorPage(PartialHTMLPage):
error_msg = (
CleanText('//p[contains(text(), "temporairement indisponible")]')(self.doc),
CleanText('//p[contains(text(), "maintenance est en cours")]')(self.doc),
# parsing for false 500 error page
CleanText('//div[contains(@class, "error-page")]//span[contains(@class, "subtitle") and contains(text(), "Chargement de page impossible")]')(self.doc)
)
for error in error_msg:
if error:
raise BrowserUnavailable(error_msg)
raise BrowserUnavailable(error)
......
......@@ -412,13 +412,17 @@ class HomePage(LoggedPage, MyHTMLPage):
url = self.browser.absurl('/portailinternet/Transactionnel/Pages/CyberIntegrationPage.aspx')
headers = {'Referer': self.url}
# Sometime, the page is a 302 and redirect to a page where there are no information that we need,
# so we try with 2 others url to further fetch token when empty page
r = self.browser.open(url, data='taskId=aUniversMesComptes', params={'vary': vary}, headers=headers)
if not int(r.headers.get('Content-Length', 0)):
url = self.browser.absurl('/portailinternet/Transactionnel/Pages/CyberIntegrationPage.aspx')
headers = {'Referer': self.url}
r = self.browser.open(url, data='taskId=aUniversMesComptes', headers=headers)
if not int(r.headers.get('Content-Length', 0)):
r = self.browser.open(url, data={'taskId': 'equipementDom'}, params={'vary': vary}, headers=headers)
doc = r.page.doc
date = None
for script in doc.xpath('//script'):
......
......@@ -65,8 +65,8 @@ class BNPEnterprise(LoginBrowser):
renew_pass = URL('/sommaire/PseRedirectPasswordConnect', ActionNeededPage)
def __init__(self, *args, **kwargs):
super(BNPEnterprise, self).__init__(*args, **kwargs)
def __init__(self, config, *args, **kwargs):
super(BNPEnterprise, self).__init__(config['login'].get(), config['password'].get(), *args, **kwargs)
def do_login(self):
self.login.go()
......
......@@ -32,7 +32,7 @@ from weboob.capabilities.contact import CapContact
from weboob.capabilities.profile import CapProfile
from weboob.capabilities.base import find_object
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword, Value
from weboob.tools.value import ValueBackendPassword, Value, ValueBool
from .enterprise.browser import BNPEnterprise
from .company.browser import BNPCompany
......@@ -52,9 +52,7 @@ class BNPorcModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapMessag
CONFIG = BackendConfig(
ValueBackendPassword('login', label=u'Numéro client', masked=False),
ValueBackendPassword('password', label=u'Code secret', regexp='^(\d{6})$'),
#ValueBackendPassword('rotating_password', default='',
# label='Password to set when the allowed uses are exhausted (6 digits)',
# regexp='^(\d{6}|)$'),
ValueBool('rotating_password', label=u'Automatically renew password every 100 connections', default=False),
Value('website', label='Type de compte', default='pp',
choices={'pp': 'Particuliers/Professionnels',
'hbank': 'HelloBank',
......@@ -73,10 +71,7 @@ class BNPorcModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapMessag
def create_default_browser(self):
b = {'ent': BNPEnterprise, 'ent2': BNPCompany, 'pp': BNPPartPro, 'hbank': HelloBank}
self.BROWSER = b[self.config['website'].get()]
if self.BROWSER is BNPPartPro:
return self.create_browser(self.config)
return self.create_browser(self.config['login'].get(),
self.config['password'].get())
return self.create_browser(self.config)
def iter_accounts(self):
for account in self.browser.get_accounts_list():
......
......@@ -114,10 +114,11 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser):
profile = URL(r'/kyc-wspl/rest/informationsClient', ProfilePage)
list_detail_card = URL(r'/udcarte-wspl/rest/listeDetailCartes', ListDetailCardPage)
def __init__(self, *args, **kwargs):
super(BNPParibasBrowser, self).__init__(*args, **kwargs)
def __init__(self, config, *args, **kwargs):
super(BNPParibasBrowser, self).__init__(config['login'].get(), config['password'].get(), *args, **kwargs)
self.accounts_list = None
self.card_to_transaction_type = {}
self.rotating_password = config['rotating_password']
@retry(ConnectionError, tries=3)
def open(self, *args, **kwargs):
......@@ -406,9 +407,7 @@ class BNPPartPro(BNPParibasBrowser):
def __init__(self, config=None, *args, **kwargs):
self.config = config
kwargs['username'] = self.config['login'].get()
kwargs['password'] = self.config['password'].get()
super(BNPPartPro, self).__init__(*args, **kwargs)
super(BNPPartPro, self).__init__(self.config, *args, **kwargs)
def switch(self, subdomain):
self.BASEURL = self.BASEURL_TEMPLATE % subdomain
......
......@@ -102,6 +102,9 @@ class ConnectionThresholdPage(HTMLPage):
msg = CleanText('//div[@class="confirmation"]//span[span]')(self.doc)
self.logger.warning('Password expired.')
if not self.browser.rotating_password:
raise BrowserPasswordExpired(msg)
if not self.looks_legit(self.browser.password):
# we may not be able to restore the password, so reject it
self.logger.warning('Unable to restore it, it is not legit.')
......@@ -421,7 +424,7 @@ class ValidateTransferPage(BNPPage):
if 'idBeneficiaire' in transfer_data and transfer_data['idBeneficiaire'] is not None:
assert transfer_data['idBeneficiaire'] == recipient.id
elif 'ibanCompteCrediteur' in transfer_data and transfer_data['ibanCompteCrediteur'] is not None:
elif transfer_data.get('ibanCompteCrediteur'):
assert transfer_data['ibanCompteCrediteur'] == recipient.iban
transfer = Transfer()
......@@ -439,7 +442,7 @@ class ValidateTransferPage(BNPPage):
else:
transfer.recipient_id = recipient.id
transfer.exec_date = parse_french_date(transfer_data['dateExecution']).date()
transfer.fees = Decimal(transfer_data['montantFrais'])
transfer.fees = Decimal(transfer_data.get('montantFrais', '0'))
transfer.label = transfer_data['motifVirement']
transfer.account_label = account.label
......
......@@ -21,8 +21,9 @@ from __future__ import unicode_literals
from datetime import timedelta, datetime
from weboob.browser import LoginBrowser, need_login, URL
from weboob.browser.browsers import LoginBrowser, need_login, URL
from weboob.capabilities.bill import Document
from .compat.weboob_tools_capabilities_bank_investments import create_french_liquidity
from .pages import (
LoginPage, HomeLendPage, PortfolioPage, OperationsPage, MAIN_ID, ProfilePage,
......@@ -51,13 +52,16 @@ class BoldenBrowser(LoginBrowser):
self.portfolio.go()
return self.page.iter_accounts()
def iter_investments(self):
self.portfolio.go()
yield create_french_liquidity(self.page.get_liquidity())
for inv in self.page.iter_investments():
yield inv
@need_login
def iter_history(self, account):
if account.id != MAIN_ID:
return []
return self._iter_all_history()
def _iter_all_history(self):
return
end = datetime.now()
while True:
start = end - timedelta(days=365)
......@@ -86,14 +90,13 @@ class BoldenBrowser(LoginBrowser):
@need_login
def iter_documents(self):
for acc in self.iter_accounts():
if acc.id == MAIN_ID:
for inv in self.iter_investments():
if inv.label == "Liquidités":
continue
doc = Document()
doc.id = acc.id
doc.url = acc._docurl
doc.label = 'Contrat %s' % acc.label
doc.id = inv.id
doc.url = inv._docurl
doc.label = 'Contrat %s' % inv.label
doc.type = 'other'
doc.format = 'pdf'
yield doc
......
import weboob.capabilities.bank as OLD
# can't import *, __all__ is incomplete...
for attr in dir(OLD):
globals()[attr] = getattr(OLD, attr)
__all__ = OLD.__all__
class CapBankWealth(CapBank):
pass
class CapBankPockets(CapBank):
pass
class Rate(BaseObject, Currency):
pass
class CapCurrencyRate(CapBank):
pass
class CapBankTransfer(OLD.CapBankTransfer):
def transfer_check_label(self, old, new):
from unidecode import unidecode
return unidecode(old) == unidecode(new)
class CapBankTransferAddRecipient(CapBankTransfer, OLD.CapBankTransferAddRecipient):
pass
class AddRecipientBankError(AddRecipientError):
code = 'bankMessage'
Account.TYPE_MORTGAGE = 17
Account.TYPE_CONSUMER_CREDIT = 18
Account.TYPE_REVOLVING_CREDIT = 19
# -*- coding: utf-8 -*-
# Copyright(C) 2017 Jonathan Schmidt
#
# 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 <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import re
from weboob.tools.compat import basestring
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.bank import Investment
def is_isin_valid(isin):
"""
Méthode générale
Table de conversion des lettres en chiffres
A=10 B=11 C=12 D=13 E=14 F=15 G=16 H=17 I=18
J=19 K=20 L=21 M=22 N=23 O=24 P=25 Q=26 R=27
S=28 T=29 U=30 V=31 W=32 X=33 Y=34 Z=35
1 - Mettre de côté la clé, qui servira de référence à la fin de la vérification.
2 - Convertir toutes les lettres en nombres via la table de conversion ci-contre. Si le nombre obtenu est supérieur ou égal à 10, prendre les deux chiffres du nombre séparément (exemple : 27 devient 2 et 7).
3 - Pour chaque chiffre, multiplier sa valeur par deux si sa position est impaire en partant de la droite. Si le nombre obtenu est supérieur ou égal à 10, garder les deux chiffres du nombre séparément (exemple : 14 devient 1 et 4).
4 - Faire la somme de tous les chiffres.
5 - Soustraire cette somme de la dizaine supérieure ou égale la plus proche (exemples : si la somme vaut 22, la dizaine « supérieure ou égale » est 30, et la clé vaut donc 8 ; si la somme vaut 30, la dizaine « supérieure ou égale » est 30, et la clé vaut 0 ; si la somme vaut 31, la dizaine « supérieure ou égale » est 40, et la clé vaut 9).
6 - Comparer la valeur obtenue à la clé mise initialement de côté.
Étapes 1 et 2 :
F R 0 0 0 3 5 0 0 0 0 (+ 8 : clé)
15 27 0 0 0 3 5 0 0 0 0
Étape 3 : le traitement se fait sur des chiffres
1 5 2 7 0 0 0 3 5 0 0 0 0
I P I P I P I P I P I P I : position en partant de la droite (P = Pair, I = Impair)
2 1 2 1 2 1 2 1 2 1 2 1 2 : coefficient multiplicateur
2 5 4 7 0 0 0 3 10 0 0 0 0 : résultat
Étape 4 :
2 + 5 + 4 + 7 + 0 + 0 + 0 + 3 + (1 + 0)+ 0 + 0 + 0 + 0 = 22
Étapes 5 et 6 : 30 - 22 = 8 (valeur de la clé)
"""
if not isinstance(isin, basestring):
return False
if not re.match(r'^[A-Z]{2}[A-Z0-9]{9}\d$', isin):
return False
isin_in_digits = ''.join(str(ord(x) - ord('A') + 10) if not x.isdigit() else x for x in isin[:-1])
key = isin[-1:]
result = ''
for k, val in enumerate(isin_in_digits[::-1], start=1):
if k % 2 == 0:
result = ''.join((result, val))
else:
result = ''.join((result, str(int(val)*2)))
return str(sum(int(x) for x in result) + int(key))[-1] == '0'
def create_french_liquidity(valuation):
"""
Automatically fills a liquidity investment with label, code and code_type.
"""
liquidity = Investment()
liquidity.label = "Liquidités"
liquidity.code = "XX-liquidity"
liquidity.code_type = NotAvailable
liquidity.valuation = valuation
return liquidity
......@@ -22,7 +22,7 @@ from __future__ import unicode_literals
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword
from weboob.capabilities.bank import CapBank, Account
from .compat.weboob_capabilities_bank import CapBankWealth, Account
from weboob.capabilities.base import find_object
from weboob.capabilities.bill import (
CapDocument, Subscription, SubscriptionNotFound, DocumentNotFound, Document,
......@@ -35,7 +35,7 @@ from .browser import BoldenBrowser
__all__ = ['BoldenModule']
class BoldenModule(Module, CapBank, CapDocument, CapProfile):
class BoldenModule(Module, CapBankWealth, CapDocument, CapProfile):
NAME = 'bolden'
DESCRIPTION = 'Bolden'
MAINTAINER = 'Vincent A'
......@@ -59,6 +59,9 @@ class BoldenModule(Module, CapBank, CapDocument, CapProfile):
def iter_history(self, account):
return self.browser.iter_history(account)
def iter_investment(self, account):
return self.browser.iter_investments()
def get_profile(self):
return self.browser.get_profile()
......
......@@ -19,15 +19,14 @@
from __future__ import unicode_literals
from decimal import Decimal
from weboob.browser.elements import ListElement, ItemElement, method, TableElement
from weboob.browser.filters.html import TableCell, Link, Attr
from weboob.browser.filters.standard import (
CleanText, CleanDecimal, Slugify, Date, Field, Format,
)
from weboob.browser.pages import HTMLPage, LoggedPage
from weboob.capabilities.bank import Account, Transaction
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.bank import Account, Transaction, Investment
from weboob.capabilities.profile import Profile
from weboob.exceptions import BrowserIncorrectPassword
from weboob.tools.compat import urljoin
......@@ -55,37 +54,46 @@ class HomeLendPage(LoggedPage, HTMLPage):
class PortfolioPage(LoggedPage, HTMLPage):
@method
class iter_accounts(ListElement):
class get_main(ItemElement):
class item(ItemElement):
klass = Account
obj_id = MAIN_ID
obj_label = 'Compte Bolden'
obj_type = Account.TYPE_CHECKING
obj_type = Account.TYPE_MARKET
obj_currency = 'EUR'
obj_balance = CleanDecimal('//div[p[has-class("investor-state") and contains(text(),"Fonds disponibles :")]]/p[has-class("investor-status")]', replace_dots=True)
#obj_coming = CleanDecimal('//div[p[has-class("investor-state") and contains(text(),"Capital restant dû :")]]/p[has-class("investor-status")]', replace_dots=True)
obj_balance = CleanDecimal('//div[p[has-class("investor-state") and contains(text(),"Total compte Bolden :")]]/p[has-class("investor-status")]', replace_dots=True)
obj_valuation_diff = CleanDecimal('//p[has-class("rent-amount strong dashboard-text")]', replace_dots=True)
@method
class iter_investments(TableElement):
head_xpath = '//div[@class="tab-wallet"]/table/thead//td'
class iter_lends(TableElement):
head_xpath = '//div[@class="tab-wallet"]/table/thead//td'
col_label = 'Emprunteur'
col_valuation = 'Capital restant dû'
col_doc = 'Contrat'
col_diff = 'Intérêts perçus'
col_label = 'Emprunteur'
col_coming = 'Capital restant dû'
col_doc = 'Contrat'
item_xpath = '//div[@class="tab-wallet"]/table/tbody/tr'
item_xpath = '//div[@class="tab-wallet"]/table/tbody/tr'
class item(ItemElement):
klass = Investment
class item(ItemElement):
klass = Account
obj_label = CleanText(TableCell('label'))
obj_id = Slugify(Field('label'))
obj_valuation = CleanDecimal(TableCell('valuation'), replace_dots=True)
obj_diff = CleanDecimal(TableCell('diff'), replace_dots=True)
obj_code = NotAvailable
obj_code_type = NotAvailable
def condition(self):
# Investments without valuation are expired.
return CleanDecimal(TableCell('valuation'))(self)
obj_label = CleanText(TableCell('label'))
obj_id = Slugify(Field('label'))
obj_type = Account.TYPE_SAVINGS
obj_currency = 'EUR'
obj_coming = CleanDecimal(TableCell('coming'), replace_dots=True)
obj_balance = Decimal('0')
def obj__docurl(self):
return urljoin(self.page.url, Link('.//a')(TableCell('doc')(self)[0]))
def obj__docurl(self):
return urljoin(self.page.url, Link('.//a')(TableCell('doc')(self)[0]))
def get_liquidity(self):
return CleanDecimal('//div[p[contains(text(), "Fonds disponibles")]]/p[@class="investor-status strong"]', replace_dots=True)(self.doc)
class OperationsPage(LoggedPage, HTMLPage):
......
......@@ -26,7 +26,7 @@ from dateutil import parser
from .compat.weboob_browser_retry import login_method, retry_on_logout, RetryLoginBrowser
from weboob.browser.browsers import need_login, StatesMixin
from weboob.browser.url import URL
from weboob.exceptions import BrowserIncorrectPassword, BrowserHTTPNotFound
from weboob.exceptions import BrowserIncorrectPassword, BrowserHTTPNotFound, NoAccountsException, BrowserUnavailable
from .compat.weboob_browser_exceptions import LoggedOut, ClientError
from .compat.weboob_capabilities_bank import (
Account, AccountNotFound, TransferError, TransferInvalidAmount,
......@@ -45,6 +45,7 @@ from .pages import (
CardsNumberPage, CalendarPage, HomePage, PEPPage,
TransferAccounts, TransferRecipients, TransferCharac, TransferConfirm, TransferSent,
AddRecipientPage, StatusPage, CardHistoryPage, CardCalendarPage, CurrencyListPage, CurrencyConvertPage,
AccountsErrorPage, NoAccountPage,
)
......@@ -68,8 +69,13 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
error = URL('/connexion/compte-verrouille',
'/infos-profil', ErrorPage)
login = URL('/connexion/', LoginPage)
accounts = URL('/dashboard/comptes\?_hinclude=300000', AccountsPage)
accounts_error = URL('/dashboard/comptes\?_hinclude=300000', AccountsErrorPage)
pro_accounts = URL(r'/dashboard/comptes-professionnels\?_hinclude=1', AccountsPage)
no_account = URL('/dashboard/comptes\?_hinclude=300000',
'/dashboard/comptes-professionnels\?_hinclude=1', NoAccountPage)
acc_tit = URL('/comptes/titulaire/(?P<webid>.*)\?_hinclude=1', AccbisPage)
acc_rep = URL('/comptes/representative/(?P<webid>.*)\?_hinclude=1', AccbisPage)
acc_pro = URL('/comptes/professionnel/(?P<webid>.*)\?_hinclude=1', AccbisPage)
......@@ -189,12 +195,46 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
@need_login
def get_accounts_list(self):
self.status.go()
exc = None
for x in range(3):
if self.accounts_list is not None:
break
self.accounts_list = []
self.accounts_list.extend(self.pro_accounts.go().iter_accounts())
self.accounts_list.extend(self.accounts.go().iter_accounts())
self.loans_list = []
# Check that there is at least one account for this user
has_account = False
self.pro_accounts.go()
if self.pro_accounts.is_here():
self.accounts_list.extend(self.page.iter_accounts())
has_account = True
else:
# We dont want to let has_account=False if we landed on an unknown page
# it has to be the no_accounts page
assert self.no_account.is_here()
try:
self.accounts.go()
except BrowserUnavailable as e:
self.logger.warning('par accounts seem unavailable, retrying')
exc = e
self.accounts_list = None
continue
else:
if self.accounts.is_here():
self.accounts_list.extend(self.page.iter_accounts())
has_account = True
else:
# We dont want to let has_account=False if we landed on an unknown page
# it has to be the no_accounts page
assert self.no_account.is_here()
exc = None
if not has_account:
# if we landed twice on NoAccountPage, it means there is neither pro accounts nor pp accounts
raise NoAccountsException()
# discard all unvalid card accounts (if opposed or not yet activated)
valid_card_url = []
......@@ -210,11 +250,19 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
# there is 1 page for all accounts (one for tit and one for pro)
break
for account in list(self.accounts_list):
if account.type == Account.TYPE_CARD and account.url not in valid_card_url:
self.accounts_list.remove(account)
elif account.type == Account.TYPE_LOAN:
# Loans details are present on another page so we create
# a Loan object and remove the corresponding Account:
self.location(account.url)
loan = self.page.get_loan()
loan.url = account.url
self.loans_list.append(loan)
self.accounts_list.remove(account)
self.accounts_list.extend(self.loans_list)
cards = [acc for acc in self.accounts_list if acc.type == Account.TYPE_CARD]
if cards:
self.go_cards_number(cards[0].url)
......@@ -222,13 +270,16 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
self.page.populate_cards_number(cards)
for account in self.accounts_list:
if account.type not in (Account.TYPE_CARD, Account.TYPE_LOAN, Account.TYPE_LIFE_INSURANCE):
if account.type not in (Account.TYPE_CARD, Account.TYPE_LOAN, Account.TYPE_CONSUMER_CREDIT, Account.TYPE_MORTGAGE, Account.TYPE_REVOLVING_CREDIT, Account.TYPE_LIFE_INSURANCE):
account.iban = self.iban.go(webid=account._webid).get_iban()
for card in cards:
checking, = [account for account in self.accounts_list if account.type == Account.TYPE_CHECKING and account.url in card.url]
card.parent = checking
if exc:
raise exc
return self.accounts_list
def get_account(self, id):
......@@ -342,6 +393,9 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
@need_login
def iter_transfer_recipients(self, account):
if account.type in (Account.TYPE_LOAN, Account.TYPE_LIFE_INSURANCE):
return []
assert account.url
url = urlsplit(account.url)
......@@ -399,6 +453,10 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
self.page.submit_info(transfer.amount, transfer.label, transfer.exec_date)
assert self.transfer_confirm.is_here()
if self.page.need_refresh():
# In some case we are not yet in the transfer_charac page, you need to refresh the page
self.location(self.url)
assert not self.page.need_refresh()
ret = self.page.get_transfer()
# at this stage, the site doesn't show the real ids/ibans, we can only guess
......
......@@ -31,13 +31,13 @@ from weboob.browser.elements import ListElement, ItemElement, method, TableEleme
from weboob.browser.filters.standard import (
CleanText, CleanDecimal, Field, Format,
Regexp, Date, AsyncLoad, Async, Eval, RegexpError, Env,
Currency as CleanCurrency,
Currency as CleanCurrency, Map,
)
from weboob.browser.filters.json import Dict
from weboob.browser.filters.html import Attr, Link, TableCell, AbsoluteLink
from .compat.weboob_capabilities_bank import (
Account, Investment, Recipient, Transfer, AccountNotFound,
AddRecipientBankError, TransferInvalidAmount,
AddRecipientBankError, TransferInvalidAmount, Loan,
)
from .compat.weboob_tools_capabilities_bank_investments import create_french_liquidity
from weboob.capabilities.base import NotAvailable, empty, Currency
......@@ -207,6 +207,7 @@ class StatusPage(LoggedPage, PartialHTMLPage):
class AccountsPage(LoggedPage, HTMLPage):
def is_here(self):
# This id appears when there are no accounts (pro and pp)
return not self.doc.xpath('//div[contains(@id, "alert-random")]')
ACCOUNT_TYPES = {u'comptes courants': Account.TYPE_CHECKING,
......@@ -240,7 +241,8 @@ class AccountsPage(LoggedPage, HTMLPage):
load_details = Field('url') & AsyncLoad
def condition(self):
return not self.is_external() and not 'automobile' in Field('url')(self)
# Ignore externally aggregated accounts and insurances:
return not self.is_external() and not any(x in Field('url')(self) for x in ('automobile', 'assurance/protection', 'assurance/comptes'))
obj_label = CleanText('.//a[has-class("account--name")] | .//div[has-class("account--name")]')
......@@ -325,12 +327,51 @@ class AccountsPage(LoggedPage, HTMLPage):
return not Async('details', CleanText(u'//h4[contains(text(), "Établissement bancaire")]'))(self) and not \
Async('details', CleanText(u'//h4/div[contains(text(), "Établissement bancaire")]'))(self)
def iter_card_ids(self):
for tr in self.doc.xpath('//table[@class="table table--accounts"]/tr[has-class("table__line--account") and count(descendant::td) > 1 and @data-line-account-href]'):
url = Attr('.//a[@class="account--name"] | .//a[2]', 'href', default='')(tr)
m = re.search(r'/([a-z0-9]+)/carte/([a-z0-9]+)', url)
if m:
yield m.groups()
class LoanPage(LoggedPage, HTMLPage):
LOAN_TYPES = {
"PRÊT PERSONNEL": Account.TYPE_CONSUMER_CREDIT,
}
@method
class get_loan(ItemElement):
klass = Loan
obj_id = CleanText('//h3[contains(@class, "account-number")]/strong')
obj_label = CleanText('//h2[contains(@class, "page-title__account")]//div[@class="account-edit-label"]/span')
obj_total_amount = CleanDecimal('//p[contains(text(), "Montant emprunt")]/span', replace_dots=True)
obj_currency = CleanCurrency('//p[contains(text(), "Montant emprunt")]/span')
obj_duration = CleanDecimal('//p[contains(text(), "Nombre prévisionnel d\'échéances restantes")]/span')
obj_rate = CleanDecimal('//p[contains(text(), "Taux nominal en vigueur du prêt")]/span')
obj_nb_payments_left = CleanDecimal('//p[contains(text(), "Nombre prévisionnel d\'échéances restantes")]/span')
obj_next_payment_amount = CleanDecimal('//p[contains(text(), "Montant de la prochaine échéance")]/span', replace_dots=True)
obj_nb_payments_total = CleanDecimal('//p[contains(text(), "Nombre d\'écheances totales") or contains(text(), "Nombre total d\'échéances")]/span')
obj_subscription_date = Date(CleanText('//p[contains(text(), "Date de départ du prêt")]/span'), parse_func=parse_french_date)
obj_maturity_date = Date(CleanText('//p[contains(text(), "Date prévisionnelle d\'échéance finale")]/span'), parse_func=parse_french_date)
def obj_balance(self):
balance = CleanDecimal('//p[contains(text(), "Capital restant dû")]/span', replace_dots=True)(self)
if balance > 0:
balance *= -1
return balance
def obj_type(self):
_type = CleanText('//h2[contains(@class, "page-title__account")]//div[@class="account-edit-label"]/span')
return Map(_type, self.page.LOAN_TYPES, default=Account.TYPE_LOAN)(self)
def obj_next_payment_date(self):
tmp = CleanText('//p[contains(text(), "Date de la prochaine échéance")]/span')(self)
if tmp == "-":
return NotAvailable
return Date(CleanText('//p[contains(text(), "Date de la prochaine échéance")]/span'), parse_func=parse_french_date)(self)
class NoAccountPage(LoggedPage, HTMLPage):
def is_here(self):
err = CleanText('//div[contains(@id, "alert-random")]/text()', children=False)(self.doc)
return "compte inconnu" in err.lower()
class CardCalendarPage(LoggedPage, RawPage):
......@@ -577,7 +618,7 @@ class MarketPage(LoggedPage, HTMLPage):
return CleanDecimal(replace_dots=True, default=NotAvailable).filter((TableCell('unitvalue')(self)[0]).xpath('./span[not(@class)]'))
def iter_investment(self):
valuation = CleanDecimal('//li[h4[contains(text(), "Solde Espèces")]]/h3', replace_dots=True, default=None)(self.doc)
valuation = CleanDecimal('//li/span[contains(text(), "Solde Espèces")]/following-sibling::span', replace_dots=True, default=None)(self.doc)
if valuation:
yield create_french_liquidity(valuation)
......@@ -762,10 +803,6 @@ class AccbisPage(LoggedPage, HTMLPage):
accounts.extend(cards)
class LoanPage(LoggedPage, HTMLPage):
pass
class ErrorPage(HTMLPage):
def on_load(self):
error = (Attr('//input[@required][@id="profile_lei_type_identifier"]', 'data-message', default=None)(self.doc) or
......@@ -810,35 +847,13 @@ class ProfilePage(LoggedPage, HTMLPage):
class CardsNumberPage(LoggedPage, HTMLPage):
def populate_cards_number(self, cards):
"""
Checking account ID used to match credit cards.
Label useful when several cards on the same checking account.
The card_details list contains tuples including the card number,
the hash of the card's parent account, and the name of the card.
"""
card_details = [
(CleanText('.//span')(o), CleanText('./@data-account-key')(o), CleanText('.//p')(o))
for o in self.doc.xpath('//div[contains(@class, "zoom-carousel__item text-center credit-card-carousel__item")]')
]
# Remove cards whose number ends with **** (means it is not activated yet)
card_details = [
(number, account_hash, name) for (number, account_hash, name) in card_details
if not number.endswith('****')
]
for card in cards:
match = [(number, account_hash, name) for (number, account_hash, name) in card_details if account_hash in card.url]
if len(match) > 1:
# Several cards matched the same bank account, so we now try
# to match them using the name of the card holder:
card_username = card.label.split('-')[1].lstrip()
match = [(number, account_hash, name) for (number, account_hash, name) in match if name.startswith(card_username)]
assert len(match) <= 1, "only one card should be matched, or zero if the card is not yet activated"
if len(match) == 1 :
card.number = match[0][0]
# The second hash of the card's url is used to get
# the card's hash on the HTML page:
card_url_hash = re.search('carte\/(.*)', card.url).group(1)
card_hash = CleanText('//nav[ul[li[a[contains(@href, "%s")]]]]/@data-card-key' % card_url_hash)(self.doc)
# With the card hash we can get the card number:
card.number = CleanText('//div[@data-card-key="%s"]/div/span' % card_hash)(self.doc)
class HomePage(LoggedPage, HTMLPage):
......@@ -938,8 +953,9 @@ class TransferCharac(LoggedPage, HTMLPage):
else:
assert self.get_option(form.el.xpath('//select[@id="Characteristics_schedulingType"]')[0], 'Différé') == '2'
form['Characteristics[schedulingType]'] = '2'
form['Characteristics[scheduledDate][day]'] = exec_date.strftime('%d')
form['Characteristics[scheduledDate][month]'] = exec_date.strftime('%m')
# If we let the 0 in the front of the month or the day like 02, the website will not interpret the good date
form['Characteristics[scheduledDate][day]'] = exec_date.strftime('%d').lstrip("0")
form['Characteristics[scheduledDate][month]'] = exec_date.strftime('%m').lstrip("0")
form['Characteristics[scheduledDate][year]'] = exec_date.strftime('%Y')
form['Characteristics[notice]'] = 'none'
......@@ -952,6 +968,9 @@ class TransferConfirm(LoggedPage, HTMLPage):
if errors: