From 999860a66a85a8a1ef04df94789726fcc6795d59 Mon Sep 17 00:00:00 2001 From: Romain Bignon Date: Sun, 31 Mar 2019 15:18:58 +0200 Subject: [PATCH] Update of modules --- modules/amazon/browser.py | 27 ++- modules/amazon/de/browser.py | 4 + modules/amazon/en/browser.py | 3 + modules/amazon/pages.py | 17 +- modules/amazon/uk/browser.py | 5 +- modules/bnporc/module.py | 1 + modules/bnporc/pp/browser.py | 120 +++++++++-- modules/bnporc/pp/pages.py | 46 +++- modules/bnppere/browser.py | 3 + modules/boursorama/__init__.py | 6 +- modules/boursorama/browser.py | 12 +- modules/boursorama/module.py | 8 +- modules/boursorama/pages.py | 25 ++- modules/boursorama/test.py | 6 +- modules/bouygues/browser.py | 4 +- modules/bp/browser.py | 5 +- modules/bp/module.py | 8 +- modules/bp/pages/transfer.py | 11 +- modules/caissedepargne/browser.py | 72 +++++-- modules/caissedepargne/cenet/pages.py | 23 +- modules/caissedepargne/module.py | 3 + modules/caissedepargne/pages.py | 95 ++++---- modules/cmes/browser.py | 2 +- modules/cmso/par/browser.py | 3 +- modules/cmso/par/pages.py | 1 + modules/cmso/par/transfer_pages.py | 3 + modules/cragr/api/transfer_pages.py | 7 +- modules/cragr/web/browser.py | 2 + modules/cragr/web/pages.py | 8 + modules/creditcooperatif/__init__.py | 6 +- .../creditcooperatif/caisseepargne_browser.py | 6 +- modules/creditcooperatif/cenet_browser.py | 6 +- .../creditcooperatif/linebourse_browser.py | 6 +- modules/creditcooperatif/module.py | 8 +- modules/creditcooperatif/proxy_browser.py | 6 +- modules/creditcooperatif/test.py | 6 +- modules/creditdunord/browser.py | 14 +- modules/creditdunord/pages.py | 6 +- modules/creditdunordpee/module.py | 2 +- modules/creditmutuel/browser.py | 26 +-- modules/creditmutuel/module.py | 14 +- modules/creditmutuel/pages.py | 54 +++-- modules/fortuneo/pages/accounts_list.py | 4 +- modules/groupamaes/browser.py | 49 +---- modules/groupamaes/module.py | 20 +- modules/groupamaes/pages.py | 204 +----------------- modules/humanis/browser.py | 2 +- modules/humanis/favicon.png | Bin 0 -> 1268 bytes modules/ing/api/accounts_page.py | 43 ++-- modules/ing/api_browser.py | 33 ++- modules/ing/browser.py | 10 +- modules/ing/module.py | 5 +- modules/ing/web/accounts_list.py | 7 +- modules/lcl/browser.py | 2 +- modules/linebourse/api/pages.py | 15 +- modules/{figgo => lucca}/__init__.py | 6 +- modules/{figgo => lucca}/browser.py | 23 +- modules/{figgo => lucca}/module.py | 45 +++- modules/{figgo => lucca}/pages.py | 35 ++- modules/orange/browser.py | 19 +- modules/orange/pages/login.py | 19 +- .../societegenerale/pages/accounts_list.py | 10 +- modules/tapatalk/module.py | 4 +- modules/themisbanque/browser.py | 8 +- modules/themisbanque/pages.py | 4 +- modules/yomoni/browser.py | 20 +- 66 files changed, 728 insertions(+), 549 deletions(-) create mode 100644 modules/humanis/favicon.png rename modules/{figgo => lucca}/__init__.py (79%) rename modules/{figgo => lucca}/browser.py (77%) rename modules/{figgo => lucca}/module.py (59%) rename modules/{figgo => lucca}/pages.py (71%) diff --git a/modules/amazon/browser.py b/modules/amazon/browser.py index 5756206d0..187898b7d 100644 --- a/modules/amazon/browser.py +++ b/modules/amazon/browser.py @@ -22,7 +22,8 @@ from datetime import date from weboob.browser import LoginBrowser, URL, need_login, StatesMixin from weboob.exceptions import ( - BrowserIncorrectPassword, BrowserUnavailable, ImageCaptchaQuestion, BrowserQuestion, ActionNeeded + BrowserIncorrectPassword, BrowserUnavailable, ImageCaptchaQuestion, BrowserQuestion, ActionNeeded, + WrongCaptchaResponse ) from weboob.tools.value import Value from weboob.browser.browsers import ClientError @@ -42,6 +43,13 @@ class AmazonBrowser(LoginBrowser, StatesMixin): L_LOGIN = 'Connexion' L_SUBSCRIBER = 'Nom : (.*) Modifier E-mail' + WRONGPASS_MESSAGES = [ + "Votre mot de passe est incorrect", + "Saisissez une adresse e-mail ou un numéro de téléphone portable valable", + "Impossible de trouver un compte correspondant à cette adresse e-mail" + ] + WRONG_CAPTCHA_RESPONSE = "Saisissez les caractères tels qu'ils apparaissent sur l'image." + login = URL(r'/ap/signin(.*)', LoginPage) home = URL(r'/$', r'/\?language=\w+$', HomePage) panel = URL('/gp/css/homepage.html/ref=nav_youraccount_ya', PanelPage) @@ -73,7 +81,9 @@ class AmazonBrowser(LoginBrowser, StatesMixin): super(AmazonBrowser, self).__init__(*args, **kwargs) def locate_browser(self, state): - self.location(state['url']) + if '/ap/cvf/verify' not in state['url']: + # don't perform a GET to this url, it's the otp url, which will be reached by otp_form + self.location(state['url']) def push_security_otp(self, pin_code): res_form = self.otp_form @@ -130,7 +140,14 @@ class AmazonBrowser(LoginBrowser, StatesMixin): self.handle_security() if self.login.is_here(): - raise BrowserIncorrectPassword() + msg = self.page.get_error_message() + + if any(wrongpass_message in msg for wrongpass_message in self.WRONGPASS_MESSAGES): + raise BrowserIncorrectPassword(msg) + elif self.WRONG_CAPTCHA_RESPONSE in msg: + raise WrongCaptchaResponse(msg) + else: + assert False, msg else: return @@ -157,7 +174,9 @@ class AmazonBrowser(LoginBrowser, StatesMixin): if captcha and not self.config['captcha_response'].get(): self.handle_captcha(captcha) else: - raise BrowserIncorrectPassword() + msg = self.page.get_error_message() + assert self.WRONGPASS_MESSAGE in msg, msg + raise BrowserIncorrectPassword(msg) def is_login(self): if self.login.is_here(): diff --git a/modules/amazon/de/browser.py b/modules/amazon/de/browser.py index 32ac03959..ed4b29bc7 100644 --- a/modules/amazon/de/browser.py +++ b/modules/amazon/de/browser.py @@ -26,3 +26,7 @@ class AmazonDeBrowser(AmazonEnBrowser): BASEURL = 'https://www.amazon.de' CURRENCY = 'EUR' LANGUAGE = 'en-GB' + + # it's in english even in for this browser + WRONGPASS_MESSAGES = ['Your password is incorrect', 'We cannot find an account with that e-mail address'] + WRONG_CAPTCHA_RESPONSE = "Enter the characters as they are shown in the image." diff --git a/modules/amazon/en/browser.py b/modules/amazon/en/browser.py index 94f3e131d..89e9a48f8 100644 --- a/modules/amazon/en/browser.py +++ b/modules/amazon/en/browser.py @@ -30,3 +30,6 @@ class AmazonEnBrowser(AmazonBrowser): L_SIGNIN = 'Sign in' L_LOGIN = 'Login' L_SUBSCRIBER = 'Name: (.*) Edit E' + + WRONGPASS_MESSAGES = ['Your password is incorrect', 'We cannot find an account with that email address'] + WRONG_CAPTCHA_RESPONSE = "Enter the characters as they are given in the challenge." diff --git a/modules/amazon/pages.py b/modules/amazon/pages.py index da584341c..7288d2825 100644 --- a/modules/amazon/pages.py +++ b/modules/amazon/pages.py @@ -108,6 +108,9 @@ class LoginPage(HTMLPage): form = self.get_form(nr=0) return form + def get_error_message(self): + return CleanText('//div[@id="auth-error-message-box"]')(self.doc) + class SubscriptionsPage(LoggedPage, HTMLPage): @method @@ -139,8 +142,6 @@ class DocumentsPage(LoggedPage, HTMLPage): obj_id = Format('%s_%s', Env('subid'), Field('_simple_id')) obj__pre_url = Format('/gp/shared-cs/ajax/invoice/invoice.html?orderId=%s&relatedRequestId=%s&isADriveSubscription=&isHFC=', Field('_simple_id'), Env('request_id')) - obj_url = Async('details') & Link('//a[contains(@href, "download")]|//a[contains(@href, "generated_invoices")]') - obj_format = 'pdf' obj_label = Format('Facture %s', Field('_simple_id')) obj_type = DocumentTypes.BILL @@ -160,6 +161,18 @@ class DocumentsPage(LoggedPage, HTMLPage): currency = Env('currency')(self) return Currency('.//div[has-class("a-col-left")]//span[has-class("value") and contains(., "%s")]' % currency)(self) + def obj_url(self): + async_page = Async('details').loaded_page(self) + url = Link('//a[contains(@href, "download")]|//a[contains(@href, "generated_invoices")]', default=NotAvailable)(async_page.doc) + if not url: + url = Link('//a[contains(text(), "Imprimer un récapitulatif de commande")]')(self) + return url + + def obj_format(self): + if 'summary' in Field('url')(self): + return 'html' + return 'pdf' + class DownloadDocumentPage(LoggedPage, PartialHTMLPage): pass diff --git a/modules/amazon/uk/browser.py b/modules/amazon/uk/browser.py index d1cce2068..e78a8dd5f 100644 --- a/modules/amazon/uk/browser.py +++ b/modules/amazon/uk/browser.py @@ -25,4 +25,7 @@ from ..en.browser import AmazonEnBrowser class AmazonUkBrowser(AmazonEnBrowser): BASEURL = 'https://www.amazon.co.uk' CURRENCY = u'£' - LANGUAGE = u'en-GB' \ No newline at end of file + LANGUAGE = u'en-GB' + + WRONGPASS_MESSAGES = ['Your password is incorrect', 'We cannot find an account with that e-mail address'] + WRONG_CAPTCHA_RESPONSE = "Enter the characters as they are shown in the image." diff --git a/modules/bnporc/module.py b/modules/bnporc/module.py index 9d13c9264..92d9c3a1f 100644 --- a/modules/bnporc/module.py +++ b/modules/bnporc/module.py @@ -53,6 +53,7 @@ class BNPorcModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapMessag ValueBackendPassword('login', label=u'Numéro client', masked=False), ValueBackendPassword('password', label=u'Code secret', regexp='^(\d{6})$'), ValueBool('rotating_password', label=u'Automatically renew password every 100 connections', default=False), + ValueBool('digital_key', label=u'User with digital key have to add recipient with digital key', default=False), Value('website', label='Type de compte', default='pp', choices={'pp': 'Particuliers/Professionnels', 'hbank': 'HelloBank', diff --git a/modules/bnporc/pp/browser.py b/modules/bnporc/pp/browser.py index 98bfd7b9b..33324a82c 100644 --- a/modules/bnporc/pp/browser.py +++ b/modules/bnporc/pp/browser.py @@ -26,7 +26,10 @@ from requests.exceptions import ConnectionError from weboob.browser.browsers import LoginBrowser, URL, need_login from weboob.capabilities.base import find_object -from weboob.capabilities.bank import AccountNotFound, Account, TransferError, AddRecipientStep +from weboob.capabilities.bank import ( + AccountNotFound, Account, AddRecipientStep, AddRecipientTimeout, + TransferInvalidRecipient, +) from weboob.capabilities.profile import ProfileMissing from weboob.tools.decorators import retry from weboob.tools.capabilities.bank.transactions import sorted_transactions @@ -34,7 +37,7 @@ from weboob.tools.json import json from weboob.browser.exceptions import ServerError from weboob.browser.elements import DataError from weboob.exceptions import BrowserIncorrectPassword -from weboob.tools.value import Value +from weboob.tools.value import Value, ValueBool from .pages import ( LoginPage, AccountsPage, AccountsIBANPage, HistoryPage, TransferInitPage, @@ -43,6 +46,7 @@ from .pages import ( MarketListPage, MarketPage, MarketHistoryPage, MarketSynPage, BNPKeyboard, RecipientsPage, ValidateTransferPage, RegisterTransferPage, AdvisorPage, AddRecipPage, ActivateRecipPage, ProfilePage, ListDetailCardPage, ListErrorPage, + UselessPage, TransferAssertionError, ) @@ -77,6 +81,9 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser): LoginPage) list_error_page = URL('https://mabanque.bnpparibas/rsc/contrib/document/properties/identification-fr-part-V1.json', ListErrorPage) + + useless_page = URL('/fr/connexion/comptes-et-contrats', UselessPage) + con_threshold = URL('/fr/connexion/100-connexions', '/fr/connexion/mot-de-passe-expire', '/fr/espace-prive/100-connexions.*', @@ -105,7 +112,8 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser): recipients = URL('/virement-wspl/rest/listerBeneficiaire', RecipientsPage) add_recip = URL('/virement-wspl/rest/ajouterBeneficiaire', AddRecipPage) - activate_recip = URL('/virement-wspl/rest/activerBeneficiaire', ActivateRecipPage) + activate_recip_sms = URL('/virement-wspl/rest/activerBeneficiaire', ActivateRecipPage) + activate_recip_digital_key = URL('/virement-wspl/rest/verifierAuthentForte', ActivateRecipPage) validate_transfer = URL('/virement-wspl/rest/validationVirement', ValidateTransferPage) register_transfer = URL('/virement-wspl/rest/enregistrerVirement', RegisterTransferPage) @@ -119,6 +127,7 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser): self.accounts_list = None self.card_to_transaction_type = {} self.rotating_password = config['rotating_password'].get() + self.digital_key = config['digital_key'].get() @retry(ConnectionError, tries=3) def open(self, *args, **kwargs): @@ -167,7 +176,7 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser): # This page might be unavailable. try: ibans.update(self.transfer_init.go(data=JSON({'modeBeneficiaire': '0'})).get_ibans_dict('Crediteur')) - except (TransferError, AttributeError): + except (TransferAssertionError, AttributeError): pass accounts = list(self.accounts.go().iter_accounts(ibans)) @@ -324,7 +333,7 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser): try: if not origin_account_id in self.transfer_init.go(data=JSON({'modeBeneficiaire': '0'})).get_ibans_dict('Debiteur'): raise NotImplementedError() - except TransferError: + except TransferAssertionError: return # avoid recipient with same iban @@ -343,19 +352,95 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser): @need_login def new_recipient(self, recipient, **params): if 'code' in params: + # for sms authentication return self.send_code(recipient, **params) - # needed to get the phone number, enabling the possibility to send sms. - self.recipients.go(data=JSON({'type': 'TOUS'})) - # post recipient data sending sms with same request + + # prepare commun data for all authentication method data = {} data['adresseBeneficiaire'] = '' data['iban'] = recipient.iban data['libelleBeneficiaire'] = recipient.label data['notification'] = True data['typeBeneficiaire'] = '' - data['typeEnvoi'] = 'SMS' - recipient = self.add_recip.go(data=json.dumps(data), headers={'Content-Type': 'application/json'}).get_recipient(recipient) - raise AddRecipientStep(recipient, Value('code', label='Saisissez le code.')) + + # provisional + if self.digital_key: + if 'digital_key' in params: + return self.new_recipient_digital_key(recipient, data) + + # need to be on recipient page send sms or mobile notification + # needed to get the phone number, enabling the possibility to send sms. + # all users with validated phone number can receive sms code + self.recipients.go(data=JSON({'type': 'TOUS'})) + + # check type of recipient activation + type_activation = 'sms' + + # provisional + if self.digital_key: + if self.page.has_digital_key(): + # force users with digital key activated to use digital key authentication + type_activation = 'digital_key' + + if type_activation == 'sms': + # post recipient data sending sms with same request + data['typeEnvoi'] = 'SMS' + recipient = self.add_recip.go( + data=json.dumps(data), + headers={'Content-Type': 'application/json'} + ).get_recipient(recipient) + raise AddRecipientStep(recipient, Value('code', label='Saisissez le code reçu par SMS.')) + elif type_activation == 'digital_key': + # recipient validated with digital key are immediatly available + recipient.enabled_date = datetime.today() + raise AddRecipientStep( + recipient, + ValueBool('digital_key', label='Validez pour recevoir une demande sur votre application bancaire. La validation de votre bénéficiaire peut prendre plusieurs minutes.') + ) + + @need_login + def send_code(self, recipient, **params): + """ + add recipient with sms otp authentication + """ + data = {} + data['idBeneficiaire'] = recipient._transfer_id + data['typeActivation'] = 1 + data['codeActivation'] = params['code'] + return self.activate_recip_sms.go(data=json.dumps(data), headers={'Content-Type': 'application/json'}).get_recipient(recipient) + + @need_login + def new_recipient_digital_key(self, recipient, data): + """ + add recipient with 'clé digitale' authentication + """ + # post recipient data, sending app notification with same request + data['typeEnvoi'] = 'AF' + self.add_recip.go(data=json.dumps(data), headers={'Content-Type': 'application/json'}) + recipient = self.page.get_recipient(recipient) + + # prepare data for polling + assert recipient._id_transaction + polling_data = {} + polling_data['idBeneficiaire'] = recipient._transfer_id + polling_data['idTransaction'] = recipient._id_transaction + polling_data['typeActivation'] = 2 + + timeout = time.time() + 300.00 # float(second), like bnp website + + # polling + while time.time() < timeout: + time.sleep(5) # like website + self.activate_recip_digital_key.go( + data = json.dumps(polling_data), + headers = {'Content-Type': 'application/json'} + ) + if self.page.is_recipient_validated(): + break + else: + raise AddRecipientTimeout() + + return recipient @need_login def prepare_transfer(self, account, recipient, amount, reason, exec_date): @@ -367,24 +452,19 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser): data['montant'] = str(amount) data['typeVirement'] = 'SEPA' if recipient.category == u'Externe': - data['idBeneficiaire'] = recipient.id + data['idBeneficiaire'] = recipient._transfer_id else: data['compteCrediteur'] = recipient.id return data @need_login def init_transfer(self, account, recipient, amount, reason, exec_date): + if recipient._web_state == 'En attente': + raise TransferInvalidRecipient(message="Le bénéficiaire sélectionné n'est pas activé") + data = self.prepare_transfer(account, recipient, amount, reason, exec_date) return self.validate_transfer.go(data=JSON(data)).handle_response(account, recipient, amount, reason) - @need_login - def send_code(self, recipient, **params): - data = {} - data['idBeneficiaire'] = recipient.id - data['typeActivation'] = 1 - data['codeActivation'] = params['code'] - return self.activate_recip.go(data=json.dumps(data), headers={'Content-Type': 'application/json'}).get_recipient(recipient) - @need_login def execute_transfer(self, transfer): self.register_transfer.go(data=JSON({'referenceVirement': transfer.id})) diff --git a/modules/bnporc/pp/pages.py b/modules/bnporc/pp/pages.py index 9c2db1b68..1d9b28fdd 100644 --- a/modules/bnporc/pp/pages.py +++ b/modules/bnporc/pp/pages.py @@ -34,8 +34,8 @@ from weboob.browser.filters.html import TableCell from weboob.browser.pages import JsonPage, LoggedPage, HTMLPage from weboob.capabilities import NotAvailable from weboob.capabilities.bank import ( - Account, Investment, Recipient, Transfer, TransferError, TransferBankError, - AddRecipientBankError, + Account, Investment, Recipient, Transfer, TransferBankError, + AddRecipientBankError, AddRecipientTimeout, ) from weboob.capabilities.contact import Advisor from weboob.capabilities.profile import Person, ProfileMissing @@ -49,6 +49,10 @@ from weboob.tools.compat import unquote_plus from weboob.tools.html import html2text +class TransferAssertionError(Exception): + pass + + class ConnectionThresholdPage(HTMLPage): NOT_REUSABLE_PASSWORDS_COUNT = 3 """BNP disallows to reuse one of the three last used passwords.""" @@ -362,7 +366,7 @@ class TransferInitPage(BNPPage): def on_load(self): message_code = BNPPage.on_load(self) if message_code is not None: - raise TransferError('%s, code=%s' % (message_code[0], message_code[1])) + raise TransferAssertionError('%s, code=%s' % (message_code[0], message_code[1])) def get_ibans_dict(self, account_type): return dict([(a['ibanCrypte'], a['iban']) for a in self.path('data.infoVirement.listeComptes%s.*' % account_type)]) @@ -381,6 +385,7 @@ class TransferInitPage(BNPPage): obj_label = Dict('libelleCompte') obj_iban = Dict('iban') obj_category = u'Interne' + obj__web_state = None def obj_bank_name(self): return u'BNP PARIBAS' @@ -398,10 +403,11 @@ class RecipientsPage(BNPPage): # For the moment, only yield ready to transfer on recipients. condition = lambda self: Dict('libelleStatut')(self.el) in [u'Activé', u'Temporisé', u'En attente'] - obj_id = Dict('idBeneficiaire') + obj_id = obj_iban = Dict('ibanNumCompte') + obj__transfer_id = Dict('idBeneficiaire') obj_label = Dict('nomBeneficiaire') - obj_iban = Dict('ibanNumCompte') obj_category = u'Externe' + obj__web_state = Dict('libelleStatut') def obj_bank_name(self): return Dict('nomBanque')(self) or NotAvailable @@ -409,6 +415,9 @@ class RecipientsPage(BNPPage): def obj_enabled_at(self): return datetime.now().replace(microsecond=0) if Dict('libelleStatut')(self) == u'Activé' else (datetime.now() + timedelta(days=5)).replace(microsecond=0) + def has_digital_key(self): + return Dict('data/infoBeneficiaire/authentForte')(self.doc) and Dict('data/infoBeneficiaire/nomDeviceAF', default=False)(self.doc) + class ValidateTransferPage(BNPPage): def check_errors(self): @@ -417,12 +426,12 @@ class ValidateTransferPage(BNPPage): def abort_if_unknown(self, transfer_data): try: - assert transfer_data['typeOperation'] in ['1', '2'] - assert transfer_data['repartitionFrais'] == '0' - assert transfer_data['devise'] == 'EUR' - assert not transfer_data['montantDeviseEtrangere'] + assert transfer_data['typeOperation'] in ['1', '2'], 'Transfer operation type is %s' % transfer_data['typeOperation'] + assert transfer_data['repartitionFrais'] == '0', 'Transfer fees is not 0' + assert transfer_data['devise'] == 'EUR', "Transfer currency is not EUR, it's %s" % transfer_data['devise'] + assert not transfer_data['montantDeviseEtrangere'], 'Transfer currency is foreign currency' except AssertionError as e: - raise TransferError(e) + raise TransferAssertionError(e) def handle_response(self, account, recipient, amount, reason): self.check_errors() @@ -431,7 +440,7 @@ class ValidateTransferPage(BNPPage): self.abort_if_unknown(transfer_data) if 'idBeneficiaire' in transfer_data and transfer_data['idBeneficiaire'] is not None: - assert transfer_data['idBeneficiaire'] == recipient.id + assert transfer_data['idBeneficiaire'] == recipient._transfer_id elif transfer_data.get('ibanCompteCrediteur'): assert transfer_data['ibanCompteCrediteur'] == recipient.iban @@ -830,9 +839,20 @@ class AddRecipPage(BNPPage): r.enabled_at = datetime.now().replace(microsecond=0) + timedelta(days=5) r.currency = u'EUR' r.bank_name = NotAvailable + r._id_transaction = self.get('data.gestionBeneficiaire.idTransactionAF') or NotAvailable return r + class ActivateRecipPage(AddRecipPage): + def is_recipient_validated(self): + authent_state = self.doc['data']['verifAuthentForte']['authentForteDone'] + assert authent_state in (0, 1, 2, 3), 'State of authent is %s' % authent_state + if authent_state == 2: + raise ActionNeeded("La demande d'ajout de bénéficiaire a été annulée.") + elif authent_state == 3: + raise AddRecipientTimeout() + return authent_state + def get_recipient(self, recipient): r = Recipient() r.iban = recipient.iban @@ -843,3 +863,7 @@ class ActivateRecipPage(AddRecipPage): r.currency = u'EUR' r.bank_name = self.get('data.activationBeneficiaire.nomBanque') return r + + +class UselessPage(LoggedPage, HTMLPage): + pass diff --git a/modules/bnppere/browser.py b/modules/bnppere/browser.py index cc08f390c..c2a1b2610 100644 --- a/modules/bnppere/browser.py +++ b/modules/bnppere/browser.py @@ -28,6 +28,9 @@ class BnppereBrowser(AbstractBrowser): PARENT = 's2e' PARENT_ATTR = 'package.browser.BnppereBrowser' + def get_profile(self): + raise NotImplementedError() + class VisiogoBrowser(LoginBrowser): BASEURL = 'https://visiogo.bnpparibas.com/' diff --git a/modules/boursorama/__init__.py b/modules/boursorama/__init__.py index e8c2ac5ab..9a2a77e7e 100644 --- a/modules/boursorama/__init__.py +++ b/modules/boursorama/__init__.py @@ -6,16 +6,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . diff --git a/modules/boursorama/browser.py b/modules/boursorama/browser.py index 190bc288c..45ff7b2a6 100644 --- a/modules/boursorama/browser.py +++ b/modules/boursorama/browser.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . @@ -31,7 +31,7 @@ from weboob.browser.exceptions import LoggedOut, ClientError from weboob.capabilities.bank import ( Account, AccountNotFound, TransferError, TransferInvalidAmount, TransferInvalidEmitter, TransferInvalidLabel, TransferInvalidRecipient, - AddRecipientStep, Recipient, Rate + AddRecipientStep, Recipient, Rate, TransferBankError, ) from weboob.capabilities.contact import Advisor from weboob.tools.captcha.virtkeyboard import VirtKeyboardError @@ -465,6 +465,10 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin): self.page.submit() assert self.transfer_sent.is_here() + transfer_error = self.page.get_transfer_error() + if transfer_error: + raise TransferBankError(transfer_error) + # the last page contains no info, return the last transfer object from init_transfer return transfer diff --git a/modules/boursorama/module.py b/modules/boursorama/module.py index 47e10b9f2..014704d1a 100644 --- a/modules/boursorama/module.py +++ b/modules/boursorama/module.py @@ -7,16 +7,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . @@ -41,7 +41,7 @@ class BoursoramaModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapPr MAINTAINER = u'Gabriel Kerneis' EMAIL = 'gabriel@kerneis.info' VERSION = '1.6' - LICENSE = 'AGPLv3+' + LICENSE = 'LGPLv3+' DESCRIPTION = u'Boursorama' CONFIG = BackendConfig(ValueBackendPassword('login', label='Identifiant', masked=False), ValueBackendPassword('password', label='Mot de passe'), diff --git a/modules/boursorama/pages.py b/modules/boursorama/pages.py index 00e8850ba..775b3882f 100644 --- a/modules/boursorama/pages.py +++ b/modules/boursorama/pages.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from __future__ import unicode_literals @@ -126,6 +126,7 @@ class Transaction(FrenchTransaction): (re.compile(r'^(?P[A-Z][\sa-z]* )?AVOIR (?P
\d{2})(?P\d{2})(?P\d{4}) (?P.*)'), FrenchTransaction.TYPE_PAYBACK), (re.compile('^REM CHQ (?P.*)'), FrenchTransaction.TYPE_DEPOSIT), (re.compile(u'^([*]{3} solde des operations cb [*]{3} )?Relevé différé Carte (.*)'), FrenchTransaction.TYPE_CARD_SUMMARY), + (re.compile(u'^[*]{3} solde des operations cb [*]{3}(.*)'), FrenchTransaction.TYPE_CARD), (re.compile(r'^Ech pret'), FrenchTransaction.TYPE_LOAN_PAYMENT), ] @@ -282,7 +283,8 @@ class AccountsPage(LoggedPage, HTMLPage): raise SkipItem() return self.obj__idparts()[1] - id = Async('details', Regexp(CleanText('//h3[has-class("account-number")]'), r'(\d+)', default=NotAvailable))(self) + # sometimes it's
sometimes it's

+ id = Async('details', Regexp(CleanText('//*[has-class("account-number")]'), r'Référence du compte : (\d+)', default=NotAvailable))(self) if not id: raise SkipItem() return id @@ -433,7 +435,7 @@ class HistoryPage(LoggedPage, HTMLPage): if self.obj.type == Transaction.TYPE_CARD_SUMMARY: return self.obj.type deferred_card_labels = [card.label for card in self.page.browser.cards_list] - if 'cartes débit différé' in Field('category')(self) or Field('_account_name')(self).upper() in deferred_card_labels: + if Field('_account_name')(self).upper() in deferred_card_labels: return Transaction.TYPE_DEFERRED_CARD if not Env('is_card', default=False)(self): if Env('coming', default=False)(self) and Field('raw')(self).startswith('CARTE '): @@ -838,23 +840,23 @@ class TransferAccounts(LoggedPage, HTMLPage): class TransferRecipients(LoggedPage, HTMLPage): @method class iter_recipients(ListElement): - item_xpath = '//a[has-class("transfer__account-wrapper")]' + item_xpath = '//div[contains(@class, "deploy__wrapper")]//label[@class="account-choice__label"]' class item(ItemElement): klass = Recipient - obj_id = CleanText('.//div[@class="transfer__account-number"]') + obj_id = CleanText('.//div[@class="c-card-ghost__sub-label"]') obj_bank_name = Regexp(CleanText('.//div[@class="transfer__account-name"]'), pattern=r'- ([^-]*)$', default=NotAvailable) def obj_label(self): - label = Regexp(CleanText('.//div[@class="transfer__account-name"]'), pattern=r'^(.*?)(?: -[^-]*)?$')(self) + label = Regexp(CleanText('.//div[@class="c-card-ghost__top-label"]'), pattern=r'^(.*?)(?: -[^-]*)?$')(self) return label.rstrip('-').rstrip() def obj_category(self): text = CleanText('./ancestor::div[has-class("deploy--item")]//a[has-class("deploy__title")]')(self) if 'Mes comptes Boursorama Banque' in text: return 'Interne' - elif 'Comptes externes' in text or 'Comptes de tiers' in text: + elif any(exp in text for exp in ('Comptes externes', 'Comptes de tiers', 'Mes bénéficiaires')): return 'Externe' def obj_iban(self): @@ -864,7 +866,7 @@ class TransferRecipients(LoggedPage, HTMLPage): def obj_enabled_at(self): return datetime.datetime.now().replace(microsecond=0) - obj__tempid = Attr('.', 'data-value') + obj__tempid = Attr('./div[@class="c-card-ghost "]', 'data-value') def condition(self): iban = Field('iban')(self) @@ -943,7 +945,8 @@ class TransferConfirm(LoggedPage, HTMLPage): class TransferSent(LoggedPage, HTMLPage): - pass + def get_transfer_error(self): + return CleanText('//form[@name="Confirm"]/div[@class="form-errors"]//li')(self.doc) class AddRecipientPage(LoggedPage, HTMLPage): diff --git a/modules/boursorama/test.py b/modules/boursorama/test.py index 41403efe6..0807d2b50 100644 --- a/modules/boursorama/test.py +++ b/modules/boursorama/test.py @@ -6,16 +6,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . diff --git a/modules/bouygues/browser.py b/modules/bouygues/browser.py index f9315c67a..513b2680b 100644 --- a/modules/bouygues/browser.py +++ b/modules/bouygues/browser.py @@ -43,7 +43,7 @@ class BouyguesBrowser(LoginBrowser): subscriptions = URL(r'/personnes/(?P\d+)/comptes-facturation', SubscriptionPage) subscriptions_details = URL(r'/comptes-facturation/(?P\d+)/contrats-payes', SubscriptionDetailPage) - document_file = URL(r'/comptes-facturation/(?P\d+)/factures/\d+/documents', DocumentFilePage) + document_file = URL(r'/comptes-facturation/(?P\d+)/factures/.*/documents', DocumentFilePage) documents = URL(r'/comptes-facturation/(?P\d+)/factures', DocumentsPage) sms_page = URL(r'https://www.secure.bbox.bouyguestelecom.fr/services/SMSIHD/sendSMS.phtml', @@ -139,7 +139,7 @@ class BouyguesBrowser(LoginBrowser): self.location(subscription.url, headers=self.headers) return self.page.iter_documents(subid=subscription.id) except HTTPNotFound as error: - if error.response.json()['error'] == 'facture_introuvable': + if error.response.json()['error'] in ('facture_introuvable', 'compte_jamais_facture'): return [] raise diff --git a/modules/bp/browser.py b/modules/bp/browser.py index 8b72f97cc..a9b296ded 100644 --- a/modules/bp/browser.py +++ b/modules/bp/browser.py @@ -42,7 +42,7 @@ from .pages.pro import RedirectPage, ProAccountsList, ProAccountHistory, Downloa from .pages.mandate import MandateAccountsList, PreMandate, PreMandateBis, MandateLife, MandateMarket from .linebourse_browser import LinebourseBrowser -from weboob.capabilities.bank import TransferError, Account, Recipient, AddRecipientStep +from weboob.capabilities.bank import Account, Recipient, AddRecipientStep from weboob.tools.value import Value __all__ = ['BPBrowser', 'BProBrowser'] @@ -457,8 +457,7 @@ class BPBrowser(LoginBrowser, StatesMixin): @need_login def execute_transfer(self, transfer, code=None): - if not self.transfer_confirm.is_here(): - raise TransferError('Case not handled.') + assert self.transfer_confirm.is_here(), 'Case not handled.' self.page.confirm() # Should only happen if double auth. if self.transfer_confirm.is_here(): diff --git a/modules/bp/module.py b/modules/bp/module.py index d40f642ef..23657ae27 100644 --- a/modules/bp/module.py +++ b/modules/bp/module.py @@ -19,7 +19,7 @@ from decimal import Decimal -from weboob.capabilities.bank import CapBankWealth, CapBankTransferAddRecipient, Account, AccountNotFound, RecipientNotFound, TransferError +from weboob.capabilities.bank import CapBankWealth, CapBankTransferAddRecipient, Account, AccountNotFound, RecipientNotFound from weboob.capabilities.contact import CapContact from weboob.capabilities.base import find_object, strict_find_object, NotAvailable from weboob.capabilities.profile import CapProfile @@ -94,11 +94,7 @@ class BPModule( if not recipient: recipient = strict_find_object(self.iter_transfer_recipients(account.id), id=transfer.recipient_id, error=RecipientNotFound) - try: - # quantize to show 2 decimals. - amount = Decimal(transfer.amount).quantize(Decimal(10) ** -2) - except (AssertionError, ValueError): - raise TransferError('something went wrong') + amount = Decimal(transfer.amount).quantize(Decimal(10) ** -2) # format label like label sent by firefox or chromium browser transfer.label = transfer.label.encode('latin-1', errors="xmlcharrefreplace").decode('latin-1') diff --git a/modules/bp/pages/transfer.py b/modules/bp/pages/transfer.py index 4831e3c4b..8d71ee192 100644 --- a/modules/bp/pages/transfer.py +++ b/modules/bp/pages/transfer.py @@ -21,7 +21,7 @@ from datetime import datetime from weboob.capabilities.bank import ( - TransferError, TransferBankError, Transfer, TransferStep, NotAvailable, Recipient, + TransferBankError, Transfer, TransferStep, NotAvailable, Recipient, AccountNotFound, AddRecipientBankError ) from weboob.capabilities.base import find_object @@ -149,11 +149,10 @@ class TransferConfirm(LoggedPage, CheckTransferError): def handle_response(self, account, recipient, amount, reason): account_txt = CleanText('//form//dl/dt[span[contains(text(), "biter")]]/following::dd[1]', replace=[(' ', '')])(self.doc) recipient_txt = CleanText('//form//dl/dt[span[contains(text(), "diter")]]/following::dd[1]', replace=[(' ', '')])(self.doc) - try: - assert account.id in account_txt or ''.join(account.label.split()) == account_txt - assert recipient.id in recipient_txt or ''.join(recipient.label.split()) == recipient_txt - except AssertionError: - raise TransferError('Something went wrong') + + assert account.id in account_txt or ''.join(account.label.split()) == account_txt, 'Something went wrong' + assert recipient.id in recipient_txt or ''.join(recipient.label.split()) == recipient_txt, 'Something went wrong' + r_amount = CleanDecimal('//form//dl/dt[span[contains(text(), "Montant")]]/following::dd[1]', replace_dots=True)(self.doc) exec_date = Date(CleanText('//form//dl/dt[span[contains(text(), "Date")]]/following::dd[1]'), dayfirst=True)(self.doc) currency = FrenchTransaction.Currency('//form//dl/dt[span[contains(text(), "Montant")]]/following::dd[1]')(self.doc) diff --git a/modules/caissedepargne/browser.py b/modules/caissedepargne/browser.py index 299b73dd0..9250f80e3 100644 --- a/modules/caissedepargne/browser.py +++ b/modules/caissedepargne/browser.py @@ -29,7 +29,7 @@ from dateutil import parser from weboob.browser import LoginBrowser, need_login, StatesMixin from weboob.browser.switch import SiteSwitch from weboob.browser.url import URL -from weboob.capabilities.bank import Account, AddRecipientStep, Recipient, TransferBankError, Transaction +from weboob.capabilities.bank import Account, AddRecipientStep, Recipient, TransferBankError, Transaction, TransferStep from weboob.capabilities.base import NotAvailable from weboob.capabilities.profile import Profile from weboob.browser.exceptions import BrowserHTTPNotFound, ClientError @@ -731,12 +731,32 @@ class CaisseEpargne(LoginBrowser, StatesMixin): @need_login def init_transfer(self, account, recipient, transfer): + self.is_send_sms = False self.pre_transfer(account) self.page.init_transfer(account, recipient, transfer) + if self.sms_option.is_here(): - raise NotImplementedError() - self.page.continue_transfer(account.label, recipient, transfer.label) - return self.page.create_transfer(account, recipient, transfer) + self.is_send_sms = True + raise TransferStep( + transfer, + Value( + 'otp_sms', + label='Veuillez renseigner le mot de passe unique qui vous a été envoyé par SMS dans le champ réponse.' + ) + ) + + self.page.continue_transfer(account.label, recipient.label, transfer.label) + return self.page.update_transfer(transfer, account, recipient) + + @need_login + def otp_sms_continue_transfer(self, transfer, **params): + self.is_send_sms = False + assert 'otp_sms' in params, 'OTP SMS is missing' + + self.otp_sms_validation(params['otp_sms']) + if self.transfer.is_here(): + self.page.continue_transfer(transfer.account_label, transfer.recipient_label, transfer.label) + return self.page.update_transfer(transfer) @need_login def execute_transfer(self, transfer): @@ -754,6 +774,32 @@ class CaisseEpargne(LoginBrowser, StatesMixin): r.bank_name = NotAvailable return r + def otp_sms_validation(self, otp_sms): + tr_id = re.search(r'transactionID=(.*)', self.page.url) + if tr_id: + transaction_id = tr_id.group(1) + else: + assert False, 'Transfer transaction id was not found in url' + + self.request_sms.go(param=transaction_id) + + key = self.page.validate_key() + data = { + 'validate': { + key: [{ + 'id': self.page.validation_id(key), + 'otp_sms': otp_sms, + 'type': 'SMS' + }] + } + } + headers = {'Content-Type': 'application/json'} + self.location(self.url + '/step', json=data, headers=headers) + + saml = self.page.get_saml() + action = self.page.get_action() + self.location(action, data={'SAMLResponse': saml}) + def post_sms_password(self, otp, otp_field_xpath): data = {} for k, v in self.recipient_form.items(): @@ -785,22 +831,8 @@ class CaisseEpargne(LoginBrowser, StatesMixin): return self.end_sms_recipient(recipient, **params) if 'otp_sms' in params: - transactionid = re.search(r'transactionID=(.*)', self.page.url).group(1) - self.request_sms.go(param=transactionid) - validation = {} - validation['validate'] = {} - key = self.page.validate_key() - validation['validate'][key] = [] - inner_param = {} - inner_param['id'] = self.page.validation_id(key) - inner_param['type'] = 'SMS' - inner_param['otp_sms'] = params['otp_sms'] - validation['validate'][key].append(inner_param) - headers = {'Content-Type': 'application/json', 'Accept': 'application/json, text/plain, */*'} - self.location(self.url + '/step', data=json.dumps(validation), headers=headers) - saml = self.page.get_saml() - action = self.page.get_action() - self.location(action, data={'SAMLResponse': saml}) + self.otp_sms_validation(params['otp_sms']) + if self.authent.is_here(): self.page.go_on() return self.facto_post_recip(recipient) diff --git a/modules/caissedepargne/cenet/pages.py b/modules/caissedepargne/cenet/pages.py index def03cdd2..80cf4b89b 100644 --- a/modules/caissedepargne/cenet/pages.py +++ b/modules/caissedepargne/cenet/pages.py @@ -125,7 +125,7 @@ class CenetLoanPage(LoggedPage, CenetJsonPage): class item(ItemElement): klass = Loan - obj_id = CleanText(Dict('IdentifiantUniqueContrat')) + obj_id = CleanText(Dict('IdentifiantUniqueContrat'), replace=[(' ', '-')]) obj_label = CleanText(Dict('Libelle')) obj_total_amount = CleanDecimal(Dict('MontantInitial/Valeur')) obj_currency = Currency(Dict('MontantInitial/Devise')) @@ -136,16 +136,25 @@ class CenetLoanPage(LoggedPage, CenetJsonPage): obj_next_payment_amount = CleanDecimal(Dict('MontantProchaineEcheance/Valeur')) def obj_subscription_date(self): - date = CleanDecimal(Dict('DateDebutEffet'))(self) / 1000 - return datetime.fromtimestamp(date).date() + sub_date = Dict('DateDebutEffet')(self) + if sub_date: + date = CleanDecimal().filter(sub_date) / 1000 + return datetime.fromtimestamp(date).date() + return NotAvailable def obj_maturity_date(self): - date = CleanDecimal(Dict('DateDerniereEcheance'))(self) / 1000 - return datetime.fromtimestamp(date).date() + mat_date = Dict('DateDerniereEcheance')(self) + if mat_date: + date = CleanDecimal().filter(mat_date) / 1000 + return datetime.fromtimestamp(date).date() + return NotAvailable def obj_next_payment_date(self): - date = CleanDecimal(Dict('DateProchaineEcheance'))(self) / 1000 - return datetime.fromtimestamp(date).date() + next_date = Dict('DateProchaineEcheance')(self) + if next_date: + date = CleanDecimal().filter(next_date) / 1000 + return datetime.fromtimestamp(date).date() + return NotAvailable class CenetCardsPage(LoggedPage, CenetJsonPage): diff --git a/modules/caissedepargne/module.py b/modules/caissedepargne/module.py index ef83f173d..43eb6d385 100644 --- a/modules/caissedepargne/module.py +++ b/modules/caissedepargne/module.py @@ -95,6 +95,9 @@ class CaisseEpargneModule(Module, CapBankWealth, CapBankTransferAddRecipient, Ca return self.browser.iter_recipients(origin_account) def init_transfer(self, transfer, **params): + if 'otp_sms' in params: + return self.browser.otp_sms_continue_transfer(transfer, **params) + self.logger.info('Going to do a new transfer') transfer.label = ' '.join(w for w in re.sub('[^0-9a-zA-Z/\-\?:\(\)\.,\'\+ ]+', '', transfer.label).split()).upper() if transfer.account_iban: diff --git a/modules/caissedepargne/pages.py b/modules/caissedepargne/pages.py index e8443d2ce..8400c8a50 100644 --- a/modules/caissedepargne/pages.py +++ b/modules/caissedepargne/pages.py @@ -34,7 +34,7 @@ from weboob.browser.filters.standard import Date, CleanDecimal, Regexp, CleanTex from weboob.browser.filters.html import Link, Attr, TableCell from weboob.capabilities import NotAvailable from weboob.capabilities.bank import ( - Account, Investment, Recipient, TransferError, TransferBankError, Transfer, + Account, Investment, Recipient, TransferBankError, Transfer, AddRecipientBankError, Loan, ) from weboob.capabilities.bill import DocumentTypes, Subscription, Document @@ -1016,8 +1016,7 @@ class TransferPage(TransferErrorPage, IndexPage): def get_origin_account_value(self, account): origin_value = [Attr('.', 'value')(o) for o in self.doc.xpath('//select[@id="MM_VIREMENT_SAISIE_VIREMENT_ddlCompteDebiter"]/option') if Regexp(CleanText('.'), '- (\d+)')(o) in account.id] - if len(origin_value) != 1: - raise TransferError('error during origin account matching') + assert len(origin_value) == 1, 'error during origin account matching' return origin_value[0] def get_recipient_value(self, recipient): @@ -1027,8 +1026,7 @@ class TransferPage(TransferErrorPage, IndexPage): elif recipient.category == u'Interne': recipient_value = [Attr('.', 'value')(o) for o in self.doc.xpath(self.RECIPIENT_XPATH) if Regexp(CleanText('.'), '- (\d+)', default=NotAvailable)(o) and Regexp(CleanText('.'), '- (\d+)', default=NotAvailable)(o) in recipient.id] - if len(recipient_value) != 1: - raise TransferError('error during recipient matching') + assert len(recipient_value) == 1, 'error during recipient matching' return recipient_value[0] def init_transfer(self, account, recipient, transfer): @@ -1047,15 +1045,30 @@ class TransferPage(TransferErrorPage, IndexPage): class iter_recipients(MyRecipients): pass - def continue_transfer(self, origin_label, recipient, label): + def get_transfer_type(self): + sepa_inputs = self.doc.xpath('//input[contains(@id, "MM_VIREMENT_SAISIE_VIREMENT_SEPA")]') + intra_inputs = self.doc.xpath('//input[contains(@id, "MM_VIREMENT_SAISIE_VIREMENT_INTRA")]') + + assert not (len(sepa_inputs) and len(intra_inputs)), 'There are sepa and intra transfer forms' + + transfer_type = None + if len(sepa_inputs): + transfer_type = 'sepa' + elif len(intra_inputs): + transfer_type = 'intra' + assert transfer_type, 'Sepa nor intra transfer form was found' + return transfer_type + + def continue_transfer(self, origin_label, recipient_label, label): form = self.get_form(id='main') - type_ = 'intra' if recipient.category == u'Interne' else 'sepa' + + transfer_type = self.get_transfer_type() fill = lambda s, t: s % (t.upper(), t.capitalize()) form['__EVENTTARGET'] = 'MM$VIREMENT$m_WizardBar$m_lnkNext$m_lnkButton' - form[fill('MM$VIREMENT$SAISIE_VIREMENT_%s$m_Virement%s$txtIdentBenef', type_)] = recipient.label - form[fill('MM$VIREMENT$SAISIE_VIREMENT_%s$m_Virement%s$txtIdent', type_)] = origin_label - form[fill('MM$VIREMENT$SAISIE_VIREMENT_%s$m_Virement%s$txtRef', type_)] = label - form[fill('MM$VIREMENT$SAISIE_VIREMENT_%s$m_Virement%s$txtMotif', type_)] = label + form[fill('MM$VIREMENT$SAISIE_VIREMENT_%s$m_Virement%s$txtIdentBenef', transfer_type)] = recipient_label + form[fill('MM$VIREMENT$SAISIE_VIREMENT_%s$m_Virement%s$txtIdent', transfer_type)] = origin_label + form[fill('MM$VIREMENT$SAISIE_VIREMENT_%s$m_Virement%s$txtRef', transfer_type)] = label + form[fill('MM$VIREMENT$SAISIE_VIREMENT_%s$m_Virement%s$txtMotif', transfer_type)] = label form.submit() def go_add_recipient(self): @@ -1082,33 +1095,43 @@ class TransferConfirmPage(TransferErrorPage, IndexPage): form['__EVENTTARGET'] = 'MM$VIREMENT$m_WizardBar$m_lnkNext$m_lnkButton' form.submit() - def create_transfer(self, account, recipient, transfer): - transfer = Transfer() - transfer.currency = FrenchTransaction.Currency('.//tr[td[contains(text(), "Montant")]]/td[not(@class)] | \ - .//tr[th[contains(text(), "Montant")]]/td[not(@class)]')(self.doc) + def update_transfer(self, transfer, account=None, recipient=None): + """update `Transfer` object with web information to use transfer check""" + + # transfer informations + transfer.label = ( + CleanText(u'.//tr[td[contains(text(), "Motif de l\'opération")]]/td[not(@class)]')(self.doc) or + CleanText(u'.//tr[td[contains(text(), "Libellé")]]/td[not(@class)]')(self.doc) or + CleanText(u'.//tr[th[contains(text(), "Libellé")]]/td[not(@class)]')(self.doc) + ) + transfer.exec_date = Date(CleanText('.//tr[th[contains(text(), "En date du")]]/td[not(@class)]'), dayfirst=True)(self.doc) transfer.amount = CleanDecimal('.//tr[td[contains(text(), "Montant")]]/td[not(@class)] | \ .//tr[th[contains(text(), "Montant")]]/td[not(@class)]', replace_dots=True)(self.doc) - transfer.account_iban = account.iban - if recipient.category == u'Externe': - for word in Upper(CleanText(u'.//tr[th[contains(text(), "Compte à créditer")]]/td[not(@class)]'))(self.doc).split(): - if is_iban_valid(word): - transfer.recipient_iban = word - break + transfer.currency = FrenchTransaction.Currency('.//tr[td[contains(text(), "Montant")]]/td[not(@class)] | \ + .//tr[th[contains(text(), "Montant")]]/td[not(@class)]')(self.doc) + + # recipient transfer informations, update information if there is no OTP SMS validation + if recipient: + transfer.recipient_label = recipient.label + transfer.recipient_id = recipient.id + + if recipient.category == u'Externe': + for word in Upper(CleanText(u'.//tr[th[contains(text(), "Compte à créditer")]]/td[not(@class)]'))(self.doc).split(): + if is_iban_valid(word): + transfer.recipient_iban = word + break + else: + assert False, 'Unable to find IBAN (original was %s)' % recipient.iban else: - raise TransferError('Unable to find IBAN (original was %s)' % recipient.iban) - else: - transfer.recipient_iban = recipient.iban - transfer.account_id = unicode(account.id) - transfer.recipient_id = unicode(recipient.id) - transfer.exec_date = Date(CleanText('.//tr[th[contains(text(), "En date du")]]/td[not(@class)]'), dayfirst=True)(self.doc) - transfer.label = (CleanText(u'.//tr[td[contains(text(), "Motif de l\'opération")]]/td[not(@class)]')(self.doc) or - CleanText(u'.//tr[td[contains(text(), "Libellé")]]/td[not(@class)]')(self.doc) or - CleanText(u'.//tr[th[contains(text(), "Libellé")]]/td[not(@class)]')(self.doc)) - transfer.account_label = account.label - transfer.recipient_label = recipient.label - transfer._account = account - transfer._recipient = recipient - transfer.account_balance = account.balance + transfer.recipient_iban = recipient.iban + + # origin account transfer informations, update information if there is no OTP SMS validation + if account: + transfer.account_id = account.id + transfer.account_iban = account.iban + transfer.account_label = account.label + transfer.account_balance = account.balance + return transfer @@ -1133,7 +1156,7 @@ class ProTransferConfirmPage(TransferConfirmPage): t.recipient_iban = word break else: - raise TransferError('Unable to find IBAN (original was %s)' % recipient.iban) + assert False, 'Unable to find IBAN (original was %s)' % recipient.iban else: t.recipient_iban = recipient.iban t.recipient_iban = recipient.iban diff --git a/modules/cmes/browser.py b/modules/cmes/browser.py index 7781a67e4..5cbb319b8 100644 --- a/modules/cmes/browser.py +++ b/modules/cmes/browser.py @@ -34,7 +34,7 @@ class CmesBrowser(LoginBrowser): accounts = URL('(?P.*)fr/espace/devbavoirs.aspx\?mode=net&menu=cpte$', AccountsPage) fcpe_investment = URL(r'/fr/.*GoPositionsParFond.*', r'/fr/espace/devbavoirs.aspx\?.*SituationParFonds.*GoOpenDetailFond.*', - r'(?P.*)fr/espace/devbavoirs.aspx\?_tabi=(C|I1)&a_mode=net&a_menu=cpte&_pid=SituationGlobale&_fid=GoPositionsParFond', + r'(?P.*)fr/espace/devbavoirs.aspx\?_tabi=(C|I1)&a_mode=net&a_menu=cpte&_pid=Situation(Globale|ParPlan)&_fid=GoPositionsParFond', r'(?P.*)fr/espace/devbavoirs.aspx\?_tabi=(C|I1)&a_mode=net&a_menu=cpte&_pid=SituationParFonds.*', FCPEInvestmentPage) ccb_investment = URL(r'(?P.*)fr/espace/devbavoirs.aspx\?_tabi=C&a_mode=net&a_menu=cpte&_pid=SituationGlobale(&_fid=GoCCB|&k_support=CCB&_fid=GoPrint)', CCBInvestmentPage) history = URL('(?P.*)fr/espace/devbavoirs.aspx\?mode=net&menu=cpte&page=operations', diff --git a/modules/cmso/par/browser.py b/modules/cmso/par/browser.py index a941ea12c..83717f3d1 100644 --- a/modules/cmso/par/browser.py +++ b/modules/cmso/par/browser.py @@ -82,7 +82,8 @@ class CmsoParBrowser(LoginBrowser, StatesMixin): login = URL('/securityapi/tokens', '/auth/checkuser', LoginPage) logout = URL('/securityapi/revoke', - '/auth/errorauthn', LogoutPage) + '/auth/errorauthn', + '/\/auth/errorauthn', LogoutPage) infos = URL('/comptes/', InfosPage) accounts = URL('/domiapi/oauth/json/accounts/synthese(?P.*)', AccountsPage) history = URL('/domiapi/oauth/json/accounts/(?P.*)', HistoryPage) diff --git a/modules/cmso/par/pages.py b/modules/cmso/par/pages.py index 77a13a738..df16bfdc7 100644 --- a/modules/cmso/par/pages.py +++ b/modules/cmso/par/pages.py @@ -289,6 +289,7 @@ class Transaction(FrenchTransaction): (re.compile(r'^(?PVIR.*)'), FrenchTransaction.TYPE_TRANSFER), (re.compile(r'^(?PANN.*)'), FrenchTransaction.TYPE_PAYBACK), (re.compile(r'^(?P(VRST|VERSEMENT).*)'), FrenchTransaction.TYPE_DEPOSIT), + (re.compile(r'^(?PCHQ.*)'), FrenchTransaction.TYPE_CHECK), (re.compile(r'^(?P.*)'), FrenchTransaction.TYPE_BANK) ] diff --git a/modules/cmso/par/transfer_pages.py b/modules/cmso/par/transfer_pages.py index d08362073..eea18b7de 100644 --- a/modules/cmso/par/transfer_pages.py +++ b/modules/cmso/par/transfer_pages.py @@ -119,6 +119,9 @@ class TransferInfoPage(LoggedPage, JsonPage): @method class iter_external_recipients(DictElement): + def condition(self): + return bool(self.el.get('listBeneficiaries')) + item_xpath = 'listBeneficiaries' ignore_duplicate = True diff --git a/modules/cragr/api/transfer_pages.py b/modules/cragr/api/transfer_pages.py index 32a0d0cb0..13aa393d4 100644 --- a/modules/cragr/api/transfer_pages.py +++ b/modules/cragr/api/transfer_pages.py @@ -27,7 +27,7 @@ from weboob.capabilities.bank import ( Account, Recipient, Transfer, TransferBankError, ) from weboob.browser.filters.standard import ( - CleanDecimal, Date, CleanText, + CleanDecimal, Date, CleanText, Coalesce, ) from weboob.browser.filters.json import Dict @@ -47,7 +47,10 @@ class RecipientsPage(LoggedPage, JsonPage): klass = Account obj_id = obj_number = Dict('accountNumber') - obj_label = Dict('accountNatureLongLabel') + obj_label = Coalesce( + Dict('accountNatureLongLabel', default=''), + Dict('accountNatureShortLabel', default='') + ) obj_iban = Dict('ibanCode') obj_currency = Dict('currencyCode') diff --git a/modules/cragr/web/browser.py b/modules/cragr/web/browser.py index 3f8f8ab71..5fe416e43 100644 --- a/modules/cragr/web/browser.py +++ b/modules/cragr/web/browser.py @@ -728,7 +728,9 @@ class Cragr(LoginBrowser, StatesMixin): def execute_transfer(self, transfer, **params): assert self.transfer_page.is_here() assert self.page.is_confirm() + self.page.submit_confirm() + self.page.check_error() assert self.page.is_sent() return self.page.get_transfer() diff --git a/modules/cragr/web/pages.py b/modules/cragr/web/pages.py index 12b194767..8153266bd 100644 --- a/modules/cragr/web/pages.py +++ b/modules/cragr/web/pages.py @@ -1414,8 +1414,16 @@ class TransferPage(RecipientAddingMixin, CollectePageMixin, MyLoggedPage, BasePa form['DEVISE'] = 'EUR' form.submit() + def check_error(self): + # this is for transfer error, it's not a `AddRecipientBankError` but a `TransferBankError` + + msg = CleanText('//tr[@bgcolor="#C74545"]', default='')(self.doc) # there is no id, class or anything... + if msg: + raise TransferBankError(message=msg) + def check_recipient_error(self): # this is a copy-paste from RecipientMiscPage, i can't test if it works on this page... + # this is for add recipient by initiate transfer msg = CleanText('//tr[@bgcolor="#C74545"]', default='')(self.doc) # there is no id, class or anything... if msg: diff --git a/modules/creditcooperatif/__init__.py b/modules/creditcooperatif/__init__.py index 3c63b907c..8d91faf94 100644 --- a/modules/creditcooperatif/__init__.py +++ b/modules/creditcooperatif/__init__.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . diff --git a/modules/creditcooperatif/caisseepargne_browser.py b/modules/creditcooperatif/caisseepargne_browser.py index 6f3995a4b..61fbdb04e 100644 --- a/modules/creditcooperatif/caisseepargne_browser.py +++ b/modules/creditcooperatif/caisseepargne_browser.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from weboob.browser import AbstractBrowser diff --git a/modules/creditcooperatif/cenet_browser.py b/modules/creditcooperatif/cenet_browser.py index d8c59a14c..37f59e956 100644 --- a/modules/creditcooperatif/cenet_browser.py +++ b/modules/creditcooperatif/cenet_browser.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from weboob.browser import AbstractBrowser diff --git a/modules/creditcooperatif/linebourse_browser.py b/modules/creditcooperatif/linebourse_browser.py index a5ebe8079..62730c702 100644 --- a/modules/creditcooperatif/linebourse_browser.py +++ b/modules/creditcooperatif/linebourse_browser.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from weboob.browser import AbstractBrowser diff --git a/modules/creditcooperatif/module.py b/modules/creditcooperatif/module.py index dd0a138ac..e2aba9b3b 100644 --- a/modules/creditcooperatif/module.py +++ b/modules/creditcooperatif/module.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from weboob.capabilities.bank import CapBankTransferAddRecipient @@ -34,7 +34,7 @@ class CreditCooperatifModule(AbstractModule, CapBankTransferAddRecipient, CapPro EMAIL = 'weboob@kevin.pouget.me' VERSION = '1.6' DESCRIPTION = u'Crédit Coopératif' - LICENSE = 'AGPLv3+' + LICENSE = 'LGPLv3+' auth_type = {'particular': "Interface Particuliers", 'weak' : "Code confidentiel (pro)", 'strong': "Sesame (pro)"} diff --git a/modules/creditcooperatif/proxy_browser.py b/modules/creditcooperatif/proxy_browser.py index 903b9a2e1..ec193ffbb 100644 --- a/modules/creditcooperatif/proxy_browser.py +++ b/modules/creditcooperatif/proxy_browser.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from weboob.browser.switch import SwitchingBrowser diff --git a/modules/creditcooperatif/test.py b/modules/creditcooperatif/test.py index 52fe2a84d..3ab64bd6d 100644 --- a/modules/creditcooperatif/test.py +++ b/modules/creditcooperatif/test.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . diff --git a/modules/creditdunord/browser.py b/modules/creditdunord/browser.py index b61c661d6..092327b3a 100644 --- a/modules/creditdunord/browser.py +++ b/modules/creditdunord/browser.py @@ -47,7 +47,7 @@ class CreditDuNordBrowser(LoginBrowser): transactions = URL('/vos-comptes/IPT/appmanager/transac/particuliers\?_nfpb=true(.*)', TransactionsPage) protransactions = URL('/vos-comptes/(.*)/transac/(professionnels|entreprises)', ProTransactionsPage) iban = URL('/vos-comptes/IPT/cdnProxyResource/transacClippe/RIB_impress.asp', IbanPage) - account_type_page = URL("/icd/zco/public-data/public-ws-menuespaceperso.json", AccountTypePage) + account_type_page = URL('/icd/zco/data/public-ws-menuespaceperso.json', AccountTypePage) labels_page = URL("/icd/zco/public-data/ws-menu.json", LabelsPage) profile_page = URL("/icd/zco/data/user.json", ProfilePage) bypass_rgpd = URL('/icd/zcd/data/gdpr-get-out-zs-client.json', RgpdPage) @@ -57,17 +57,11 @@ class CreditDuNordBrowser(LoginBrowser): self.BASEURL = "https://%s" % website super(CreditDuNordBrowser, self).__init__(*args, **kwargs) - def is_logged(self): + @property + def logged(self): return self.page is not None and not self.login.is_here() and \ not self.page.doc.xpath(u'//b[contains(text(), "vous devez modifier votre code confidentiel")]') - def home(self): - if self.is_logged(): - self.location("/icd/zco/") - self.accounts.go(account_type=self.account_type) - else: - self.do_login() - def do_login(self): self.login.go().login(self.username, self.password) if self.accounts.is_here(): @@ -84,7 +78,7 @@ class CreditDuNordBrowser(LoginBrowser): # we'll check what's happening. assert False, "Still on login page." - if not self.is_logged(): + if not self.logged: raise BrowserIncorrectPassword() def _iter_accounts(self): diff --git a/modules/creditdunord/pages.py b/modules/creditdunord/pages.py index c3085c477..19b9b4c3e 100755 --- a/modules/creditdunord/pages.py +++ b/modules/creditdunord/pages.py @@ -98,12 +98,10 @@ class CDNVirtKeyboard(GridVirtKeyboard): class RedirectPage(HTMLPage): - def on_load(self): - for script in self.doc.xpath('//script'): - self.browser.location(re.search(r'href="([^"]+)"', script.text).group(1)) + pass -class EntryPage(HTMLPage): +class EntryPage(LoggedPage, HTMLPage): pass diff --git a/modules/creditdunordpee/module.py b/modules/creditdunordpee/module.py index 24d4d5f24..5b80fc435 100644 --- a/modules/creditdunordpee/module.py +++ b/modules/creditdunordpee/module.py @@ -35,7 +35,7 @@ class CreditdunordpeeModule(AbstractModule, CapBank): MAINTAINER = u'Ludovic LANGE' EMAIL = 'llange@users.noreply.github.com' LICENSE = 'LGPLv3+' - VERSION = '1.4' + VERSION = '1.6' CONFIG = BackendConfig( ValueBackendPassword('login', label='Identifiant', masked=False), ValueBackendPassword('password', label='Code secret', regexp='^(\d{6})$'), diff --git a/modules/creditmutuel/browser.py b/modules/creditmutuel/browser.py index ffd9f8e14..f6ef29c3c 100644 --- a/modules/creditmutuel/browser.py +++ b/modules/creditmutuel/browser.py @@ -21,7 +21,6 @@ from __future__ import unicode_literals import re from datetime import datetime -from dateutil.relativedelta import relativedelta from itertools import groupby from operator import attrgetter @@ -300,20 +299,18 @@ class CreditMutuelBrowser(LoginBrowser, StatesMixin): if account.type == Account.TYPE_SAVINGS and "Capital Expansion" in account.label: self.page.go_on_history_tab() - # Getting about 6 months history on new website + if self.li.is_here(): + return self.page.iter_history() + if self.is_new_website and self.page: try: - # Submit search form two times, at first empty, then filled based on available fields - for x in range(2): - form = self.page.get_form(id="I1:fm", submit='//input[@name="_FID_DoActivateSearch"]') - if x == 1: - form.update({ - next(k for k in form.keys() if "DateStart" in k): (datetime.now() - relativedelta(months=7)).strftime('%d/%m/%Y'), - next(k for k in form.keys() if "DateEnd" in k): datetime.now().strftime('%d/%m/%Y') - }) - for k in form.keys(): - if "_FID_Do" in k and "DoSearch" not in k: - form.pop(k, None) + for page in range(1, 50): + # Need to reach the page with all transactions + if not self.page.has_more_operations(): + break + form = self.page.get_form(id="I1:P:F") + form['_FID_DoLoadMoreTransactions'] = '' + form['_wxf2_pseq'] = page form.submit() # IndexError when form xpath returns [], StopIteration if next called on empty iterable except (StopIteration, FormNotFound): @@ -339,9 +336,6 @@ class CreditMutuelBrowser(LoginBrowser, StatesMixin): break raise - if self.li.is_here(): - return self.page.iter_history() - if not self.operations.is_here(): return iter([]) diff --git a/modules/creditmutuel/module.py b/modules/creditmutuel/module.py index 237a88ca1..5228acf79 100644 --- a/modules/creditmutuel/module.py +++ b/modules/creditmutuel/module.py @@ -24,7 +24,7 @@ from decimal import Decimal from weboob.capabilities.base import find_object, NotAvailable from weboob.capabilities.bank import ( CapBankWealth, CapBankTransferAddRecipient, AccountNotFound, RecipientNotFound, - Account, TransferError + Account, ) from weboob.capabilities.contact import CapContact from weboob.capabilities.profile import CapProfile @@ -114,14 +114,10 @@ class CreditMutuelModule( else: recipient = find_object(self.iter_transfer_recipients(account.id), id=transfer.recipient_id, error=RecipientNotFound) - try: - assert account.id.isdigit() - except AssertionError: - raise TransferError('Account id is invalid') - try: # quantize to show 2 decimals. - amount = Decimal(transfer.amount).quantize(Decimal(10) ** -2) - except ValueError: - raise TransferError('Transfer amount is invalid') + assert account.id.isdigit(), 'Account id is invalid' + + # quantize to show 2 decimals. + amount = Decimal(transfer.amount).quantize(Decimal(10) ** -2) # drop characters that can crash website transfer.label = transfer.label.encode('cp1252', errors="ignore").decode('cp1252') diff --git a/modules/creditmutuel/pages.py b/modules/creditmutuel/pages.py index 4213db9f4..b1966916a 100644 --- a/modules/creditmutuel/pages.py +++ b/modules/creditmutuel/pages.py @@ -41,7 +41,7 @@ from weboob.exceptions import ( from weboob.capabilities import NotAvailable from weboob.capabilities.base import empty, find_object from weboob.capabilities.bank import ( - Account, Investment, Recipient, TransferError, TransferBankError, + Account, Investment, Recipient, TransferBankError, Transfer, AddRecipientBankError, AddRecipientStep, Loan, ) from weboob.capabilities.contact import Advisor @@ -697,6 +697,9 @@ class OperationsPage(LoggedPage, HTMLPage): else: return a.attrib['href'] + def has_more_operations(self): + return bool(self.doc.xpath('//a/span[contains(text(), "Plus d\'opérations")]')) + class CardsOpePage(OperationsPage): def select_card(self, card_number): @@ -1105,10 +1108,16 @@ class LIAccountsPage(LoggedPage, HTMLPage): obj__is_inv = True obj__card_number = None + @pagination @method class iter_history(ListElement): item_xpath = '//table[has-class("liste")]/tbody/tr' + def next_page(self): + next_page = Link('//a[img[@alt="Page suivante"]]', default=None)(self.el) + if next_page: + return next_page + class item(ItemElement): klass = FrenchTransaction @@ -1352,7 +1361,7 @@ class InternalTransferPage(LoggedPage, HTMLPage): if account.endswith(acct): return inp.attrib['value'] else: - raise TransferError("account %s not found" % account) + assert False, 'Transfer origin account %s not found' % account def get_from_account_index(self, account): return self.get_account_index('data_input_indiceCompteADebiter', account) @@ -1400,8 +1409,8 @@ class InternalTransferPage(LoggedPage, HTMLPage): def check_success(self): # look for the known "all right" message - if not self.doc.xpath('//span[contains(text(), $msg)]', msg=self.READY_FOR_TRANSFER_MSG): - raise TransferError('The expected message "%s" was not found.' % self.READY_FOR_TRANSFER_MSG) + assert self.doc.xpath('//span[contains(text(), $msg)]', msg=self.READY_FOR_TRANSFER_MSG), \ + 'The expected transfer message "%s" was not found.' % self.READY_FOR_TRANSFER_MSG def check_data_consistency(self, account_id, recipient_id, amount, reason): assert account_id in CleanText('//div[div[p[contains(text(), "Compte à débiter")]]]', @@ -1451,8 +1460,8 @@ class InternalTransferPage(LoggedPage, HTMLPage): transfer_ok_message = ['Votre virement a été exécuté', 'Ce virement a été enregistré ce jour', 'Ce virement a été enregistré ce jour'] - if not any(msg for msg in transfer_ok_message if msg in content): - raise TransferError('The expected message "%s" was not found.' % transfer_ok_message) + assert any(msg for msg in transfer_ok_message if msg in content), \ + 'The expected transfer message "%s" was not found.' % transfer_ok_message exec_date, r_amount, currency = self.check_data_consistency(transfer.account_id, transfer.recipient_id, transfer.amount, transfer.label) @@ -1465,7 +1474,7 @@ class InternalTransferPage(LoggedPage, HTMLPage): if valid_state in state: break else: - raise TransferError('Transfer state is %r' % state) + assert False, 'Transfer state is %r' % state assert transfer.amount == r_amount assert transfer.exec_date == exec_date @@ -1887,7 +1896,7 @@ class NewCardsListPage(LoggedPage, HTMLPage): def condition(self): # Numerous cards are not differed card, we keep the card only if there is a coming - return CleanText('.//div[1]/p')(self) == 'Active' and 'Dépenses' in CleanText('.//tr[1]/td/a[contains(@id,"C:more-card")]')(self) + return 'Dépenses' in CleanText('.//tr[1]/td/a[contains(@id,"C:more-card")]')(self) and (CleanText('.//div[1]/p')(self) == 'Active' or Field('coming')(self) != 0) obj_balance = 0 obj_type = Account.TYPE_CARD @@ -1937,18 +1946,23 @@ class NewCardsListPage(LoggedPage, HTMLPage): def parse(self, el): # We have to reach the good page with the information of the type of card history_page = self.page.browser.open(Field('_link_id')(self)).page - card_type_page = Link('//div/ul/li/a[contains(text(), "Fonctions")]')(history_page.doc) - doc = self.page.browser.open(card_type_page).page.doc - card_type_line = doc.xpath('//tbody/tr[th[contains(text(), "Débit des paiements")]]') - if card_type_line: - if CleanText('./td')(card_type_line[0]) != 'Différé': - raise SkipItem() - elif doc.xpath('//div/p[contains(text(), "Vous n\'avez pas l\'autorisation")]'): - self.logger.warning("The user can't reach this page") - elif doc.xpath('//td[contains(text(), "Problème technique")]'): - raise BrowserUnavailable(CleanText(doc.xpath('//td[contains(text(), "Problème technique")]'))(self)) - else: - assert False, 'xpath for card type information could have changed' + card_type_page = Link('//div/ul/li/a[contains(text(), "Fonctions")]', default=NotAvailable)(history_page.doc) + if card_type_page: + doc = self.page.browser.open(card_type_page).page.doc + card_type_line = doc.xpath('//tbody/tr[th[contains(text(), "Débit des paiements")]]') + if card_type_line: + if CleanText('./td')(card_type_line[0]) != 'Différé': + raise SkipItem() + elif doc.xpath('//div/p[contains(text(), "Vous n\'avez pas l\'autorisation")]'): + self.logger.warning("The user can't reach this page") + elif doc.xpath('//td[contains(text(), "Problème technique")]'): + raise BrowserUnavailable(CleanText(doc.xpath('//td[contains(text(), "Problème technique")]'))(self)) + else: + assert False, 'xpath for card type information could have changed' + elif not CleanText('//ul//a[contains(@title, "Consulter le différé")]')(history_page.doc): + # If the card is not active the "Fonction" button is absent. + # However we can check "Consulter le différé" button is present + raise SkipItem() def get_unavailable_cards(self): cards = [] diff --git a/modules/fortuneo/pages/accounts_list.py b/modules/fortuneo/pages/accounts_list.py index 0c18a5fe4..9ae95a975 100644 --- a/modules/fortuneo/pages/accounts_list.py +++ b/modules/fortuneo/pages/accounts_list.py @@ -467,9 +467,7 @@ class AccountsList(LoggedPage, HTMLPage): if local_error_message: raise BrowserUnavailable(CleanText('.')(local_error_message[0])) - number = RawText('./a[contains(@class, "numero_compte")]')(cpt).replace(u'N° ', '') - - account.id = CleanText(None).filter(number).replace(u'N°', '') + account.id = account.number = CleanText('./a[contains(@class, "numero_compte")]/div')(cpt).replace(u'N° ', '') account._ca = CleanText('./a[contains(@class, "numero_compte")]/@rel')(cpt) account._card_links = [] diff --git a/modules/groupamaes/browser.py b/modules/groupamaes/browser.py index 13e257e75..28b151955 100644 --- a/modules/groupamaes/browser.py +++ b/modules/groupamaes/browser.py @@ -17,54 +17,19 @@ # You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . +from weboob.browser import AbstractBrowser, URL -from weboob.browser import LoginBrowser, URL, need_login -from weboob.exceptions import BrowserIncorrectPassword -from weboob.tools.date import LinearDateGuesser -from weboob.tools.capabilities.bank.transactions import sorted_transactions - -from .pages import LoginPage, LoginErrorPage, GroupamaesPage, GroupamaesPocketPage +from .pages import LoginPage __all__ = ['GroupamaesBrowser'] -class GroupamaesBrowser(LoginBrowser): - BASEURL = 'https://www.gestion-epargne-salariale.fr' +class GroupamaesBrowser(AbstractBrowser): + PARENT = 'cmes' login = URL('/groupama-es/espace-client/fr/identification/authentification.html', LoginPage) - login_error = URL('/groupama-es/fr/identification/default.cgi', LoginErrorPage) - groupamaes_page = URL('/groupama-es/fr/espace/devbavoirs.aspx\?mode=net&menu=cpte(?P.*)', GroupamaesPage) - groupamaes_pocket = URL('/groupama-es/fr/espace/devbavoirs.aspx\?_tabi=C&a_mode=net&a_mode=net&menu=cpte(?P.*)', GroupamaesPocketPage) - - def do_login(self): - self.login.stay_or_go() - - self.page.login(self.username, self.password) - - if not self.page.logged or self.login_error.is_here(): - raise BrowserIncorrectPassword() - - @need_login - def get_accounts_list(self): - return self.groupamaes_page.stay_or_go(page='&page=situglob').iter_accounts() - - @need_login - def get_history(self): - transactions = list(self.groupamaes_page.go(page='&_pid=MenuOperations&_fid=GoOperationsTraitees').get_history(date_guesser=LinearDateGuesser())) - transactions = sorted_transactions(transactions) - return transactions - - @need_login - def get_coming(self): - transactions = list(self.groupamaes_page.go(page='&_pid=OperationsTraitees&_fid=GoWaitingOperations').get_history(date_guesser=LinearDateGuesser(), coming=True)) - transactions = sorted_transactions(transactions) - return transactions - - @need_login - def iter_investment(self, account): - return self.groupamaes_pocket.go(page='&_pid=SituationParPlan&_fid=GoPositionsDetaillee').iter_investment(account.label) - @need_login - def iter_pocket(self, account): - return self.groupamaes_pocket.go(page='&_pid=SituationParPlan&_fid=GoPositionsDetaillee').iter_pocket(account.label) + def __init__(self, login, password, baseurl, subsite, *args, **kwargs): + self.weboob = kwargs['weboob'] + super(GroupamaesBrowser, self).__init__(login, password, baseurl, subsite, *args, **kwargs) diff --git a/modules/groupamaes/module.py b/modules/groupamaes/module.py index f771fafd7..260e5fa69 100644 --- a/modules/groupamaes/module.py +++ b/modules/groupamaes/module.py @@ -42,22 +42,24 @@ class GroupamaesModule(Module, CapBankPockets): ValueBackendPassword('password', label='Mot de passe')) 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(), + 'https://www.gestion-epargne-salariale.fr', + 'groupama-es/', + weboob=self.weboob) - def iter_accounts(self): - return self.browser.get_accounts_list() + def get_account(self, _id): + return find_object(self.browser.iter_accounts(), id=_id, error=AccountNotFound) - def iter_coming(self, account): - return self.browser.get_coming() + def iter_accounts(self): + return self.browser.iter_accounts() def iter_history(self, account): - return self.browser.get_history() + return self.browser.iter_history(account) def iter_investment(self, account): return self.browser.iter_investment(account) - def get_account(self, _id): - return find_object(self.browser.get_accounts_list(), id=_id, error=AccountNotFound) - def iter_pocket(self, account): return self.browser.iter_pocket(account) diff --git a/modules/groupamaes/pages.py b/modules/groupamaes/pages.py index 593a69fd4..369612fbf 100644 --- a/modules/groupamaes/pages.py +++ b/modules/groupamaes/pages.py @@ -17,205 +17,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . -from decimal import Decimal -from datetime import date -import re -from collections import OrderedDict +from weboob.browser.pages import AbstractPage -from weboob.browser.pages import HTMLPage, LoggedPage -from weboob.browser.elements import TableElement, ItemElement, method -from weboob.browser.filters.standard import CleanText, CleanDecimal, Date, Env, Regexp, Field -from weboob.browser.filters.html import TableCell -from weboob.capabilities.bank import Account, Transaction, Investment, Pocket -from weboob.capabilities.base import NotAvailable - -class LoginPage(HTMLPage): - def login(self, login, passwd): - form = self.get_form(nr=0) - form['_cm_user'] = login - form['_cm_pwd'] = passwd - form.submit() - - -class LoginErrorPage(HTMLPage): - pass - - -class GroupamaesPage(LoggedPage, HTMLPage): - NEGATIVE_AMOUNT_LABELS = [u'Retrait', u'Transfert sortant', u'Frais'] - - TYPES = OrderedDict([ - ('perco', Account.TYPE_PERCO), - ('pee', Account.TYPE_PEE), - ]) - - @method - class iter_accounts(TableElement): - item_xpath = u'//table[@summary="Liste des échéances"]/tbody/tr' - head_xpath = u'//table[@summary="Liste des échéances"]/thead/tr/th/text()' - - col_name = u'Plan' - col_value = u'Evaluation en' - - class item(ItemElement): - klass = Account - - def condition(self): - return u'Vous n\'avez pas d\'avoirs.' not in CleanText(TableCell('name'))(self) - - obj_id = obj_label = CleanText(TableCell('name')) - obj_balance = CleanDecimal(TableCell('value'), replace_dots=True, default=Decimal(0)) - obj_currency = CleanText(u'//table[@summary="Liste des échéances"]/thead/tr/th/small/text()') - - def obj_type(self): - for k, v in self.page.TYPES.items(): - if k in Field('label')(self).lower(): - return v - return Account.TYPE_PEE - - def iter_investment(self): - item = self.doc.xpath(u'//table[@summary="Liste des échéances"]/tfoot/tr/td[@class="tot _c1 d _c1"]')[0] - total = CleanDecimal(Regexp(CleanText('.'), '(.*) .*'), - default=1, replace_dots=True)(item) - - item_xpath = u'((//table[@summary="Liste des échéances"])[1]/tbody/tr)[position() < last() and not(contains(./td[1]/@class, "tittot"))]' - - obj = None - for tr in self.doc.xpath(item_xpath): - tds = tr.xpath('./td') - if len(tds) > 3: - if obj is not None: - obj.portfolio_share = (obj.valuation / total).quantize(Decimal('.0001')) - yield obj - - obj = Investment() - obj.label = CleanText('.')(tds[0]) - obj.vdate = date.today() # * En réalité derniere date de valorisation connue - obj.unitvalue = CleanDecimal('.', replace_dots=True)(tds[2]) - obj.valuation = CleanDecimal('.', replace_dots=True)(tds[5]) - obj.quantity = CleanDecimal('.', replace_dots=True)(tds[4]) - - elif obj is not None: - obj.quantity += CleanDecimal('.', replace_dots=True)(tds[1]) - obj.valuation += CleanDecimal('.', replace_dots=True)(tds[2]) - - if obj is not None: - obj.portfolio_share = (obj.valuation / total).quantize(Decimal('.0001')) - yield obj - - @method - class get_history(TableElement): - head_xpath = u'(//table[@summary="Liste des opérations"])[1]/thead/tr/th/text()' - item_xpath = u'(//table[@summary="Liste des opérations"])[1]/tbody/tr' - - col_date = u'Date' - col_operation = u'Opération' - col_montant = [u'Montant net en EUR', 'Montant en'] - - class item(ItemElement): - klass = Transaction - - def condition(self): - return u'Aucune opération' not in CleanText(TableCell('date'))(self) - - obj_date = Date(CleanText(TableCell('date')), Env('date_guesser')) - obj_type = Transaction.TYPE_UNKNOWN - obj_label = CleanText(TableCell('operation')) - - def obj_amount(self): - amount = CleanDecimal(TableCell('montant'), replace_dots=True, default=NotAvailable)(self) - if amount is NotAvailable: - assert self.env.get('coming') - return amount - - for pattern in GroupamaesPage.NEGATIVE_AMOUNT_LABELS: - if Field('label')(self).startswith(pattern): - amount = -amount - return amount - - @method - class get_list(TableElement): - head_xpath = u'//table[@summary="Liste des opérations en attente"]/thead/tr/th/text()' - item_xpath = u'//table[@summary="Liste des opérations en attente"]/tbody/tr' - - col_date = u'Date' - col_operation = u'Opération' - col_etat = u'Etat' - col_montant = [u'Montant net en', re.compile(u'Montant en')] - col_action = u'Action' - - class item(ItemElement): - klass = Transaction - - def condition(self): - return u'Aucune opération en attente' not in CleanText(TableCell('date'))(self) - - obj_date = Date(CleanText(TableCell('date')), Env('date_guesser')) - obj_type = Transaction.TYPE_UNKNOWN - obj_label = CleanText(TableCell('operation')) - obj_amount = CleanDecimal(TableCell('montant'), replace_dots=True) - - -class GroupamaesPocketPage(LoggedPage, HTMLPage): - CONDITIONS = { - u'immédiate': Pocket.CONDITION_AVAILABLE, - u'à': Pocket.CONDITION_RETIREMENT, - } - - def iter_investment(self, label): - for tr in self.doc.xpath(u'//table[@summary="Liste des échéances"]/tbody/tr'): - # list containing the numerical values - # element 1 is the quantity - # element 2 is the valuation - tds = tr.findall('td[@class="i d"]') - # var containing the label - td_label = tr.find('td[@class="i g"]') - inv = Investment() - - if len(tds) <= 2: - continue - - inv.label = CleanText(td_label)(tr) - inv.quantity = CleanDecimal(tds[1], replace_dots=True)(tr) - inv.valuation = CleanDecimal(tds[2], replace_dots=True)(tr) - if 'PEI' in label.split()[0]: - label = 'PEE' - if Regexp(CleanText(td_label), '\(([\w]+).*\)$')(tr) not in label.split()[0]: - continue - - yield inv - - def iter_pocket(self, label): - date_available, condition = 0, 0 - for tr in self.doc.xpath(u'//table[@summary="Liste des échéances"]/tbody/tr'): - tds = tr.findall('td') - - pocket = Pocket() - i = 0 - - if len(tds) <= 2: - continue - elif len(tds) < 6: - pocket.availability_date = date_available - pocket.condition = condition - else: - i += 1 - pocket.availability_date = Date(Regexp(CleanText(tds[0]), '([\d\/]+)', default=NotAvailable), default=NotAvailable)(tr) - date_available = pocket.availability_date - - pocket.condition = (Pocket.CONDITION_DATE if pocket.availability_date is not NotAvailable else - self.CONDITIONS.get(CleanText(tds[0])(tr).lower().split()[0], Pocket.CONDITION_UNKNOWN)) - condition = pocket.condition - - pocket.label = CleanText(tds[i])(tr) - pocket.quantity = CleanDecimal(tds[i+3], replace_dots=True)(tr) - pocket.amount = CleanDecimal(tds[i+4], replace_dots=True)(tr) - - if 'PEI' in label.split()[0]: - label = 'PEE' - if Regexp(CleanText(tds[i]), '\(([\w]+).*\)$')(tr) not in label.split()[0]: - continue - - yield pocket +class LoginPage(AbstractPage): + PARENT = 'cmes' + PARENT_URL = 'login' + BROWSER_ATTR = 'package.browser.CmesBrowser' diff --git a/modules/humanis/browser.py b/modules/humanis/browser.py index 4f52f0f47..7b4cbb5fb 100644 --- a/modules/humanis/browser.py +++ b/modules/humanis/browser.py @@ -25,7 +25,7 @@ from .pages import LoginPage class HumanisBrowser(AbstractBrowser): PARENT = 'cmes' - login = URL('humanis/fr/identification/login.cgi', LoginPage) + login = URL('epsens/fr/identification/authentification.html', LoginPage) def __init__(self, login, password, baseurl, subsite, *args, **kwargs): self.weboob = kwargs['weboob'] diff --git a/modules/humanis/favicon.png b/modules/humanis/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..b32884a492805a8b31d32f538576fed568e25e61 GIT binary patch literal 1268 zcmVe^K{PM_`c+m3A05(qOssxGKJs-HB7({HYJngc;jzc^H%up!z%@1F$VMAB2)* zb5(g)oV;R}zd!~or*-h4ttrZIfBQ6Py2xAv#Csi1at2rqaifkfGGh^tG&#AP2nd7a zI=Hfokm0hrap!4F#q-umz@3B8YBAW4RdE}P!Wsdl6#&RiL8)wJY^?-jr&`PEh7WGg zsYwIcJ-|QH?-yY#t6Pf(v=Z=27G{dgWiv%lZVE~W2wT%*Sux}P{sZ5A3;9oBaS=M_ zMVS}JbK-2`7~F$8MA_Ey2U5;Dy_esfw~3U;|xUuxAf^RE4)L z!i3!{s|rdyU~mHz$HhgX?hU{H25+B&hi*Zk+GzV^?YmNey`RA6b5I`FV>~GG053{q zc=-NE_uQ*eGXdp6@X-t$z6Gueb3ix3_`f>ga)t05s&HWmDjp@Cx8MnnFXq35KQ_a~YcP8SwylTJ?eOa3 z(BC6ZKjj1z3eCTNt-?8)lMof)oC^cMCg3cv06bHG1H0h#D6GFvjegPzD3u}qGvt2} z-#^?Ch7ABiz)u8cU|irw1OfYie&E?h;He?m8a6{J0lg<+pa(`CfRO=sZ3Mo_!PlO{ z&w~V~p{!aR2;%{|*U8i_7*Ywi`9^D*yQ7(aO~8KO0=^hftD6M21H-`9tunO>hVpID zt-|C<`13YQBaE{Vk^gF~%{HLdBl8tdxF7oZ;OHW*^L6h0LTl5-MlHg1KxQTzF&YDVBD*S1rsh*-h|Fh z6&eLY83Ds*VfIkK{_Vo+AHwzPu)kZ(^p-8~*btn^i`_xjJpeQW!60lr3Aw|;S)ib2 zHI*=y6AVj%Ikx&7fEPvx7~B|MH(`r_rwc|1IJP^we!|wG24IMQ7k8K#H5kj{UBD#P e2r#XHY5WhQqF5ID9C(2M0000\d{2})/(?P\d{2})/(?P\d{4}) (?P.*)'), FrenchTransaction.TYPE_WITHDRAWAL), - # Withdrawal in foreign currencies will look like "retrait 123 currency" - (re.compile(u'^retrait (?P.*)'), FrenchTransaction.TYPE_WITHDRAWAL), - (re.compile(u'^carte (?P
\d{2})/(?P\d{2})/(?P\d{4}) (?P.*)'), FrenchTransaction.TYPE_CARD), - (re.compile(u'^virement (sepa )?(emis vers|recu|emis)? (?P.*)'), FrenchTransaction.TYPE_TRANSFER), - (re.compile(u'^remise cheque(?P.*)'), FrenchTransaction.TYPE_DEPOSIT), - (re.compile(u'^cheque (?P.*)'), FrenchTransaction.TYPE_CHECK), - (re.compile(u'^prelevement (?P.*)'), FrenchTransaction.TYPE_ORDER), - (re.compile(u'^prlv sepa (?P.*?) : .*'), FrenchTransaction.TYPE_ORDER), - (re.compile(u'^prélèvement sepa en faveur de (?P.*)'), FrenchTransaction.TYPE_ORDER), - (re.compile(u'^commission sur (?P.*)'), FrenchTransaction.TYPE_BANK), - ] + PATTERNS = [ + (re.compile(u'^retrait dab (?P
\d{2})/(?P\d{2})/(?P\d{4}) (?P.*)'), FrenchTransaction.TYPE_WITHDRAWAL), + # Withdrawal in foreign currencies will look like "retrait 123 currency" + (re.compile(u'^retrait (?P.*)'), FrenchTransaction.TYPE_WITHDRAWAL), + (re.compile(u'^carte (?P
\d{2})/(?P\d{2})/(?P\d{4}) (?P.*)'), FrenchTransaction.TYPE_CARD), + (re.compile(u'^virement (sepa )?(emis vers|recu|emis)? (?P.*)'), FrenchTransaction.TYPE_TRANSFER), + (re.compile(u'^remise cheque(?P.*)'), FrenchTransaction.TYPE_DEPOSIT), + (re.compile(u'^cheque (?P.*)'), FrenchTransaction.TYPE_CHECK), + (re.compile(u'^prelevement (?P.*)'), FrenchTransaction.TYPE_ORDER), + (re.compile(u'^prlv sepa (?P.*?) : .*'), FrenchTransaction.TYPE_ORDER), + (re.compile(u'^prélèvement sepa en faveur de (?P.*)'), FrenchTransaction.TYPE_ORDER), + (re.compile(u'^commission sur (?P.*)'), FrenchTransaction.TYPE_BANK), + ] + + TYPES = { + 'PURCHASE_CARD': FrenchTransaction.TYPE_CARD, + 'TRANSFER': FrenchTransaction.TYPE_TRANSFER, + 'SEPA_DEBIT': FrenchTransaction.TYPE_ORDER, + 'CARD_WITHDRAWAL': FrenchTransaction.TYPE_WITHDRAWAL, + 'FEES': FrenchTransaction.TYPE_BANK, + 'CHECK': FrenchTransaction.TYPE_CHECK, + 'OTHER': FrenchTransaction.TYPE_UNKNOWN, + } class AccountsPage(LoggedPage, JsonPage): @@ -73,9 +84,12 @@ class HistoryPage(LoggedPage, JsonPage): class item(ItemElement): klass = Transaction - obj_id = Eval(str, Dict('id')) + # Not sure that Dict('id') is unique and persist + # wait for the full API migration + obj__web_id = Eval(str, Dict('id')) obj_amount = CleanDecimal(Dict('amount')) obj_date = Date(Dict('effectiveDate')) + obj_type = Map(Upper(Dict('type')), Transaction.TYPES, Transaction.TYPE_UNKNOWN) def obj_raw(self): return Transaction.Raw(Lower(Dict('detail')))(self) or Format('%s %s', Field('date'), Field('amount'))(self) @@ -92,6 +106,7 @@ class ComingPage(LoggedPage, JsonPage): obj_amount = CleanDecimal(Dict('amount')) obj_date = Date(Dict('effectiveDate')) obj_vdate = Date(Dict('operationDate')) + obj_type = Map(Upper(Dict('type')), Transaction.TYPES, Transaction.TYPE_UNKNOWN) def obj_raw(self): return Transaction.Raw(Lower(Dict('label')))(self) or Format('%s %s', Field('date'), Field('amount'))(self) diff --git a/modules/ing/api_browser.py b/modules/ing/api_browser.py index 315d0120d..942c3b0a0 100644 --- a/modules/ing/api_browser.py +++ b/modules/ing/api_browser.py @@ -64,6 +64,15 @@ def need_to_be_on_website(website): def decorator(func): @wraps(func) def wrapper(self, *args, **kwargs): + # if on other website than web or api, redirect to old website + if self.old_browser.url: + if 'https://bourse.ing.fr/' in self.old_browser.url: + self.old_browser.return_from_titre_page.go() + elif 'https://ingdirectvie.ing.fr/' in self.old_browser.url: + self.old_browser.lifeback.go() + elif 'https://subscribe.ing.fr/' in self.old_browser.url: + self.old_browser.return_from_loan_site() + if website == 'web' and self.is_on_new_website: self.redirect_to_old_browser() elif website == 'api' and not self.is_on_new_website: @@ -112,14 +121,16 @@ class IngAPIBrowser(LoginBrowser): if error['code'] == 'AUTHENTICATION.INVALID_PIN_CODE': raise BrowserIncorrectPassword(error['message']) - elif error['code'] == 'AUTHENTICATION.ACCOUNT_INACTIVE': + elif error['code'] in ('AUTHENTICATION.ACCOUNT_INACTIVE', 'AUTHENTICATION.ACCOUNT_LOCKED', + 'AUTHENTICATION.NO_COMPLETE_ACCOUNT_FOUND'): raise ActionNeeded(error['message']) assert error['code'] != 'INPUT_INVALID', error['message'] raise BrowserUnavailable(error['message']) def do_login(self): - assert self.password.isdigit() assert self.birthday.isdigit() + if not self.password.isdigit(): + raise BrowserIncorrectPassword() # login on new website # update cookies @@ -213,6 +224,11 @@ class IngAPIBrowser(LoginBrowser): else: assert False, 'There should be same account in web and api website' + # can use this to use export session on old browser + # new website is an API, export session is not relevant + if self.logger.settings.get('export_session'): + self.logger.debug('logged in with session: %s', json.dumps(self.export_session())) + @need_to_be_on_website('web') def get_web_history(self, account): """iter history on old website""" @@ -238,7 +254,7 @@ class IngAPIBrowser(LoginBrowser): for tr in self.page.iter_history(): # transaction id is decreasing - first_transaction_id = int(tr.id) + first_transaction_id = int(tr._web_id) yield tr # like website, add 1 to the last transaction id of the list to get next transactions page @@ -308,18 +324,23 @@ class IngAPIBrowser(LoginBrowser): ############# CapDocument ############# @need_login + @need_to_be_on_website('web') def get_subscriptions(self): - raise BrowserUnavailable() + return self.old_browser.get_subscriptions() @need_login + @need_to_be_on_website('web') def get_documents(self, subscription): - raise BrowserUnavailable() + return self.old_browser.get_documents(subscription) + @need_login + @need_to_be_on_website('web') def download_document(self, bill): - raise BrowserUnavailable() + return self.old_browser.download_document(bill) ############# CapProfile ############# @need_login + @need_to_be_on_website('api') def get_profile(self): self.informations.go() return self.page.get_profile() diff --git a/modules/ing/browser.py b/modules/ing/browser.py index 2b39d9532..55ef197d5 100644 --- a/modules/ing/browser.py +++ b/modules/ing/browser.py @@ -108,6 +108,8 @@ class IngBrowser(LoginBrowser): # New website redirection api_redirection_url = URL(r'/general\?command=goToSecureUICommand&redirectUrl=transfers', ApiRedirectionPage) + # Old website redirection from bourse website + return_from_titre_page = URL(r'https://bourse.ing.fr/priv/redirectIng\.php\?pageIng=CC') __states__ = ['where'] @@ -451,6 +453,7 @@ class IngBrowser(LoginBrowser): self.titrerealtime.go() for inv in self.page.iter_investments(account): yield inv + self.return_from_titre_page.go() elif self.page.asv_has_detail or account._jid: self.accountspage.stay_or_go() shares = {} @@ -483,6 +486,7 @@ class IngBrowser(LoginBrowser): isin_codes = {} for tr in self.page.iter_history(): transactions.append(tr) + self.return_from_titre_page.go() if self.asv_history.is_here(): for tr in transactions: page = tr._detail.result().page if tr._detail else None @@ -513,11 +517,7 @@ class IngBrowser(LoginBrowser): @need_login def get_subscriptions(self): self.billpage.go() - if self.loginpage.is_here(): - self.do_login() - subscriptions = list(self.billpage.go().iter_subscriptions()) - else: - subscriptions = list(self.page.iter_subscriptions()) + subscriptions = list(self.page.iter_subscriptions()) self.cache['subscriptions'] = {} for sub in subscriptions: diff --git a/modules/ing/module.py b/modules/ing/module.py index 6d85a17ed..7595d0075 100644 --- a/modules/ing/module.py +++ b/modules/ing/module.py @@ -40,10 +40,11 @@ class INGModule(Module, CapBankWealth, CapBankTransfer, CapDocument, CapProfile) EMAIL = 'weboob@flo.fourcot.fr' VERSION = '1.6' LICENSE = 'LGPLv3+' - DESCRIPTION = 'ING Direct' + DESCRIPTION = 'ING France' CONFIG = BackendConfig(ValueBackendPassword('login', label='Numéro client', - masked=False), + masked=False, + regexp='^(\d{1,10})$'), ValueBackendPassword('password', label='Code secret', regexp='^(\d{6})$'), diff --git a/modules/ing/web/accounts_list.py b/modules/ing/web/accounts_list.py index 94028edf5..01ed8c84b 100644 --- a/modules/ing/web/accounts_list.py +++ b/modules/ing/web/accounts_list.py @@ -180,7 +180,12 @@ class AccountsList(LoggedPage, HTMLPage): return -abs(balance) if Field('type')(self) == Account.TYPE_LOAN else balance def obj__id(self): - return CleanText('./span[@class="account-number"]')(self) or CleanText('./span[@class="life-insurance-application"]')(self) + return CleanText('./span[@class="account-number"]')(self) + + def condition(self): + # do not return accounts in application state + # they are not displayed on new website + return not CleanText('./span[@class="life-insurance-application"]')(self) @method class get_detailed_loans(ListElement): diff --git a/modules/lcl/browser.py b/modules/lcl/browser.py index 0fd0d3f71..7813ca0da 100644 --- a/modules/lcl/browser.py +++ b/modules/lcl/browser.py @@ -434,7 +434,7 @@ class LCLBrowser(LoginBrowser, StatesMixin): @need_login def send_code(self, recipient, **params): - res = self.open('/outil/UWBE/Otp/getValidationCodeOtp?codeOtp=%s' % params['code']) + res = self.open('/outil/UWBE/Otp/validationCodeOtp?codeOtp=%s' % params['code']) if res.text == 'false': raise AddRecipientBankError(message='Mauvais code sms.') self.recip_recap.go().check_values(recipient.iban, recipient.label) diff --git a/modules/linebourse/api/pages.py b/modules/linebourse/api/pages.py index f1b5aff83..b0ec69def 100644 --- a/modules/linebourse/api/pages.py +++ b/modules/linebourse/api/pages.py @@ -26,7 +26,7 @@ from weboob.browser.filters.standard import ( ) from weboob.browser.pages import JsonPage, HTMLPage, LoggedPage from weboob.capabilities.bank import Investment, Transaction -from weboob.capabilities.base import NotAvailable +from weboob.capabilities.base import NotAvailable, empty from weboob.tools.capabilities.bank.investments import is_isin_valid @@ -49,6 +49,12 @@ class PortfolioPage(LoggedPage, JsonPage): class item(ItemElement): klass = Investment + def condition(self): + # Some rows do not contain an expected item format, + # There is no valuation (mnt) because some buy/sell orders are not yet finished. + # We want invalid values to fail in the CleanDecimal filter so we catch only when mnt is missing + return Dict('mnt', default=NotAvailable)(self) is not NotAvailable + obj_label = Dict('libval') obj_code = Dict('codval') obj_code_type = Eval( @@ -59,7 +65,6 @@ class PortfolioPage(LoggedPage, JsonPage): obj_unitvalue = CleanDecimal(Dict('crs')) obj_valuation = CleanDecimal(Dict('mnt')) obj_vdate = Env('date') - obj_portfolio_share = Eval(lambda x: x / 100, CleanDecimal(Dict('pourcentageActif'))) def parse(self, el): symbols = { @@ -88,6 +93,12 @@ class PortfolioPage(LoggedPage, JsonPage): elif Dict('pourcentagePlv', default=None)(self): return CleanDecimal(Dict('pourcentagePlv'), sign=lambda x: Env('sign')(self))(self) + def obj_portfolio_share(self): + active_percent = Dict('pourcentageActif', default=NotAvailable)(self) + if empty(active_percent): + return NotAvailable + return Eval(lambda x: x / 100, CleanDecimal(active_percent))(self) + class AccountCodesPage(LoggedPage, JsonPage): def get_contract_number(self, account_id): diff --git a/modules/figgo/__init__.py b/modules/lucca/__init__.py similarity index 79% rename from modules/figgo/__init__.py rename to modules/lucca/__init__.py index 980a6e762..40e880ddb 100644 --- a/modules/figgo/__init__.py +++ b/modules/lucca/__init__.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from __future__ import unicode_literals diff --git a/modules/figgo/browser.py b/modules/lucca/browser.py similarity index 77% rename from modules/figgo/browser.py rename to modules/lucca/browser.py index 2199b81bd..2792bc406 100644 --- a/modules/figgo/browser.py +++ b/modules/lucca/browser.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from __future__ import unicode_literals @@ -26,7 +26,10 @@ from weboob.browser.exceptions import ClientError from weboob.exceptions import BrowserIncorrectPassword from weboob.tools.date import new_datetime -from .pages import LoginPage, CalendarPage, HomePage, UsersPage +from .pages import ( + LoginPage, CalendarPage, HomePage, UsersPage, + DocumentsPage, SubscriptionPage, +) class LuccaBrowser(LoginBrowser): @@ -36,6 +39,8 @@ class LuccaBrowser(LoginBrowser): home = URL('/home', HomePage) calendar = URL('/api/leaveAMPMs', CalendarPage) users = URL(r'/api/departments\?fields=id%2Cname%2Ctype%2Clevel%2Cusers.id%2Cusers.displayName%2Cusers.dtContractStart%2Cusers.dtContractEnd%2Cusers.manager.id%2Cusers.manager2.id%2Cusers.legalEntityID%2Cusers.calendar.id&date=since%2C1970-01-01', UsersPage) + subscriptions = URL(r'/api/v3/users/me\?fields=id,firstName,lastName,allowsElectronicPayslip,culture,login,mail,personalemail', SubscriptionPage) + payslips = URL(r'/api/v3/payslips\?fields=id,import\[name,endDate\]&orderby=import\.endDate,desc,import\.startDate,desc,import\.creationDate,desc&ownerID=(?P\d+)', DocumentsPage) def __init__(self, subdomain, *args, **kwargs): super(LuccaBrowser, self).__init__(*args, **kwargs) @@ -87,3 +92,13 @@ class LuccaBrowser(LoginBrowser): last = new_datetime(event.start_date) start = window_end + + @need_login + def get_subscription(self): + self.subscriptions.go() + return self.page.get_subscription() + + @need_login + def iter_documents(self, subid): + self.payslips.go(subid=subid) + return self.page.iter_documents(subid) diff --git a/modules/figgo/module.py b/modules/lucca/module.py similarity index 59% rename from modules/figgo/module.py rename to modules/lucca/module.py index 04d0356fc..e921abbe1 100644 --- a/modules/figgo/module.py +++ b/modules/lucca/module.py @@ -5,16 +5,16 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from __future__ import unicode_literals @@ -22,7 +22,12 @@ from __future__ import unicode_literals from weboob.tools.backend import Module, BackendConfig from weboob.tools.value import Value, ValueBackendPassword +from weboob.capabilities.base import find_object from weboob.capabilities.calendar import CapCalendarEvent +from weboob.capabilities.bill import ( + CapDocument, DocumentTypes, SubscriptionNotFound, DocumentNotFound, + Subscription, +) from .browser import LuccaBrowser @@ -30,12 +35,12 @@ from .browser import LuccaBrowser __all__ = ['LuccaModule'] -class LuccaModule(Module, CapCalendarEvent): - NAME = 'figgo' - DESCRIPTION = 'Figgo - Lucca congés et absences' +class LuccaModule(Module, CapDocument, CapCalendarEvent): + NAME = 'lucca' + DESCRIPTION = 'Lucca RH' MAINTAINER = 'Vincent A' EMAIL = 'dev@indigo.re' - LICENSE = 'AGPLv3+' + LICENSE = 'LGPLv3+' VERSION = '1.6' BROWSER = LuccaBrowser @@ -46,6 +51,8 @@ class LuccaModule(Module, CapCalendarEvent): ValueBackendPassword('password', label='Mot de passe'), ) + accepted_document_types = (DocumentTypes.BILL,) + def create_default_browser(self): return self.create_browser( self.config['subdomain'].get(), @@ -74,3 +81,27 @@ class LuccaModule(Module, CapCalendarEvent): yield ev # TODO merge contiguous events? + + def iter_subscription(self): + return [self.browser.get_subscription()] + + def get_subscription(self, id): + return find_object(self.iter_subscription(), id=id, error=SubscriptionNotFound) + + def iter_documents(self, subscription): + if not isinstance(subscription, str): + subscription = subscription.id + return self.browser.iter_documents(subscription) + + def get_document(self, id): + subid = id.split('_')[0] + return find_object(self.iter_documents(subid), id=id, error=DocumentNotFound) + + def download_document(self, document): + return self.browser.open(document.url).content + + def iter_resources(self, objs, split_path): + if Subscription in objs: + return CapDocument.iter_resources(self, objs, split_path) + return CapCalendarEvent.iter_resources(self, objs, split_path) + diff --git a/modules/figgo/pages.py b/modules/lucca/pages.py similarity index 71% rename from modules/figgo/pages.py rename to modules/lucca/pages.py index 98388c28b..749ec6ab5 100644 --- a/modules/figgo/pages.py +++ b/modules/lucca/pages.py @@ -5,26 +5,29 @@ # This file is part of a weboob module. # # This weboob module is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by +# 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. # # This weboob module is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU Affero General Public License +# You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . from __future__ import unicode_literals from datetime import timedelta -from dateutil.parser import parse as parse_date from weboob.browser.pages import HTMLPage, LoggedPage, JsonPage from weboob.capabilities.calendar import BaseCalendarEvent, STATUS -from weboob.tools.date import new_date +from weboob.capabilities.bill import ( + Subscription, Document, DocumentTypes, +) +from weboob.tools.date import new_date, parse_date +from weboob.tools.compat import urljoin class LoginPage(HTMLPage): @@ -96,3 +99,25 @@ class CalendarPage(LoggedPage, JsonPage): continue yield ev + + +class SubscriptionPage(LoggedPage, JsonPage): + def get_subscription(self): + sub = Subscription() + sub.id = str(self.doc['data']['id']) + sub.subscriber = sub.label = self.doc['header']['principal'] + return sub + + +class DocumentsPage(LoggedPage, JsonPage): + def iter_documents(self, subid): + for d in self.doc['data']['items']: + doc = Document() + doc.id = '%s_%s' % (subid, d['id']) + doc._docid = d['id'] + doc.label = d['import']['name'] + doc.date = parse_date(d['import']['endDate']) + doc.url = urljoin(self.url, '/pagga/download/%s' % doc._docid) + doc.type = DocumentTypes.BILL + doc.format = 'pdf' + yield doc diff --git a/modules/orange/browser.py b/modules/orange/browser.py index ccd9d3c7a..fc942b560 100644 --- a/modules/orange/browser.py +++ b/modules/orange/browser.py @@ -22,8 +22,9 @@ from __future__ import unicode_literals from requests.exceptions import ConnectTimeout from weboob.browser import LoginBrowser, URL, need_login -from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable +from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded from .pages import LoginPage, BillsPage +from .pages.login import ManageCGI, HomePage from .pages.bills import SubscriptionsPage, BillsApiPage, ContractsPage from .pages.profile import ProfilePage from weboob.browser.exceptions import ClientError, ServerError @@ -37,12 +38,14 @@ __all__ = ['OrangeBillBrowser'] class OrangeBillBrowser(LoginBrowser): BASEURL = 'https://espaceclientv3.orange.fr' + home_page = URL('https://businesslounge.orange.fr/$', HomePage) loginpage = URL('https://login.orange.fr/\?service=sosh&return_url=https://www.sosh.fr/', 'https://login.orange.fr/front/login', LoginPage) contracts = URL('https://espaceclientpro.orange.fr/api/contracts\?page=1&nbcontractsbypage=15', ContractsPage) subscriptions = URL(r'https://espaceclientv3.orange.fr/js/necfe.php\?zonetype=bandeau&idPage=gt-home-page', SubscriptionsPage) + manage_cgi = URL('https://eui.orange.fr/manage_eui/bin/manage.cgi', ManageCGI) billspage = URL('https://m.espaceclientv3.orange.fr/\?page=factures-archives', 'https://.*.espaceclientv3.orange.fr/\?page=factures-archives', @@ -87,7 +90,19 @@ class OrangeBillBrowser(LoginBrowser): @need_login def get_subscription_list(self): try: - profile = self.profile.go().get_profile() + self.profile.go() + + assert self.profile.is_here() or self.manage_cgi.is_here() + + # we land on manage_cgi page when there is cgu to validate + if self.manage_cgi.is_here(): + # but they are not in this page, we have to go to home_page to get message + self.home_page.go() + msg = self.page.get_error_message() + assert "Nos Conditions Générales d'Utilisation ont évolué" in msg, msg + raise ActionNeeded(msg) + else: + profile = self.page.get_profile() except ConnectTimeout: # sometimes server just doesn't answer raise BrowserUnavailable() diff --git a/modules/orange/pages/login.py b/modules/orange/pages/login.py index 6fcaba5ce..76aae259a 100644 --- a/modules/orange/pages/login.py +++ b/modules/orange/pages/login.py @@ -18,20 +18,31 @@ # along with this weboob module. If not, see . -from weboob.browser.pages import HTMLPage +from weboob.browser.pages import HTMLPage, LoggedPage +from weboob.tools.json import json +from weboob.browser.filters.standard import CleanText, Format class LoginPage(HTMLPage): def login(self, username, password): json_data = { - 'forcePwd': False, 'login': username, - 'mem': True, + 'mem': False, } - self.browser.location('https://login.orange.fr/front/login', json=json_data) + response = self.browser.location('https://login.orange.fr/front/login', json=json_data) json_data = { 'login': username, 'password': password, + 'loginEncrypt': json.loads(response.json()['options'])['loginEncrypt'] } self.browser.location('https://login.orange.fr/front/password', json=json_data) + + +class ManageCGI(HTMLPage): + pass + + +class HomePage(LoggedPage, HTMLPage): + def get_error_message(self): + return Format('%s %s', CleanText('//div[has-class("modal-dialog")]//h3'), CleanText('//div[has-class("modal-dialog")]//p[1]'))(self.doc) diff --git a/modules/societegenerale/pages/accounts_list.py b/modules/societegenerale/pages/accounts_list.py index 388f04482..2b788a741 100644 --- a/modules/societegenerale/pages/accounts_list.py +++ b/modules/societegenerale/pages/accounts_list.py @@ -304,6 +304,8 @@ class Transaction(FrenchTransaction): (re.compile(r'^(?PVIR(EMEN)?T? \w+) (?P.*)'), FrenchTransaction.TYPE_TRANSFER), (re.compile(r'^(CHEQUE) (?P.*)'), FrenchTransaction.TYPE_CHECK), + (re.compile(r'^REMISE CHEQUE (?P.*)'), + FrenchTransaction.TYPE_CHECK), (re.compile(r'^(FRAIS) (?P.*)'), FrenchTransaction.TYPE_BANK), (re.compile(r'^(?PECHEANCEPRET)(?P.*)'), FrenchTransaction.TYPE_LOAN_PAYMENT), @@ -317,6 +319,8 @@ class Transaction(FrenchTransaction): FrenchTransaction.TYPE_CARD_SUMMARY), (re.compile(r'^CREDIT MENSUEL CARTE (?P.*)'), FrenchTransaction.TYPE_CARD_SUMMARY), + (re.compile(r'^Paiements CB (?P.*)'), + FrenchTransaction.TYPE_CARD_SUMMARY), (re.compile(r'^CARTE \w+ (?P
\d{2})\/(?P\d{2}) (?P.*)'), FrenchTransaction.TYPE_CARD), ] @@ -415,6 +419,9 @@ class HistoryPage(JsonBasePage): def condition(self): return Dict('statutOperation')(self) == 'COMPTABILISE' + obj_raw = Dict('libOpe') + obj_type = Transaction.TYPE_DEFERRED_CARD + @pagination @method class iter_intraday_comings(DictElement): @@ -469,6 +476,7 @@ class CardHistoryPage(LoggedPage, HTMLPage): klass = Transaction obj_label = CleanText('.//td[@headers="Libelle"]/span') + obj_type = Transaction.TYPE_DEFERRED_CARD def obj_date(self): if not 'TOTAL DES FACTURES' in Field('label')(self): @@ -484,7 +492,7 @@ class CardHistoryPage(LoggedPage, HTMLPage): def obj_raw(self): if not 'TOTAL DES FACTURES' in Field('label')(self): - return Transaction.Raw(CleanText('.//td[@headers="Libelle"]/span'))(self) + return CleanText('.//td[@headers="Libelle"]/span')(self) return NotAvailable diff --git a/modules/tapatalk/module.py b/modules/tapatalk/module.py index 395b9fb41..9cc6a7c99 100644 --- a/modules/tapatalk/module.py +++ b/modules/tapatalk/module.py @@ -74,7 +74,7 @@ class TapatalkModule(Module, CapMessages): CONFIG = BackendConfig(Value('username', label='Username', default=''), ValueBackendPassword('password', label='Password', default=''), - Value('url', label='Site URL', default="https://support.tapatalk.com/")) + Value('url', label='Site URL', default="https://support.tapatalk.com/mobiquo/mobiquo.php")) def __init__(self, *args, **kwargs): super(TapatalkModule, self).__init__(*args, **kwargs) @@ -83,7 +83,7 @@ class TapatalkModule(Module, CapMessages): @property def _conn(self): if self._xmlrpc_client is None: - url = self.config['url'].get().rstrip('/') + "/mobiquo/mobiquo.php" + url = self.config['url'].get().rstrip('/') username = self.config['username'].get() password = self.config['password'].get() self._xmlrpc_client = TapatalkServerProxy(url) diff --git a/modules/themisbanque/browser.py b/modules/themisbanque/browser.py index 282153668..cfe71c944 100644 --- a/modules/themisbanque/browser.py +++ b/modules/themisbanque/browser.py @@ -27,6 +27,8 @@ from .pages import LoginPage, LoginConfirmPage, AccountsPage, RibPage, RibPDFPag class ThemisBrowser(LoginBrowser): BASEURL = 'https://esab.themisbanque.eu/' + TIMEOUT = 90 + home = URL('/es@b/fr/esab.jsp') login = URL('/es@b/fr/codeident.jsp', LoginPage) login_confirm = URL('/es@b/servlet/internet0.ressourceWeb.servlet.Login', LoginConfirmPage) @@ -57,8 +59,10 @@ class ThemisBrowser(LoginBrowser): @need_login def get_history(self, account): - self.location(account._link) - return self.page.get_operations() + if account._link: + self.location(account._link) + return self.page.get_operations() + return [] @need_login def get_profile(self): diff --git a/modules/themisbanque/pages.py b/modules/themisbanque/pages.py index e2b11278c..43d99aced 100644 --- a/modules/themisbanque/pages.py +++ b/modules/themisbanque/pages.py @@ -28,7 +28,7 @@ from weboob.capabilities.bank import Account from weboob.capabilities.base import NotAvailable from weboob.capabilities.profile import Profile from weboob.browser.filters.standard import CleanText, CleanDecimal, Async, Regexp, Join, Field -from weboob.browser.filters.html import Attr, Link, TableCell, ColumnNotFound +from weboob.browser.filters.html import Link, TableCell, ColumnNotFound from weboob.tools.capabilities.bank.transactions import FrenchTransaction from weboob.tools.capabilities.bank.iban import is_iban_valid from weboob.tools.compat import basestring @@ -99,7 +99,7 @@ class AccountsPage(LoggedPage, HTMLPage): obj_balance = CleanDecimal(TableCell('balance'), replace_dots=True) def obj__link(self): - return Attr(TableCell('id')(self)[0].xpath('./a'), 'href')(self) + return Link(TableCell('id')(self)[0].xpath('./a'), default=None)(self) def obj__url(self): return Link(TableCell('rib')(self)[0].xpath('./a[img[starts-with(@alt, "RIB")]]'), default=None)(self) diff --git a/modules/yomoni/browser.py b/modules/yomoni/browser.py index c6a7bbebc..4ce980511 100644 --- a/modules/yomoni/browser.py +++ b/modules/yomoni/browser.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with this weboob module. If not, see . + +from __future__ import unicode_literals + + from base64 import b64encode from functools import wraps import json @@ -28,6 +32,7 @@ from weboob.browser.filters.standard import CleanDecimal, Date from weboob.exceptions import BrowserIncorrectPassword, ActionNeeded from weboob.capabilities.bank import Account, Investment, Transaction from weboob.capabilities.base import NotAvailable +from weboob.tools.capabilities.bank.investments import is_isin_valid def need_login(func): @@ -137,12 +142,21 @@ class YomoniBrowser(APIBrowser): i = Investment() i.label = "%s - %s" % (inv['classification'], inv['description']) i.code = inv['isin'] - i.code_type = Investment.CODE_TYPE_ISIN - i.quantity = CleanDecimal().filter(inv['nombreParts']) + if not is_isin_valid(i.code): + i.code = NotAvailable + i.code_type = NotAvailable + if u'Solde Espèces' in i.label: + i.code = 'XX-liquidity' + else: + i.code_type = Investment.CODE_TYPE_ISIN + + i.quantity = CleanDecimal(default=NotAvailable).filter(inv['nombreParts']) i.unitprice = CleanDecimal().filter(inv['prixMoyenAchat']) i.unitvalue = CleanDecimal().filter(inv['valeurCotation']) i.valuation = CleanDecimal().filter(inv['montantEuro']) - i.vdate = Date().filter(inv['datePosition']) + # For some invests the vdate returned is None + # Consequently we set the default value at NotAvailable + i.vdate = Date(default=NotAvailable).filter(inv['datePosition']) # performanceEuro is null sometimes in the JSON we retrieve. if inv['performanceEuro']: i.diff = CleanDecimal().filter(inv['performanceEuro']) -- GitLab