Commit 552496c7 authored by Vincent A's avatar Vincent A

backport master modules fixes

parent 22355a1c
Pipeline #3416 failed with stages
in 4 minutes and 42 seconds
......@@ -193,7 +193,7 @@ class JsonHistory(LoggedPage, JsonPage):
obj_amount = Eval(lambda x: -float_to_decimal(x), Dict('amount'))
obj_original_currency = Dict('foreign_details/iso_alpha_currency_code', default=NotAvailable)
obj_commission = CleanDecimal(Dict('foreign_details/commission_amount', default=NotAvailable), sign='-', default=NotAvailable)
obj__owner = CleanText(Dict('embossed_name'))
obj__owner = Dict('embossed_name')
obj_id = Dict('reference_id', default=NotAvailable)
def obj_original_amount(self):
......@@ -23,9 +23,9 @@ from base64 import b64encode
from .compat.weboob_browser_browsers import APIBrowser
from weboob.exceptions import BrowserIncorrectPassword, BrowserBanned
from weboob.capabilities.captcha import (
ImageCaptchaJob, RecaptchaJob, RecaptchaV3Job, NocaptchaJob, FuncaptchaJob, CaptchaError,
InsufficientFunds, UnsolvableCaptcha, InvalidCaptcha,
from .compat.weboob_capabilities_captcha import (
ImageCaptchaJob, RecaptchaJob, RecaptchaV3Job, NocaptchaJob, FuncaptchaJob, HcaptchaJob,
CaptchaError, InsufficientFunds, UnsolvableCaptcha, InvalidCaptcha,
......@@ -61,6 +61,9 @@ class AnticaptchaBrowser(APIBrowser):
def post_nocaptcha(self, url, key):
return self.post_gcaptcha(url, key, 'NoCaptcha')
def post_hcaptcha(self, url, key):
return self.post_gcaptcha(url, key, 'HCaptcha')
def post_gcaptcha(self, url, key, prefix):
data = {
"clientKey": self.apikey,
......@@ -147,7 +150,7 @@ class AnticaptchaBrowser(APIBrowser):
elif isinstance(job, RecaptchaJob):
job.solution = sol['recaptchaResponse']
job.solution_challenge = sol['recaptchaChallenge']
elif isinstance(job, NocaptchaJob) or isinstance(job, RecaptchaV3Job):
elif isinstance(job, (NocaptchaJob, RecaptchaV3Job, HcaptchaJob)):
job.solution = sol['gRecaptchaResponse']
elif isinstance(job, FuncaptchaJob):
job.solution = sol['token']
# -*- coding: utf-8 -*-
# Copyright(C) 2018 Vincent A
# This file is part of weboob.
# weboob is free software: you can redistribute it and/or modify
# 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.
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see <>.
from time import sleep
from weboob.capabilities.base import Capability, BaseObject, StringField, UserError, BytesField
from weboob.exceptions import (
RecaptchaQuestion, RecaptchaV3Question, NocaptchaQuestion, FuncaptchaQuestion,
ImageCaptchaQuestion, HcaptchaQuestion,
__all__ = [
'SolverJob', 'RecaptchaJob', 'NocaptchaJob', 'ImageCaptchaJob', 'HcaptchaJob',
'CaptchaError', 'UnsolvableCaptcha', 'InvalidCaptcha', 'InsufficientFunds',
from weboob.capabilities.captcha import SolverJob as _SolverJob
class SolverJob(_SolverJob):
solution = StringField('CAPTCHA solution')
from weboob.capabilities.captcha import RecaptchaJob as _RecaptchaJob
class RecaptchaJob(_RecaptchaJob):
site_url = StringField('Site URL for ReCaptcha service')
site_key = StringField('Site key for ReCaptcha service')
solution_challenge = StringField('Challenge ID of the solution (output value)')
from weboob.capabilities.captcha import RecaptchaV3Job as _RecaptchaV3Job
class RecaptchaV3Job(_RecaptchaV3Job):
site_url = StringField('Site URL for ReCaptcha service')
site_key = StringField('Site key for ReCaptcha service')
action = StringField('Website owner defines what user is doing on the page through this parameter.')
from weboob.capabilities.captcha import NocaptchaJob as _NocaptchaJob
class NocaptchaJob(_NocaptchaJob):
site_url = StringField('Site URL for NoCaptcha service')
site_key = StringField('Site key for NoCaptcha service')
from weboob.capabilities.captcha import FuncaptchaJob as _FuncaptchaJob
class FuncaptchaJob(_FuncaptchaJob):
site_url = StringField('Site URL for FunCaptcha service')
site_key = StringField('Site key for FunCaptcha service')
sub_domain = StringField('Required for some complex cases, but Funcaptcha integrations run without it')
class HcaptchaJob(SolverJob):
site_url = StringField('Site URL for HCaptcha service')
site_key = StringField('Site key for HCaptcha service')
from weboob.capabilities.captcha import ImageCaptchaJob as _ImageCaptchaJob
class ImageCaptchaJob(_ImageCaptchaJob):
image = BytesField('data of the image to solve')
from weboob.capabilities.captcha import CaptchaError as _CaptchaError
class CaptchaError(_CaptchaError):
"""Generic solving error"""
from weboob.capabilities.captcha import InvalidCaptcha as _InvalidCaptcha
class InvalidCaptcha(_InvalidCaptcha):
"""CAPTCHA cannot be used (e.g. invalid image format)"""
from weboob.capabilities.captcha import UnsolvableCaptcha as _UnsolvableCaptcha
class UnsolvableCaptcha(_UnsolvableCaptcha):
"""CAPTCHA is too hard or impossible"""
from weboob.capabilities.captcha import InsufficientFunds as _InsufficientFunds
class InsufficientFunds(_InsufficientFunds):
"""Not enough funds to pay solution"""
def exception_to_job(exc):
if isinstance(exc, RecaptchaQuestion):
job = RecaptchaJob()
job.site_url = exc.website_url
job.site_key = exc.website_key
elif isinstance(exc, RecaptchaV3Question):
job = RecaptchaV3Job()
job.site_url = exc.website_url
job.site_key = exc.website_key
job.action = exc.action
elif isinstance(exc, NocaptchaQuestion):
job = NocaptchaJob()
job.site_url = exc.website_url
job.site_key = exc.website_key
elif isinstance(exc, FuncaptchaQuestion):
job = FuncaptchaJob()
job.site_url = exc.website_url
job.site_key = exc.website_key
job.sub_domain = exc.sub_domain
elif isinstance(exc, ImageCaptchaQuestion):
job = ImageCaptchaJob()
job.image = exc.image_data
elif isinstance(exc, HcaptchaQuestion):
job = HcaptchaJob()
job.site_url = exc.website_url
job.site_key = exc.website_key
raise NotImplementedError()
return job
from weboob.capabilities.captcha import CapCaptchaSolver as _CapCaptchaSolver
class CapCaptchaSolver(_CapCaptchaSolver):
Provide CAPTCHA solving
def create_job(self, job):
"""Start a CAPTCHA solving job
The `` shall be filled. The CAPTCHA is not solved yet when the method returns.
:param job: job to start
:type job: :class:`SolverJob`
:raises: :class:`NotImplementedError` if CAPTCHA type is not supported
:raises: :class:`CaptchaError` in case of other error
raise NotImplementedError()
def poll_job(self, job):
"""Check if a job was solved
If `job` is solved, return True and fill `job.solution`.
Return False if solution is still pending.
In case of solving problem, an exception may be raised.
It should not wait for the solution but return the current state.
:param job: job to check and to fill when solved
:type job: :class:`SolverJob`
:returns: True if the job was solved
:rtype: bool
:raises: :class:`CaptchaError`
raise NotImplementedError()
def solve_catpcha_blocking(self, job):
"""Start a CAPTCHA solving job and wait for its solution
:param job: job to start and solve
:type job: :class:`SolverJob`
:raises: :class:`CaptchaError`
for i in range(self.RETRIES):
if self.poll_job(job):
return job
def report_wrong_solution(self, job):
"""Report a solved job as a wrong solution
Sometimes, jobs are solved, but the solution is rejected by the CAPTCHA
site because the solution is wrong.
This method reports the solution as wrong to the CAPTCHA solver.
:param job: job to flag
:type job: :class:`SolverJob`
raise NotImplementedError()
def get_balance(self):
"""Get the prepaid balance left
:rtype: float
raise NotImplementedError()
......@@ -21,8 +21,9 @@ from __future__ import unicode_literals
from import Module, BackendConfig
from weboob.capabilities.captcha import (
CapCaptchaSolver, ImageCaptchaJob, RecaptchaJob, RecaptchaV3Job, NocaptchaJob, FuncaptchaJob
from .compat.weboob_capabilities_captcha import (
CapCaptchaSolver, ImageCaptchaJob, RecaptchaJob, RecaptchaV3Job, NocaptchaJob, FuncaptchaJob,
from .compat.weboob_tools_value import ValueBackendPassword
......@@ -61,6 +62,8 @@ class AnticaptchaModule(Module, CapCaptchaSolver): = self.browser.post_nocaptcha(job.site_url, job.site_key)
elif isinstance(job, FuncaptchaJob): = self.browser.post_funcaptcha(job.site_url, job.site_key, job.sub_domain)
elif isinstance(job, HcaptchaJob): = self.browser.post_hcaptcha(job.site_url, job.site_key)
raise NotImplementedError()
......@@ -83,6 +83,12 @@ class InvestmentPage(LoggedPage, HTMLPage):
obj_valuation_diff = CleanDecimal.French('//h3[contains(., "value latente")]/following-sibling::p[1]', default=NotAvailable)
obj_type = MapIn(Lower(CleanText('//h3[contains(text(), "Type de produit")]/following-sibling::p')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN)
# Opening date titles may have slightly different names and apostrophe characters
obj_opening_date = Coalesce(
Date(CleanText('''//h3[contains(text(), "Date d'effet de l'adhésion")]/following-sibling::p'''), dayfirst=True, default=NotAvailable),
Date(CleanText('''//h3[contains(text(), "Date d’effet d’adhésion")]/following-sibling::p'''), dayfirst=True, default=NotAvailable),
def get_history_link(self):
history_link = self.doc.xpath('//li/a[contains(text(), "Historique")]/@href')
......@@ -178,8 +178,8 @@ class InvestmentMonAxaPage(LoggedPage, HTMLPage):
klass = Investment
obj_label = CleanText(TableCell('label'))
obj_code = IsinCode(TableCell('code'), default=NotAvailable)
obj_code_type = IsinType(TableCell('code'), default=NotAvailable)
obj_code = IsinCode(CleanText(TableCell('code')), default=NotAvailable)
obj_code_type = IsinType(CleanText(TableCell('code')))
obj_asset_category = CleanText(TableCell('asset_category'))
obj_valuation = CleanDecimal.French(TableCell('valuation'), default=NotAvailable)
......@@ -24,6 +24,8 @@ from weboob.browser.switch import SiteSwitch
from import sorted_transactions
from import basestring
from .corporate.browser import BnpcartesentrepriseCorporateBrowser
from .pages import (
LoginPage, ErrorPage, AccountsPage, TransactionsPage,
TiCardPage, TiHistoPage, ComingPage, HistoPage, HomePage,
......@@ -70,6 +72,8 @@ class BnpcartesentrepriseBrowser(LoginBrowser):
self.is_corporate = False
self.transactions_dict = {}
self.corporate_browser = None
def do_login(self):
assert isinstance(self.username, basestring)
assert isinstance(self.password, basestring)
......@@ -87,7 +91,9 @@ class BnpcartesentrepriseBrowser(LoginBrowser):
raise BrowserPasswordExpired(
if self.type == '2' and'Manager corporate connection')
raise SiteSwitch('corporate')
# Even if we are are on a manager corporate connection, we may still have business cards.
# For that case we need to fetch data from both the corporate browser and the default one.
self.corporate_browser = BnpcartesentrepriseCorporateBrowser(self.type, self.username, self.password)
# ti corporate and ge corporate are not detected the same way ..
if 'corporate' in'Carholder corporate connection')
......@@ -65,6 +65,7 @@ class AccountsPage(LoggedPage, HTMLPage):
obj_currency = 'EUR'
obj_url = Link('./td[2]/a')
obj__company = Env('company', default=None) # this field is something used to make the module work, not something meant to be displayed to end users
obj__is_corporate = True
def get_link(self, account_id, owner):
......@@ -65,13 +65,29 @@ class BnpcartesentrepriseModule(Module, CapBank):
for acc in self.browser.iter_accounts():
acc._bisoftcap = {'all': {'softcap_day':5,'day_for_softcap':100}}
yield acc
def iter_coming(self, account):
for tr in self.browser.get_transactions(account):
if tr._coming:
yield tr
# If this browser exists we have corporate cards, that we also need to fetch
if self.browser.corporate_browser:
for acc in self.browser.corporate_browser.iter_accounts():
acc._bisoftcap = {'all': {'softcap_day': 5, 'day_for_softcap': 100}}
yield acc
def iter_history(self, account):
for tr in self.browser.get_transactions(account):
if getattr(account, '_is_corporate', False):
get_transactions = self.browser.corporate_browser.get_transactions
get_transactions = self.browser.get_transactions
for tr in get_transactions(account):
if not tr._coming:
yield tr
def iter_coming(self, account):
if getattr(account, '_is_corporate', False):
get_transactions = self.browser.corporate_browser.get_transactions
get_transactions = self.browser.get_transactions
for tr in get_transactions(account):
if not tr._coming:
yield tr
......@@ -40,6 +40,7 @@ from .compat.weboob_capabilities_bank import (
TransferInvalidEmitter, TransferInvalidLabel, TransferInvalidRecipient,
AddRecipientStep, Rate, TransferBankError, AccountOwnership, RecipientNotFound,
AddRecipientTimeout, TransferDateType, Emitter, TransactionType,
from weboob.capabilities.base import empty, find_object
from import Advisor
......@@ -55,7 +56,7 @@ from .pages import (
TransferAccounts, TransferRecipients, TransferCharacteristics, TransferConfirm, TransferSent,
AddRecipientPage, StatusPage, CardHistoryPage, CardCalendarPage, CurrencyListPage, CurrencyConvertPage,
AccountsErrorPage, NoAccountPage, TransferMainPage, PasswordPage, NewTransferWizard,
NewTransferConfirm, NewTransferSent, CardSumDetailPage,
NewTransferConfirm, NewTransferSent, CardSumDetailPage, MinorPage,
from .transfer_pages import TransferListPage, TransferInfoPage
......@@ -84,7 +85,7 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser):
login = URL(r'/connexion/saisie-mot-de-passe', PasswordPage)
minor = URL(r'/connexion/mineur', MinorPage)
accounts = URL(r'/dashboard/comptes\?_hinclude=300000', AccountsPage)
accounts_error = URL(r'/dashboard/comptes\?_hinclude=300000', AccountsErrorPage)
pro_accounts = URL(r'/dashboard/comptes-professionnels\?_hinclude=1', AccountsPage)
......@@ -263,7 +264,9 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser):
self.login.go(), self.password)
if self.error.is_here():
if self.minor.is_here():
raise NoAccountsException(
elif self.error.is_here():
raise BrowserIncorrectPassword()
elif self.login.is_here():
error =
......@@ -768,26 +771,46 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser):
def init_new_recipient(self, recipient):
self.recipient_form = None # so it is reset when a new recipient is added
# so it is reset when a new recipient is added
self.recipient_form = None
# get url
# If an account was provided for the recipient, use it
# otherwise use the first checking account available
account = None
for account in self.get_accounts_list():
if account.url:
if not account.url:
if recipient.origin_account_id is None:
if account.type == Account.TYPE_CHECKING:
elif == recipient.origin_account_id:
suffix = 'virements/comptes-externes/nouveau'
if account.url.endswith('/'):
target = account.url + suffix
target = account.url + '/' + suffix
raise AddRecipientBankError(message="Compte ne permettant pas l'ajout de bénéficiaires")
except AccountNotFound:
raise AddRecipientBankError(message="Compte ne permettant pas d'emettre des virements")
assert (
or self.new_transfer_wizard.is_here()
), 'Should be on recipients page'
if not
raise AddRecipientBankError(message="Compte ne permettant pas l'ajout de bénéficiaires")
target = '%s/virements/comptes-externes/nouveau' % account.url.rstrip('/')
assert, 'Not on the page to add recipients.'
# fill recipient form
recipient.origin_account_id =
if recipient.origin_account_id is None:
recipient.origin_account_id =
# confirm sending sms
assert, 'Cannot reach the page asking to send a sms.'
......@@ -802,7 +825,8 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser):
self.recipient_form['account_url'] = account.url
raise AddRecipientStep(recipient, Value('otp_sms', label='Veuillez saisir le code recu par sms'))
# if the add recipient is restarted after the sms has been confirmed recently, the sms step is not presented again
# if the add recipient is restarted after the sms has been confirmed recently,
# the sms step is not presented again
return self.rcpt_after_sms(recipient, account.url)
def new_recipient(self, recipient, **kwargs):
......@@ -812,8 +836,9 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser):
# validating the sms code directly adds the recipient
account_url = self.send_recipient_form(kwargs['otp_sms'])
return self.rcpt_after_sms(recipient, account_url)
# step 3 of new_recipient (not always used)
elif 'otp_email' in kwargs:
if 'otp_email' in kwargs:
account_url = self.send_recipient_form(kwargs['otp_email'])
return self.check_and_update_recipient(recipient, account_url)
......@@ -855,7 +880,7 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser):
# We are taking it from the recipient list page
# because there is no summary of the adding
self.go_recipients_list(account_url, recipient.origin_account_id)
return find_object(,, error=RecipientNotFound)
return find_object(, iban=recipient.iban, error=RecipientNotFound)
def iter_transfers(self, account):
......@@ -40,7 +40,7 @@ from .compat.weboob_browser_filters_standard import (
MapIn, Lower, Base,
from weboob.browser.filters.json import Dict
from weboob.browser.filters.html import Attr, Link, TableCell
from weboob.browser.filters.html import Attr, HasElement, Link, TableCell
from .compat.weboob_capabilities_bank import (
Account as BaseAccount, Recipient, Transfer, TransferDateType, AccountNotFound,
AddRecipientBankError, TransferInvalidAmount, Loan, AccountOwnership,
......@@ -1038,10 +1038,10 @@ class SavingMarketPage(MarketPage):
item_xpath = '//table/tbody/tr'
head_xpath = '//table/thead/tr/th'
col_label = u'Opération'
col_amount = u'Montant'
col_date = u'Date opération'
col_vdate = u'Date Val'
col_label = 'Opération'
col_amount = 'Montant'
col_date = 'Date opération'
col_vdate = 'Date Valeur'
next_page = Link('//li[@class="pagination__next"]/a')
......@@ -1111,6 +1111,11 @@ class ErrorPage(HTMLPage):
raise ActionNeeded(error)
class MinorPage(HTMLPage):
def get_error_message(self):
return CleanText('//div[@id="modal-main-content"]//p')(self.doc)
class ExpertPage(LoggedPage, HTMLPage):
......@@ -1287,6 +1292,9 @@ class TransferRecipients(LoggedPage, HTMLPage):
form['CreditAccount[creditAccountKey]'] = tempid
def is_new_recipient_allowed(self):
return True
class NewTransferWizard(LoggedPage, HTMLPage):
def get_errors(self):
......@@ -1333,15 +1341,22 @@ class NewTransferWizard(LoggedPage, HTMLPage):
klass = Recipient
obj_id = CleanText(
'.//span[contains(@class, "sub-label")]/span[not(contains(@class,"sub-label"))]',
'.//span[contains(@class, "account-sub-label")]/span[not(contains(@class,"account-before-sub-label"))]',
replace=[(' ', '')],
obj_label = CleanText(Regexp(
CleanText('.//span[contains(@class, "account-label")]'),
# bank name finish with the following text " •"
obj_bank_name = CleanText('.//span[contains(@class, "account-before-sub-label")]', symbols=['•'])
def obj_label(self):
bank_name = Field('bank_name')(self)
label = CleanText('.//span[contains(@class, "account-label")]')(self)
# Sometimes, Boursorama appends the bank name at the end of the label
if not empty(bank_name):
label = label.replace('- %s' % bank_name, '').strip()
return label
def obj_category(self):
text = CleanText(
......@@ -1373,6 +1388,13 @@ class NewTransferWizard(LoggedPage, HTMLPage):
def is_new_recipient_allowed(self):
except FormNotFound:
return False
return HasElement('//input[@id="CreditAccount_newBeneficiary"]')(self.doc)
# STEP 3 -
# If using existing recipient: select the amount
# If new beneficiary: select if new recipient is own account or third party one
......@@ -53,7 +53,7 @@ from .pages import (
SubscriptionPage, DownloadPage, ProSubscriptionPage,
TwoFAPage, Validated2FAPage, SmsPage, DecoupledPage, HonorTransferPage,
RecipientSubmitDevicePage, OtpErrorPage,
CerticodePlusSubmitDevicePage, OtpErrorPage,
from .pages.accounthistory import (
LifeInsuranceInvest, LifeInsuranceHistory, LifeInsuranceHistoryInv, RetirementHistory,
......@@ -281,8 +281,8 @@ class BPBrowser(LoginBrowser, StatesMixin):
# the following url is already used in transfer_summary
# but we need it to detect the case where the website displaies the list of devices
# The following url is already used in transfer_summary
# but we need it to detect the case where the website displays the list of devices
# when a transfer is made with an otp or decoupled
......@@ -301,9 +301,9 @@ class BPBrowser(LoginBrowser, StatesMixin):
recipient_submit_device = URL(
certicode_plus_submit_device = URL(
rcpt_code = URL(
......@@ -842,15 +842,26 @@ class BPBrowser(LoginBrowser, StatesMixin):
# If we just validated a code we land on transfer_summary.
# If we just initiated the transfer we land on transfer_confirm.
if self.transfer_confirm.is_here():
# This will send a sms if a certicode validation is needed
# This will send a sms or an app validation if a certicode
# or certicode+ validation is needed.
if self.transfer_confirm.is_here() and
if self.transfer_confirm.is_here():
self.need_reload_state = True
self.sms_form =
raise TransferStep(
Value('code', label='Veuillez saisir le code de validation reçu par SMS'),
self.sms_form =
raise TransferStep(
Value('code', label='Veuillez saisir le code de validation reçu par SMS'),
device_choice_url =
self.certicode_plus_submit_device.go(params={'deviceSelected': 0})
message =
raise AppValidation(
......@@ -879,7 +890,7 @@ class BPBrowser(LoginBrowser, StatesMixin):
return True
def end_new_recipient_with_polling(self, recipient):
def end_with_polling(self, obj):
polling_url = self.absurl(
......@@ -889,7 +900,7 @@ class BPBrowser(LoginBrowser, StatesMixin):
return recipient
return obj
def init_new_recipient(self, recipient, is_bp_account=False, **params):
......@@ -914,7 +925,7 @@ class BPBrowser(LoginBrowser, StatesMixin):
# force to use the first device like in the login to receive notification
# this url send mobile notification
self.recipient_submit_device.go(params={'deviceSelected': 0})
self.certicode_plus_submit_device.go(params={'deviceSelected': 0})
# Can do transfer to these recipient 48h after
recipient.enabled_at = + timedelta(days=2)
......@@ -928,7 +939,7 @@ class BPBrowser(LoginBrowser, StatesMixin):
def new_recipient(self, recipient, is_bp_account=False, **params):
if params.get('resume') or self.resume:
# Case of mobile app validation
return self.end_new_recipient_with_polling(recipient)
return self.end_with_polling(recipient)
if 'code' in params:
# Case of SMS OTP
......@@ -108,6 +108,8 @@ class BPModule(
return self.browser.validate_transfer_eligibility(transfer, **params)
elif 'code' in params:
return self.browser.validate_transfer_code(transfer, params['code'])
elif 'resume' in params:
return self.browser.end_with_polling(transfer)'Going to do a new transfer')
account = strict_find_object(self.iter_accounts(), iban=transfer.account_iban)
......@@ -29,7 +29,7 @@ from .transfer import (
TransferChooseAccounts, CompleteTransfer, TransferConfirm,
TransferSummary, CreateRecipient, ValidateRecipient,
ValidateCountry, ConfirmPage, RcptSummary,
HonorTransferPage, RecipientSubmitDevicePage,
HonorTransferPage, CerticodePlusSubmitDevicePage,
from .subscription import SubscriptionPage, DownloadPage, ProSubscriptionPage
......@@ -40,5 +40,5 @@ __all__ = [
'AccountDesactivate', 'TransferChooseAccounts', 'CompleteTransfer', 'TransferConfirm', 'TransferSummary', 'UnavailablePage',
'CardsList', 'AccountRIB', 'Advisor', 'CreateRecipient', 'ValidateRecipient', 'ValidateCountry', 'ConfirmPage', 'RcptSummary',
'SubscriptionPage', 'DownloadPage', 'ProSubscriptionPage', 'RevolvingAttributesPage', 'Validated2FAPage', 'TwoFAPage',
'SmsPage', 'DecoupledPage', 'HonorTransferPage', 'RecipientSubmitDevicePage', 'OtpErrorPage',
'SmsPage', 'DecoupledPage', 'HonorTransferPage', 'CerticodePlusSubmitDevicePage', 'OtpErrorPage',
......@@ -224,6 +224,16 @@ class TransferConfirm(LoggedPage, CheckTransferError):
def is_certicode_needed(self):
return CleanText('//div[contains(text(), "veuillez saisir votre code de validation reçu par SMS")]')(self.doc)
def is_certicode_plus_needed(self):
return CleanText('//script[contains(text(), "popupChoixDevice")]')(self.doc)