diff --git a/modules/americanexpress/pages.py b/modules/americanexpress/pages.py
index b77d9f1bb8e7b63a212a1dab56b9197cc6477bc6..f91f2fc477e5eaad114f43f870ba0de2e2e23b0a 100644
--- a/modules/americanexpress/pages.py
+++ b/modules/americanexpress/pages.py
@@ -193,7 +193,7 @@ def obj_date(self):
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):
diff --git a/modules/anticaptcha/browser.py b/modules/anticaptcha/browser.py
index 6967c8c905928a9dc5ba1d792f96f7754598e07d..902e0429ab656ee6a1ca497afd6846f51359d880 100644
--- a/modules/anticaptcha/browser.py
+++ b/modules/anticaptcha/browser.py
@@ -23,9 +23,9 @@
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 @@ def post_recaptcha(self, url, key):
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 @@ def poll(self, job):
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']
diff --git a/modules/anticaptcha/compat/weboob_capabilities_captcha.py b/modules/anticaptcha/compat/weboob_capabilities_captcha.py
new file mode 100644
index 0000000000000000000000000000000000000000..dcb89fc96a74b6434485773864bb5ccd25f730a0
--- /dev/null
+++ b/modules/anticaptcha/compat/weboob_capabilities_captcha.py
@@ -0,0 +1,202 @@
+# -*- 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
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# 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__ = [
+ 'CapCaptchaSolver',
+ 'SolverJob', 'RecaptchaJob', 'NocaptchaJob', 'ImageCaptchaJob', 'HcaptchaJob',
+ 'CaptchaError', 'UnsolvableCaptcha', 'InvalidCaptcha', 'InsufficientFunds',
+ 'exception_to_job',
+]
+
+
+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
+ else:
+ raise NotImplementedError()
+
+ return job
+
+
+from weboob.capabilities.captcha import CapCaptchaSolver as _CapCaptchaSolver
+class CapCaptchaSolver(_CapCaptchaSolver):
+ """
+ Provide CAPTCHA solving
+ """
+
+ RETRIES = 30
+ WAIT_TIME = 2
+
+ def create_job(self, job):
+ """Start a CAPTCHA solving job
+
+ The `job.id` 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`
+ """
+
+ self.create_job(job)
+ for i in range(self.RETRIES):
+ sleep(self.WAIT_TIME)
+ 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()
+
diff --git a/modules/anticaptcha/module.py b/modules/anticaptcha/module.py
index 94327438b594f690b30739770e105a6655b31054..ffcd0a22904c750f7747a6dabd118568a3c411ff 100644
--- a/modules/anticaptcha/module.py
+++ b/modules/anticaptcha/module.py
@@ -21,8 +21,9 @@
from weboob.tools.backend 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,
+ HcaptchaJob,
)
from .compat.weboob_tools_value import ValueBackendPassword
@@ -61,6 +62,8 @@ def create_job(self, job):
job.id = self.browser.post_nocaptcha(job.site_url, job.site_key)
elif isinstance(job, FuncaptchaJob):
job.id = self.browser.post_funcaptcha(job.site_url, job.site_key, job.sub_domain)
+ elif isinstance(job, HcaptchaJob):
+ job.id = self.browser.post_hcaptcha(job.site_url, job.site_key)
else:
raise NotImplementedError()
diff --git a/modules/aviva/pages/detail_pages.py b/modules/aviva/pages/detail_pages.py
index 97c12a131840d743e57160f62b9d4fad96715006..d19a33106300f843472d1183feca75f3acbe9e4f 100644
--- a/modules/aviva/pages/detail_pages.py
+++ b/modules/aviva/pages/detail_pages.py
@@ -83,6 +83,12 @@ class fill_account(ItemElement):
)
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),
+ default=NotAvailable
+ )
def get_history_link(self):
history_link = self.doc.xpath('//li/a[contains(text(), "Historique")]/@href')
diff --git a/modules/axabanque/pages/wealth.py b/modules/axabanque/pages/wealth.py
index 4f22f9bd331677699b3913f9c94e60332b5d5341..8da92fd9d2595be4a37be391b58739a7e18efd37 100644
--- a/modules/axabanque/pages/wealth.py
+++ b/modules/axabanque/pages/wealth.py
@@ -178,8 +178,8 @@ class item(ItemElement):
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)
diff --git a/modules/bnpcards/browser.py b/modules/bnpcards/browser.py
index 95590af48413433843e3a74bd99d37241d665ebe..e08f25c717940a3efc14355abcad0ca30b0056c8 100644
--- a/modules/bnpcards/browser.py
+++ b/modules/bnpcards/browser.py
@@ -24,6 +24,8 @@
from weboob.tools.capabilities.bank.transactions import sorted_transactions
from weboob.tools.compat import basestring
+from .corporate.browser import BnpcartesentrepriseCorporateBrowser
+
from .pages import (
LoginPage, ErrorPage, AccountsPage, TransactionsPage,
TiCardPage, TiHistoPage, ComingPage, HistoPage, HomePage,
@@ -70,6 +72,8 @@ def __init__(self, type, *args, **kwargs):
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 @@ def do_login(self):
raise BrowserPasswordExpired(self.page.get_error_msg())
if self.type == '2' and self.page.is_corporate():
self.logger.info('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 self.page.url:
self.logger.info('Carholder corporate connection')
diff --git a/modules/bnpcards/corporate/pages.py b/modules/bnpcards/corporate/pages.py
index f760cbd8345cd33253bdcd0a8178723028f80769..329cde06d0c11dcc8938a39e618603528fb8cb58 100644
--- a/modules/bnpcards/corporate/pages.py
+++ b/modules/bnpcards/corporate/pages.py
@@ -65,6 +65,7 @@ class item(ItemElement):
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
@pagination
def get_link(self, account_id, owner):
diff --git a/modules/bnpcards/module.py b/modules/bnpcards/module.py
index bd853813e39142f701655d00843946bfd5928f51..d46f9011deec3748c0aa5d2fe7a2951ea8c1ae8a 100644
--- a/modules/bnpcards/module.py
+++ b/modules/bnpcards/module.py
@@ -65,13 +65,29 @@ def iter_accounts(self):
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
+ else:
+ 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
+ else:
+ get_transactions = self.browser.get_transactions
+
+ for tr in get_transactions(account):
+ if not tr._coming:
+ break
+ yield tr
diff --git a/modules/boursorama/browser.py b/modules/boursorama/browser.py
index 6ea72b8c2006233e5317711e16f07389a40a3bb8..7514c0630ba398bf8077996434d47501b123fcf9 100644
--- a/modules/boursorama/browser.py
+++ b/modules/boursorama/browser.py
@@ -40,6 +40,7 @@
TransferInvalidEmitter, TransferInvalidLabel, TransferInvalidRecipient,
AddRecipientStep, Rate, TransferBankError, AccountOwnership, RecipientNotFound,
AddRecipientTimeout, TransferDateType, Emitter, TransactionType,
+ AddRecipientBankError,
)
from weboob.capabilities.base import empty, find_object
from weboob.capabilities.contact import Advisor
@@ -55,7 +56,7 @@
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):
ErrorPage
)
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 @@ def init_login(self):
self.login.go()
self.page.enter_password(self.username, self.password)
- if self.error.is_here():
+ if self.minor.is_here():
+ raise NoAccountsException(self.page.get_error_message())
+ elif self.error.is_here():
raise BrowserIncorrectPassword()
elif self.login.is_here():
error = self.page.get_error()
@@ -768,26 +771,46 @@ def execute_transfer(self, transfer, **kwargs):
@need_login
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:
+ continue
+ if recipient.origin_account_id is None:
+ if account.type == Account.TYPE_CHECKING:
+ break
+ elif account.id == recipient.origin_account_id:
break
-
- suffix = 'virements/comptes-externes/nouveau'
- if account.url.endswith('/'):
- target = account.url + suffix
else:
- target = account.url + '/' + suffix
+ raise AddRecipientBankError(message="Compte ne permettant pas l'ajout de bénéficiaires")
+
+ try:
+ self.go_recipients_list(account.url, account.id)
+ except AccountNotFound:
+ raise AddRecipientBankError(message="Compte ne permettant pas d'emettre des virements")
+ assert (
+ self.recipients_page.is_here()
+ or self.new_transfer_wizard.is_here()
+ ), 'Should be on recipients page'
+
+ if not self.page.is_new_recipient_allowed():
+ raise AddRecipientBankError(message="Compte ne permettant pas l'ajout de bénéficiaires")
+
+ target = '%s/virements/comptes-externes/nouveau' % account.url.rstrip('/')
self.location(target)
+
assert self.page.is_characteristics(), 'Not on the page to add recipients.'
# fill recipient form
self.page.submit_recipient(recipient)
- recipient.origin_account_id = account.id
+ if recipient.origin_account_id is None:
+ recipient.origin_account_id = account.id
# confirm sending sms
assert self.page.is_confirm_send_sms(), 'Cannot reach the page asking to send a sms.'
@@ -802,7 +825,8 @@ def init_new_recipient(self, recipient):
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 @@ def new_recipient(self, recipient, **kwargs):
# 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 @@ def check_and_update_recipient(self, recipient, account_url):
# 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(self.page.iter_recipients(), id=recipient.id, error=RecipientNotFound)
+ return find_object(self.page.iter_recipients(), iban=recipient.iban, error=RecipientNotFound)
@need_login
def iter_transfers(self, account):
diff --git a/modules/boursorama/pages.py b/modules/boursorama/pages.py
index 2558a0ac7bc1a9a73626f830258cf9976928a677..ccd69f75006d3b178536f5ca473532a18b7836d5 100644
--- a/modules/boursorama/pages.py
+++ b/modules/boursorama/pages.py
@@ -40,7 +40,7 @@
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 iter_history(TableElement):
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 @@ def on_load(self):
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):
pass
@@ -1287,6 +1292,9 @@ def submit_recipient(self, tempid):
form['CreditAccount[creditAccountKey]'] = tempid
form.submit()
+ def is_new_recipient_allowed(self):
+ return True
+
class NewTransferWizard(LoggedPage, HTMLPage):
def get_errors(self):
@@ -1333,15 +1341,22 @@ class item(ItemElement):
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")]'),
- r'([^-]+)',
- '\\1',
- ))
+ # 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 @@ def submit_recipient(self, tempid):
form.submit()
+ def is_new_recipient_allowed(self):
+ try:
+ self.get_form(name='CreditAccount')
+ 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
diff --git a/modules/bp/browser.py b/modules/bp/browser.py
index bf6c815247721c1eadcd1b2f2b36507da55222c3..5f26569056e29f24e02a446767bc89a926fae4d8 100644
--- a/modules/bp/browser.py
+++ b/modules/bp/browser.py
@@ -53,7 +53,7 @@
SubscriptionPage, DownloadPage, ProSubscriptionPage,
RevolvingAttributesPage,
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):
r'/voscomptes/canalXHTML/virement/virementSafran_sepa/confirmerInformations-virementSepa.ea',
r'/voscomptes/canalXHTML/virement/virementSafran_national/valider-creerVirementNational.ea',
r'/voscomptes/canalXHTML/virement/virementSafran_national/validerVirementNational-virementNational.ea',
- # 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
r'/voscomptes/canalXHTML/virement/virementSafran_sepa/confirmer-creerVirementSepa.ea',
TransferConfirm
@@ -301,9 +301,9 @@ class BPBrowser(LoginBrowser, StatesMixin):
r'/voscomptes/canalXHTML/virement/mpiGestionBeneficiairesVirementsCreationBeneficiaire/valider-creationBeneficiaire.ea',
ValidateRecipient
)
- recipient_submit_device = URL(
+ certicode_plus_submit_device = URL(
r'/voscomptes/canalXHTML/securisation/mpin/demandeCreation-securisationMPIN.ea',
- RecipientSubmitDevicePage
+ CerticodePlusSubmitDevicePage
)
rcpt_code = URL(
r'/voscomptes/canalXHTML/virement/mpiGestionBeneficiairesVirementsCreationBeneficiaire/validerRecapBeneficiaire-creationBeneficiaire.ea',
@@ -842,15 +842,26 @@ def execute_transfer(self, transfer):
# 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.
self.page.confirm()
- if self.transfer_confirm.is_here() and self.page.is_certicode_needed():
+ if self.transfer_confirm.is_here():
self.need_reload_state = True
- self.sms_form = self.page.get_sms_form()
- raise TransferStep(
- transfer,
- Value('code', label='Veuillez saisir le code de validation reçu par SMS'),
- )
+ if self.page.is_certicode_needed():
+ self.sms_form = self.page.get_sms_form()
+ raise TransferStep(
+ transfer,
+ Value('code', label='Veuillez saisir le code de validation reçu par SMS'),
+ )
+ elif self.page.is_certicode_plus_needed():
+ device_choice_url = self.page.get_device_choice_url()
+ self.location(device_choice_url)
+ self.certicode_plus_submit_device.go(params={'deviceSelected': 0})
+ message = self.page.get_app_validation_message()
+ raise AppValidation(
+ resource=transfer,
+ message=message,
+ )
return self.page.handle_response(transfer)
@@ -879,7 +890,7 @@ def post_code(self, code):
return True
- def end_new_recipient_with_polling(self, recipient):
+ def end_with_polling(self, obj):
polling_url = self.absurl(
'/voscomptes/canalXHTML/securisation/mpin/validerOperation-securisationMPIN.ea',
base=True
@@ -889,7 +900,7 @@ def end_new_recipient_with_polling(self, recipient):
'/voscomptes/canalXHTML/securisation/mpin/operationSucces-securisationMPIN.ea',
base=True
))
- return recipient
+ return obj
@need_login
def init_new_recipient(self, recipient, is_bp_account=False, **params):
@@ -914,7 +925,7 @@ def init_new_recipient(self, recipient, is_bp_account=False, **params):
self.location(device_choice_url)
# 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 = datetime.now().replace(microsecond=0) + timedelta(days=2)
@@ -928,7 +939,7 @@ def init_new_recipient(self, recipient, is_bp_account=False, **params):
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
diff --git a/modules/bp/module.py b/modules/bp/module.py
index cb1782e122e065ea953ecd37ed092427d925a5c4..37a9896cd9f1fa9bb6d0461190cfb80a7c3494a3 100644
--- a/modules/bp/module.py
+++ b/modules/bp/module.py
@@ -108,6 +108,8 @@ def init_transfer(self, transfer, **params):
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)
self.logger.info('Going to do a new transfer')
account = strict_find_object(self.iter_accounts(), iban=transfer.account_iban)
diff --git a/modules/bp/pages/__init__.py b/modules/bp/pages/__init__.py
index a2ad2d3bc30a4adbd62f1a38a7f82d95111c0fd4..2423993f022b1bebbe0aa7b91850c42e386dc7db 100644
--- a/modules/bp/pages/__init__.py
+++ b/modules/bp/pages/__init__.py
@@ -29,7 +29,7 @@
TransferChooseAccounts, CompleteTransfer, TransferConfirm,
TransferSummary, CreateRecipient, ValidateRecipient,
ValidateCountry, ConfirmPage, RcptSummary,
- HonorTransferPage, RecipientSubmitDevicePage,
+ HonorTransferPage, CerticodePlusSubmitDevicePage,
OtpErrorPage,
)
from .subscription import SubscriptionPage, DownloadPage, ProSubscriptionPage
@@ -40,5 +40,5 @@
'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',
]
diff --git a/modules/bp/pages/transfer.py b/modules/bp/pages/transfer.py
index 45c124275b09fbf104c06a8bad027df5f9338700..01d28dbeb3d3d03087040e3030eb9de692d43d9a 100644
--- a/modules/bp/pages/transfer.py
+++ b/modules/bp/pages/transfer.py
@@ -224,6 +224,16 @@ def is_here(self):
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)
+
+ def get_device_choice_url(self):
+ device_choice_popup_js = CleanText('//script[contains(text(), "popupChoixDevice")]')(self.doc)
+ if device_choice_popup_js:
+ device_choice_url = re.search(r'(?<=urlPopin = )\"(.*popUpDeviceChoice\.jsp)\";', device_choice_popup_js)
+ if device_choice_url:
+ return device_choice_url.group(1)
+
def get_sms_form(self):
form = self.get_form(name='SaisieOTP')
# Confirmation url is relative to the current page. We need to
@@ -414,7 +424,7 @@ def get_error(self):
return CleanText('//form//span[@class="warning" or @class="app_erreur"]')(self.doc)
-class RecipientSubmitDevicePage(LoggedPage, MyHTMLPage):
+class CerticodePlusSubmitDevicePage(LoggedPage, MyHTMLPage):
def get_app_validation_message(self):
# Mobile app message is too long, like this:
# """ Une notification vous a été envoyée sur l’appareil que vous avez choisi: [PHONE].
@@ -426,7 +436,7 @@ def get_app_validation_message(self):
app_validation_message = CleanText(
'//main[@id="main"]//div[contains(text(), "Une notification vous a")]'
)(self.doc)
- assert app_validation_message, 'The notification message for new recipient is missing'
+ assert app_validation_message, 'The notification message is missing'
msg_first_part = re.search(r'(.*)\. Vous pouvez', app_validation_message)
if msg_first_part:
diff --git a/modules/cmb/par/browser.py b/modules/cmb/par/browser.py
index 1e7d0ca7d0b01f7d54ad935cda3c485cbb6b8171..e7b3222b8dd7a2c224479d2520823c0250073207 100644
--- a/modules/cmb/par/browser.py
+++ b/modules/cmb/par/browser.py
@@ -25,6 +25,13 @@ class CmbParBrowser(AbstractBrowser):
PARENT_ATTR = 'package.par.browser.CmsoParBrowser'
BASEURL = 'https://api.cmb.fr'
+ authorization_uri = 'https://api.cmb.fr/oauth/authorize'
+ access_token_uri = 'https://api.cmb.fr/oauth/token'
+ authorization_codegen_uri = 'https://api.cmb.fr/oauth/authorization-code'
+ redirect_uri = 'https://mon.cmb.fr/auth/checkuser'
+ error_uri = 'https://mon.cmb.fr/auth/errorauthn'
+ client_uri = 'com.arkea.cmb.siteaccessible'
+
name = 'cmb'
arkea = '01'
arkea_si = '001'
diff --git a/modules/cmso/par/browser.py b/modules/cmso/par/browser.py
index 992e5d968739f060597b6b96629e79ca3ec2d9cf..d8093240052a1abefeb2b5cdcc2f2332d3fdda55 100644
--- a/modules/cmso/par/browser.py
+++ b/modules/cmso/par/browser.py
@@ -22,8 +22,11 @@
from __future__ import unicode_literals
import time
+import os
+import base64
from datetime import date
from functools import wraps
+from hashlib import sha256
from .compat.weboob_browser_browsers import TwoFactorBrowser, URL, need_login
from weboob.browser.exceptions import ClientError, ServerError
@@ -88,7 +91,6 @@ class CmsoParBrowser(TwoFactorBrowser):
BASEURL = 'https://api.cmso.com'
login = URL(
- r'/oauth-implicit/token',
r'/auth/checkuser',
LoginPage
)
@@ -136,6 +138,13 @@ class CmsoParBrowser(TwoFactorBrowser):
json_headers = {'Content-Type': 'application/json'}
+ authorization_uri = 'https://api.cmso.com/oauth/authorize'
+ access_token_uri = 'https://api.cmso.com/oauth/token'
+ authorization_codegen_uri = 'https://api.cmso.com/oauth/authorization-code'
+ redirect_uri = 'https://mon.cmso.com/auth/checkuser'
+ error_uri = 'https://mon.cmso.com/auth/errorauthn'
+ client_uri = 'com.arkea.cmso.siteaccessible'
+
# Values needed for login which are specific for each arkea child
name = 'cmso'
arkea = '03'
@@ -158,22 +167,59 @@ def __init__(self, website, config, *args, **kwargs):
'code': self.handle_sms,
}
+ def code_challenge(self, verifier):
+ digest = sha256(verifier.encode('utf8')).digest()
+ return base64.b64encode(digest).decode('ascii')
+
+ def code_verifier(self):
+ return base64.b64encode(os.urandom(128)).decode('ascii')
+
def init_login(self):
self.location(self.original_site)
if self.headers:
self.session.headers = self.headers
else:
- self.set_profile(self.PROFILE) # reset headers but don't clear them
self.session.cookies.clear()
self.accounts_list = []
- data = self.get_login_data()
- self.login.go(data=data)
+ # authorization request
+ verifier = self.code_verifier()
+ challenge = self.code_challenge(verifier)
+ params = {
+ 'redirect_uri': self.redirect_uri,
+ 'client_id': self.arkea_client_id,
+ 'response_type': 'code',
+ 'error_uri': self.error_uri,
+ 'code_challenge_method': 'S256',
+ 'code_challenge': challenge,
+ }
+ response = self.location(self.authorization_uri, params=params)
+
+ # get session_id in param location url
+ location_params = dict(parse_qsl(urlparse(response.headers['Location']).fragment))
+
+ self.set_profile(self.PROFILE) # reset headers but don't clear them
+
+ # authorization-code generation
+ data = self.get_authcode_data()
+ response = self.location(self.authorization_codegen_uri, data=data, params=location_params)
+ location_params = dict(parse_qsl(urlparse(response.headers['Location']).fragment))
- if self.logout.is_here():
- raise BrowserIncorrectPassword()
+ if location_params.get('error'):
+ if location_params.get('error_description') == 'authentication-failed':
+ raise BrowserIncorrectPassword()
+ # we encounter this case when an error comes from the website
+ elif location_params['error'] == 'server_error':
+ raise BrowserUnavailable()
- self.update_authentication_headers()
+ # authentication token generation
+ json = self.get_tokengen_data(location_params['code'], verifier)
+ response = self.location(self.access_token_uri, json=json)
+ self.update_authentication_headers(response.json())
+
+ if location_params.get('scope') == 'consent':
+ self.check_interactive()
+ self.send_sms()
def send_sms(self):
contact_information = self.location('/securityapi/person/coordonnees', method='POST').json()
@@ -204,9 +250,9 @@ def get_sms_data(self):
'otpValue': self.code,
'typeMedia': 'WEB',
'userAgent': 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0',
- 'redirectUri': '%s/auth/checkuser' % self.original_site,
- 'errorUri': '%s/auth/errorauthn' % self.original_site,
- 'clientId': 'com.arkea.%s.siteaccessible' % self.name,
+ 'redirectUri': self.redirect_uri,
+ 'errorUri': self.error_uri,
+ 'clientId': self.client_uri,
'redirect': 'true',
'client_id': self.arkea_client_id,
'accessInfos': {
@@ -215,38 +261,28 @@ def get_sms_data(self):
},
}
- def get_login_data(self):
+ def get_authcode_data(self):
return {
- 'client_id': self.arkea_client_id,
- 'responseType': 'token',
- 'accessCode': self.username,
+ 'access_code': self.username,
'password': self.password,
- 'clientId': 'com.arkea.%s.siteaccessible' % self.name,
- 'redirectUri': '%s/auth/checkuser' % self.original_site,
- 'errorUri': '%s/auth/errorauthn' % self.original_site,
- 'fingerprint': 'b61a924d1245beb7469fef44db132e96',
}
- def update_authentication_headers(self):
- hidden_params = dict(parse_qsl(urlparse(self.url).fragment))
+ def get_tokengen_data(self, code, verifier):
+ return {
+ 'client_id': self.arkea_client_id,
+ 'code': code,
+ 'grant_type': 'authorization_code',
+ 'code_verifier': verifier,
+ 'redirect_uri': self.redirect_uri,
+ }
- self.session.headers.update({
- 'Authorization': "Bearer %s" % hidden_params['access_token'],
- 'X-ARKEA-EFS': self.arkea,
- 'X-Csrf-Token': hidden_params['access_token'],
- 'X-REFERER-TOKEN': 'RWDPART',
- })
+ def update_authentication_headers(self, params):
+ self.session.headers['Authorization'] = "Bearer %s" % params['access_token']
+ self.session.headers['X-ARKEA-EFS'] = self.arkea
+ self.session.headers['X-Csrf-Token'] = params['access_token']
+ self.session.headers['X-REFERER-TOKEN'] = 'RWDPART'
self.headers = self.session.headers
- scope = hidden_params.get('scope')
-
- # if there is no scope, 2FA is not needed
- if scope and scope == 'consent':
- # 2FA is needed
- # consent is the only scope that should send a sms
- self.check_interactive()
- self.send_sms()
-
def get_account(self, _id):
return find_object(self.iter_accounts(), id=_id, error=AccountNotFound)
@@ -375,7 +411,7 @@ def iter_history(self, account):
elif account.type in (Account.TYPE_PEA, Account.TYPE_MARKET):
try:
self._go_market_history('historiquePortefeuille')
- if not self.page.go_account(account.label, account._owner):
+ if not self.page.go_account(account.id):
return
if not self.page.go_account_full():
@@ -396,16 +432,25 @@ def iter_history(self, account):
finally:
self._return_from_market()
- # Getting a year of history
- # We have to finish by "SIX_DERNIERES_SEMAINES" to get in priority the transactions with ids.
- # In "SIX_DERNIERES_SEMAINES" you can have duplicates transactions without ids of the previous two months.
- nbs = ["DEUX", "TROIS", "QUATRE", "CINQ", "SIX", "SEPT", "HUIT", "NEUF", "DIX", "ONZE", "DOUZE", "SIX_DERNIERES_SEMAINES"]
- trs = []
-
self.history.go(json={"index": account._index}, page="pendingListOperations")
-
has_deferred_cards = self.page.has_deferred_cards()
+ # 1.fetch the last 6 weeks transactions but keep only the current month ones
+ # those don't have any id and include 'hier' and 'Plus tôt dans la semaine'
+ trs = []
+ self.history.go(
+ json={
+ 'index': account._index,
+ 'filtreOperationsComptabilisees': "SIX_DERNIERES_SEMAINES",
+ },
+ page="detailcompte"
+ )
+ for tr in self.page.iter_history(index=account._index, last_trs=True):
+ trs.append(tr)
+
+ # 2. get the month by month transactions
+ # and avoid duplicates based on ids
+ nbs = ["DEUX", "TROIS", "QUATRE", "CINQ", "SIX", "SEPT", "HUIT", "NEUF", "DIX", "ONZE", "DOUZE"]
self.history.go(
json={
'index': account._index,
@@ -414,7 +459,6 @@ def iter_history(self, account):
page="detailcompte"
)
self.trs = set()
-
for tr in self.page.iter_history(index=account._index, nbs=nbs):
# Check for duplicates
if tr._operationid in self.trs or (tr.id and tr.id in self.trs):
@@ -475,7 +519,7 @@ def iter_investment(self, account):
elif account.type in (Account.TYPE_MARKET, Account.TYPE_PEA):
try:
self._go_market_history('situationPortefeuille')
- if self.page.go_account(account.label, account._owner):
+ if self.page.go_account(account.id):
return self.page.iter_investment()
return []
finally:
@@ -490,7 +534,7 @@ def iter_market_orders(self, account):
try:
self._go_market_history('carnetOrdre')
- if self.page.go_account(account.label, account._owner):
+ if self.page.go_account(account.id):
orders_list_url = self.url
error_message = self.page.get_error_message()
if error_message:
diff --git a/modules/cmso/par/pages.py b/modules/cmso/par/pages.py
index c44b396e1807d4e4a70e509da2ee4a44a163d855..15dad402f4c313793ec0ea1aa61bb325b30afb92 100644
--- a/modules/cmso/par/pages.py
+++ b/modules/cmso/par/pages.py
@@ -261,7 +261,7 @@ def obj_id(self):
if number:
return number
elif type in (Account.TYPE_PEA, Account.TYPE_MARKET):
- number = self.get_market_number()
+ number = Dict('idTechnique')(self)[5:] # first 5 characters are the bank id
if number:
return number
@@ -287,14 +287,6 @@ def obj_ownership(self):
return AccountOwnership.OWNER
return AccountOwnership.ATTORNEY
- def get_market_number(self):
- label = Field('label')(self)
- try:
- page = self.page.browser._go_market_history('historiquePortefeuille')
- return page.get_account_id(label, Field('_owner')(self))
- finally:
- self.page.browser._return_from_market()
-
def get_lifenumber(self):
index = Dict('index')(self)
data = json.loads(self.page.browser.redirect_insurance.open(accid=index).text)
@@ -417,9 +409,8 @@ class iter_history(DictElement):
def next_page(self):
if len(Env('nbs', default=[])(self)):
data = {'index': Env('index')(self)}
- if Env('nbs')(self)[0] != "SIX_DERNIERES_SEMAINES":
- data.update({'filtreOperationsComptabilisees': "MOIS_MOINS_%s" % Env('nbs')(self)[0]})
- Env('nbs')(self).pop(0)
+ next_month = Env('nbs')(self).pop(0)
+ data.update({'filtreOperationsComptabilisees': "MOIS_MOINS_%s" % next_month})
return requests.Request('POST', data=json.dumps(data))
def parse(self, el):
@@ -470,6 +461,12 @@ def parse(self, el):
break
self.obj._deferred_date = self.FromTimestamp().filter(deferred_date)
+ def validate(self, obj):
+ if Env('last_trs', default=None)(self):
+ # keep only current month transactions
+ return obj.date.month >= datetime.date.today().month
+ return True
+
class RedirectInsurancePage(LoggedPage, JsonPage):
def get_url(self):
@@ -583,7 +580,7 @@ def obj_diff_ratio(self):
class MarketPage(LoggedPage, HTMLPage):
- def find_account(self, acclabel, accowner):
+ def find_account(self, account_id):
# Depending on what we're fetching (history, invests or orders),
# the parameter to choose the account has a different name.
if 'carnetOrdre' in self.url:
@@ -591,23 +588,20 @@ def find_account(self, acclabel, accowner):
else:
param_name = 'indiceCompte'
# first name and last name may not be ordered the same way on market site...
- accowner = sorted(accowner.lower().split())
- def get_ids(ref, acclabel, accowner, param_name):
- ids = None
+ def get_ids(ref, account_id, param_name):
+ # Market account IDs contain 3 parts:
+ # - the first 5 and last 2 digits identify the account
+ # - the 9 digits in the middle identify the owner of the account
+ # These info are separated on the page so we need to get them from the id to match the account.
+ owner_id = account_id[5:14]
+ account_number = '%s%s' % (account_id[:5], account_id[-2:])
for a in self.doc.xpath('//a[contains(@%s, "%s")]' % (ref, param_name)):
self.logger.debug("get investment from %s" % ref)
- label = CleanText('.')(a)
- owner = CleanText('./ancestor::tr/preceding-sibling::tr[@class="LnMnTiers"][1]')(a)
- owner = re.sub(r' \(.+', '', owner)
- owner = sorted(owner.lower().split())
- if label == acclabel and owner == accowner:
- ids = list(
- re.search(r'%s[^\d]+(\d+).*idRacine[^\d]+(\d+)' % param_name, Attr('.', ref)(a)).groups()
- )
- ids.append(CleanText('./ancestor::td/preceding-sibling::td')(a))
- self.logger.debug("assign value to ids: {}".format(ids))
- return ids
+ number = CleanText('./ancestor::td/preceding-sibling::td')(a).replace(' ', '')
+ if number in (account_id, account_number):
+ index = re.search(r'%s[^\d]+(\d+).*idRacine' % param_name, Attr('.', ref)(a)).group(1)
+ return [index, owner_id, number]
# Check if history is present
if CleanText(default=None).filter(self.doc.xpath('//body/p[contains(text(), "indisponible pour le moment")]')):
@@ -615,22 +609,17 @@ def get_ids(ref, acclabel, accowner, param_name):
ref = CleanText(self.doc.xpath('//a[contains(@href, "%s")]' % param_name))(self)
if not ref:
- return get_ids('onclick', acclabel, accowner, param_name)
+ return get_ids('onclick', account_id, param_name)
else:
- return get_ids('href', acclabel, accowner, param_name)
-
- def get_account_id(self, acclabel, owner):
- account = self.find_account(acclabel, owner)
- if account:
- return account[2].replace(' ', '')
+ return get_ids('href', account_id, param_name)
- def go_account(self, acclabel, owner):
+ def go_account(self, account_id):
if 'carnetOrdre' in self.url:
param_name = 'idCompte'
else:
param_name = 'indiceCompte'
- ids = self.find_account(acclabel, owner)
+ ids = self.find_account(account_id)
if not ids:
return
diff --git a/modules/cragr/browser.py b/modules/cragr/browser.py
index e27c1a7988cac3468339ea8078568260ea078535..ba4e77bf4a28d11e1bb779cc75081307140415c6 100644
--- a/modules/cragr/browser.py
+++ b/modules/cragr/browser.py
@@ -715,7 +715,7 @@ def iter_investment(self, account):
if (
account.type == Account.TYPE_LIFE_INSURANCE
- and ('rothschild' in account.label.lower() or re.match(r'^open (perspective|strat)', account.label, re.I))
+ and re.match(r'(rothschild)|(^patrimoine st honor)|(^open (perspective|strat))', account.label, re.I)
):
# We must go to the right perimeter before trying to access the Life Insurance investments
self.go_to_account_space(account._contract)
diff --git a/modules/cragr/pages.py b/modules/cragr/pages.py
index 61e4cd123e519473ebd878f9176d7ba18698ce3f..ae186fa156a5f6cd3b215ea00939910bf98f1a77 100644
--- a/modules/cragr/pages.py
+++ b/modules/cragr/pages.py
@@ -576,6 +576,14 @@ class item(ItemElement):
# we do not use it.
obj_date = Date(CleanText(Dict('dateOperation')))
+ obj_label = CleanText(
+ Format(
+ '%s %s',
+ CleanText(Dict('libelleTypeOperation', default='')),
+ CleanText(Dict('libelleOperation'))
+ )
+ )
+
# Transactions in foreign currencies have no 'libelleTypeOperation'
# and 'libelleComplementaire' keys, hence the default values.
# The CleanText() gets rid of additional spaces.
@@ -604,11 +612,6 @@ def obj_rdate(self):
return rdate
return date
- obj_label = CleanText(
- Format(
- '%s %s', CleanText(Dict('libelleTypeOperation', default='')), CleanText(Dict('libelleOperation'))
- )
- )
obj_amount = Eval(float_to_decimal, Dict('montant'))
obj_type = Map(
CleanText(Dict('libelleTypeOperation', default='')), TRANSACTION_TYPES, Transaction.TYPE_UNKNOWN
@@ -655,8 +658,8 @@ class iter_card_history(DictElement):
class item(ItemElement):
klass = Transaction
- obj_raw = CleanText(Dict('libelleOperation'))
obj_label = CleanText(Dict('libelleOperation'))
+ obj_raw = Transaction.Raw(CleanText(Dict('libelleOperation')))
obj_amount = Eval(float_to_decimal, Dict('montant'))
obj_type = Transaction.TYPE_DEFERRED_CARD
obj_bdate = Field('rdate')
diff --git a/modules/creditdunord/pages.py b/modules/creditdunord/pages.py
index 35980240533ab06b32ac3774e1f54fc0d01d0416..3bef8aa5c103f451219c2b4a400b76ad6e65fab3 100755
--- a/modules/creditdunord/pages.py
+++ b/modules/creditdunord/pages.py
@@ -720,11 +720,19 @@ class item(ItemElement):
klass = Investment
obj_label = CleanText(TableCell('label', colspan=True))
- obj_valuation = MyDecimal(TableCell('valuation', colspan=True))
- obj_quantity = MyDecimal(TableCell('quantity', colspan=True))
- obj_unitvalue = MyDecimal(TableCell('unitvalue', colspan=True))
- obj_unitprice = MyDecimal(TableCell('unitprice', colspan=True))
- obj_portfolio_share = Eval(lambda x: x / 100, MyDecimal(TableCell('portfolio_share')))
+ obj_valuation = CleanDecimal.French(TableCell('valuation', colspan=True))
+ obj_quantity = CleanDecimal.French(TableCell('quantity', colspan=True), default=NotAvailable)
+ obj_unitvalue = CleanDecimal.French(TableCell('unitvalue', colspan=True), default=NotAvailable)
+ obj_unitprice = CleanDecimal.French(
+ TableCell('unitprice', colspan=True, default=None),
+ default=NotAvailable
+ )
+
+ def obj_portfolio_share(self):
+ portfolio_share_percent = CleanDecimal.French(TableCell('portfolio_share'), default=None)(self)
+ if portfolio_share_percent is not None:
+ return portfolio_share_percent / 100
+ return NotAvailable
def obj_code(self):
for code in Field('label')(self).split():
diff --git a/modules/creditmutuel/browser.py b/modules/creditmutuel/browser.py
index 903120c5c231d8a05b8ca2286bffb08f02cc9d32..1284d2398e9d43340aaac5282eee5e191071bd61 100644
--- a/modules/creditmutuel/browser.py
+++ b/modules/creditmutuel/browser.py
@@ -135,7 +135,7 @@ class CreditMutuelBrowser(TwoFactorBrowser):
r'/(?P.*)fr/validation/(?!change_password|verif_code|image_case|infos).*',
EmptyPage)
por = URL(
- r'/(?P.*)fr/banque/PORT_Synthese.aspx\?entete=1',
+ r'/(?P.*)fr/banque/SYNT_Synthese.aspx\?entete=1',
r'/(?P.*)fr/banque/PORT_Synthese.aspx',
r'/(?P.*)fr/banque/SYNT_Synthese.aspx',
PorPage
@@ -365,7 +365,10 @@ def check_redirections(self):
self.twofa_auth_state = {}
self.check_interactive()
elif location:
- self.location(location, allow_redirects=False)
+ allow_redirects = 'conditions-generales' in location
+ # Don't stay on this 302
+ # This URL is still caught by ConditionsPage
+ self.location(location, allow_redirects=allow_redirects)
def check_auth_methods(self):
self.getCurrentSubBank()
@@ -981,6 +984,8 @@ def init_transfer(self, transfer, account, recipient):
self.key_form = self.page.get_personal_key_card_code_form()
raise TransferStep(transfer, Value('Clé', label=self.page.get_question()))
+ elif self.page.needs_otp_validation():
+ raise AuthMethodNotImplemented("La validation des transferts avec un code sms n'est pas encore disponible.")
msg = self.page.get_validation_msg()
if msg:
diff --git a/modules/creditmutuel/pages.py b/modules/creditmutuel/pages.py
index a55d32b385ad751b8f8d4b77b0f2139c98e8ca4b..4361a204ae22719db5080f192e63a2475355f184 100644
--- a/modules/creditmutuel/pages.py
+++ b/modules/creditmutuel/pages.py
@@ -1608,22 +1608,25 @@ def condition(self):
self.env['id'] = CleanText('.//a', replace=[(' ', '')])(self)
self.env['balance'] = CleanDecimal.French(TableCell('balance'), default=None)(self)
is_total = 'TOTAL VALO' in CleanText('.')(self)
+ is_liquidity = (
+ 'LIQUIDITE' in CleanText(TableCell('raw_label'))(self)
+ or 'TOTAL Compte espèces' in CleanText('.')(self)
+ )
is_global_view = Env('id')(self) == 'Vueconsolidée'
has_empty_balance = Env('balance')(self) is None
return (
not is_total
+ and not is_liquidity
and not is_global_view
and not has_empty_balance
)
# This values are defined for other types of accounts
obj__is_inv = True
-
- # IDs on the old page were differentiated with 5 digits in front of the ID, but not here.
- # We still need to differentiate them so we add ".1" at the end.
- obj_id = Format('%s.1', Env('id'))
-
- obj_label = Base(TableCell('raw_label'), CleanText('.', children=False))
+ obj_label = Coalesce(
+ Base(TableCell('raw_label'), CleanText('.', children=False)),
+ Base(TableCell('raw_label'), CleanText('./span[not(.//a)]')),
+ )
obj_number = Base(TableCell('raw_label'), CleanText('./a', replace=[(' ', '')]))
obj_balance = Env('balance')
@@ -1631,7 +1634,11 @@ def condition(self):
obj_valuation_diff = CleanDecimal.French(TableCell('valuation_diff'), default=NotAvailable)
- obj__link_id = Regexp(Link('.//a', default=NotAvailable), r'ddp=([^&]*)', default=NotAvailable)
+ obj__link_id = Regexp(Link('.//a', default=''), r'ddp=([^&]*)', default=NotAvailable)
+
+ # IDs on the old page were differentiated with 5 digits in front of the ID, but not here.
+ # We still need to differentiate them so we add ".1" at the end.
+ obj_id = Format('%s.1', Env('id'))
def obj_type(self):
return self.page.get_type(Field('label')(self))
@@ -1925,9 +1932,9 @@ class item(ItemElement):
klass = MarketOrder
def condition(self):
- return 'Remboursement' not in CleanText('.')(self)
+ return Base(TableCell('direction'), Link('.//a', default=None))(self) is not None
- obj_id = Base(TableCell('direction'), Regexp(Link('.//a', default=NotAvailable), r'ref=([^&]+)'))
+ obj_id = Base(TableCell('direction'), Regexp(Link('.//a', default=''), r'ref=([^&]+)', default=None))
obj_direction = Map(
CleanText(TableCell('direction')),
MARKET_ORDER_DIRECTIONS,
@@ -2049,6 +2056,9 @@ class InternalTransferPage(LoggedPage, HTMLPage, AppValidationPage):
def needs_personal_key_card_validation(self):
return bool(CleanText('//div[contains(@class, "alerte")]/p[contains(text(), "Cette opération nécessite une sécurité supplémentaire")]')(self.doc))
+ def needs_otp_validation(self):
+ return bool(self.doc.xpath('//input[@name="otp_password"]'))
+
def can_transfer_pro(self, origin_account):
for li in self.doc.xpath('//ul[@id="idDetailsListCptDebiterVertical:ul"]//ul/li'):
if CleanText(li.xpath('.//span[@class="_c1 doux _c1"]'), replace=[(' ', '')])(self) in origin_account:
@@ -2331,7 +2341,7 @@ class VerifCodePage(LoggedPage, HTMLPage):
'55ffe065456d33e70152ad860154d190': 'C7',
'13a927f61873ba6f2615fb529608629f': 'C8',
'e48146297f68ce172b9d4092827fbd2c': 'D1',
- '92ee176c2ee21821066747ca22ab42f0': 'D2',
+ ('92ee176c2ee21821066747ca22ab42f0', 'fece6856f73a859cb2c17bbca6fd2c03'): 'D2',
'b405d1912ba172052c198b14b50db18f': 'D3',
'6a65689653e2465fc50e8765b8d5f89b': 'D4',
'de0f615ea01463a764e5031a696160a2': 'D5',
@@ -2342,14 +2352,14 @@ class VerifCodePage(LoggedPage, HTMLPage):
'c42126f7c01365992c2a99d6164c6599': 'E2',
'978172427932c2a2a867baa25eb68ee0': 'E3',
'837c374cba2c11cfea800aaff06ca0b1': 'E4',
- '041deaaff4b0d312f99afd5d9256af6c': 'E5',
+ ('041deaaff4b0d312f99afd5d9256af6c', 'f779b306a255a996739dbac816ad99f2'): 'E5',
'a3d2eea803f71200b851146d6f57998b': 'E6',
'9cd913b53b6cd028bd609b8546af9b0d': 'E7',
- '17308564239363735a6a9f34021d26a9': 'E8',
+ ('17308564239363735a6a9f34021d26a9', '173ce025e2ca0a9610954e438710db9a'): 'E8',
'89b913bc935a3788bf4fe6b35778a372': 'F1',
'7651835218b5a7538b5b9d20546d014b': 'F2',
'f32bcdac80720bf39927dde41a8a21b8': 'F3',
- '4ed222ecfd6676fcb6a4908ce915e63d': 'F4',
+ ('4ed222ecfd6676fcb6a4908ce915e63d', 'dc104bd7d4efffde4ccddb8d6eb9f219'): 'F4',
'4151f3c6531cde9bc6a1c44e89d9e47a': 'F5',
'6a2987e43cccc6a265c37aa73bb18703': 'F6',
'67f777297fec2040638378fae4113aa5': 'F7',
@@ -2367,7 +2377,7 @@ class VerifCodePage(LoggedPage, HTMLPage):
'cb4c92a05ef2c621b49b3b12bdc1676e': 'H3',
'641883bd5878f512b6bcd60c53872749': 'H4',
'9e5541bd54865ba57514466881b9db41': 'H5',
- '03cc8d41cdf5e3d8d7e3f11b25f1cd5c': 'H6',
+ ('03cc8d41cdf5e3d8d7e3f11b25f1cd5c', '0571d352020fde0463904e6e09c7f309'): 'H6',
'203ec0695ec93bfd947c33b41802562b': 'H7',
'cbd1e9d2276ecc9cd7e6cae9b0127d58': 'H8',
}
diff --git a/modules/hsbc/browser.py b/modules/hsbc/browser.py
index 5b2b9f831e1a848d0873456dfe46609bd7e23a3b..8fd8d78f1e5c4bf83f3015883fdedb925a58e4cb 100644
--- a/modules/hsbc/browser.py
+++ b/modules/hsbc/browser.py
@@ -33,7 +33,7 @@
from weboob.tools.compat import parse_qsl, urlparse
from .compat.weboob_tools_value import Value
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, BrowserQuestion
-from weboob.browser.browsers import URL, need_login
+from weboob.browser.browsers import URL, need_login,
from .compat.weboob_browser_browsers import TwoFactorBrowser
from weboob.browser.exceptions import HTTPNotFound
from weboob.capabilities.base import find_object
diff --git a/modules/hsbc/pages/account_pages.py b/modules/hsbc/pages/account_pages.py
index 4bc00ad9fdb07992f6a5a3a5daf82b98da1ba829..0edac09ac4bce7e045a61bbb7f194d59976244bd 100644
--- a/modules/hsbc/pages/account_pages.py
+++ b/modules/hsbc/pages/account_pages.py
@@ -141,11 +141,13 @@ def filter(self, text):
class AccountsPage(GenericLandingPage):
+ IS_HERE_CONDITIONS = '//p[contains(text(), "Tous mes comptes au ")]|//span[contains(text(), "Tous mes comptes au ")]'
+
def is_here(self):
return (
CleanText('//h1[contains(text(), "Synthèse")]')(self.doc)
or CleanText(
- '//p[contains(text(), "Tous mes comptes au ")]|//span[contains(text(), "Tous mes comptes au ")]'
+ self.IS_HERE_CONDITIONS
)(self.doc)
)
diff --git a/modules/lcl/browser.py b/modules/lcl/browser.py
index 0a7eef286619db092325136c5e9e2187b9343499..4a2c0230d11a9950719bc9a6f4511df785200511 100644
--- a/modules/lcl/browser.py
+++ b/modules/lcl/browser.py
@@ -33,7 +33,7 @@
AuthMethodNotImplemented, BrowserQuestion,
AppValidation, AppValidationCancelled, AppValidationExpired,
)
-from weboob.browser.browsers import URL, need_login
+from weboob.browser.browsers import URL, need_login,
from .compat.weboob_browser_browsers import TwoFactorBrowser
from weboob.browser.exceptions import ServerError, ClientError
from weboob.capabilities.base import NotAvailable
@@ -205,10 +205,6 @@ class LCLBrowser(TwoFactorBrowser):
IDENTIFIANT_ROUTING = 'CLI'
def __init__(self, config, *args, **kwargs):
- self.config = config
- kwargs['username'] = self.config['login'].get()
- kwargs['password'] = self.config['password'].get()
-
super(LCLBrowser, self).__init__(config, *args, **kwargs)
self.accounts_list = None
self.current_contract = None
diff --git a/modules/lcl/enterprise/browser.py b/modules/lcl/enterprise/browser.py
index 93d739d6a03f6682b9e534919f1b1379e803e820..a81bd54dd28f392d45e7205e348ccc86755e00e9 100644
--- a/modules/lcl/enterprise/browser.py
+++ b/modules/lcl/enterprise/browser.py
@@ -42,7 +42,7 @@ class LCLEnterpriseBrowser(LoginBrowser):
)
profile = URL('/outil/IQGA/FicheUtilisateur/maFicheUtilisateur', ProfilePage)
- def __init__(self, *args, **kwargs):
+ def __init__(self, config, *args, **kwargs):
super(LCLEnterpriseBrowser, self).__init__(*args, **kwargs)
self.accounts = None
self.owner_type = AccountOwnerType.ORGANIZATION
diff --git a/modules/lcl/module.py b/modules/lcl/module.py
index 18da2acd938fd6f685aef15924a95d22a1898a45..1425884c5d9728df0ff40ba2f2071f78ade5b5cf 100644
--- a/modules/lcl/module.py
+++ b/modules/lcl/module.py
@@ -108,6 +108,8 @@ def create_default_browser(self):
return self.create_browser(
self.config,
+ self.config['login'].get(),
+ self.config['password'].get()
)
def iter_accounts(self):
diff --git a/modules/lcl/pages.py b/modules/lcl/pages.py
index 5e703f1aae41a82d06509ed42f017d718ef765df..b680fec082ba98db5fd431626bca6890195b1ccb 100644
--- a/modules/lcl/pages.py
+++ b/modules/lcl/pages.py
@@ -819,6 +819,11 @@ def condition(self):
return True
+MARKET_TRANSACTION_TYPES = {
+ 'VIREMENT': Transaction.TYPE_TRANSFER,
+}
+
+
class BoursePage(LoggedPage, HTMLPage):
ENCODING = 'latin-1'
REFRESH_MAX = 0
@@ -977,7 +982,7 @@ class item(ItemElement):
klass = Transaction
obj_date = Date(CleanText(TableCell('date')), dayfirst=True)
- obj_type = Transaction.TYPE_BANK
+ obj_type = MapIn(Field('label'), MARKET_TRANSACTION_TYPES, Transaction.TYPE_BANK)
obj_amount = CleanDecimal(TableCell('amount'), replace_dots=True)
obj_investments = Env('investments')
diff --git a/modules/myfoncia/browser.py b/modules/myfoncia/browser.py
index 3c6f029d839dc06e82e9eb5ce55559e2d8e4e945..6bdaf17054b94ecfb1df4d1389e101cf5fe0529a 100644
--- a/modules/myfoncia/browser.py
+++ b/modules/myfoncia/browser.py
@@ -23,7 +23,7 @@
from weboob.browser import LoginBrowser, need_login, URL
from weboob.exceptions import BrowserIncorrectPassword
-from .pages import LoginPage, MonBienPage, MesChargesPage
+from .pages import LoginPage, MonBienPage, MesChargesPage, DocumentsPage
class MyFonciaBrowser(LoginBrowser):
@@ -32,6 +32,7 @@ class MyFonciaBrowser(LoginBrowser):
login = URL(r'/login', LoginPage)
monBien = URL(r'/espace-client/espace-de-gestion/mon-bien', MonBienPage)
mesCharges = URL(r'/espace-client/espace-de-gestion/mes-charges/(?P.+)', MesChargesPage)
+ documents = URL(r'/espace-client/espace-de-gestion/mes-documents/(?P.+)/(?P[A-Z])', DocumentsPage)
def do_login(self):
self.login.stay_or_go().do_login(self.username, self.password)
@@ -45,5 +46,14 @@ def get_subscriptions(self):
return self.monBien.stay_or_go().get_subscriptions()
@need_login
- def get_documents(self, subscription):
- return self.mesCharges.stay_or_go(subscription=subscription).get_documents()
+ def get_documents(self, subscription_id):
+ # the last char of subscription_id is a letter, we need this to put this at the end of the url
+ if not subscription_id[-1:].isupper():
+ self.logger.debug('The last char of subscription id is not an uppercase')
+ self.documents.go(subscription=subscription_id, letter=subscription_id[-1:])
+ for doc in self.page.iter_documents():
+ yield doc
+
+ self.mesCharges.go(subscription=subscription_id)
+ for bill in self.page.get_documents():
+ yield bill
diff --git a/modules/myfoncia/module.py b/modules/myfoncia/module.py
index bcee3cc46cbbbcb7edd57439b5c6b252da0ce53c..0f1128d5206196dfcf9a4d4c6b1985ce28f067da 100644
--- a/modules/myfoncia/module.py
+++ b/modules/myfoncia/module.py
@@ -22,9 +22,11 @@
from weboob.tools.backend import Module, BackendConfig
from weboob.capabilities.base import find_object
-from weboob.capabilities.bill import (CapDocument, Bill, DocumentNotFound,
- Subscription)
-from .compat.weboob_tools_value import Value, ValueBackendPassword
+from weboob.capabilities.bill import (
+ CapDocument, DocumentNotFound,
+ Subscription, DocumentTypes, Document,
+)
+from .compat.weboob_tools_value import ValueBackendPassword
from .browser import MyFonciaBrowser
@@ -40,20 +42,18 @@ class MyFonciaModule(Module, CapDocument):
LICENSE = 'LGPLv3+'
VERSION = '2.0'
CONFIG = BackendConfig(
- Value(
- 'login',
- label='Email address or Foncia ID'
- ),
- ValueBackendPassword(
- 'password',
- label='Password'
- )
+ ValueBackendPassword('login', label='Email address or Foncia ID'),
+ ValueBackendPassword('password', label='Password'),
)
BROWSER = MyFonciaBrowser
+ accepted_document_types = (DocumentTypes.BILL, DocumentTypes.REPORT,)
+
def create_default_browser(self):
- return self.create_browser(self.config['login'].get(),
- self.config['password'].get())
+ return self.create_browser(
+ self.config['login'].get(),
+ self.config['password'].get()
+ )
def iter_subscription(self):
return self.browser.get_subscriptions()
@@ -65,18 +65,17 @@ def iter_documents(self, subscription):
subscription_id = subscription
return self.browser.get_documents(subscription_id)
- def get_document(self, bill):
- return find_object(
- self.iter_documents(bill.split("#")[0]),
- id=bill,
- error=DocumentNotFound
- )
+ def get_document(self, _id):
+ subid = _id.rsplit('_', 1)[0]
+ subscription = self.get_subscription(subid)
+
+ return find_object(self.iter_documents(subscription), id=_id, error=DocumentNotFound)
- def download_document(self, bill):
- if not isinstance(bill, Bill):
- bill = self.get_document(bill)
+ def download_document(self, document):
+ if not isinstance(document, Document):
+ document = self.get_document(document)
- if not bill.url:
+ if not document.url:
return None
- return self.browser.open(bill.url).content
+ return self.browser.open(document.url).content
diff --git a/modules/myfoncia/pages.py b/modules/myfoncia/pages.py
index 118f4cd9b6306d977ed8c988791fb993a3c11468..c94fdd44c08aae140b36db57c4e9b11f8d02119b 100644
--- a/modules/myfoncia/pages.py
+++ b/modules/myfoncia/pages.py
@@ -21,10 +21,10 @@
from .compat.weboob_browser_pages import HTMLPage, LoggedPage
from .compat.weboob_browser_filters_standard import CleanDecimal, CleanText, Date, Env, Format
-from weboob.browser.filters.html import Attr, Link, XPathNotFound
+from weboob.browser.filters.html import Attr, Link, XPathNotFound, AbsoluteLink
from weboob.browser.elements import ItemElement, ListElement, method
from weboob.capabilities.base import NotAvailable
-from weboob.capabilities.bill import Bill, Subscription
+from weboob.capabilities.bill import Bill, Subscription, Document, DocumentTypes
from weboob.tools.compat import urljoin
@@ -72,7 +72,7 @@ class item(ItemElement):
klass = Bill
obj_id = Format(
- '%s#%s',
+ '%s_%s',
Env('subscription'),
Attr('.', 'id')
)
@@ -100,3 +100,22 @@ def obj_url(self):
)
except XPathNotFound:
return NotAvailable
+
+
+class DocumentsPage(LoggedPage, HTMLPage):
+ @method
+ class iter_documents(ListElement):
+ item_xpath = '//main[@role="main"]//article'
+
+ class item(ItemElement):
+ klass = Document
+
+ def condition(self):
+ return CleanText('.//p[@data-behat="descOfUtilityRecord"]')(self) == 'CRG'
+
+ obj_id = Format('%s_%s', Attr('.', 'id'), Env('subscription'))
+ obj_date = Date(CleanText('.//p[@data-behat="dateOfUtilityRecord"]'), dayfirst=True)
+ obj_label = CleanText('.//p[@data-behat="descOfUtilityRecord"]')
+ obj_url = AbsoluteLink('.//a[@class="Download"]')
+ obj_format = 'pdf'
+ obj_type = DocumentTypes.REPORT
diff --git a/modules/orange/browser.py b/modules/orange/browser.py
index e366e6865fbc417e44d3305af1129963943adfc1..5594df5389387d090d6f7d02c8e90400a709d408 100644
--- a/modules/orange/browser.py
+++ b/modules/orange/browser.py
@@ -110,6 +110,10 @@ def do_login(self):
data = self.page.do_login_and_get_token(self.username, self.password)
self.password_page.go(json=data)
+ error_message = self.page.get_change_password_message()
+ if error_message:
+ raise BrowserPasswordExpired(error_message)
+
self.portal_page.go()
except ClientError as error:
@@ -120,11 +124,6 @@ def do_login(self):
raise BrowserIncorrectPassword(error.response.json())
raise
- if self.password_page.is_here():
- error_message = self.page.get_change_password_message()
- if error_message:
- raise BrowserPasswordExpired(error_message)
-
def get_nb_remaining_free_sms(self):
raise NotImplementedError()
diff --git a/modules/orange/pages/captcha_symbols.py b/modules/orange/pages/captcha_symbols.py
index b209e8232e26983c436a9f04c23586698f03ac0d..67c6b7b09794d74bda274ebf4dbce69c513b29fb 100644
--- a/modules/orange/pages/captcha_symbols.py
+++ b/modules/orange/pages/captcha_symbols.py
@@ -180,8 +180,9 @@
'0000000000111111111111111111111111111011111100001111110000010000000101000000011100001000000000000000'
],
'chien': [
- '1111111111111111111111011111111110111111111111111111111111111111111111111111111111111101111111111111',
- '0001100000111110010011101011000111111100001111110010000111101100111110000011111000000100000011111111',
+ '1111111111111111111111111111111110111111111111111111111111111111111111111101011111111101111111111111',
+ '1111111111111011111111011111111111111111111111111111111111111111111111111111111111111111111111111111',
+ '0001100000111110010011111011000111111100001111110010000111101100111110000011111000000100000011111110',
'1111111000111010001000100011111110101111000011000001111000011111110001111111111111111111111111111111',
'0000000011000000010000000001000100000001100001000110000011111000011111100111010110011111011000011101',
'1111111111111111111111101101111111101111111111111111111111111111111011111110111111111111111111111111',
diff --git a/modules/orange/pages/login.py b/modules/orange/pages/login.py
index 2f3d181f693fb308747df8c4ccbb64e9144971aa..e1da9ab59152c57328e1639f1c63f29480711efd 100644
--- a/modules/orange/pages/login.py
+++ b/modules/orange/pages/login.py
@@ -47,6 +47,7 @@ def do_login_and_get_token(self, username, password):
class PasswordPage(JsonPage):
+ ENCODING = 'utf-8'
def get_change_password_message(self):
if self.doc.get('stage') != 'changePassword':
# when stage is not present everything is okay, and if it's not changePassword we prefer do nothing here
diff --git a/modules/societegenerale/sgpe/json_pages.py b/modules/societegenerale/sgpe/json_pages.py
index e5d507c3118f164f3fecdcf72ef92449cd513109..7df31d5175cde4668f59c7cf2a5a27673b5a6d2a 100644
--- a/modules/societegenerale/sgpe/json_pages.py
+++ b/modules/societegenerale/sgpe/json_pages.py
@@ -63,6 +63,8 @@ class AccountsJsonPage(LoggedPage, JsonPage):
'Ldd': Account.TYPE_SAVINGS,
'Livret': Account.TYPE_SAVINGS,
'PEL': Account.TYPE_SAVINGS,
+ 'CPTE TRAVAUX': Account.TYPE_SAVINGS,
+ 'EPARGNE': Account.TYPE_SAVINGS,
'Plan Epargne': Account.TYPE_SAVINGS,
'PEA': Account.TYPE_PEA,
'Prêt': Account.TYPE_LOAN,
diff --git a/modules/sogecartenet/ent_pages.py b/modules/sogecartenet/ent_pages.py
index bdc8b962f0a03c444ca9f5c42858641294507d24..86acb39d3586eca9485ecdb5d8acf27daf541f68 100644
--- a/modules/sogecartenet/ent_pages.py
+++ b/modules/sogecartenet/ent_pages.py
@@ -119,6 +119,8 @@ def build_doc(self, content):
class AccountsXlsPage(LoggedPage, XLSPage):
+ HEADER = 2 # the first row is empty, the second contains headers
+
@method
class iter_accounts(DictElement):
class item(ItemElement):
@@ -285,9 +287,8 @@ def select_account(self, account):
self.browser.wait_xpath_visible('//p[contains(@class, "Notification-description")][contains(text(), "a bien été sélectionnée")]')
-
class HistoryXlsPage(LoggedPage, XLSPage):
- HEADER = 4
+ HEADER = 5
@method
class iter_history(DictElement):
diff --git a/modules/wiseed/pages.py b/modules/wiseed/pages.py
index c18be662ff22f2e6f770c485649d7938334eae8e..d178fbb6044578608861e2fa316993b8aaccd2cf 100644
--- a/modules/wiseed/pages.py
+++ b/modules/wiseed/pages.py
@@ -21,9 +21,12 @@
from .compat.weboob_browser_pages import LoggedPage, HTMLPage
from weboob.browser.filters.html import TableCell
-from .compat.weboob_browser_filters_standard import CleanText, CleanDecimal, Regexp
+from .compat.weboob_browser_filters_standard import (
+ CleanText, CleanDecimal, Regexp, Coalesce,
+)
from weboob.browser.elements import method, ItemElement, TableElement
from weboob.exceptions import BrowserIncorrectPassword
+from weboob.capabilities.base import NotAvailable
from .compat.weboob_capabilities_wealth import Investment
from weboob.tools.capabilities.bank.investments import create_french_liquidity
@@ -54,7 +57,10 @@ def get_user_id(self):
)(self.doc)
def get_liquidities(self):
- value = CleanDecimal.French(CleanText('//a[starts-with(text(),"Compte de paiement")]'))(self.doc)
+ value = Coalesce(
+ CleanDecimal.French('//a[starts-with(text(),"Compte de paiement")]', default=NotAvailable),
+ CleanDecimal.US('//a[starts-with(text(),"Compte de paiement")]', default=NotAvailable),
+ )(self.doc)
return create_french_liquidity(value)
@method
@@ -115,7 +121,9 @@ class item(ItemElement):
klass = Investment
obj_label = CleanText(TableCell('label'))
- obj_valuation = CleanDecimal.French(Regexp(
- CleanText(TableCell('details')),
- r'^(.*?) €', # can be 100,00 € + Frais de 0,90 €
- ))
+
+ # Can be "100,00 € + Frais de 0,90 €" or "€100.00"
+ obj_valuation = Coalesce(
+ CleanDecimal.French(Regexp(CleanText(TableCell('details')), r'^(.*?) €', default=None), default=None),
+ CleanDecimal.US(Regexp(CleanText(TableCell('details')), r'^€([^ ]+)', default=None), default=None),
+ )