Commit 3226d416 authored by Quentin Defenouillere's avatar Quentin Defenouillere Committed by Romain Bignon

[bnporc] Included the "Assurance Vie" domain to BNP iter_accounts

The BNP module did not scrape the "Assurance Vie" space yet, which
may contain additional accounts that are not included in their API.
Among those we can find Life Insurance, Capitalisation contracts and
PERP accounts, and maybe more.
I also implemented the iter_investments() method for this space, however
the domain does not seem to display any kind of transaction history for
these accounts.

Closes: 5016@zendesk
parent bc45857d
......@@ -39,9 +39,10 @@ from weboob.tools.value import Value
from .pages import (
LoginPage, AccountsPage, AccountsIBANPage, HistoryPage, TransferInitPage,
ConnectionThresholdPage, LifeInsurancesPage, LifeInsurancesHistoryPage,
LifeInsurancesDetailPage, MarketListPage, MarketPage, MarketHistoryPage,
MarketSynPage, RecipientsPage, ValidateTransferPage, RegisterTransferPage,
AdvisorPage, AddRecipPage, ActivateRecipPage, ProfilePage, ListDetailCardPage,
LifeInsurancesDetailPage, NatioVieProPage, CapitalisationPage,
MarketListPage, MarketPage, MarketHistoryPage, MarketSynPage,
RecipientsPage, ValidateTransferPage, RegisterTransferPage, AdvisorPage,
AddRecipPage, ActivateRecipPage, ProfilePage, ListDetailCardPage,
)
......@@ -91,6 +92,9 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser):
lifeinsurances_history = URL('mefav-wspl/rest/listMouvements', LifeInsurancesHistoryPage)
lifeinsurances_detail = URL('mefav-wspl/rest/detailMouvement', LifeInsurancesDetailPage)
natio_vie_pro = URL('/mefav-wspl/rest/natioViePro', NatioVieProPage)
capitalisation_page = URL('https://www.clients.assurance-vie.fr/servlets/helios.cinrj.htmlnav.runtime.FrontServlet', CapitalisationPage)
market_list = URL('pe-war/rpc/SAVaccountDetails/get', MarketListPage)
market_syn = URL('pe-war/rpc/synthesis/get', MarketSynPage)
market = URL('pe-war/rpc/portfolioDetails/get', MarketPage)
......@@ -167,6 +171,16 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser):
break
self.accounts_list.append(account)
# Fetching capitalisation contracts from the "Assurances Vie" space (some are not in the BNP API):
params = self.natio_vie_pro.go().get_params()
self.capitalisation_page.go(params=params)
if self.capitalisation_page.is_here() and self.page.has_contracts():
for account in self.page.iter_capitalisation():
# Life Insurance accounts may appear BOTH in the API and the "Assurances Vie" domain,
# It is better to keep the API version since it contains the unitvalue:
if account.number not in [a.number for a in self.accounts_list]:
self.accounts_list.append(account)
return iter(self.accounts_list)
@need_login
......@@ -175,6 +189,9 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser):
@need_login
def iter_history(self, account, coming=False):
# The accounts from the "Assurances Vie" space have no available history:
if hasattr(account, '_details'):
return []
if account.type == Account.TYPE_PEA and account.label.endswith('Espèces'):
return []
if account.type == account.TYPE_LIFE_INSURANCE:
......@@ -234,11 +251,27 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser):
def iter_investment(self, account):
if account.type == Account.TYPE_PEA and account.label.endswith('Espèces'):
return []
if account.type in (account.TYPE_LIFE_INSURANCE, account.TYPE_PERP):
self.lifeinsurances.go(data=JSON({
"ibanCrypte": account.id,
}))
return self.page.iter_investments()
# Life insurances and PERP may be scraped from the API or from the "Assurance Vie" space,
# so we need to discriminate between both using account._details:
if account.type in (account.TYPE_LIFE_INSURANCE, account.TYPE_PERP, account.TYPE_CAPITALISATION):
if hasattr(account, '_details'):
# Going to the "Assurances Vie" page
natiovie_params = self.natio_vie_pro.go().get_params()
self.capitalisation_page.go(params=natiovie_params)
# Fetching the form to get the contract investments:
capitalisation_params = self.page.get_params(account)
self.capitalisation_page.go(params=capitalisation_params)
return self.page.iter_investments()
else:
# No capitalisation contract has yet been found in the API:
assert account.type != account.TYPE_CAPITALISATION
self.lifeinsurances.go(data=JSON({
"ibanCrypte": account.id,
}))
return self.page.iter_investments()
elif account.type in (account.TYPE_MARKET, account.TYPE_PEA):
try:
self.market_list.go(data=JSON({}))
......@@ -256,6 +289,7 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser):
self.logger.warning("An Internal Server Error occured")
break
return self.page.iter_investments()
return iter([])
@need_login
......
......@@ -26,9 +26,10 @@ from random import randint
from decimal import Decimal
from datetime import datetime, timedelta
from weboob.browser.elements import DictElement, ListElement, ItemElement, method
from weboob.browser.elements import DictElement, ListElement, TableElement, ItemElement, method
from weboob.browser.filters.json import Dict
from weboob.browser.filters.standard import Format, Regexp, CleanText, Date
from weboob.browser.filters.standard import Format, Eval, Regexp, CleanText, Date, CleanDecimal, Field
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, AddRecipientError
......@@ -39,6 +40,8 @@ from weboob.tools.capabilities.bank.iban import rib2iban, rebuild_rib, is_iban_v
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from weboob.tools.captcha.virtkeyboard import GridVirtKeyboard
from weboob.tools.date import parse_french_date
from weboob.tools.capabilities.bank.investments import is_isin_valid
from weboob.tools.compat import unquote_plus
class ConnectionThresholdPage(HTMLPage):
......@@ -598,6 +601,126 @@ class LifeInsurancesDetailPage(LifeInsurancesPage):
investments_path = 'data.detailMouvement.listeSupport.*'
class NatioVieProPage(BNPPage):
# This form is required to go to the capitalisation contracts page.
def get_params(self):
params = {
'app': 'BNPNET',
'hageGroup': 'consultationBnpnet',
'init': 'true',
'multiInit': 'false',
}
params['a0'] = self.doc['data']['nationVieProInfos']['a0']
# The number of "p" keys may vary (p0, p1, p2 ... up to p13 or more)
for key, value in self.doc['data']['nationVieProInfos']['listeP'].items():
params[key] = value
# We must decode the values before constructing the URL:
for k, v in params.items():
params[k] = unquote_plus(v)
return params
class CapitalisationPage(LoggedPage, HTMLPage):
def has_contracts(self):
# This message will appear if the page "Assurance Vie" contains no contract.
return not CleanText('//td[@class="message"]/text()[starts-with(., "Pour toute information")]')(self.doc)
# To be completed with other account labels and types seen on the "Assurance Vie" space:
ACCOUNT_TYPES = {
'BNP Paribas Multiplacements': Account.TYPE_LIFE_INSURANCE,
'BNP Paribas Multiciel Privilège': Account.TYPE_CAPITALISATION,
'Plan Epargne Retraite Particulier': Account.TYPE_PERP,
"Plan d'Épargne Retraite des Particuliers": Account.TYPE_PERP,
}
@method
class iter_capitalisation(TableElement):
# Other types of tables may appear on the page (such as Alternative Emprunteur/Capital Assuré)
# But they do not contain bank accounts so we must avoid them.
item_xpath = '//table/tr[preceding-sibling::tr[th[text()="Libellé du contrat"]]][td[@class="ligneTableau"]]'
head_xpath = '//table/tr/th[@class="headerTableau"]'
col_label = 'Libellé du contrat'
col_id = 'Numéro de contrat'
col_balance = 'Montant'
col_currency = "Monnaie d'affichage"
class item(ItemElement):
klass = Account
obj_label = CleanText(TableCell('label'))
obj_id = CleanText(TableCell('id'))
obj_number = CleanText(TableCell('id'))
obj_balance = CleanDecimal(TableCell('balance'), replace_dots=True)
obj_coming = None
obj_iban = None
def obj_type(self):
for k, v in self.page.ACCOUNT_TYPES.items():
if Field('label')(self).startswith(k):
return v
return Account.TYPE_UNKNOWN
def obj_currency(self):
currency = CleanText(TableCell('currency')(self))(self)
return Account.get_currency(currency)
# Required to get the investments of each "Assurances Vie" account:
def obj__details(self):
raw_details = CleanText((TableCell('balance')(self)[0]).xpath('./a/@href'))(self)
m = re.search(r"Window\('(.*?)',window", raw_details)
if m:
return m.group(1)
def get_params(self, account):
form = self.get_form(xpath='//form[@name="formListeContrats"]')
form['postValue'] = account._details
return form
# The investments vdate is out of the investments table and is the same for all investments:
def get_vdate(self):
return parse_french_date(CleanText('//table[tr[th[text()[contains(., "Date de valorisation")]]]]/tr[2]/td[2]')(self.doc))
@method
class iter_investments(TableElement):
# Investment lines contain at least 5 <td> tags
item_xpath = '//table[tr[th[text()[contains(., "Libellé")]]]]/tr[count(td)>=5]'
head_xpath = '//table[tr[th[text()[contains(., "Libellé")]]]]/tr/th[@class="headerTableau"]'
col_label = 'Libellé'
col_code = 'Code ISIN'
col_quantity = 'Nombre de parts'
col_valuation = 'Montant'
col_portfolio_share = 'Montant en %'
class item(ItemElement):
klass = Investment
obj_label = CleanText(TableCell('label'))
obj_valuation = CleanDecimal(TableCell('valuation'), replace_dots=True)
obj_portfolio_share = Eval(lambda x: x / 100, CleanDecimal(TableCell('portfolio_share'), replace_dots=True))
# There is no "unitvalue" information available on the "Assurances Vie" space.
def obj_quantity(self):
quantity = TableCell('quantity')(self)
if CleanText(quantity)(self) == '-':
return NotAvailable
return CleanDecimal(quantity, replace_dots=True)(self)
def obj_code(self):
isin = CleanText(TableCell('code')(self))(self)
return isin or NotAvailable
def obj_code_type(self):
if is_isin_valid(Field('code')(self)):
return Investment.CODE_TYPE_ISIN
return NotAvailable
def obj_vdate(self):
return self.page.get_vdate()
class MarketListPage(BNPPage):
def get_list(self):
return self.get('securityAccountsList') or []
......
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