Commit ec6ccd05 authored by Fong NGO's avatar Fong NGO Committed by Vincent A

[bp] rework pro website

Only for "professionnels" connections (current BProBrowser). It is the
first part of the rework, other changes will come next. What is
addressed so far:

- login without sca, accounts, history, profile

By the way, it also has the benefit of removing dead code: the variable
`accounts_and_loans_url` doesn't seem to be used

TODO:

- implement the SCA

- check if there are changes for wealth and bill with appropriate
connections, and do the changes if needed

- add new account types, etc...
parent 2721e516
......@@ -36,7 +36,7 @@ from weboob.exceptions import (
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.accounthistory import (
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 @@ class BPBrowser(LoginBrowser, StatesMixin):
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<account_id>\w+)/operations\?typeOperations=IMPUTEES',
ProAccountHistory
)
# market
useless2 = URL(
r'.*/voscomptes/bourseenligne/lancementBourseEnLigne-bourseenligne.ea\?numCompte=(?P<account>\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<base_url>.*)/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 @@ class BProBrowser(BPBrowser):
@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 @@ class BProBrowser(BPBrowser):
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):
......
......@@ -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 @@ class RedirectPage(LoggedPage, MyHTMLPage):
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 RibPage(LoggedPage, MyHTMLPage):
class RedirectAfterVKPage(MyHTMLPage):
pass
class SwitchQ5CPage(MyHTMLPage):
pass
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment