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

backport fixes from unstable

......@@ -278,7 +278,7 @@ class UnavailablePage(LoggedPage, MyHTMLPage):
if "est indisponible" in h1:
raise BrowserUnavailable(h1)
body = CleanText(".")(self.doc)
if "An unexpected error has occurred." in body:
if "An unexpected error has occurred." in body or "Une erreur s'est produite" in body:
raise BrowserUnavailable(body)
a = Link('//a[@class="btn"][1]', default=None)(self.doc)
......
......@@ -394,12 +394,13 @@ class CardPage(AbstractAccountPage):
return False
def do_account_attachment(self, accounts):
caccount_aid = Regexp(CleanText('//span[@id="C4__QUE_B160DC66D26AA39615599"]'), r'-(.*?)-')(self.doc)
for account in accounts:
if account.id == re.sub(r'\s', '', caccount_aid):
return account
caccount_aid = CleanText('//span[@id="C4__QUE_B160DC66D26AA39615599"]')(self.doc)
m = re.search('-(.*?)-', caccount_aid)
if m:
regex = m.group(1)
for account in accounts:
if account.id == re.sub(r'\s', '', regex):
return account
return NotAvailable
def has_history(self):
......
......@@ -118,7 +118,7 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser):
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']
self.rotating_password = config['rotating_password'].get()
@retry(ConnectionError, tries=3)
def open(self, *args, **kwargs):
......@@ -372,10 +372,8 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser):
@need_login
def send_code(self, recipient, **params):
# depending on whether recipient is a weboob or a budgea backend object.
_id = recipient.webid if hasattr(recipient, 'webid') else recipient.id
data = {}
data['idBeneficiaire'] = _id
data['idBeneficiaire'] = recipient.id
data['typeActivation'] = 1
data['codeActivation'] = params['code']
return self.activate_recip.go(data=json.dumps(data), headers={'Content-Type': 'application/json'}).get_recipient(recipient)
......
......@@ -205,15 +205,16 @@ class LoginPage(JsonPage):
if not msg:
msg = self.get('message')
wrongpass_codes = [201, 21510, 203, 202, 1001, 7]
wrongpass_codes = [201, 21510, 203, 202, 7]
actionNeeded_codes = [21501, 3, 4, 50]
websiteUnavailable_codes = [207, 1001]
if error in wrongpass_codes:
raise BrowserIncorrectPassword(msg)
elif error == 21: # "Ce service est momentanément indisponible. Veuillez renouveler votre demande ultérieurement." -> In reality, account is blocked because of too much wrongpass
raise ActionNeeded(u"Compte bloqué")
elif error in actionNeeded_codes:
raise ActionNeeded(msg)
elif error == 207:
elif error in websiteUnavailable_codes:
raise BrowserUnavailable(msg)
else:
assert False, 'Unexpected error at login: "%s" (code=%s)' % (msg, error)
......@@ -461,7 +462,7 @@ class RegisterTransferPage(ValidateTransferPage):
transfer_data = self.doc['data']['enregistrementVirement']
transfer.id = transfer_data['reference']
assert transfer.exec_date == parse_french_date(self.doc['data']['enregistrementVirement']['dateExecution']).date()
transfer.exec_date = parse_french_date(self.doc['data']['enregistrementVirement']['dateExecution']).date()
# Timestamp at which the bank registered the transfer
register_date = re.sub(' 24:', ' 00:', self.doc['data']['enregistrementVirement']['dateEnregistrement'])
transfer._register_date = parse_french_date(register_date)
......@@ -828,7 +829,7 @@ class ActivateRecipPage(AddRecipPage):
def get_recipient(self, recipient):
r = Recipient()
r.iban = recipient.iban
r.id = recipient.webid if hasattr(recipient, 'webid') else recipient.id
r.id = recipient.id
r.label = recipient.label
r.category = u'Externe'
r.enabled_at = datetime.now().replace(microsecond=0) + timedelta(days=5)
......
......@@ -89,6 +89,7 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
incident = URL('/compte/cav/(?P<webid>.*)/mes-incidents.*', IncidentPage)
transfer_accounts = URL(r'/compte/(?P<type>[^/]+)/(?P<webid>\w+)/virements/nouveau/(?P<id>\w+)/1',
r'/compte/(?P<type>[^/]+)/(?P<webid>\w+)/virements/nouveau$',
TransferAccounts)
recipients_page = URL(r'/compte/(?P<type>[^/]+)/(?P<webid>\w+)/virements/$',
r'/compte/(?P<type>[^/]+)/(?P<webid>\w+)/virements/nouveau/(?P<id>\w+)/2',
......@@ -357,7 +358,7 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
@retry_on_logout()
@need_login
def get_history(self, account, coming=False):
if account.type is Account.TYPE_LOAN or '/compte/derive' in account.url:
if account.type in (Account.TYPE_LOAN, Account.TYPE_CONSUMER_CREDIT) or '/compte/derive' in account.url:
return []
if account.type is Account.TYPE_SAVINGS and u"PLAN D'\xc9PARGNE POPULAIRE" in account.label:
return []
......
......@@ -242,7 +242,7 @@ class AccountsPage(LoggedPage, HTMLPage):
def condition(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'))
return not self.is_external() and not any(x in Field('url')(self) for x in ('automobile', 'assurance/protection', 'assurance/comptes', 'assurance/famille'))
obj_label = CleanText('.//a[has-class("account--name")] | .//div[has-class("account--name")]')
......@@ -343,13 +343,13 @@ class LoanPage(LoggedPage, HTMLPage):
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_duration = CleanDecimal('//p[contains(text(), "Nombre prévisionnel d\'échéances restantes")]/span', default=NotAvailable)
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_left = CleanDecimal('//p[contains(text(), "Nombre prévisionnel d\'échéances restantes")]/span', default=NotAvailable)
obj_next_payment_amount = CleanDecimal('//p[contains(text(), "Montant de la prochaine échéance")]/span', replace_dots=True, default=NotAvailable)
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)
obj_maturity_date = Date(CleanText('//p[contains(text(), "Date prévisionnelle d\'échéance finale")]/span'), parse_func=parse_french_date, default=NotAvailable)
def obj_balance(self):
balance = CleanDecimal('//p[contains(text(), "Capital restant dû")]/span', replace_dots=True)(self)
......
......@@ -398,7 +398,9 @@ class BPBrowser(LoginBrowser, StatesMixin):
if account.type in (account.TYPE_PEA, account.TYPE_MARKET):
self.go_linebourse(account)
return self.linebourse.iter_investment(account.id)
investments = list(self.linebourse.iter_investment(account.id))
investments.append(self.linebourse.get_liquidity(account.id))
return investments
if account.type != Account.TYPE_LIFE_INSURANCE:
return iter([])
......
......@@ -246,6 +246,11 @@ class CaisseEpargne(LoginBrowser, StatesMixin):
self.multi_type = True
if self.inexttype < len(data['account']):
if data['account'][self.inexttype] == 'EU' and not self.nuser:
# when EU is present and not alone, it tends to come first
# if nuser is unset though, user probably doesn't want 'EU'
self.inexttype += 1
self.typeAccount = data['account'][self.inexttype]
else:
assert False, 'should have logged in with at least one connection type'
......
......@@ -996,7 +996,7 @@ class TransferPage(TransferErrorPage, IndexPage):
def get_recipient_value(self, recipient):
if recipient.category == u'Externe':
recipient_value = [Attr('.', 'value')(o) for o in self.doc.xpath(self.RECIPIENT_XPATH) if
Regexp(CleanText('.'), ' - (.*) -', default=NotAvailable)(o) == recipient.iban]
Regexp(CleanText('.'), '.* - ([A-Za-z0-9]*) -', default=NotAvailable)(o) == recipient.iban]
elif recipient.category == u'Interne':
recipient_value = [Attr('.', 'value')(o) for o in self.doc.xpath(self.RECIPIENT_XPATH) if
Regexp(CleanText('.'), '- (\d+)', default=NotAvailable)(o) and Regexp(CleanText('.'), '- (\d+)', default=NotAvailable)(o) in recipient.id]
......
......@@ -33,7 +33,7 @@ from weboob.tools.date import LinearDateGuesser
from .pages import (
LoginPage, PasswordCreationPage, AccountsPage, HistoryPage, ChoiceLinkPage, SubscriptionPage, InvestmentPage,
InvestmentAccountPage, UselessPage, TokenPage, SSODomiPage, AuthCheckUser, SecurityCheckUser,
InvestmentAccountPage, UselessPage, TokenPage, SSODomiPage, AuthCheckUser,
)
from ..par.pages import ProfilePage
......@@ -57,7 +57,6 @@ class CmsoProBrowser(LoginBrowser):
tokens = URL('/domiweb/prive/espacesegment/selectionnerAbonnement/3-selectionnerAbonnement.act', TokenPage)
ssoDomiweb = URL('https://pro.(?P<website>[\w.]+)/domiapi/oauth/json/ssoDomiwebEmbedded', SSODomiPage)
auth_checkuser = URL('https://pro.(?P<website>[\w.]+)/auth/checkuser', AuthCheckUser)
security_checkuser = URL('https://pro.(?P<website>[\w.]+)/securityapi/checkuser', SecurityCheckUser)
def __init__(self, website, *args, **kwargs):
super(CmsoProBrowser, self).__init__(*args, **kwargs)
......@@ -148,17 +147,6 @@ class CmsoProBrowser(LoginBrowser):
self.location(area)
self.location('/domiweb/accueil.jsp')
self.auth_checkuser.go(website=self.website)
self.security_checkuser.go(
website=self.website,
json={'appOrigin': 'domiweb', 'espaceApplication': 'PRO'},
headers={'Authentication': 'Bearer %s' % self.token,
'Authorization': 'Bearer %s' % self.csrf,
'X-Csrf-Token': self.csrf,
'Accept': 'application/json',
'X-REFERER-TOKEN': 'RWDPRO',
'X-ARKEA-EFS': self.arkea,
'ADRIM': 'isAjax:true',
})
@need_login
def iter_accounts(self):
......
......@@ -292,6 +292,3 @@ class TokenPage(CMSOPage, UpdateTokenMixin):
class AuthCheckUser(HTMLPage):
pass
class SecurityCheckUser(JsonPage):
pass
......
......@@ -206,6 +206,7 @@ class AccountsPage(MyLoggedPage, BasePage):
u'EKO' : Account.TYPE_CHECKING,
u'DAV NANTI': Account.TYPE_SAVINGS,
u'LIV A': Account.TYPE_SAVINGS,
u'LIV A ASS': Account.TYPE_SAVINGS,
u'LDD': Account.TYPE_SAVINGS,
u'PEL': Account.TYPE_SAVINGS,
u'CEL': Account.TYPE_SAVINGS,
......@@ -989,6 +990,11 @@ class MarketHomePage(MarketPage):
COL_ID_LABEL = 1
COL_VALUATION = 5
def on_load(self):
action_needed_msg = CleanText('//div[contains(text(), "Afin de finaliser le paramétrage de votre environnement Bourse")]', replace=[('Enregistrer', '')])(self.doc)
if action_needed_msg:
raise ActionNeeded(action_needed_msg)
@method
class get_list(TableElement):
item_xpath = '//table[has-class("tableau_comptes_details")]//tr[td[2]]'
......
......@@ -613,8 +613,11 @@ class CardsListPage(LoggedPage, HTMLPage):
page = page.browser.open(Link('//form//a[text()="Contrat"]', default=None)(page.doc)).page
xpath = '//table[has-class("liste")]/tbody/tr'
active_card = CleanText('%s[td[text()="Active"]][1]/td[2]' % xpath, replace=[(' ', '')], default=None)(page.doc)
for cards in page.doc.xpath(xpath):
if CleanText(cards.xpath('./td[1]'))(self) != 'Active':
self.page.browser.unavailablecards.append(CleanText(cards.xpath('./td[2]'), replace=[(' ', '')])(self))
if not active_card or len(page.doc.xpath(xpath)) != 1:
if not active_card and len(page.doc.xpath(xpath)) != 1:
raise SkipItem()
self.env['id'] = active_card or CleanText('%s[1]/td[2]' % xpath, replace=[(' ', '')])(page.doc)
......@@ -1835,9 +1838,14 @@ class SubscriptionPage(LoggedPage, HTMLPage):
class NewCardsListPage(LoggedPage, HTMLPage):
@pagination
@method
class iter_accounts(ListElement):
item_xpath = '//li[@class="item"]'
def next_page(self):
other_cards = self.el.xpath('//span/a[contains(text(), "Autres cartes")]')
if other_cards:
return Link(other_cards)(self)
class item(ItemElement):
klass = Account
......@@ -1899,6 +1907,8 @@ class NewCardsListPage(LoggedPage, HTMLPage):
raise SkipItem()
elif doc.xpath('//div/p[contains(text(), "Vous n\'avez pas l\'autorisation")]'):
self.logger.warning("The user can't reach this page")
elif doc.xpath('//td[contains(text(), "Problème technique")]'):
raise BrowserUnavailable(CleanText(doc.xpath('//td[contains(text(), "Problème technique")]'))(self))
else:
assert False, 'xpath for card type information could have changed'
......
......@@ -183,16 +183,22 @@ class RegisterTransferPage(LoggedPage, HTMLPage):
form['typeDeVirement'] = 'VI'
form['dateDeVirement'] = exec_date.strftime('%d/%m/%Y')
form['montantVirement'] = amount
form['libelleVirementSaisie'] = label
form['libelleVirementSaisie'] = label.encode(self.encoding, errors='xmlcharrefreplace').decode(self.encoding)
form.submit()
class ValidateTransferPage(LoggedPage, HTMLPage):
def on_load(self):
if self.doc.xpath('//form[@id="SaisieVirementForm"]/p[has-class("error")]'):
raise TransferBankError(CleanText(
'//form[@id="SaisieVirementForm"]/p[has-class("error")]/label'
)(self.doc))
errors_msg = (
CleanText('//form[@id="SaisieVirementForm"]/p[has-class("error")]/label')(self.doc),
CleanText('//div[@id="error" and @class="erreur_texte"]/p[contains(text(), "n\'est pas autorisé")]')(self.doc),
)
for error in errors_msg:
if error:
raise TransferBankError(error)
other_error_msg = self.doc.xpath('//div[@id="error" and @class="erreur_texte"]')
assert not other_error_msg, 'Error "other_error_msg" is not handled yet'
def check_transfer_data(self, transfer_data):
for t_data in transfer_data:
......
......@@ -36,7 +36,7 @@ from weboob.capabilities.base import find_object
from .pages.account_pages import (
AccountsPage, OwnersListPage, CBOperationPage, CPTOperationPage, LoginPage,
AppGonePage, RibPage, UnavailablePage, OtherPage, FrameContainer, ProfilePage,
AppGonePage, RibPage, UnavailablePage, OtherPage, FrameContainer, ProfilePage, ScpiHisPage
)
from .pages.life_insurances import (
LifeInsurancesPage, LifeInsurancePortal, LifeInsuranceMain, LifeInsuranceUseless,
......@@ -58,6 +58,7 @@ class HSBC(LoginBrowser):
app_gone = False
scpi_investment_page = URL(r'https://www.hsbc.fr/1/[0-9]/.*', ScpiInvestmentPage)
scpi_his_page = URL(r'https://www.hsbc.fr/1/[0-9]/.*', ScpiHisPage)
connection = URL(r'https://www.hsbc.fr/1/2/hsbc-france/particuliers/connexion', LoginPage)
login = URL(r'https://www.hsbc.fr/1/*', LoginPage)
cptPage = URL(r'/cgi-bin/emcgi.*\&Cpt=.*',
......@@ -157,11 +158,12 @@ class HSBC(LoginBrowser):
if self.login.is_here():
self.page.useless_form()
# This shitty website has 2 baseurl with only one difference: the 's' at the end of 'client'
# This wonderful website has 2 baseurl with only one difference: the 's' at the end of 'client'
new_base_url = 'https://clients.hsbc.fr/'
if new_base_url in self.url:
self.BASEURL = new_base_url
home_url = None
if self.frame_page.is_here():
home_url = self.page.get_frame()
self.js_url = self.page.get_js_url()
......@@ -324,6 +326,21 @@ class HSBC(LoginBrowser):
if account.url.startswith('javascript') or '&Crd=' in account.url or account.type == Account.TYPE_LOAN:
raise NotImplementedError()
if account.type == Account.TYPE_MARKET and not 'BOURSE_INV' in account.url:
# Clean account url
m = re.search(r"'(.*)'", account.url)
if m:
account_url = m.group(1)
else:
account_url = account.url
# Need to be on accounts page to go on scpi page
self.accounts.go()
# Go on scpi page
self.location(account_url)
self.location(self.page.go_scpi_his_detail_page())
return self.page.iter_history()
if account.type in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_CAPITALISATION):
if coming is True:
return []
......
......@@ -26,10 +26,10 @@ from weboob.capabilities.bank import Account
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from weboob.tools.compat import urljoin
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded
from weboob.browser.elements import ListElement, ItemElement, method
from weboob.browser.pages import HTMLPage, pagination
from weboob.browser.elements import ListElement, ItemElement, method, TableElement
from weboob.browser.pages import HTMLPage, pagination, LoggedPage
from weboob.browser.filters.standard import (
Filter, Env, CleanText, CleanDecimal, Field, DateGuesser, Regexp, Currency,
Filter, Env, CleanText, CleanDecimal, Field, DateGuesser, Regexp, Currency, Format, Date
)
from weboob.browser.filters.html import AbsoluteLink, TableCell
from weboob.browser.filters.javascript import JSVar
......@@ -93,12 +93,13 @@ class AccountsType(Filter):
(r'livret', Account.TYPE_SAVINGS),
(r'livjeu', Account.TYPE_SAVINGS),
(r'csljun', Account.TYPE_SAVINGS),
(r'ldds', Account.TYPE_SAVINGS),
(r'compte', Account.TYPE_CHECKING),
(r'cpte', Account.TYPE_CHECKING),
(r'scpi', Account.TYPE_MARKET),
(r'account', Account.TYPE_CHECKING),
(r'\bpret\b', Account.TYPE_LOAN),
(r'\bvie\b', Account.TYPE_LIFE_INSURANCE),
(r'\bvie2?\b', Account.TYPE_LIFE_INSURANCE),
(r'strategie patr.', Account.TYPE_LIFE_INSURANCE),
(r'essentiel', Account.TYPE_LIFE_INSURANCE),
(r'elysee', Account.TYPE_LIFE_INSURANCE),
......@@ -112,6 +113,7 @@ class AccountsType(Filter):
(r'hsbc evol pat capi', Account.TYPE_CAPITALISATION),
(r'bourse libre', Account.TYPE_MARKET),
(r'plurival', Account.TYPE_LIFE_INSURANCE),
(r'europep', Account.TYPE_LIFE_INSURANCE),
]
def filter(self, label):
......@@ -431,3 +433,25 @@ class ProfilePage(OtherPage):
obj_name = CleanText('//div[@id="div_adr_P1"]//p/label[contains(text(), "Nom")]/parent::p/strong')
obj_address = CleanText('//div[@id="div_adr_P1"]//p/label[contains(text(), "Adresse")]/parent::p/strong')
class ScpiHisPage(LoggedPage, HTMLPage):
def is_here(self):
return self.doc.xpath('//h3[contains(text(), "HISTORIQUE DES MOUVEMENTS")]')
@method
class iter_history(TableElement):
item_xpath = '//table[@class="csTable"]//tbody//tr'
head_xpath = '//table[@class="csTable"]//thead//th/a'
col_date = 'Date'
col_amount = 'Montant brut (en €)'
col_operation = 'Opération'
col_nature = 'Nature'
class item(ItemElement):
klass = Transaction
obj_label = Format('%s - %s', CleanText(TableCell('operation')), CleanText(TableCell('nature')))
obj_rdate = Date(CleanText(TableCell('date')), dayfirst=True)
obj_amount = CleanDecimal(TableCell('amount'), sign=lambda x: -1, replace_dots=True)
......
......@@ -16,7 +16,7 @@ from weboob.browser.pages import HTMLPage, JsonPage, LoggedPage
from weboob.browser.filters.standard import (
CleanText, CleanDecimal, Regexp, Currency, Field, Env,
)
from weboob.browser.filters.html import TableCell
from weboob.browser.filters.html import TableCell, Link
from weboob.browser.filters.json import Dict
from weboob.browser.filters.javascript import JSVar
from weboob.exceptions import BrowserUnavailable
......@@ -524,6 +524,9 @@ class ScpiInvestmentPage(LoggedPage, HTMLPage):
assert len(detail_page) == 1
self.browser.location('https://www.hsbc.fr' + CleanText('./@href')(detail_page[0]))
def go_scpi_his_detail_page(self):
return Link('//div/a[contains(text(), "Historique de vos mouvements de parts")]')(self.doc)
@method
class iter_scpi_investment(TableElement):
item_xpath = '//table[@class="csTable"]//tbody//tr'
......
......@@ -145,7 +145,7 @@ class LCLModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapContact,
return self.browser.execute_transfer(transfer)
def transfer_check_label(self, old, new):
old = re.sub(r"[/<\?='!]", '', old).strip()
old = re.sub(r"[/<\?='!\+]", '', old).strip()
old = old.encode('latin-1', errors='replace').decode('latin-1')
return super(LCLModule, self).transfer_check_label(old, new)
......
......@@ -72,7 +72,24 @@ class LinebourseBrowser(LoginBrowser):
assert self.invest.is_here()
if not self.page.is_on_right_portfolio(account_id):
self.invest.go(id=self.page.get_compte(account_id))
return self.page.iter_investment()
return self.page.iter_investments()
# Method used only by bp module
def get_liquidity(self, account_id):
self.main.go()
self.invest.go()
if self.message.is_here():
self.page.submit()
self.invest.go()
if self.broken.is_here():
return iter([])
assert self.invest.is_here()
if not self.page.is_on_right_portfolio(account_id):
self.invest.go(id=self.page.get_compte(account_id))
return self.page.get_liquidity()
def iter_history(self, account_id):
self.main.go()
......
# -*- 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
......@@ -30,6 +30,7 @@ from weboob.browser.filters.html import TableCell
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.bank import Investment
from weboob.tools.capabilities.bank.transactions import FrenchTransaction as Transaction
from .compat.weboob_tools_capabilities_bank_investments import create_french_liquidity
from weboob.tools.compat import quote_plus
from weboob.exceptions import ActionNeeded
......@@ -110,7 +111,7 @@ class HistoryPage(AccountPage):
class InvestmentPage(AccountPage):
@method
class get_investment(TableElement):
class iter_investments(TableElement):
col_label = 'Valeur'
col_quantity = u'Quantité'
col_valuation = u'Valorisation EUR'
......@@ -139,9 +140,11 @@ class InvestmentPage(AccountPage):
obj_label = CleanText(Regexp(CleanText('./preceding-sibling::tr/td[1]'), '(.*)- .*'))
obj_code = Regexp(CleanText('./preceding-sibling::tr/td[1]'), '- (.*)')
def iter_investment(self):
for inv in self.get_investment():
yield inv
# Only used by bp modules since others quality websites provide another account with the liquidities
def get_liquidity(self):
liquidity = CleanDecimal('//table//tr[@class="titreAvant"]/td[contains(text(), "Liquidit")]/following-sibling::td', replace_dots=True)(self.doc)
if liquidity:
return create_french_liquidity(liquidity)
class MessagePage(LoggedPage, HTMLPage):
......
from weboob.exceptions import *
class AuthMethodNotImplemented(Exception):
pass
class CaptchaQuestion(Exception):
"""Site requires solving a CAPTCHA (base class)"""
# could be improved to pass the name of the backendconfig key
def __init__(self, type=None, **kwargs):
super(CaptchaQuestion, self).__init__("The site requires solving a captcha")
self.type = type
for key, value in kwargs.items():
setattr(self, key, value)
class ImageCaptchaQuestion(CaptchaQuestion):
type = 'image_captcha'
image_data = None
def __init__(self, image_data):
super(ImageCaptchaQuestion, self).__init__(self.type, image_data=image_data)
class NocaptchaQuestion(CaptchaQuestion):
type = 'g_recaptcha'
website_key = None
website_url = None
def __init__(self, website_key, website_url):
super(NocaptchaQuestion, self).__init__(self.type, website_key=website_key, website_url=website_url)
class RecaptchaQuestion(CaptchaQuestion):
type = 'g_recaptcha'
website_key = None
website_url = None
def __init__(self, website_key, website_url):
super(RecaptchaQuestion, self).__init__(self.type, website_key=website_key, website_url=website_url)
......@@ -31,8 +31,9 @@ from weboob.capabilities.base import Currency
from weboob.capabilities import NotAvailable
from weboob.capabilities.bank import Account
from weboob.capabilities.bill import Document, Subscription
from weboob.exceptions import (
from .compat.weboob_exceptions import (
BrowserUnavailable, NoAccountsException, BrowserIncorrectPassword, BrowserPasswordExpired,
AuthMethodNotImplemented,
)
from weboob.tools.capabilities.bank.iban import is_iban_valid
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
......@@ -66,6 +67,8 @@ class AccountsJsonPage(LoggedPage, JsonPage):
raise BrowserIncorrectPassword('Vos identifiants sont incorrects')
elif reason == 'chgt_mdp_oblig':
raise BrowserPasswordExpired('Veuillez renouveler votre mot de passe')
elif reason == 'oob_insc_oblig':
raise AuthMethodNotImplemented("L'authentification par Secure Access n'est pas prise en charge")
raise BrowserUnavailable(reason)
@method
......