Commit 99da3e9f authored by Roger Philibert's avatar Roger Philibert Committed by Vincent A

backport devel modules fixes

parent a84c2f3b
Pipeline #3734 failed with stages
in 63 minutes and 47 seconds
......@@ -33,7 +33,7 @@
from .pages import (
LoginPage, SubscriptionsPage, DocumentsPage, DownloadDocumentPage, HomePage,
SecurityPage, LanguagePage, HistoryPage, PasswordExpired, ApprovalPage, PollingPage,
ResetPasswordPage,
ResetPasswordPage, AccountSwitcherLoadingPage, AccountSwitcherPage, SwitchedAccountPage,
)
......@@ -63,10 +63,15 @@ class AmazonBrowser(LoginBrowser, StatesMixin):
"Impossible de trouver un compte correspondant à cette adresse e-mail",
"L'adresse e-mail est déjà utilisée",
"Numéro de téléphone incorrect",
"Votre adresse e-mail ou mot de passe étaient incorrects",
]
WRONG_CAPTCHA_RESPONSE = "Saisissez les caractères tels qu'ils apparaissent sur l'image."
login = URL(r'/ap/signin(.*)', LoginPage)
account_switcher_loading = URL(r'/ap/signin(.*)', AccountSwitcherLoadingPage)
account_switcher = URL(r'/ap/cvf/request.embed\?arb=(?P<token>.*)', AccountSwitcherPage)
switched_account = URL(r'/ap/switchaccount', SwitchedAccountPage)
home = URL(r'/$', r'/\?language=.+$', HomePage)
subscriptions = URL(r'/gp/profile', SubscriptionsPage)
history = URL(
......@@ -281,6 +286,11 @@ def do_login(self):
else:
self.history.go()
if self.account_switcher_loading.is_here():
self.account_switcher.go(token=self.page.get_arb_token())
self.page.validate_account()
self.location(self.page.get_redirect_url())
if not self.login.is_here():
return
......
......@@ -20,9 +20,10 @@
from __future__ import unicode_literals
from woob.browser.exceptions import ServerError
from woob.browser.pages import HTMLPage, LoggedPage, FormNotFound, PartialHTMLPage, pagination
from woob.browser.pages import HTMLPage, LoggedPage, FormNotFound, PartialHTMLPage, pagination, JsonPage
from woob.browser.elements import ItemElement, ListElement, method
from woob.browser.filters.html import Link, Attr
from woob.browser.filters.json import Dict
from woob.browser.filters.standard import (
CleanText, CleanDecimal, Env, Regexp, Format, RawText,
Field, Currency, Date, Async, AsyncLoad,
......@@ -135,7 +136,31 @@ class LanguagePage(HTMLPage):
pass
class AccountSwitcherLoadingPage(HTMLPage):
def is_here(self):
return bool(self.doc.xpath('//div[@id="ap-account-switcher-container"]'))
def get_arb_token(self):
# Get the token from attribute data-arbToken (data-arbtoken using the Attr filter)
return Attr('//div[@id="ap-account-switcher-container"]', 'data-arbtoken')(self.doc)
class AccountSwitcherPage(PartialHTMLPage):
def validate_account(self):
form = self.get_form(xpath='//form[@action="/ap/switchaccount"]')
form['switch_account_request'] = Attr('//a[@data-name="switch_account_request"]', 'data-value')(self.doc)
form.submit()
class SwitchedAccountPage(JsonPage):
def get_redirect_url(self):
return Dict('redirectUrl')(self.doc)
class LoginPage(PartialHTMLPage):
def is_here(self):
return not bool(self.doc.xpath('//div[@id="ap-account-switcher-container"]'))
ENCODING = 'utf-8'
def login(self, login, password, captcha=None):
......@@ -271,20 +296,21 @@ def obj_url(self):
)(self)
if not url:
download_elements = async_page.doc.xpath('//a[contains(@href, "download")]')
order_summary_link = Link(
'//a[contains(text(), "Récapitulatif de commande")]|//a[contains(text(), "Order Summary")]',
default=NotAvailable
)
if download_elements and len(download_elements) > 1:
# Sometimes there are multiple invoices for one order and to avoid missing the other invoices
# we are taking the order summary instead
url = Link(
'//a[contains(text(), "Récapitulatif de commande")]',
default=NotAvailable
)(async_page.doc)
url = order_summary_link(async_page.doc)
else:
url = Coalesce(
Link(
'//a[contains(@href, "download")]|//a[contains(@href, "generated_invoices")]',
default=NotAvailable,
),
Link('//a[contains(text(), "Récapitulatif de commande")]', default=NotAvailable),
order_summary_link,
default=NotAvailable
)(async_page.doc)
doc_id = Field('id')(self)
......
......@@ -28,7 +28,7 @@
from dateutil.relativedelta import relativedelta
from woob.browser import LoginBrowser, URL, need_login
from woob.exceptions import ActionNeeded, BrowserIncorrectPassword, BrowserUnavailable
from woob.exceptions import ActionNeeded, BrowserIncorrectPassword
from woob.tools.capabilities.bill.documents import merge_iterators
from .pages import (
......@@ -86,9 +86,6 @@ def do_login(self):
raise BrowserIncorrectPassword(err_msg)
raise AssertionError('Unhandled error at login %s' % err_msg)
if self.error_page.is_here():
raise BrowserUnavailable(self.page.get_error_message())
if self.cgu_page.is_here():
raise ActionNeeded(self.page.get_cgu_message())
......
......@@ -30,6 +30,7 @@
from woob.browser.filters.json import Dict
from woob.browser.pages import LoggedPage, HTMLPage, PartialHTMLPage, RawPage, JsonPage
from woob.capabilities.bill import Subscription, Bill, Document, DocumentTypes
from woob.exceptions import BrowserUnavailable
from woob.tools.compat import html_unescape
from woob.tools.date import parse_french_date
from woob.tools.json import json
......@@ -71,8 +72,10 @@ def get_cgu_message(self):
class ErrorPage(HTMLPage):
def get_error_message(self):
return html_unescape(CleanText('//div[@class="mobile"]/p')(self.doc))
def on_load(self):
# message is: "Oups... votre compte ameli est momentanément indisponible. Il sera de retour en pleine forme très bientôt."
# nothing we can do, but retry later
raise BrowserUnavailable(html_unescape(CleanText('//div[@class="mobile"]/p')(self.doc)))
class SubscriptionPage(LoggedPage, HTMLPage):
......
......@@ -42,7 +42,7 @@
JsonBalances2, CurrencyPage, LoginPage, NoCardPage,
NotFoundPage, HomeLoginPage,
ReadAuthChallengePage, UpdateAuthTokenPage,
SLoginPage,
SHomePage, SLoginPage,
)
from .fingerprint import FingerprintPage
......@@ -487,7 +487,7 @@ def iter_coming(self, account):
class AmericanExpressSeleniumFingerprintBrowser(SeleniumBrowser):
BASEURL = 'https://global.americanexpress.com'
home_login = URL(r'/login\?inav=fr_utility_logout')
home_login = URL(r'/login\?inav=fr_utility_logout', SHomePage)
login = URL(r'https://www.americanexpress.com/en-us/account/login', SLoginPage)
HEADLESS = True # Always change to True for prod
......
......@@ -237,5 +237,9 @@ def obj_original_amount(self):
obj__ref = Dict('identifier')
class SHomePage(SeleniumPage):
pass
class SLoginPage(SeleniumPage):
pass
......@@ -758,6 +758,7 @@ class GenericAccountsPage(LoggedPage, MyHTMLPage):
(re.compile(r'.*Livret.*'), Account.TYPE_SAVINGS),
(re.compile(r'.*Titres Pea.*'), Account.TYPE_PEA),
(re.compile(r".*Plan D'epargne En Actions.*"), Account.TYPE_PEA),
(re.compile(r".*Plan 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),
......
......@@ -71,6 +71,7 @@ class BforbankBrowser(TwoFactorBrowser):
card_history = URL('espace-client/consultation/encoursCarte/.*', CardHistoryPage)
card_page = URL(r'/espace-client/carte/(?P<account>\d+)$', CardPage)
lifeinsurance = URL(r'/espace-client/assuranceVie/(?P<account_id>\d+)')
lifeinsurance_list = URL(r'/client/accounts/lifeInsurance/lifeInsuranceSummary.action', LifeInsuranceList)
lifeinsurance_iframe = URL(
r'https://(?:www|client).bforbank.com/client/accounts/lifeInsurance/consultationDetailSpirica.action',
......@@ -383,7 +384,7 @@ def get_coming(self, account):
raise NotImplementedError()
def goto_lifeinsurance(self, account):
self.location('https://client.bforbank.com/espace-client/assuranceVie')
self.lifeinsurance.go(account_id=account.id)
self.lifeinsurance_list.go()
@retry(AccountNotFound, tries=5)
......
......@@ -124,7 +124,7 @@ def populate_rib(self, accounts):
if 'selected' in option.attrib:
self.get_iban(accounts)
else:
page = self.browser.rib.go(id=re.sub(r'[^\d]', '', Attr('.', 'value')(option)))
page = self.browser.rib.go(id=Regexp(Attr('.', 'value'), r'/(.+)')(option))
page.get_iban(accounts)
def get_iban(self, accounts):
......
......@@ -511,7 +511,12 @@ def iter_investment(self, account):
# Life insurances and PERP may be scraped from the API or from the "Assurance Vie" space,
# so we need to discriminate between both using account._details:
if account.type in (account.TYPE_LIFE_INSURANCE, account.TYPE_PERP, account.TYPE_CAPITALISATION):
if account.type in (
account.TYPE_LIFE_INSURANCE,
account.TYPE_PERP,
account.TYPE_CAPITALISATION,
account.TYPE_PER,
):
if hasattr(account, '_details'):
# Going to the "Assurances Vie" page
natiovie_params = self.natio_vie_pro.go().get_params()
......
......@@ -250,6 +250,8 @@ def validate(self, obj):
'PEA PME Espèces': Account.TYPE_PEA,
'PEA Titres': Account.TYPE_PEA,
'PEL': Account.TYPE_SAVINGS,
'BNP Paribas Multiplacements PER': Account.TYPE_PER,
'BNPP Multiplacements Privilège PER': Account.TYPE_PER,
'BNPP MP PERP': Account.TYPE_PERP,
'Plan Epargne Retraite Particulier': Account.TYPE_PERP,
'Crédit immobilier': Account.TYPE_MORTGAGE,
......
......@@ -1372,6 +1372,10 @@ def obj_label(self):
if not empty(bank_name):
label = label.replace('- %s' % bank_name, '').strip()
# There is an exceptional case where the recipient has an empty label.
# In such a case, at least use the name of the bank
if label == '':
label = bank_name
return label
def obj_category(self):
......
......@@ -50,6 +50,7 @@
TokenPage, MoveUniversePage, SwitchPage,
LoansPage, AccountsPage, IbanPage, LifeInsurancesPage,
SearchPage, ProfilePage, ErrorPage, ErrorCodePage, LinebourseLoginPage,
UnavailablePage,
)
from .transfer_pages import (
RecipientListPage, EmittersListPage, ListAuthentPage,
......@@ -84,6 +85,7 @@ class BredBrowser(TwoFactorBrowser):
search = URL(r'/transactionnel/services/applications/operations/getSearch/', SearchPage)
profile = URL(r'/transactionnel/services/rest/User/user', ProfilePage)
error_code = URL(r'/.*\?errorCode=.*', ErrorCodePage)
unavailable_page = URL(r'/ERREUR/', UnavailablePage)
accounts_twofa = URL(r'/transactionnel/v2/services/rest/Account/accounts', AccountsTwoFAPage)
list_authent = URL(r'/transactionnel/services/applications/authenticationstrong/listeAuthent/(?P<context>\w+)', ListAuthentPage)
......@@ -219,6 +221,13 @@ def get_connection_twofa_method(self):
# The order and tests are taken from the bred website code.
# Keywords in scripts.js: showSMS showEasyOTP showOTP
methods = self.context['liste']
# Overriding default order of tests with 'preferred_sca' configuration item
preferred_auth_methods = tuple(self.config.get('preferred_sca', '').get().split())
for auth_method in preferred_auth_methods:
if methods.get(auth_method):
return auth_method
if methods.get('sms'):
return 'sms'
elif methods.get('notification') and methods.get('otp'):
......@@ -299,7 +308,7 @@ def enrol_device(self):
self.update_headers()
data = {
'uuid': self.device_id, # Called an uuid but it's just a 50 digits long string.
'deviceName': 'Accès BudgetInsight pour agrégation', # clear message for user
'deviceName': self.config.get('device_name', 'Accès BudgetInsight pour agrégation').get(), # clear message for user
'biometricEnabled': False,
'securedBiometricEnabled': False,
'notificationEnabled': False,
......@@ -336,11 +345,11 @@ def move_to_universe(self, univers):
@need_login
def get_accounts_list(self):
accounts = []
for universe_key in self.get_universes():
for universe_key in sorted(self.get_universes()):
self.move_to_universe(universe_key)
universe_accounts = []
universe_accounts.extend(self.get_list())
universe_accounts.extend(self.get_life_insurance_list(accounts))
universe_accounts.extend(self.get_life_insurance_list())
universe_accounts.extend(self.get_loans_list())
linebourse_accounts = self.get_linebourse_accounts(universe_key)
for account in universe_accounts:
......@@ -354,8 +363,17 @@ def get_accounts_list(self):
accounts.extend(universe_accounts)
# Life insurances are sometimes in multiple universes, we have to remove duplicates
unique_accounts = {account.id: account for account in accounts}
return sorted(unique_accounts.values(), key=operator.attrgetter('_univers'))
unique_accounts = {account.id: account for account in accounts}.values()
# Fill parents with resulting accounts when relevant:
for account in unique_accounts:
if account.type not in [Account.TYPE_CARD, Account.TYPE_LIFE_INSURANCE]:
continue
account.parent = find_object(
unique_accounts, _number=account._parent_number, type=Account.TYPE_CHECKING
)
return sorted(unique_accounts, key=operator.attrgetter('_univers'))
@need_login
def get_linebourse_accounts(self, universe_key):
......@@ -387,17 +405,12 @@ def get_loans_list(self):
@need_login
def get_list(self):
self.accounts.go()
for acc in self.page.iter_accounts(accnum=self.accnum, current_univers=self.current_univers):
yield acc
return self.page.iter_accounts(accnum=self.accnum, current_univers=self.current_univers)
@need_login
def get_life_insurance_list(self, accounts):
def get_life_insurance_list(self):
self.life_insurances.go()
for ins in self.page.iter_lifeinsurances(univers=self.current_univers):
ins.parent = find_object(accounts, _number=ins._parent_number, type=Account.TYPE_CHECKING)
yield ins
return self.page.iter_lifeinsurances(univers=self.current_univers)
@need_login
def _make_api_call(self, account, start_date, end_date, offset, max_length=50):
......@@ -422,8 +435,7 @@ def get_history(self, account, coming=False):
if account.type in (Account.TYPE_LOAN, Account.TYPE_LIFE_INSURANCE) or not account._consultable:
raise NotImplementedError()
if account._univers != self.current_univers:
self.move_to_universe(account._univers)
self.move_to_universe(account._univers)
today = date.today()
seen = set()
......@@ -473,8 +485,7 @@ def iter_investments(self, account):
elif account.type in (Account.TYPE_PEA, Account.TYPE_MARKET):
if 'Portefeuille Titres' in account.label:
if account._is_in_linebourse:
if account._univers != self.current_univers:
self.move_to_universe(account._univers)
self.move_to_universe(account._univers)
self.linebourse.location(
self.linebourse_urls[account._univers],
data={'SJRToken': self.linebourse_tokens[account._univers]}
......@@ -498,8 +509,7 @@ def iter_market_orders(self, account):
if 'Portefeuille Titres' in account.label:
if account._is_in_linebourse:
if account._univers != self.current_univers:
self.move_to_universe(account._univers)
self.move_to_universe(account._univers)
self.linebourse.location(
self.linebourse_urls[account._univers],
data={'SJRToken': self.linebourse_tokens[account._univers]}
......@@ -523,8 +533,8 @@ def fill_account(self, account, fields):
@need_login
def iter_transfer_recipients(self, account):
self.move_to_universe(account._univers)
self.update_headers()
try:
self.emitters_list.go(json={
'typeVirement': 'C',
......@@ -676,6 +686,8 @@ def init_new_recipient(self, recipient, **params):
@need_login
def init_transfer(self, transfer, account, recipient, **params):
self.move_to_universe(account._univers)
account_id = account.id.split('.')[0]
poste = account.id.split('.')[1]
......
......@@ -25,14 +25,13 @@
from woob.tools.date import parse_french_date
from woob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded
from woob.capabilities.base import find_object
from woob.browser.pages import JsonPage, LoggedPage, HTMLPage
from woob.capabilities import NotAvailable
from woob.capabilities.bank import Account
from woob.capabilities.wealth import Investment
from woob.tools.capabilities.bank.investments import is_isin_valid
from woob.capabilities.profile import Person
from woob.browser.filters.standard import CleanText, CleanDecimal, Env, Eval
from woob.browser.filters.standard import CleanText, CleanDecimal, Env, Eval, Field
from woob.browser.filters.html import Link
from woob.browser.filters.json import Dict
from woob.browser.elements import DictElement, ItemElement, method
......@@ -140,6 +139,7 @@ def iter_loans(self, current_univers):
a.balance = -Decimal(str(content['montantCapitalDu']['valeur']))
a.currency = content['montantCapitalDu']['monnaie']['code'].strip()
a._univers = current_univers
a._number = Field('id')
yield a
......@@ -147,7 +147,11 @@ class AccountsPage(LoggedPage, MyJsonPage):
ACCOUNT_TYPES = {
'000': Account.TYPE_CHECKING, # Compte à vue
'001': Account.TYPE_SAVINGS, # Livret Ile de France
'002': Account.TYPE_SAVINGS, # Livret Seine-et-Marne & Aisne
'003': Account.TYPE_SAVINGS, # Livret Normandie
'004': Account.TYPE_SAVINGS, # Livret Guadeloupe
'005': Account.TYPE_SAVINGS, # Livret Martinique/Guyane
'006': Account.TYPE_SAVINGS, # Livret Réunion/Mayotte
'011': Account.TYPE_CARD, # Carte bancaire
'013': Account.TYPE_LOAN, # LCR (Lettre de Change Relevé)
'020': Account.TYPE_SAVINGS, # Compte sur livret
......@@ -171,8 +175,6 @@ class AccountsPage(LoggedPage, MyJsonPage):
def iter_accounts(self, accnum, current_univers):
seen = set()
accounts_list = []
for content in self.get_content():
if accnum != '00000000000' and content['numero'] != accnum:
continue
......@@ -183,6 +185,7 @@ def iter_accounts(self, accnum, current_univers):
a._codeSousPoste = poste['codeSousPoste'] if 'codeSousPoste' in poste else None
a._consultable = poste['consultable']
a._univers = current_univers
a._parent_number = None
a.id = '%s.%s' % (a._number, a._nature)
if content['comptePEA']:
......@@ -192,8 +195,8 @@ def iter_accounts(self, accnum, current_univers):
if a.type == Account.TYPE_UNKNOWN:
self.logger.warning("unknown type %s" % poste['codeNature'])
if a.type == Account.TYPE_CARD:
a.parent = find_object(accounts_list, _number=a._number, type=Account.TYPE_CHECKING)
if a.type != Account.TYPE_CHECKING:
a._parent_number = a._number
if 'numeroDossier' in poste and poste['numeroDossier']:
a._file_number = poste['numeroDossier']
......@@ -205,7 +208,8 @@ def iter_accounts(self, accnum, current_univers):
a.currency = poste['montantTitres']['monnaie']['code'].strip()
if not a.balance and not a.currency and 'dateTitres' not in poste:
continue
accounts_list.append(a)
yield a
continue
if 'libelle' not in poste:
continue
......@@ -227,9 +231,7 @@ def iter_accounts(self, accnum, current_univers):
continue
seen.add(a.id)
accounts_list.append(a)
return accounts_list
yield a
class IbanPage(LoggedPage, MyJsonPage):
......@@ -264,6 +266,7 @@ class item(ItemElement):
obj_type = Account.TYPE_LIFE_INSURANCE
obj_currency = 'EUR'
obj__univers = Env('univers')
obj__number = Field('id')
def obj_id(self):
return Eval(str, Dict('numero'))(self)
......@@ -399,3 +402,14 @@ def on_load(self):
raise BrowserUnavailable(msg)
assert False, 'Error %s is not handled yet.' % code
class UnavailablePage(HTMLPage):
def is_here(self):
return CleanText('//h1[contains(text(), "Site en maintenance")]', default=None)(self.doc)
def on_load(self):
msg = CleanText('//div[contains(text(), "intervention technique est en cours")]', default=None)(self.doc)
if msg:
raise BrowserUnavailable(msg)
raise AssertionError('Ended up to this error page, message not handled yet.')
......@@ -49,6 +49,10 @@ def can_account_emit_transfer(self, account_id):
# Nous vous précisons que votre pouvoir ne vous permet pas
# d'effectuer des virements de ce type au débit du compte sélectionné.
return False
elif code == '90600':
# "Votre demande de virement ne peut être prise en compte actuellement
# The user is probably not allowed to do transfers
return False
elif code != '0':
raise AssertionError('Unhandled code %s in transfer emitter selection' % code)
......@@ -68,6 +72,9 @@ class RecipientListPage(LoggedPage, JsonPage):
@method
class iter_external_recipients(DictElement):
item_xpath = 'content/listeComptesCExternes'
# The id is the iban, and exceptionally there could be the same
# recipient multiple times when the bic of the recipient changed
ignore_duplicate = True
class item(ItemElement):
klass = Recipient
......
......@@ -48,6 +48,8 @@ class BredModule(Module, CapBankWealth, CapProfile, CapBankTransferAddRecipient)
ValueBackendPassword('login', label='Identifiant', masked=False, regexp=r'.{1,32}'),
ValueBackendPassword('password', label='Mot de passe'),
Value('accnum', label='Numéro du compte bancaire (optionnel)', default='', masked=False),
Value('preferred_sca', label='Mécanisme(s) d\'authentification forte préferrés (optionnel, un ou plusieurs (séparés par des espaces) parmi: elcard usb sms otp mail password svi notification whatsApp)', default='', masked=False),
Value('device_name', label='Nom du device qui sera autorisé pour 90j suite à l\'authentication forte', default='', masked=False),
ValueTransient('request_information'),
ValueTransient('resume'),
ValueTransient('otp_sms'),
......@@ -95,6 +97,9 @@ def fill_account(self, account, fields):
def iter_transfer_recipients(self, account):
if not isinstance(account, Account):
account = find_object(self.iter_accounts(), id=account)
elif not hasattr(account, '_univers'):
# We need a Bred filled Account to know the "univers" associated with the account
account = find_object(self.iter_accounts(), id=account.id)
return self.browser.iter_transfer_recipients(account)
......
......@@ -1277,6 +1277,12 @@ def add_owner_accounts(self):
if self.home.is_here():
for account in self.page.get_list(owner_name):
if account.id not in [acc.id for acc in self.accounts]:
if account.type == Account.TYPE_LIFE_INSURANCE:
# For life insurance accounts, we check if the contract is still open
if not self.go_life_insurance_investments(account):
return
if self.page.is_contract_closed():
continue
self.accounts.append(account)
wealth_not_accessible = False
......@@ -1500,13 +1506,11 @@ def _get_history_invests(self, account):
self.life_insurance_history.go()
# Life insurance transactions are not sorted by date in the JSON
return sorted_transactions(self.page.iter_history())
except (IndexError, AttributeError) as e:
self.logger.error(e)
return []
except ServerError as e:
if e.response.status_code == 500:
raise BrowserUnavailable()
raise
return self.page.iter_history()
@need_login
......@@ -1540,7 +1544,10 @@ def match_cb(tr):
self.linebourse.session.cookies.update(self.session.cookies)
self.update_linebourse_token()
return self.linebourse.iter_history(account.id)
history = self.linebourse.iter_history(account.id)
# We need to go back to the synthesis, else we can not go home later
self.home_tache.go(tache='CPTSYNT0')
return history
hist = self._get_history(account._info, False)
return omit_deferred_transactions(hist)
......@@ -1622,8 +1629,8 @@ def get_investment(self, account):
else:
self.home.go()
self.page.go_history(account._info)
if account.type in (Account.TYPE_MARKET, Account.TYPE_PEA):
self.page.go_history(account._info)
# Some users may not have access to this.
if not self.market.is_here():
return
......@@ -1653,22 +1660,7 @@ def get_investment(self, account):
yield tr
return
try:
# Some life insurances are not on the accounts summary
self.home_tache.go(tache='EPASYNT0')
self.page.go_life_insurance(account)
if self.home.is_here():
# no detail is available for this account
return
elif not self.market.is_here() and not self.message.is_here():
# life insurance website is not always available
raise BrowserUnavailable()
self.page.submit()
self.life_insurance_investments.go()
except (IndexError, AttributeError) as e:
self.logger.error(e)
if not self.go_life_insurance_investments(account):
return
if self.garbage.is_here():
......@@ -1679,6 +1671,21 @@ def get_investment(self, account):
if self.market.is_here():
self.page.come_back()
@need_login
def go_life_insurance_investments(self, account):
# Returns whether it managed to go to the page
self.home_tache.go(tache='EPASYNT0')
self.page.go_life_insurance(account)
if self.home.is_here():
# no detail is available for this account
return False
elif not self.market.is_here() and not self.message.is_here():
# life insurance website is not always available
raise BrowserUnavailable()
self.page.submit()
self.life_insurance_investments.go()
return True
@need_login
def iter_market_orders(self, account):
if account.type not in (Account.TYPE_MARKET, Account.TYPE_PEA):
......@@ -1694,8 +1701,12 @@ def iter_market_orders(self, account):
return
self.linebourse.session.cookies.update(self.session.cookies)
self.update_linebourse_token()
for order in self.linebourse.iter_market_orders(account.id):
yield order
try:
for order in self.linebourse.iter_market_orders(account.id):
yield order
finally:
# We need to go back to the synthesis, else we can not go home later
self.home_tache.go(tache='CPTSYNT0')
@need_login
def get_advisor(self):
......
......@@ -603,6 +603,7 @@ def _get_account_info(self, a, accounts):
r"PostBack(Options)?\([\"'][^\"']+[\"'],\s*['\"]([HISTORIQUE_\w|SYNTHESE_ASSURANCE_CNP|BOURSE|COMPTE_TITRE][\d\w&]+)?['\"]",
a.attrib.get('href', '')
)
if m is None:
return None
else:
......@@ -876,9 +877,9 @@ def get_loan_list(self):
account.currency = account.get_currency(CleanText('./a')(tds[4]))
accounts[account.id] = account
website = 'old'
website = 'new'
if accounts:
website = 'new'
website = 'old'
self.logger.debug('we are on the %s website', website)
if len(accounts) == 0:
......@@ -1858,6 +1859,9 @@ def obj_unitvalue(self):
obj_code = IsinCode(CleanText(Dict('codeIsin', default='')), default=NotAvailable)
obj_code_type = IsinType(CleanText(Dict('codeIsin', default='')))
def is_contract_closed(self):
return Dict('etatContrat/code')(self.doc) == "01"
class NatixisLIHis(LoggedPage, JsonPage):
@method
......
......@@ -275,6 +275,10 @@ def parse(self, el):
# If it's an internal account, we should always find only one account with _id in it's id.
# Type card account contains their parent account id, and should not be listed in recipient account.
match = [acc for acc in accounts if _id in acc.id and acc.type != Account.TYPE_CARD]
# Not all internal accounts are returned by get_accounts_list
if not match:
self.logger.warning('skipping internal recipient without a matching account: %r', _id)
raise SkipItem()
assert len(match) == 1
match = match[0]
self.env['id'] = match.id
......
......@@ -27,7 +27,7 @@
from woob.browser import LoginBrowser, URL, need_login, StatesMixin
from woob.exceptions import (
BrowserIncorrectPassword, RecaptchaV2Question, BrowserUnavailable,
AuthMethodNotImplemented,
ActionNeeded, AuthMethodNotImplemented,
)
from woob.capabilities.bank import Account
from woob.tools.compat import basestring
......@@ -128,9 +128,7 @@ def do_login(self):
self.page.enter_password(self.password)
if not self.home.is_here():
if self.page.has_2fa():
raise AuthMethodNotImplemented("L'authentification forte Clé Secure n'est pas prise en charge.")
if self.login.is_here():
error = self.page.get_error_message()
# Sometimes some connections aren't able to login because of a
# maintenance randomly occuring.
......@@ -140,11 +138,45 @@ def do_login(self):
elif 'saisies ne correspondent pas à l\'identifiant' in error: