Commit e7940425 authored by Quentin Defenouillere's avatar Quentin Defenouillere Committed by Romain Bignon

[lcl] Implement iter_history and iter_investment for Life Insurance API

LCL just implemented a new API for the Life Insurance Space.
This patch handles navigation to the history and investments JSON pages
and the iteration of Transaction() and Investment() objects according to
their new website model.

Closes: 7713@zendesk
parent 3f9f2481
......@@ -18,15 +18,17 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from datetime import datetime, timedelta, date
from functools import wraps
from weboob.exceptions import BrowserIncorrectPassword
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
from weboob.browser.exceptions import ServerError
from weboob.browser.pages import FormNotFound
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.bank import Account, Investment, AddRecipientBankError, AddRecipientStep, Recipient
from weboob.capabilities.bank import Account, AddRecipientBankError, AddRecipientStep, Recipient
from weboob.tools.capabilities.bank.investments import create_french_liquidity
from weboob.tools.compat import basestring, urlsplit, parse_qsl, unicode
from weboob.tools.value import Value
......@@ -36,7 +38,7 @@ from .pages import LoginPage, AccountsPage, AccountHistoryPage, \
HomePage, LoansPage, TransferPage, AddRecipientPage, \
RecipientPage, RecipConfirmPage, SmsPage, RecipRecapPage, \
LoansProPage, Form2Page, DocumentsPage, ClientPage, SendTokenPage, \
CaliePage, ProfilePage, DepositPage
CaliePage, ProfilePage, DepositPage, AVHistoryPage, AVInvestmentsPage
__all__ = ['LCLBrowser', 'LCLProBrowser', 'ELCLBrowser']
......@@ -91,7 +93,10 @@ class LCLBrowser(LoginBrowser, StatesMixin):
assurancevie = URL('/outil/UWVI/AssuranceVie/accesSynthese',
'/outil/UWVI/AssuranceVie/accesDetail.*',
AVPage)
avdetail = URL('https://assurance-vie-et-prevoyance.secure.lcl.fr.*', AVDetailPage)
avdetail = URL('https://assurance-vie-et-prevoyance.secure.lcl.fr/consultation/epargne', AVDetailPage)
av_history = URL('https://assurance-vie-et-prevoyance.secure.lcl.fr/rest/assurance/historique', AVHistoryPage)
av_investments = URL('https://assurance-vie-et-prevoyance.secure.lcl.fr/rest/detailEpargne/contrat', AVInvestmentsPage)
loans = URL('/outil/UWCR/SynthesePar/', LoansPage)
loans_pro = URL('/outil/UWCR/SynthesePro/', LoansProPage)
......@@ -302,29 +307,37 @@ class LCLBrowser(LoginBrowser, StatesMixin):
yield tr
for tr in self.get_cb_operations(account, 1):
yield tr
elif account.type == Account.TYPE_LIFE_INSURANCE and account._form:
elif account.type == Account.TYPE_LIFE_INSURANCE:
if not account._form:
self.logger.warning('This account is limited, there is no available history.')
return
self.assurancevie.stay_or_go()
account._form.submit()
# The website often returns an error so we try again:
# "L’accès au service est momentanément indisponible."
try:
account._form.submit()
except BrowserUnavailable:
self.logger.warning("Service unavailable, we submit the form again.")
self.assurancevie.stay_or_go()
account._form.submit()
if self.calie.is_here():
# come back to syntese
# Get back to Synthèse
self.assurancevie.go()
return
# certain users will get a message : "Ne détenant pas de compte dépôt
# Some users will get a message : "Ne détenant pas de compte dépôt
# chez LCL, l'accès à ce service vous est indisponible."
if self.form2.is_here() and self.page.assurancevie_hist_not_available():
return
assert self.avdetail.is_here()
self.avdetail.go()
self.av_history.go()
for tr in self.page.iter_history():
yield tr
try:
self.page.get_details(account, "OHIPU")
except FormNotFound:
assert self.page.is_restricted()
self.logger.warning('restricted access to account %s', account)
else:
for tr in self.page.iter_history():
yield tr
self.avdetail.go()
self.page.come_back()
@go_contract
......@@ -357,33 +370,46 @@ class LCLBrowser(LoginBrowser, StatesMixin):
@go_contract
@need_login
def get_investment(self, account):
if account.type == Account.TYPE_LIFE_INSURANCE and account._form:
if account.type == Account.TYPE_LIFE_INSURANCE:
if not account._form:
self.logger.warning('This account is limited, there is no available investment.')
return
self.assurancevie.stay_or_go()
account._form.submit()
# The website often returns an error so we try again:
# "L’accès au service est momentanément indisponible."
try:
account._form.submit()
except BrowserUnavailable:
self.logger.warning("Service unavailable, we submit the form again.")
self.assurancevie.stay_or_go()
account._form.submit()
if self.calie.is_here():
# come back to syntese
# Get back to Synthèse
self.assurancevie.go()
return
if self.page.is_restricted():
self.logger.warning('restricted access to account %s', account)
else:
for inv in self.page.iter_investment():
yield inv
if self.avdetail.is_here():
self.page.come_back()
# Some users will get a message : "Ne détenant pas de compte dépôt
# chez LCL, l'accès à ce service vous est indisponible."
if self.form2.is_here() and self.page.assurancevie_hist_not_available():
return
self.avdetail.go()
self.av_investments.go()
for inv in self.page.iter_investment():
yield inv
self.avdetail.go()
self.page.come_back()
elif hasattr(account, '_market_link') and account._market_link:
self.connexion_bourse()
for inv in self.location(account._market_link).page.iter_investment():
yield inv
self.deconnexion_bourse()
elif account.id in self.get_bourse_accounts_ids():
inv = Investment()
inv.id = account.id
inv.code = 'XX-Liquidity'
inv.label = "Liquidités"
inv.valuation = account.balance
yield inv
yield create_french_liquidity(account.balance)
def locate_browser(self, state):
if state['url'] == 'https://particuliers.secure.lcl.fr/outil/UWBE/Creation/creationConfirmation':
......
......@@ -35,15 +35,15 @@ from weboob.capabilities.bank import (
from weboob.capabilities.bill import Document, Subscription, DocumentTypes
from weboob.capabilities.profile import Person, ProfileMissing
from weboob.capabilities.contact import Advisor
from weboob.browser.elements import method, ListElement, TableElement, ItemElement, SkipItem
from weboob.browser.elements import method, ListElement, TableElement, ItemElement, SkipItem, DictElement
from weboob.exceptions import ParseError
from weboob.browser.exceptions import ServerError
from weboob.browser.pages import LoggedPage, HTMLPage, FormNotFound, pagination
from weboob.browser.pages import LoggedPage, HTMLPage, JsonPage, FormNotFound, pagination
from weboob.browser.filters.html import Attr, Link, TableCell, AttributeNotFound
from weboob.browser.filters.standard import (
CleanText, Field, Regexp, Format, Date, CleanDecimal, Map, AsyncLoad, Async, Env,
Eval, Slugify,
CleanText, Field, Regexp, Format, Date, CleanDecimal, Map, AsyncLoad, Async, Env, Slugify,
)
from weboob.browser.filters.json import Dict
from weboob.exceptions import BrowserUnavailable, BrowserIncorrectPassword
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from weboob.tools.captcha.virtkeyboard import MappedVirtKeyboard, VirtKeyboardError
......@@ -251,6 +251,7 @@ class AccountsPage(LoggedPage, HTMLPage):
'007': Account.TYPE_SAVINGS,
'012': Account.TYPE_SAVINGS,
'023': Account.TYPE_CHECKING,
'036': Account.TYPE_SAVINGS,
'046': Account.TYPE_SAVINGS,
'047': Account.TYPE_SAVINGS,
'049': Account.TYPE_SAVINGS,
......@@ -770,22 +771,13 @@ class Form2Page(LoggedPage, LCLBasePage):
msg = "Ne détenant pas de compte dépôt chez LCL, l'accès à ce service vous est indisponible"
return msg in CleanText('//div[@id="attTxt"]')(self.doc)
def is_restricted(self):
return self.assurancevie_hist_not_available()
def on_load(self):
if self.assurancevie_hist_not_available():
return
error = CleanText('//div[@id="attTxt"]/text()[1]')(self.doc)
if "L’accès au service est momentanément indisponible" in error:
raise BrowserUnavailable(error)
form = self.get_form(name="formulaire")
cName = self.get_from_js('.cName.value = "', '";')
if cName:
form['cName'] = cName
form['cValue'] = self.get_from_js('.cValue.value = "', '";')
form['cMaxAge'] = '-1'
form = self.get_form()
return form.submit()
......@@ -800,19 +792,6 @@ class AVDetailPage(LoggedPage, LCLBasePage):
def get_account_id(self):
return Regexp(CleanText('//div[@class="libelletitrepage"]/h1'), r"N° (\w+)")(self.doc)
def sub(self):
form = self.get_form(name="formulaire")
cName = self.get_from_js('.cName.value = "', '";')
if cName:
form['cName'] = cName
form['cValue'] = self.get_from_js('.cValue.value = "', '";')
form['cMaxAge'] = '-1'
return form.submit()
def submit_simple(self):
form = self.get_form(name="formulaire")
return form.submit()
def come_back(self):
session = self.get_from_js('idSessionSag = "', '"')
params = {}
......@@ -824,78 +803,66 @@ class AVDetailPage(LoggedPage, LCLBasePage):
params['stbzn'] = 'bnc'
return self.browser.location('https://assurance-vie-et-prevoyance.secure.lcl.fr/filiale/entreeBam', params=params)
def get_details(self, account, act=None):
form = self.get_form(id="frm_fwk")
form.submit()
if act is not None:
self.browser.location("entreeBam?sessionSAG=%s&act=%s" % (form['sessionSAG'], act))
def is_restricted(self):
msg = CleanText('//div[has-class("titre_libelle_erreur")]')(self.doc)
return msg in (
"Pour ce type d’opération, nous vous conseillons de vous rapprocher de votre conseiller afin de bénéficier du conseil personnalisé le mieux adapté à vos objectifs.",
"Vous n’avez pas accès à l’espace assurances de personnes.",
)
class AVHistoryPage(LoggedPage, JsonPage):
@method
class iter_investment(TableElement):
head_xpath = '//table[@class="table"][ends-with(@id,"CD_UCT")]/thead//th'
item_xpath = '//table[@class="table"][ends-with(@id,"CD_UCT")]/tbody/tr'
col_label = 'Le(s) support(s) financier(s) de votre contrat'
col_unitvalue = 'Valeur de la part'
col_vdate = 'En date du :'
col_quantity = 'Nombre de parts'
col_valuation = 'Total'
col_portfolio_share = u'Répartition'
class iter_history(DictElement):
item_xpath = 'listeOperations'
class item(ItemElement):
klass = Investment
klass = Transaction
obj_label = CleanText(TableCell('label'))
obj_label = CleanText(Dict('lcope'))
obj_amount = CleanDecimal(Dict('mtope'))
obj_type = Transaction.TYPE_BANK
obj_investments = NotAvailable
def obj_code(self):
td = TableCell('label')(self)[0]
return Attr('.//a', 'id', default=NotAvailable)(td)
# The 'idope' key contains a string such as "70_6660666 2018-03-182018-03-16-20.55.27.960852"
# 70= N° transaction, 6660666= N° account, 2018-03-18= date and 2018-03-16=rdate.
# We thus use "70_6660666" for the transaction ID.
obj_code_type = Investment.CODE_TYPE_ISIN
obj_id = Regexp(CleanText(Dict('idope')), '(\d+_\d+)')
def obj_quantity(self):
return MyDecimal(TableCell('quantity'))(self) or NotAvailable
def obj__dates(self):
raw = CleanText(Dict('idope'))(self)
m = re.findall('\d{4}-\d{2}-\d{2}', raw)
# We must verify that the two dates are correctly fetched
assert len(m) == 2
return m
def obj_unitvalue(self):
return MyDecimal(TableCell('unitvalue'))(self) or NotAvailable
def obj_date(self):
return Date().filter(Field('_dates')(self)[0])
obj_valuation = MyDecimal(TableCell('valuation'))
obj_portfolio_share = Eval(lambda x: x / 100, CleanDecimal(TableCell('portfolio_share'), replace_dots=True))
def obj_rdate(self):
return Date().filter(Field('_dates')(self)[1])
obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True, default=NotAvailable)
@pagination
class AVInvestmentsPage(LoggedPage, JsonPage):
@method
class iter_history(TableElement):
item_xpath = '//table[@class="table"]/tbody/tr'
head_xpath = '//table[@class="table"]/thead/tr/th'
col_date = 'Date d\'effet'
col_label = u'Opération(s)'
col_amount = 'Montant'
def next_page(self):
if Link('//a[@class="pictoSuivant"]', default=None)(self):
form = self.page.get_form(id="frm_fwk")
form['fwkaction'] = "precSuivDet"
form['fwkcodeaction'] = "Executer"
form['ACTION_CHOISIE'] = "suivant"
return requests.Request("POST", form.url, data=dict(form))
class iter_investment(DictElement):
item_xpath = 'listeSupports/support'
class item(ItemElement):
klass = Transaction
klass = Investment
obj_label = CleanText(TableCell('label'))
obj_type = Transaction.TYPE_BANK
obj_date = Date(CleanText(TableCell('date')), dayfirst=True)
obj_amount = MyDecimal(TableCell('amount'))
obj_label = CleanText(Dict('lcspt'))
obj_valuation = CleanDecimal(Dict('mtvalspt'))
obj_code = CleanText(Dict('cdsptisn'), default=NotAvailable)
obj_unitvalue = CleanDecimal(Dict('mtliqpaaspt'), default=NotAvailable)
obj_quantity = CleanDecimal(Dict('qtpaaspt'), default=NotAvailable)
obj_portfolio_share = CleanDecimal(Dict('txrpaspt'), default=NotAvailable)
obj_diff = CleanDecimal(Dict('mtpmvspt'), default=NotAvailable)
def obj_vdate(self):
time = Dict('dvspt')(self)
if not time:
return NotAvailable
return datetime.fromtimestamp(time//1000)
def obj_code_type(self):
if is_isin_valid(Field('code')(self)):
return Investment.CODE_TYPE_ISIN
return NotAvailable
class RibPage(LoggedPage, LCLBasePage):
......
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