diff --git a/modules/bp/browser.py b/modules/bp/browser.py index adaef9fb216dc2e48db20ac18ec87b5f8306652f..73e9c27bfa1810bc53273f9481d60f4c613ae0c9 100644 --- a/modules/bp/browser.py +++ b/modules/bp/browser.py @@ -36,7 +36,7 @@ BrowserUnavailable, ActionNeeded, NeedInteractiveFor2FA, BrowserQuestion, AppValidation, AppValidationCancelled, AppValidationExpired, ) -from weboob.tools.compat import urlsplit, urlunsplit, parse_qsl +from weboob.tools.compat import urlsplit, urlunsplit from weboob.tools.decorators import retry from weboob.capabilities.bank import ( Account, Recipient, AddRecipientStep, TransferStep, @@ -62,7 +62,10 @@ from .pages.accountlist import ( MarketLoginPage, UselessPage, ProfilePage, MarketCheckPage, MarketHomePage, ) -from .pages.pro import RedirectPage, ProAccountsList, ProAccountHistory, DownloadRib, RibPage, RedirectAfterVKPage +from .pages.pro import ( + RedirectPage, ProAccountsList, ProAccountHistory, DownloadRib, RibPage, RedirectAfterVKPage, + SwitchQ5CPage, +) from .pages.mandate import MandateAccountsList, PreMandate, PreMandateBis, MandateLife, MandateMarket from .linebourse_browser import LinebourseAPIBrowser @@ -1026,6 +1029,9 @@ def iter_emitters(self): class BProBrowser(BPBrowser): + BASEURL = 'https://banqueenligne.entreprises.labanquepostale.fr' + + # login login_url = "https://banqueenligne.entreprises.labanquepostale.fr/wsost/OstBrokerWeb/loginform?TAM_OP=login&ERROR_CODE=0x00000000&URL=%2Fws_q47%2Fvoscomptes%2Fidentification%2Fidentification.ea%3Forigin%3Dprofessionnels" # Landing page after virtual keyboard. The response is a redirection to @@ -1035,22 +1041,40 @@ class BProBrowser(BPBrowser): r'.*voscomptes/identification/identification.ea.*', RedirectAfterVKPage ) + switch_q5c = URL( + r'/ws_q47/voscomptes/switchQ5C/redirectSyntheseQ5C-switchQ5C.ea', + SwitchQ5CPage + ) - accounts_and_loans_url = None - - pro_accounts_list = URL(r'.*voscomptes/synthese/synthese.ea', ProAccountsList) - + # bank + pro_accounts_list = URL( + r'/ws_q5c/api/pmo/recupererSyntheseComptes', + ProAccountsList + ) + rib_choice = URL( + r'/ws_q47/voscomptes/rib/retourQ5C-rib.ea', + DownloadRib + ) + rib = URL( + r'/ws_q47/voscomptes/rib/preparerRIB-rib.ea', + RibPage + ) pro_history = URL( - r'.*voscomptes/historique(ccp|cne)/(\d+-)?historique(operationnel)?(ccp|cne).*', + r'/ws_q5c/api/pmo/comptes/(?P\w+)/operations\?typeOperations=IMPUTEES', ProAccountHistory ) + # market useless2 = URL( r'.*/voscomptes/bourseenligne/lancementBourseEnLigne-bourseenligne.ea\?numCompte=(?P\d+)', UselessPage ) - market_login = URL(r'.*/voscomptes/bourseenligne/oicformautopost.jsp', MarketLoginPage) + market_login = URL( + r'.*/voscomptes/bourseenligne/oicformautopost.jsp', + MarketLoginPage + ) + # bill subscription = URL( r'(?P.*)/voscomptes/relevespdf/histo-consultationReleveCompte.ea', r'.*/voscomptes/relevespdf/rechercheHistoRelevesCompte-consultationReleveCompte.ea', @@ -1069,16 +1093,12 @@ class BProBrowser(BPBrowser): RedirectPage ) - BASEURL = 'https://banqueenligne.entreprises.labanquepostale.fr' - - def set_variables(self): - v = urlsplit(self.url) - version = v.path.split('/')[1] - - self.base_url = 'https://banqueenligne.entreprises.labanquepostale.fr/%s' % version - self.accounts_url = self.base_url + '/voscomptes/synthese/synthese.ea' + def do_login(self): + self.login_without_2fa() + # TODO: implement SCA: requests have changed in comparison to par website def go_linebourse(self, account): + # TODO: update self.location(account.url) self.location('../bourseenligne/oicformautopost.jsp') self.linebourse.session.cookies.update(self.session.cookies) @@ -1087,25 +1107,18 @@ def go_linebourse(self, account): @need_login def get_history(self, account): if account.type in (account.TYPE_PEA, account.TYPE_MARKET): + # TODO: NOT TESTED self.go_linebourse(account) return self.linebourse.iter_history(account.id) - transactions = [] - v = urlsplit(account.url) - args = dict(parse_qsl(v.query)) - args['typeRecherche'] = 10 + self.pro_history.go(account_id=account.id) # seems to fetch by default max nb of transactions without pagination (last 3 months) + return self.page.iter_history() - self.location(v.path, params=args) - - self.first_transactions = [] - for tr in self.page.iter_history(): - transactions.append(tr) - transactions.sort(key=lambda tr: tr.rdate, reverse=True) - - return transactions - - def _get_coming_transactions(self, account): - return [] + @need_login + def get_coming(self, account): + if account.type == Account.TYPE_CARD: + raise AssertionError('new pro website: implement for cards') + return [] # TODO: add condition if "gestion sous-mandat" def check_accounts_list_error(self): error = self.page.get_errors() @@ -1114,53 +1127,50 @@ def check_accounts_list_error(self): raise ActionNeeded(error) raise BrowserUnavailable(error) - @need_login - def get_accounts_list(self): - if self.accounts is None: - self.set_variables() + def go_to_rib(self, account): + self.rib_choice.go(params={'numeroCompte': account.id}) # iban only available from RIB + value = self.page.get_rib_value(account.id) + if value: + self.rib.go(params={'idxSelection': value}) + else: + # TODO: no select value: connection with no rib or only one account ? + self.logger.info('rib: handle when there is no html select choice') - accounts = [] - ids = set() - - self.location(self.accounts_url) - assert self.pro_accounts_list.is_here() - - self.check_accounts_list_error() - for account in self.page.iter_accounts(): - ids.add(account.id) - accounts.append(account) - - if self.accounts_and_loans_url: - self.location(self.accounts_and_loans_url) - assert self.pro_accounts_list.is_here() - - self.check_accounts_list_error() - for account in self.page.iter_accounts(): - if account.id not in ids: - ids.add(account.id) - accounts.append(account) - - for acc in accounts: - self.location('%s/voscomptes/rib/init-rib.ea' % self.base_url) - value = self.page.get_rib_value(acc.id) - if value: - self.location('%s/voscomptes/rib/preparerRIB-rib.ea?idxSelection=%s' % (self.base_url, value)) - if self.rib.is_here(): - acc.iban = self.page.get_iban() + def set_iban(self, account): + self.go_to_rib(account) + if self.rib.is_here(): + account.iban = self.page.get_iban() - self.accounts = accounts - - return self.accounts + @need_login + def get_accounts_list(self): + self.pro_accounts_list.go() + accounts = self.page.iter_accounts() + for account in accounts: + self.set_iban(account) + yield account @need_login def get_profile(self): - acc = self.get_accounts_list()[0] - self.location('%s/voscomptes/rib/init-rib.ea' % self.base_url) - value = self.page.get_rib_value(acc.id) - if value: - self.location('%s/voscomptes/rib/preparerRIB-rib.ea?idxSelection=%s' % (self.base_url, value)) - if self.rib.is_here(): - return self.page.get_profile() + accounts = list(self.get_accounts_list()) + if not accounts: + return + acc = accounts[0] + self.go_to_rib(acc) + if self.rib.is_here(): + return self.page.get_profile() + + @need_login + def iter_investment(self, account): + # TODO: new pro website + # iter_investment of previous pro website uses BPBrowser.iter_investment + # which uses "account.url". The .url attribute doesn't seem useful for pro website, + # try to find connections with wealth accounts to see if we need to use .url attribute + # as it was done before and if we can keep using BPBrowser.iter_investment + if account.type == Account.TYPE_MARKET: + # only wealth type is market for pro website (cf. account types in page.pro.py) + # TODO: need to properly type accounts first + raise AssertionError('new pro website: to implement') + return [] @need_login def iter_subscriptions(self): diff --git a/modules/bp/pages/pro.py b/modules/bp/pages/pro.py index b6975e9d16dde58484612f3df587765038069991..9ddc2af8ebbb71af423975f73b9a116348ea5a6b 100644 --- a/modules/bp/pages/pro.py +++ b/modules/bp/pages/pro.py @@ -21,13 +21,12 @@ from __future__ import unicode_literals -from weboob.browser.elements import ListElement, ItemElement, method -from weboob.browser.filters.standard import CleanText, CleanDecimal, Coalesce, Currency, Date, Map, Field, Regexp -from weboob.browser.filters.html import AbsoluteLink, Link -from weboob.browser.pages import LoggedPage, pagination +from weboob.browser.elements import DictElement, ItemElement, method +from weboob.browser.filters.json import Dict +from weboob.browser.filters.standard import CleanText, CleanDecimal, Date, Map, Field +from weboob.browser.pages import LoggedPage, JsonPage from weboob.capabilities.bank import Account from weboob.capabilities.profile import Company -from weboob.capabilities.base import NotAvailable from .accounthistory import Transaction from .base import MyHTMLPage @@ -45,91 +44,66 @@ def is_logged(self): ACCOUNT_TYPES = { - 'Comptes titres': Account.TYPE_MARKET, - 'Comptes épargne': Account.TYPE_SAVINGS, - 'Comptes courants': Account.TYPE_CHECKING, + # TODO: add new type names and remove old ones + 'Comptes titres': Account.TYPE_MARKET, # old + 'Comptes épargne': Account.TYPE_SAVINGS, # old + 'COMPTE_COURANT': Account.TYPE_CHECKING, } +TRANSACTION_TYPES = { + # TODO: 12+ categories ? (bank type id is at least up to 12) + 'Prélèvement': Transaction.TYPE_ORDER, + 'Achat CB': Transaction.TYPE_CHECK, + 'Virement': Transaction.TYPE_TRANSFER, + 'Frais/Taxes/Agios': Transaction.TYPE_BANK, + 'Versement': Transaction.TYPE_CASH_DEPOSIT, + 'Chèque': Transaction.TYPE_CHECK, +} -class ProAccountsList(LoggedPage, MyHTMLPage): - # TODO Be careful about connections with personnalized account groups - # According to their presentation video (https://www.labanquepostale.fr/pmo/nouvel-espace-client-business.html), - # on the new website people are able to make personnalized groups of account instead of the usual drop-down categories on which to parse to find a match in ACCOUNT_TYPES - # If clients use the functionnality we might need to add entries new in ACCOUNT_TYPES - - def get_errors(self): - # Full message for the second error is : - # Vous êtes uniquement habilité à accéder à OPnet. - # Pour toute modification de vos accès, veuillez-vous rapprocher - # du Mandataire Principal de votre contrat de banque en ligne. - return ( - CleanText( - '//div[@id="erreur_generale"]//p[contains(text(), "Le service est momentanément indisponible")]' - )(self.doc) - or CleanText( - '//p[contains(text(), "veuillez-vous rapprocher du Mandataire Principal de votre contrat")]' - )(self.doc) - ) +class ProAccountsList(LoggedPage, JsonPage): @method - class iter_accounts(ListElement): - item_xpath = '//div[@id="mainContent"]//div[h3/a]' + class iter_accounts(DictElement): + item_xpath = 'comptesBancaires/comptes' class item(ItemElement): klass = Account - obj_id = Regexp(CleanText('./h3/a/@title'), r'([A-Z\d]{4}[A-Z\d\*]{3}[A-Z\d]{4})') - obj_balance = CleanDecimal.French('./span/text()[1]') # This website has the good taste of leaving hard coded HTML comments. This is the way to pin point to the righ text item. - obj_currency = Currency('./span') - obj_url = AbsoluteLink('./h3/a') - - # account are grouped in /div based on their type, we must fetch the closest one relative to item_xpath - obj_type = Map( - CleanText('./ancestor::div[1]/preceding-sibling::h2[1]/button/div[@class="title-accordion"]'), - ACCOUNT_TYPES, - Account.TYPE_UNKNOWN - ) + obj_id = Dict('numero') + obj_balance = CleanDecimal.US(Dict('solde')) + obj_currency = 'EUR' + obj_type = Map(Dict('type'), ACCOUNT_TYPES, Account.TYPE_UNKNOWN) def obj_label(self): - """ Need to get rid of the id wherever we find it in account labels like "LIV A 0123456789N MR MOMO" (livret A) as well as "0123456789N MR MOMO" (checking account) """ - return CleanText('./h3/a/@title')(self).replace('%s ' % Field('id')(self), '') + # Comment from code of last pro website: + # Need to get rid of the id wherever we find it in account labels + # like "LIV A 0123456789N MR MOMO" (livret A) as well as + # "0123456789N MR MOMO" (checking account) + label = Dict('intituleLong')(self).replace(Field('id')(self), '') + return CleanText().filter(label) -class ProAccountHistory(LoggedPage, MyHTMLPage): - @pagination +class ProAccountHistory(LoggedPage, JsonPage): @method - class iter_history(ListElement): - item_xpath = '//div[@id="tabReleve"]//tbody/tr' - - def next_page(self): - # The next page on the website can return pages already visited without logical mechanism - # Nevertheless we can skip these pages with the comparaison of the first transaction of the page - next_page_xpath = '//div[@class="pagination"]/a[@title="Aller à la page suivante"]' - tr_xpath = '//tbody/tr[1]' - self.page.browser.first_transactions.append(CleanText(tr_xpath)(self.el)) - next_page_link = Link(next_page_xpath)(self.el) - next_page = self.page.browser.location(next_page_link) - first_transaction = CleanText(tr_xpath)(next_page.page.doc) - count = 0 # avoid an infinite loop - - while first_transaction in self.page.browser.first_transactions and count < 30: - next_page = self.page.browser.location(next_page_link) - next_page_link = Link(next_page_xpath)(next_page.page.doc) - first_transaction = CleanText(tr_xpath)(next_page.page.doc) - count += 1 - - if count < 30: - return next_page.page - + class iter_history(DictElement): class item(ItemElement): klass = Transaction - obj_date = Date(CleanText('.//td[@headers="date"]'), dayfirst=True) - obj_raw = Transaction.Raw('.//td[@headers="libelle"]') - obj_amount = Coalesce( - CleanDecimal.French('.//td[@headers="debit"]', default=NotAvailable), - CleanDecimal.French('.//td[@headers="credit"]', default=NotAvailable), - ) + obj_label = Dict('libelle') + obj_date = Date(Dict('date')) # skip time since it is always 00:00:00. Days last. + + # transaction typing: don't rely on labels as the bank already provides types. + obj_type = Map(Dict('libelleNature'), TRANSACTION_TYPES, Transaction.TYPE_UNKNOWN) + + def obj_amount(self): + amount = CleanDecimal.US(Dict('montant'))(self) # absolute value + sign = Dict('codeSens')(self) + if sign == 'D': # debit + return - amount + elif sign == 'C': # credit + return amount + else: + raise AssertionError('unhandled value for transaction sign') class DownloadRib(LoggedPage, MyHTMLPage): @@ -161,3 +135,7 @@ class get_profile(ItemElement): class RedirectAfterVKPage(MyHTMLPage): pass + + +class SwitchQ5CPage(MyHTMLPage): + pass