Skip to content
Commits on Source (82)
...@@ -21,7 +21,9 @@ ...@@ -21,7 +21,9 @@
from datetime import date from datetime import date
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ImageCaptchaQuestion, BrowserQuestion from weboob.exceptions import (
BrowserIncorrectPassword, BrowserUnavailable, ImageCaptchaQuestion, BrowserQuestion, ActionNeeded
)
from weboob.tools.value import Value from weboob.tools.value import Value
from weboob.browser.browsers import ClientError from weboob.browser.browsers import ClientError
...@@ -85,6 +87,11 @@ def push_security_otp(self, pin_code): ...@@ -85,6 +87,11 @@ def push_security_otp(self, pin_code):
self.location('/ap/signin', data=res_form, headers=self.otp_headers) self.location('/ap/signin', data=res_form, headers=self.otp_headers)
def handle_security(self): def handle_security(self):
otp_type = self.page.get_otp_type()
if otp_type == '/ap/signin':
# this otp will be always present until user deactivate it
raise ActionNeeded('You have enabled otp in your options, please deactivate it before synchronize')
if self.page.doc.xpath('//span[@class="a-button-text"]'): if self.page.doc.xpath('//span[@class="a-button-text"]'):
self.page.send_code() self.page.send_code()
......
...@@ -45,9 +45,20 @@ def get_sub_link(self): ...@@ -45,9 +45,20 @@ def get_sub_link(self):
class SecurityPage(HTMLPage): class SecurityPage(HTMLPage):
def get_otp_type(self):
# amazon send us otp in two cases:
# - if it's the first time we connect to this account for an ip => manage it normally
# - if user has activated otp in his options => raise ActionNeeded, an ask user to deactivate it
form = self.get_form(xpath='//form[.//h1]')
url = form.url.replace(self.browser.BASEURL, '')
# verify: this otp is sent by amazon when we connect to the account for the first time from a new ip or computer
# /ap/signin: this otp is a user activated otp which is always present
assert url in ('verify', '/ap/signin'), url
return url
def get_otp_message(self): def get_otp_message(self):
message = self.doc.xpath('//div[@class="a-box-inner"]/p') return CleanText('//div[@class="a-box-inner"]/p')(self.doc)
return message[0] if message else None
def send_code(self): def send_code(self):
form = self.get_form() form = self.get_form()
......
...@@ -61,8 +61,8 @@ class item(ItemElement): ...@@ -61,8 +61,8 @@ class item(ItemElement):
obj_label = 'Compte Bolden' obj_label = 'Compte Bolden'
obj_type = Account.TYPE_MARKET obj_type = Account.TYPE_MARKET
obj_currency = 'EUR' obj_currency = 'EUR'
obj_balance = CleanDecimal('//div[p[has-class("investor-state") and contains(text(),"Total compte Bolden :")]]/p[has-class("investor-status")]', replace_dots=True) obj_balance = CleanDecimal.French('//div[p[has-class("investor-state") and contains(text(),"Total compte Bolden :")]]/p[has-class("investor-status")]')
obj_valuation_diff = CleanDecimal('//p[has-class("rent-amount strong dashboard-text")]', replace_dots=True) obj_valuation_diff = CleanDecimal.French('//div[has-class("rent-total")]')
@method @method
class iter_investments(TableElement): class iter_investments(TableElement):
...@@ -93,7 +93,7 @@ def obj__docurl(self): ...@@ -93,7 +93,7 @@ def obj__docurl(self):
return urljoin(self.page.url, Link('.//a')(TableCell('doc')(self)[0])) return urljoin(self.page.url, Link('.//a')(TableCell('doc')(self)[0]))
def get_liquidity(self): def get_liquidity(self):
return CleanDecimal('//div[p[contains(text(), "Fonds disponibles")]]/p[@class="investor-status strong"]', replace_dots=True)(self.doc) return CleanDecimal.French('//div[p[contains(text(), "Fonds disponibles")]]/p[has-class("investor-status")]')(self.doc)
class OperationsPage(LoggedPage, HTMLPage): class OperationsPage(LoggedPage, HTMLPage):
......
...@@ -74,7 +74,10 @@ class BPBrowser(LoginBrowser, StatesMixin): ...@@ -74,7 +74,10 @@ class BPBrowser(LoginBrowser, StatesMixin):
'/voscomptes/canalXHTML/pret/encours/detaillerOffrePretConsoListe-encoursPrets.ea', '/voscomptes/canalXHTML/pret/encours/detaillerOffrePretConsoListe-encoursPrets.ea',
'/voscomptes/canalXHTML/pret/creditRenouvelable/init-consulterCreditRenouvelable.ea', '/voscomptes/canalXHTML/pret/creditRenouvelable/init-consulterCreditRenouvelable.ea',
'/voscomptes/canalXHTML/pret/encours/rechercherPret-encoursPrets.ea', '/voscomptes/canalXHTML/pret/encours/rechercherPret-encoursPrets.ea',
'/voscomptes/canalXHTML/sso/commun/init-integration.ea\?partenaire',
'/voscomptes/canalXHTML/sso/lbpf/souscriptionCristalFormAutoPost.jsp',
AccountList) AccountList)
par_accounts_revolving = URL('https://espaceclientcreditconso.labanquepostale.fr/sav/accueil.do', AccountList)
accounts_rib = URL(r'.*voscomptes/canalXHTML/comptesCommun/imprimerRIB/init-imprimer_rib.ea.*', accounts_rib = URL(r'.*voscomptes/canalXHTML/comptesCommun/imprimerRIB/init-imprimer_rib.ea.*',
'/voscomptes/canalXHTML/comptesCommun/imprimerRIB/init-selection_rib.ea', AccountRIB) '/voscomptes/canalXHTML/comptesCommun/imprimerRIB/init-selection_rib.ea', AccountRIB)
...@@ -103,10 +106,11 @@ class BPBrowser(LoginBrowser, StatesMixin): ...@@ -103,10 +106,11 @@ class BPBrowser(LoginBrowser, StatesMixin):
par_account_checking_history = URL('/voscomptes/canalXHTML/CCP/releves_ccp/init-releve_ccp.ea\?typeRecherche=10&compte.numero=(?P<accountId>.*)', par_account_checking_history = URL('/voscomptes/canalXHTML/CCP/releves_ccp/init-releve_ccp.ea\?typeRecherche=10&compte.numero=(?P<accountId>.*)',
'/voscomptes/canalXHTML/CCP/releves_ccp/afficher-releve_ccp.ea', AccountHistory) '/voscomptes/canalXHTML/CCP/releves_ccp/afficher-releve_ccp.ea', AccountHistory)
deferred_card_history = URL(r'/voscomptes/canalXHTML/CB/releveCB/init-mouvementsCarteDD.ea\?compte.numero=(?P<accountId>\w+)&indexCarte=(?P<cardIndex>\d+)&typeListe=(?P<type>\d+)', AccountHistory) deferred_card_history = URL(r'/voscomptes/canalXHTML/CB/releveCB/init-mouvementsCarteDD.ea\?compte.numero=(?P<accountId>\w+)&indexCompte=(?P<cardIndex>\d+)&typeListe=(?P<type>\d+)', AccountHistory)
deferred_card_history_multi = URL(r'/voscomptes/canalXHTML/CB/releveCB/preparerRecherche-mouvementsCarteDD.ea\?compte.numero=(?P<accountId>\w+)&indexCarte=(?P<cardIndex>\d+)&typeListe=(?P<type>\d+)', AccountHistory) # &typeRecherche=10 deferred_card_history_multi = URL(r'/voscomptes/canalXHTML/CB/releveCB/preparerRecherche-mouvementsCarteDD.ea\?indexCompte=(?P<accountId>\w+)&indexCarte=(?P<cardIndex>\d+)&typeListe=(?P<type>\d+)', AccountHistory) # &typeRecherche=10
par_account_checking_coming = URL('/voscomptes/canalXHTML/CCP/releves_ccp_encours/preparerRecherche-releve_ccp_encours.ea\?compte.numero=(?P<accountId>.*)&typeRecherche=1', par_account_checking_coming = URL('/voscomptes/canalXHTML/CCP/releves_ccp_encours/preparerRecherche-releve_ccp_encours.ea\?compte.numero=(?P<accountId>.*)&typeRecherche=1',
'/voscomptes/canalXHTML/CB/releveCB/init-mouvementsCarteDD.ea\?compte.numero=(?P<accountId>.*)&typeListe=1&typeRecherche=10', AccountHistory) '/voscomptes/canalXHTML/CB/releveCB/init-mouvementsCarteDD.ea\?compte.numero=(?P<accountId>.*)&typeListe=1&typeRecherche=10',
'/voscomptes/canalXHTML/CCP/releves_ccp_encours/preparerRecherche-releve_ccp_encours.ea\?indexCompte', AccountHistory)
par_account_savings_and_invests_history = URL('/voscomptes/canalXHTML/CNE/releveCNE/init-releve_cne.ea\?typeRecherche=10&compte.numero=(?P<accountId>.*)', par_account_savings_and_invests_history = URL('/voscomptes/canalXHTML/CNE/releveCNE/init-releve_cne.ea\?typeRecherche=10&compte.numero=(?P<accountId>.*)',
'/voscomptes/canalXHTML/CNE/releveCNE/releveCNE-releve_cne.ea', AccountHistory) '/voscomptes/canalXHTML/CNE/releveCNE/releveCNE-releve_cne.ea', AccountHistory)
...@@ -218,6 +222,10 @@ def do_login(self): ...@@ -218,6 +222,10 @@ def do_login(self):
@need_login @need_login
def get_accounts_list(self): def get_accounts_list(self):
if self.session.cookies.get('indicateur'):
# Malformed cookie to delete to reach other spaces
del self.session.cookies['indicateur']
if self.accounts is None: if self.accounts is None:
accounts = [] accounts = []
to_check = [] to_check = []
...@@ -237,7 +245,7 @@ def get_accounts_list(self): ...@@ -237,7 +245,7 @@ def get_accounts_list(self):
for account in self.page.iter_accounts(): for account in self.page.iter_accounts():
if account.type == Account.TYPE_LOAN: if account.type == Account.TYPE_LOAN:
self.location(account.url) self.location(account.url)
if 'CreditRenouvelable' not in account.url: if 'initSSO' not in account.url:
for loan in self.page.iter_loans(): for loan in self.page.iter_loans():
loan.currency = account.currency loan.currency = account.currency
accounts.append(loan) accounts.append(loan)
...@@ -248,9 +256,11 @@ def get_accounts_list(self): ...@@ -248,9 +256,11 @@ def get_accounts_list(self):
student_loan.currency = account.currency student_loan.currency = account.currency
accounts.append(student_loan) accounts.append(student_loan)
else: else:
for loan in self.page.iter_revolving_loans(): # The main revolving page is not accessible, we can reach it by this new way
loan.currency = account.currency self.location(self.absurl('/voscomptes/canalXHTML/sso/lbpf/souscriptionCristalFormAutoPost.jsp'))
accounts.append(loan) self.page.go_revolving()
revolving_loan = self.page.get_revolving_attributes(account)
accounts.append(revolving_loan)
page.go() page.go()
elif account.type == Account.TYPE_PERP: elif account.type == Account.TYPE_PERP:
...@@ -308,7 +318,7 @@ def get_history(self, account): ...@@ -308,7 +318,7 @@ def get_history(self, account):
self.go_linebourse(account) self.go_linebourse(account)
return self.linebourse.iter_history(account.id) return self.linebourse.iter_history(account.id)
if account.type == Account.TYPE_LOAN: if account.type in (Account.TYPE_LOAN, Account.TYPE_REVOLVING_CREDIT):
return [] return []
if account.type == Account.TYPE_CARD: if account.type == Account.TYPE_CARD:
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
from weboob.browser.elements import ListElement, ItemElement, method, TableElement from weboob.browser.elements import ListElement, ItemElement, method, TableElement
from weboob.browser.pages import LoggedPage, RawPage, PartialHTMLPage, HTMLPage from weboob.browser.pages import LoggedPage, RawPage, PartialHTMLPage, HTMLPage
from weboob.browser.filters.html import Link, TableCell from weboob.browser.filters.html import Link, TableCell
from weboob.browser.filters.standard import CleanText, CleanDecimal, Regexp, Env, Field, BrowserURL, Currency, Async, Date, Format from weboob.browser.filters.standard import CleanText, CleanDecimal, Regexp, Env, Field, Currency, Async, Date, Format
from weboob.exceptions import BrowserUnavailable from weboob.exceptions import BrowserUnavailable
from weboob.tools.compat import urljoin, unicode from weboob.tools.compat import urljoin, unicode
...@@ -58,6 +58,8 @@ def condition(self): ...@@ -58,6 +58,8 @@ def condition(self):
def obj_url(self): def obj_url(self):
url = Link(u'./a', default=NotAvailable)(self) url = Link(u'./a', default=NotAvailable)(self)
if url: if url:
if 'CreditRenouvelable' in url:
url = Link(u'.//a[contains(text(), "espace de gestion crédit renouvelable")]')(self.el)
return urljoin(self.page.url, url) return urljoin(self.page.url, url)
return url return url
...@@ -74,9 +76,9 @@ def obj_coming(self): ...@@ -74,9 +76,9 @@ def obj_coming(self):
has_coming = False has_coming = False
coming = 0 coming = 0
self.page.browser.open(Field('url')(self)) details_page = self.page.browser.open(Field('url')(self))
coming_operations = self.page.browser.open( coming_op_link = Regexp(Link(u'//a[contains(text(), "Opérations à venir")]'), r'../(.*)')(details_page.page.doc)
BrowserURL('par_account_checking_coming', accountId=Field('id'))(self)) coming_operations = self.page.browser.open(self.page.browser.BASEURL + '/voscomptes/canalXHTML/CCP/' + coming_op_link)
if CleanText('//span[@id="amount_total"]')(coming_operations.page.doc): if CleanText('//span[@id="amount_total"]')(coming_operations.page.doc):
has_coming = True has_coming = True
...@@ -91,10 +93,11 @@ def obj_coming(self): ...@@ -91,10 +93,11 @@ def obj_coming(self):
return NotAvailable return NotAvailable
def obj_iban(self): def obj_iban(self):
response = self.page.browser.open( rib_link = Link('//a[abbr[contains(text(), "RIB")]]', default=NotAvailable)(self.el)
'/voscomptes/canalXHTML/comptesCommun/imprimerRIB/init-imprimer_rib.ea?numeroCompte=%s' % Field('id')( if rib_link:
self)) response = self.page.browser.open(rib_link)
return response.page.get_iban() return response.page.get_iban()
return NotAvailable
def obj_type(self): def obj_type(self):
types = {'comptes? bancaires?': Account.TYPE_CHECKING, types = {'comptes? bancaires?': Account.TYPE_CHECKING,
...@@ -137,6 +140,10 @@ def on_load(self): ...@@ -137,6 +140,10 @@ def on_load(self):
raise BrowserUnavailable() raise BrowserUnavailable()
def go_revolving(self):
form = self.get_form()
form.submit()
@property @property
def no_accounts(self): def no_accounts(self):
return len(self.doc.xpath('//iframe[contains(@src, "/comptes_contrats/sans_")] |\ return len(self.doc.xpath('//iframe[contains(@src, "/comptes_contrats/sans_")] |\
...@@ -156,6 +163,22 @@ class item_account(item_account_generic): ...@@ -156,6 +163,22 @@ class item_account(item_account_generic):
def condition(self): def condition(self):
return item_account_generic.condition(self) return item_account_generic.condition(self)
def get_revolving_attributes(self, account):
loan = Loan()
loan.id = account.id
loan.label = '%s - %s' %(account.label, account.id)
loan.currency = account.currency
loan.url = account.url
loan.available_amount = CleanDecimal('//tr[td[contains(text(), "Montant Maximum Autorisé") or contains(text(), "Montant autorisé")]]/td[2]')(self.doc)
loan.used_amount = loan.used_amount = CleanDecimal('//tr[td[contains(text(), "Montant Utilisé") or contains(text(), "Montant utilisé")]]/td[2]')(self.doc)
loan.available_amount = CleanDecimal(Regexp(CleanText('//tr[td[contains(text(), "Montant Disponible") or contains(text(), "Montant disponible")]]/td[2]'), r'(.*) au'))(self.doc)
loan._has_cards = False
loan.type = Account.TYPE_REVOLVING_CREDIT
return loan
@method @method
class iter_revolving_loans(ListElement): class iter_revolving_loans(ListElement):
item_xpath = '//div[@class="bloc Tmargin"]//dl' item_xpath = '//div[@class="bloc Tmargin"]//dl'
......
...@@ -333,7 +333,7 @@ def loans_conso(self): ...@@ -333,7 +333,7 @@ def loans_conso(self):
days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
month = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') month = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
now = datetime.datetime.today() now = datetime.datetime.today()
d = '%s %s %s %s:%s:%s GMT 0100 (CET)' % (days[now.weekday()], month[now.month - 1], now.year, now.hour, format(now.minute, "02"), now.second) d = '%s %s %s %s %s:%s:%s GMT+0100 (heure normale d’Europe centrale)' % (days[now.weekday()], now.day, month[now.month - 1], now.year, now.hour, format(now.minute, "02"), now.second)
if self.home.is_here(): if self.home.is_here():
msg = self.page.loan_unavailable_msg() msg = self.page.loan_unavailable_msg()
if msg: if msg:
...@@ -597,7 +597,7 @@ def get_coming(self, account): ...@@ -597,7 +597,7 @@ def get_coming(self, account):
@need_login @need_login
def get_investment(self, account): def get_investment(self, account):
self.deleteCTX() self.deleteCTX()
if account.type not in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_MARKET, Account.TYPE_PEA) or 'measure_id' in account._info: if account.type not in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_CAPITALISATION, Account.TYPE_MARKET, Account.TYPE_PEA) or 'measure_id' in account._info:
raise NotImplementedError() raise NotImplementedError()
if account.type == Account.TYPE_PEA and account.label == 'PEA NUMERAIRE': if account.type == Account.TYPE_PEA and account.label == 'PEA NUMERAIRE':
...@@ -622,7 +622,7 @@ def get_investment(self, account): ...@@ -622,7 +622,7 @@ def get_investment(self, account):
yield investment yield investment
return return
elif account.type == Account.TYPE_LIFE_INSURANCE: elif account.type in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_CAPITALISATION):
if "MILLEVIE" in account.label: if "MILLEVIE" in account.label:
self.page.go_life_insurance(account) self.page.go_life_insurance(account)
label = account.label.split()[-1] label = account.label.split()[-1]
......
...@@ -296,6 +296,8 @@ def _add_account(self, accounts, link, label, account_type, balance): ...@@ -296,6 +296,8 @@ def _add_account(self, accounts, link, label, account_type, balance):
account.type = self.ACCOUNT_TYPES.get(label, info['acc_type'] if 'acc_type' in info else account_type) account.type = self.ACCOUNT_TYPES.get(label, info['acc_type'] if 'acc_type' in info else account_type)
if 'PERP' in account.label: if 'PERP' in account.label:
account.type = Account.TYPE_PERP account.type = Account.TYPE_PERP
if 'NUANCES CAPITALISATI' in account.label:
account.type = Account.TYPE_CAPITALISATION
balance = balance or self.get_balance(account) balance = balance or self.get_balance(account)
account.balance = Decimal(FrenchTransaction.clean_amount(balance)) if balance and balance is not NotAvailable else NotAvailable account.balance = Decimal(FrenchTransaction.clean_amount(balance)) if balance and balance is not NotAvailable else NotAvailable
...@@ -315,10 +317,10 @@ def _add_account(self, accounts, link, label, account_type, balance): ...@@ -315,10 +317,10 @@ def _add_account(self, accounts, link, label, account_type, balance):
accounts[account.id] = account accounts[account.id] = account
def get_balance(self, account): def get_balance(self, account):
if account.type not in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_PERP): if account.type not in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_PERP, Account.TYPE_CAPITALISATION):
return NotAvailable return NotAvailable
page = self.go_history(account._info).page page = self.go_history(account._info).page
balance = page.doc.xpath('.//tr[td[ends-with(@id,"NumContrat")]/a[contains(text(),$id)]]/td[@class="somme"]', id=account.id) balance = page.doc.xpath('.//tr[td[contains(@id,"NumContrat")]]/td[@class="somme"]/a[contains(@href, $id)]', id=account.id)
if len(balance) > 0: if len(balance) > 0:
balance = CleanText('.')(balance[0]) balance = CleanText('.')(balance[0])
balance = balance if balance != u'' else NotAvailable balance = balance if balance != u'' else NotAvailable
...@@ -408,7 +410,7 @@ def go_loans_conso(self, tr): ...@@ -408,7 +410,7 @@ def go_loans_conso(self, tr):
if m: if m:
account = m.group(1) account = m.group(1)
form = self.get_form(name="main") form = self.get_form(id="main")
form['__EVENTTARGET'] = 'MM$SYNTHESE_CREDITS' form['__EVENTTARGET'] = 'MM$SYNTHESE_CREDITS'
form['__EVENTARGUMENT'] = 'ACTIVDESACT_CREDITCONSO&%s' % account form['__EVENTARGUMENT'] = 'ACTIVDESACT_CREDITCONSO&%s' % account
form['m_ScriptManager'] = 'MM$m_UpdatePanel|MM$SYNTHESE_CREDITS' form['m_ScriptManager'] = 'MM$m_UpdatePanel|MM$SYNTHESE_CREDITS'
...@@ -459,6 +461,9 @@ def get_loan_list(self): ...@@ -459,6 +461,9 @@ def get_loan_list(self):
account._card_links = [] account._card_links = []
if "renouvelables" in CleanText('.')(title): if "renouvelables" in CleanText('.')(title):
if 'JSESSIONID' in self.browser.session.cookies:
# Need to delete this to access the consumer loans space (a new one will be created)
del self.browser.session.cookies['JSESSIONID']
self.go_loans_conso(tr) self.go_loans_conso(tr)
d = self.browser.loans_conso() d = self.browser.loans_conso()
if d: if d:
...@@ -502,11 +507,10 @@ class item(ItemElement): ...@@ -502,11 +507,10 @@ class item(ItemElement):
obj_currency = Currency(MyTableCell("balance")) obj_currency = Currency(MyTableCell("balance"))
obj_last_payment_date = Date(CleanText(MyTableCell("last_payment_date"))) obj_last_payment_date = Date(CleanText(MyTableCell("last_payment_date")))
obj_next_payment_amount = MyDecimal(MyTableCell("next_payment_amount")) obj_next_payment_amount = MyDecimal(MyTableCell("next_payment_amount"))
obj_next_payment_date = Date(CleanText(MyTableCell("next_payment_date"))) obj_next_payment_date = Date(CleanText(MyTableCell("next_payment_date", default=''), default=NotAvailable), default=NotAvailable)
def go_list(self): def go_list(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTARGUMENT'] = "CPTSYNT0" form['__EVENTARGUMENT'] = "CPTSYNT0"
...@@ -525,7 +529,7 @@ def go_list(self): ...@@ -525,7 +529,7 @@ def go_list(self):
# On some pages, navigate to indexPage does not lead to the list of measures, so we need this form ... # On some pages, navigate to indexPage does not lead to the list of measures, so we need this form ...
def go_measure_list(self): def go_measure_list(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTARGUMENT'] = "MESLIST0" form['__EVENTARGUMENT'] = "MESLIST0"
form['__EVENTTARGET'] = 'Menu_AJAX' form['__EVENTTARGET'] = 'Menu_AJAX'
...@@ -537,7 +541,7 @@ def go_measure_list(self): ...@@ -537,7 +541,7 @@ def go_measure_list(self):
# This function goes to the accounts page of one measure giving its id # This function goes to the accounts page of one measure giving its id
def go_measure_accounts_list(self, measure_id): def go_measure_accounts_list(self, measure_id):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTARGUMENT'] = "CPTSYNT0" form['__EVENTARGUMENT'] = "CPTSYNT0"
...@@ -556,7 +560,7 @@ def go_measure_accounts_list(self, measure_id): ...@@ -556,7 +560,7 @@ def go_measure_accounts_list(self, measure_id):
form.submit() form.submit()
def go_loan_list(self): def go_loan_list(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTARGUMENT'] = "CRESYNT0" form['__EVENTARGUMENT'] = "CRESYNT0"
...@@ -573,7 +577,7 @@ def go_loan_list(self): ...@@ -573,7 +577,7 @@ def go_loan_list(self):
form.submit() form.submit()
def go_history(self, info, is_cbtab=False): def go_history(self, info, is_cbtab=False):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = 'MM$%s' % (info['type'] if is_cbtab else 'SYNTHESE') form['__EVENTTARGET'] = 'MM$%s' % (info['type'] if is_cbtab else 'SYNTHESE')
form['__EVENTARGUMENT'] = info['link'] form['__EVENTARGUMENT'] = info['link']
...@@ -587,7 +591,7 @@ def go_history(self, info, is_cbtab=False): ...@@ -587,7 +591,7 @@ def go_history(self, info, is_cbtab=False):
def get_form_to_detail(self, transaction): def get_form_to_detail(self, transaction):
m = re.match('.*\("(.*)", "(DETAIL_OP&[\d]+).*\)\)', transaction._link) m = re.match('.*\("(.*)", "(DETAIL_OP&[\d]+).*\)\)', transaction._link)
# go to detailcard page # go to detailcard page
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = m.group(1) form['__EVENTTARGET'] = m.group(1)
form['__EVENTARGUMENT'] = m.group(2) form['__EVENTARGUMENT'] = m.group(2)
fix_form(form) fix_form(form)
...@@ -650,7 +654,7 @@ def go_next(self): ...@@ -650,7 +654,7 @@ def go_next(self):
# <a id="MM_HISTORIQUE_CB_lnkSuivante" class="next" href="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(&quot;MM$HISTORIQUE_CB$lnkSuivante&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, false, true))">Suivant<span class="arrow">></span></a> # <a id="MM_HISTORIQUE_CB_lnkSuivante" class="next" href="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(&quot;MM$HISTORIQUE_CB$lnkSuivante&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, false, true))">Suivant<span class="arrow">></span></a>
link = self.doc.xpath('//a[contains(@id, "lnkSuivante")]') link = self.doc.xpath('//a[contains(@id, "lnkSuivante")]')
if len(link) == 0 or 'disabled' in link[0].attrib: if len(link) == 0 or 'disabled' in link[0].attrib or link[0].attrib.get('class') == 'aspNetDisabled':
return False return False
account_type = 'COMPTE' account_type = 'COMPTE'
...@@ -658,7 +662,7 @@ def go_next(self): ...@@ -658,7 +662,7 @@ def go_next(self):
if m: if m:
account_type = m.group(1) account_type = m.group(1)
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = "MM$HISTORIQUE_%s$lnkSuivante" % account_type form['__EVENTTARGET'] = "MM$HISTORIQUE_%s$lnkSuivante" % account_type
form['__EVENTARGUMENT'] = '' form['__EVENTARGUMENT'] = ''
...@@ -679,7 +683,7 @@ def go_life_insurance(self, account): ...@@ -679,7 +683,7 @@ def go_life_insurance(self, account):
link = self.doc.xpath('//tr[td[contains(., ' + account.id + ') ]]//a')[0] link = self.doc.xpath('//tr[td[contains(., ' + account.id + ') ]]//a')[0]
m = re.search("PostBackOptions?\([\"']([^\"']+)[\"'],\s*['\"]((REDIR_ASS_VIE)?[\d\w&]+)?['\"]", link.attrib.get('href', '')) m = re.search("PostBackOptions?\([\"']([^\"']+)[\"'],\s*['\"]((REDIR_ASS_VIE)?[\d\w&]+)?['\"]", link.attrib.get('href', ''))
if m is not None: if m is not None:
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = m.group(1) form['__EVENTTARGET'] = m.group(1)
form['__EVENTARGUMENT'] = m.group(2) form['__EVENTARGUMENT'] = m.group(2)
...@@ -712,7 +716,7 @@ def go_transfer(self, account): ...@@ -712,7 +716,7 @@ def go_transfer(self, account):
else: else:
link = link[0] link = link[0]
m = re.search("PostBackOptions?\([\"']([^\"']+)[\"'],\s*['\"]([^\"']+)?['\"]", link.attrib.get('href', '')) m = re.search("PostBackOptions?\([\"']([^\"']+)[\"'],\s*['\"]([^\"']+)?['\"]", link.attrib.get('href', ''))
form = self.get_form(name='main') form = self.get_form(id='main')
if 'MM$HISTORIQUE_COMPTE$btnCumul' in form: if 'MM$HISTORIQUE_COMPTE$btnCumul' in form:
del form['MM$HISTORIQUE_COMPTE$btnCumul'] del form['MM$HISTORIQUE_COMPTE$btnCumul']
form['__EVENTTARGET'] = m.group(1) form['__EVENTTARGET'] = m.group(1)
...@@ -728,7 +732,7 @@ def loan_unavailable_msg(self): ...@@ -728,7 +732,7 @@ def loan_unavailable_msg(self):
return msg return msg
def go_subscription(self): def go_subscription(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['m_ScriptManager'] = 'MM$m_UpdatePanel|MM$Menu_Ajax' form['m_ScriptManager'] = 'MM$m_UpdatePanel|MM$Menu_Ajax'
form['__EVENTTARGET'] = 'MM$Menu_Ajax' form['__EVENTTARGET'] = 'MM$Menu_Ajax'
link = Link('//a[contains(@title, "e-Documents") or contains(@title, "Relevés en ligne")]')(self.doc) link = Link('//a[contains(@title, "e-Documents") or contains(@title, "Relevés en ligne")]')(self.doc)
...@@ -1005,7 +1009,7 @@ def get_recipient_value(self, recipient): ...@@ -1005,7 +1009,7 @@ def get_recipient_value(self, recipient):
return recipient_value[0] return recipient_value[0]
def init_transfer(self, account, recipient, transfer): def init_transfer(self, account, recipient, transfer):
form = self.get_form(name='main') form = self.get_form(id='main')
form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteDebiter'] = self.get_origin_account_value(account) form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteDebiter'] = self.get_origin_account_value(account)
form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteCrediter'] = self.get_recipient_value(recipient) form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteCrediter'] = self.get_recipient_value(recipient)
form['MM$VIREMENT$SAISIE_VIREMENT$txtLibelleVirement'] = transfer.label form['MM$VIREMENT$SAISIE_VIREMENT$txtLibelleVirement'] = transfer.label
...@@ -1021,7 +1025,7 @@ class iter_recipients(MyRecipients): ...@@ -1021,7 +1025,7 @@ class iter_recipients(MyRecipients):
pass pass
def continue_transfer(self, origin_label, recipient, label): def continue_transfer(self, origin_label, recipient, label):
form = self.get_form(name='main') form = self.get_form(id='main')
type_ = 'intra' if recipient.category == u'Interne' else 'sepa' type_ = 'intra' if recipient.category == u'Interne' else 'sepa'
fill = lambda s, t: s % (t.upper(), t.capitalize()) fill = lambda s, t: s % (t.upper(), t.capitalize())
form['__EVENTTARGET'] = 'MM$VIREMENT$m_WizardBar$m_lnkNext$m_lnkButton' form['__EVENTTARGET'] = 'MM$VIREMENT$m_WizardBar$m_lnkNext$m_lnkButton'
...@@ -1032,7 +1036,7 @@ def continue_transfer(self, origin_label, recipient, label): ...@@ -1032,7 +1036,7 @@ def continue_transfer(self, origin_label, recipient, label):
form.submit() form.submit()
def go_add_recipient(self): def go_add_recipient(self):
form = self.get_form(name='main') form = self.get_form(id='main')
link = self.doc.xpath(u'//a[span[contains(text(), "Ajouter un compte bénéficiaire")]]')[0] link = self.doc.xpath(u'//a[span[contains(text(), "Ajouter un compte bénéficiaire")]]')[0]
m = re.search("PostBackOptions?\([\"']([^\"']+)[\"'],\s*['\"]([^\"']+)?['\"]", link.attrib.get('href', '')) m = re.search("PostBackOptions?\([\"']([^\"']+)[\"'],\s*['\"]([^\"']+)?['\"]", link.attrib.get('href', ''))
form['__EVENTTARGET'] = m.group(1) form['__EVENTTARGET'] = m.group(1)
...@@ -1051,7 +1055,7 @@ def is_here(self): ...@@ -1051,7 +1055,7 @@ def is_here(self):
return bool(CleanText(u'//h2[contains(text(), "Confirmer mon virement")]')(self.doc)) return bool(CleanText(u'//h2[contains(text(), "Confirmer mon virement")]')(self.doc))
def confirm(self): def confirm(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = 'MM$VIREMENT$m_WizardBar$m_lnkNext$m_lnkButton' form['__EVENTTARGET'] = 'MM$VIREMENT$m_WizardBar$m_lnkNext$m_lnkButton'
form.submit() form.submit()
...@@ -1152,7 +1156,7 @@ class iter_recipients(MyRecipients): ...@@ -1152,7 +1156,7 @@ class iter_recipients(MyRecipients):
pass pass
def init_transfer(self, account, recipient, transfer): def init_transfer(self, account, recipient, transfer):
form = self.get_form(name='main') form = self.get_form(id='main')
form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteDebiter'] = self.get_origin_account_value(account) form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteDebiter'] = self.get_origin_account_value(account)
form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteCrediterPro'] = self.get_recipient_value(recipient) form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteCrediterPro'] = self.get_recipient_value(recipient)
form['MM$VIREMENT$SAISIE_VIREMENT$Libelle'] = transfer.label form['MM$VIREMENT$SAISIE_VIREMENT$Libelle'] = transfer.label
...@@ -1166,7 +1170,7 @@ def init_transfer(self, account, recipient, transfer): ...@@ -1166,7 +1170,7 @@ def init_transfer(self, account, recipient, transfer):
form.submit() form.submit()
def go_add_recipient(self): def go_add_recipient(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = 'MM$VIREMENT$SAISIE_VIREMENT$ddlCompteCrediterPro' form['__EVENTTARGET'] = 'MM$VIREMENT$SAISIE_VIREMENT$ddlCompteCrediterPro'
form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteCrediterPro'] = 'AC' form['MM$VIREMENT$SAISIE_VIREMENT$ddlCompteCrediterPro'] = 'AC'
form.submit() form.submit()
...@@ -1228,7 +1232,7 @@ def is_here(self): ...@@ -1228,7 +1232,7 @@ def is_here(self):
return bool(CleanText(u'//h2[contains(text(), "Authentification réussie")]')(self.doc)) return bool(CleanText(u'//h2[contains(text(), "Authentification réussie")]')(self.doc))
def go_on(self): def go_on(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = 'MM$RETOUR_OK_SOL$m_ChoiceBar$lnkRight' form['__EVENTTARGET'] = 'MM$RETOUR_OK_SOL$m_ChoiceBar$lnkRight'
form.submit() form.submit()
...@@ -1247,7 +1251,7 @@ def is_here(self): ...@@ -1247,7 +1251,7 @@ def is_here(self):
//h2[contains(text(), "Confirmer l\'ajout d\'un compte bénéficiaire")]')(self.doc)) //h2[contains(text(), "Confirmer l\'ajout d\'un compte bénéficiaire")]')(self.doc))
def post_recipient(self, recipient): def post_recipient(self, recipient):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = '%s$m_WizardBar$m_lnkNext$m_lnkButton' % self.EVENTTARGET form['__EVENTTARGET'] = '%s$m_WizardBar$m_lnkNext$m_lnkButton' % self.EVENTTARGET
form['%s$m_RibIban$txtTitulaireCompte' % self.FORM_FIELD_ADD] = recipient.label form['%s$m_RibIban$txtTitulaireCompte' % self.FORM_FIELD_ADD] = recipient.label
for i in range(len(recipient.iban) // 4 + 1): for i in range(len(recipient.iban) // 4 + 1):
...@@ -1255,7 +1259,7 @@ def post_recipient(self, recipient): ...@@ -1255,7 +1259,7 @@ def post_recipient(self, recipient):
form.submit() form.submit()
def confirm_recipient(self): def confirm_recipient(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = 'MM$WIZARD_AJOUT_COMPTE_EXTERNE$m_WizardBar$m_lnkNext$m_lnkButton' form['__EVENTTARGET'] = 'MM$WIZARD_AJOUT_COMPTE_EXTERNE$m_WizardBar$m_lnkNext$m_lnkButton'
form.submit() form.submit()
...@@ -1270,7 +1274,7 @@ def is_here(self): ...@@ -1270,7 +1274,7 @@ def is_here(self):
return self.need_auth() and self.doc.xpath('//span[@id="MM_ANR_WS_AUTHENT_ANR_WS_AUTHENT_SAISIE_lblProcedure1"]') return self.need_auth() and self.doc.xpath('//span[@id="MM_ANR_WS_AUTHENT_ANR_WS_AUTHENT_SAISIE_lblProcedure1"]')
def set_browser_form(self): def set_browser_form(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = 'MM$ANR_WS_AUTHENT$m_WizardBar$m_lnkNext$m_lnkButton' form['__EVENTTARGET'] = 'MM$ANR_WS_AUTHENT$m_WizardBar$m_lnkNext$m_lnkButton'
self.browser.recipient_form = dict((k, v) for k, v in form.items()) self.browser.recipient_form = dict((k, v) for k, v in form.items())
self.browser.recipient_form['url'] = form.url self.browser.recipient_form['url'] = form.url
...@@ -1306,8 +1310,9 @@ class get_detail(TableElement): ...@@ -1306,8 +1310,9 @@ class get_detail(TableElement):
def next_page(self): def next_page(self):
# only for new website, don't have any accounts with enough deferred card transactions on old webiste # only for new website, don't have any accounts with enough deferred card transactions on old webiste
if self.page.doc.xpath('//a[contains(@id, "lnkSuivante") and not(contains(@disabled,"disabled"))]'): if self.page.doc.xpath('//a[contains(@id, "lnkSuivante") and not(contains(@disabled,"disabled")) \
form = self.page.get_form(name='main') and not(contains(@class, "aspNetDisabled"))]'):
form = self.page.get_form(id='main')
form['__EVENTTARGET'] = "MM$ECRITURE_GLOBALE$lnkSuivante" form['__EVENTTARGET'] = "MM$ECRITURE_GLOBALE$lnkSuivante"
form['__EVENTARGUMENT'] = '' form['__EVENTARGUMENT'] = ''
fix_form(form) fix_form(form)
...@@ -1329,12 +1334,12 @@ def go_form_to_summary(self): ...@@ -1329,12 +1334,12 @@ def go_form_to_summary(self):
# return to first page # return to first page
to_history = Link(self.doc.xpath(u'//a[contains(text(), "Retour à l\'historique")]'))(self.doc) to_history = Link(self.doc.xpath(u'//a[contains(text(), "Retour à l\'historique")]'))(self.doc)
n = re.match('.*\([\'\"](MM\$.*?)[\'\"],.*\)$', to_history) n = re.match('.*\([\'\"](MM\$.*?)[\'\"],.*\)$', to_history)
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = n.group(1) form['__EVENTTARGET'] = n.group(1)
form.submit() form.submit()
def go_newsite_back_to_summary(self): def go_newsite_back_to_summary(self):
form = self.get_form(name='main') form = self.get_form(id='main')
form['__EVENTTARGET'] = "MM$ECRITURE_GLOBALE$lnkRetourHisto" form['__EVENTTARGET'] = "MM$ECRITURE_GLOBALE$lnkRetourHisto"
form.submit() form.submit()
...@@ -1363,7 +1368,7 @@ def condition(self): ...@@ -1363,7 +1368,7 @@ def condition(self):
def go_document_list(self, sub_id): def go_document_list(self, sub_id):
target = Attr('//select[contains(@id, "ClientsBancaires")]', 'id')(self.doc) target = Attr('//select[contains(@id, "ClientsBancaires")]', 'id')(self.doc)
form = self.get_form(name='main') form = self.get_form(id='main')
form['m_ScriptManager'] = target form['m_ScriptManager'] = target
if 'palatine' in self.browser.BASEURL: if 'palatine' in self.browser.BASEURL:
form['MM$CONSULTATION_NUMERISATION_PALATINE$cboClientsBancaires'] = sub_id form['MM$CONSULTATION_NUMERISATION_PALATINE$cboClientsBancaires'] = sub_id
...@@ -1383,16 +1388,25 @@ class iter_documents(ListElement): ...@@ -1383,16 +1388,25 @@ class iter_documents(ListElement):
class item(ItemElement): class item(ItemElement):
klass = Document klass = Document
obj_label = Format('%s %s', CleanText('./preceding::h3[1]'), CleanText('./span'))
obj_date = Date(CleanText('./span'), dayfirst=True)
obj_type = DocumentTypes.OTHER obj_type = DocumentTypes.OTHER
obj_format = 'pdf' obj_format = 'pdf'
obj_url = Regexp(Link('.'), r'WebForm_PostBackOptions\("(\S*)"') obj_url = Regexp(Link('.'), r'WebForm_PostBackOptions\("(\S*)"')
obj_id = Format('%s_%s_%s', Env('sub_id'), CleanText('./span', symbols='/'), Regexp(Field('url'), r'ctl(.*)')) obj_id = Format('%s_%s_%s', Env('sub_id'), CleanText('./span', symbols='/', replace=[(' ', '_')]), Regexp(Field('url'), r'ctl(.*)'))
obj__event_id = Regexp(Attr('.', 'onclick'), r"val\('(.*)'\);", default=None) obj__event_id = Regexp(Attr('.', 'onclick'), r"val\('(.*)'\);", default=None)
def obj_label(self):
if 'Récapitulatif de frais bancaires' in CleanText('./span')(self.el):
return CleanText('./span')(self.el)
return Format('%s %s', CleanText('./preceding::h3[1]'), CleanText('./span'))(self.el)
def obj_date(self):
if 'Récapitulatif de frais bancaires' in CleanText('./span')(self.el):
year = Regexp(CleanText('./span'), r'(\d{4})')(self.el)
return Date(dayfirst=True).filter('31/12/%s' %year)
return Date(CleanText('./span'), dayfirst=True)(self.el)
def download_document(self, document): def download_document(self, document):
form = self.get_form(name='main') form = self.get_form(id='main')
form['m_ScriptManager'] = document.url form['m_ScriptManager'] = document.url
form['__EVENTTARGET'] = document.url form['__EVENTTARGET'] = document.url
form['MM$COMPTE_EDOCUMENTS$ctrlEDocumentsConsultationDocument$eventId'] = document._event_id form['MM$COMPTE_EDOCUMENTS$ctrlEDocumentsConsultationDocument$eventId'] = document._event_id
......
...@@ -19,9 +19,9 @@ ...@@ -19,9 +19,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from weboob.capabilities.bank import CapBankWealth, AccountNotFound from weboob.capabilities.bank import CapBankTransfer, CapBankWealth, Account, AccountNotFound, RecipientNotFound
from weboob.capabilities.contact import CapContact from weboob.capabilities.contact import CapContact
from weboob.capabilities.base import find_object from weboob.capabilities.base import find_object, strict_find_object
from weboob.capabilities.profile import CapProfile from weboob.capabilities.profile import CapProfile
from weboob.tools.backend import Module, BackendConfig from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import Value, ValueBackendPassword from weboob.tools.value import Value, ValueBackendPassword
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
__all__ = ['CmsoModule'] __all__ = ['CmsoModule']
class CmsoModule(Module, CapBankWealth, CapContact, CapProfile): class CmsoModule(Module, CapBankTransfer, CapBankWealth, CapContact, CapProfile):
NAME = 'cmso' NAME = 'cmso'
MAINTAINER = 'Romain Bignon' MAINTAINER = 'Romain Bignon'
EMAIL = 'romain@weboob.org' EMAIL = 'romain@weboob.org'
...@@ -69,6 +69,39 @@ def iter_coming(self, account): ...@@ -69,6 +69,39 @@ def iter_coming(self, account):
def iter_investment(self, account): def iter_investment(self, account):
return self.browser.iter_investment(account) return self.browser.iter_investment(account)
def iter_transfer_recipients(self, origin_account):
if self.config['website'].get() != "par":
raise NotImplementedError()
if not isinstance(origin_account, Account):
origin_account = self.get_account(origin_account)
return self.browser.iter_recipients(origin_account)
def init_transfer(self, transfer, **params):
if self.config['website'].get() != "par":
raise NotImplementedError()
self.logger.info('Going to do a new transfer')
account = strict_find_object(
self.iter_accounts(),
error=AccountNotFound,
iban=transfer.account_iban,
id=transfer.account_id
)
recipient = strict_find_object(
self.iter_transfer_recipients(account.id),
error=RecipientNotFound,
iban=transfer.recipient_iban,
id=transfer.recipient_id
)
return self.browser.init_transfer(account, recipient, transfer.amount, transfer.label, transfer.exec_date)
def execute_transfer(self, transfer, **params):
return self.browser.execute_transfer(transfer, **params)
def iter_contacts(self): def iter_contacts(self):
if self.config['website'].get() != "par": if self.config['website'].get() != "par":
raise NotImplementedError() raise NotImplementedError()
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
import re import re
import json import json
from datetime import date
from functools import wraps from functools import wraps
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
...@@ -33,8 +34,9 @@ ...@@ -33,8 +34,9 @@
from .pages import ( from .pages import (
LogoutPage, InfosPage, AccountsPage, HistoryPage, LifeinsurancePage, MarketPage, LogoutPage, InfosPage, AccountsPage, HistoryPage, LifeinsurancePage, MarketPage,
AdvisorPage, LoginPage, RecipientsPage, ProfilePage, AdvisorPage, LoginPage, ProfilePage,
) )
from .transfer_pages import TransferInfoPage, RecipientsListPage, TransferPage
def retry(exc_check, tries=4): def retry(exc_check, tries=4):
...@@ -92,7 +94,11 @@ class CmsoParBrowser(LoginBrowser, StatesMixin): ...@@ -92,7 +94,11 @@ class CmsoParBrowser(LoginBrowser, StatesMixin):
'https://www.*/domiweb/prive/particulier', MarketPage) 'https://www.*/domiweb/prive/particulier', MarketPage)
advisor = URL('/edrapi/v(?P<version>\w+)/oauth/(?P<page>\w+)', AdvisorPage) advisor = URL('/edrapi/v(?P<version>\w+)/oauth/(?P<page>\w+)', AdvisorPage)
recipients = URL(r'/domiapi/oauth/json/transfer/transferinfos', RecipientsPage) # transfer
transfer_info = URL(r'/domiapi/oauth/json/transfer/transferinfos', TransferInfoPage)
recipients_list = URL(r'/domiapi/oauth/json/transfer/beneficiariesListTransfer', RecipientsListPage)
init_transfer_page = URL(r'/domiapi/oauth/json/transfer/controlTransferOperation', TransferPage)
execute_transfer_page = URL(r'/domiapi/oauth/json/transfer/transferregister', TransferPage)
profile = URL(r'/domiapi/oauth/json/edr/infosPerson', ProfilePage) profile = URL(r'/domiapi/oauth/json/edr/infosPerson', ProfilePage)
...@@ -158,7 +164,7 @@ def iter_accounts(self): ...@@ -158,7 +164,7 @@ def iter_accounts(self):
seen = {} seen = {}
self.recipients.go(data='{"beneficiaryType":"INTERNATIONAL"}', headers=self.json_headers) self.transfer_info.go(json={"beneficiaryType":"INTERNATIONAL"})
numbers = self.page.get_numbers() numbers = self.page.get_numbers()
# First get all checking accounts... # First get all checking accounts...
...@@ -305,6 +311,74 @@ def iter_investment(self, account): ...@@ -305,6 +311,74 @@ def iter_investment(self, account):
return [] return []
raise NotImplementedError() raise NotImplementedError()
@retry((ClientError, ServerError))
@need_login
def iter_recipients(self, account):
self.transfer_info.go(json={"beneficiaryType":"INTERNATIONAL"})
if account.type in (Account.TYPE_LOAN, ):
return
if not account._eligible_debit:
return
# internal recipient
for rcpt in self.page.iter_titu_accounts():
if rcpt.id != account.id:
yield rcpt
for rcpt in self.page.iter_manda_accounts():
if rcpt.id != account.id:
yield rcpt
for rcpt in self.page.iter_legal_rep_accounts():
if rcpt.id != account.id:
yield rcpt
# external recipient
for rcpt in self.page.iter_external_recipients():
yield rcpt
@need_login
def init_transfer(self, account, recipient, amount, reason, exec_date):
self.recipients_list.go(json={"beneficiaryType":"INTERNATIONAL"})
transfer_data = {
'beneficiaryIndex': self.page.get_rcpt_index(recipient),
'debitAccountIndex': account._index,
'devise': account.currency,
'deviseReglement': account.currency,
'montant': amount,
'nature': 'externesepa',
'transferToBeneficiary': True,
}
if exec_date and exec_date > date.today():
transfer_data['date'] = int(exec_date.strftime('%s')) * 1000
else:
transfer_data['immediate'] = True
# check if recipient is internal or external
if recipient.id != recipient.iban:
transfer_data['nature'] = 'interne'
transfer_data['transferToBeneficiary'] = False
self.init_transfer_page.go(json=transfer_data)
transfer = self.page.handle_transfer(account, recipient, amount, reason, exec_date)
# transfer_data is used in execute_transfer
transfer._transfer_data = transfer_data
return transfer
@need_login
def execute_transfer(self, transfer, **params):
assert transfer._transfer_data
transfer._transfer_data.update({
'enregistrerNouveauBeneficiaire': False,
'creditLabel': 'de %s' % transfer.account_label if not transfer.label else transfer.label,
'debitLabel': 'vers %s' % transfer.recipient_label,
'typeFrais': 'SHA'
})
self.execute_transfer_page.go(json=transfer._transfer_data)
transfer.id = self.page.get_transfer_confirm_id()
return transfer
@retry((ClientError, ServerError)) @retry((ClientError, ServerError))
@need_login @need_login
def get_advisor(self): def get_advisor(self):
......
...@@ -135,6 +135,8 @@ class item(ItemElement): ...@@ -135,6 +135,8 @@ class item(ItemElement):
# Iban is available without last 5 numbers, or by sms # Iban is available without last 5 numbers, or by sms
obj_iban = NotAvailable obj_iban = NotAvailable
obj__index = Dict('index') obj__index = Dict('index')
# to know if we can do transfer on account
obj__eligible_debit = Dict('eligibiliteDebit', default=False)
def obj_balance(self): def obj_balance(self):
balance = CleanDecimal(Dict('soldeEuro', default="0"))(self) balance = CleanDecimal(Dict('soldeEuro', default="0"))(self)
...@@ -194,6 +196,8 @@ class item(ItemElement): ...@@ -194,6 +196,8 @@ class item(ItemElement):
obj_coming = CleanDecimal(Dict('AVenir', default=None), default=NotAvailable) obj_coming = CleanDecimal(Dict('AVenir', default=None), default=NotAvailable)
obj__index = Dict('index') obj__index = Dict('index')
obj__owner = Dict('nomTitulaire') obj__owner = Dict('nomTitulaire')
# to know if we can do transfer on account
obj__eligible_debit = Dict('eligibiliteDebit', default=False)
def obj_id(self): def obj_id(self):
type = Field('type')(self) type = Field('type')(self)
...@@ -549,35 +553,6 @@ class update_agency(ItemElement): ...@@ -549,35 +553,6 @@ class update_agency(ItemElement):
obj_address = Format('%s %s', Dict('adresse1'), Dict('adresse3')) obj_address = Format('%s %s', Dict('adresse1'), Dict('adresse3'))
class RecipientsPage(LoggedPage, JsonPage):
def get_numbers(self):
# If account information is not available when asking for the
# recipients (server error for ex.), return an empty dictionary
# that will be filled later after being returned the json of the
# account page (containing the accounts IDs too).
if 'listCompteTitulaireCotitulaire' not in self.doc and 'exception' in self.doc:
return {}
ret = {}
ret.update({
d['index']: d['numeroContratSouscrit']
for d in self.doc['listCompteTitulaireCotitulaire']
})
ret.update({
d['index']: d['numeroContratSouscrit']
for p in self.doc['listCompteMandataire'].values()
for d in p
})
ret.update({
d['index']: d['numeroContratSouscrit']
for p in self.doc['listCompteLegalRep'].values()
for d in p
})
return ret
class ProfilePage(LoggedPage, JsonPage): class ProfilePage(LoggedPage, JsonPage):
# be careful, this page is used in CmsoProBrowser too! # be careful, this page is used in CmsoProBrowser too!
......
# -*- coding: utf-8 -*-
# Copyright(C) 2019 Sylvie Ye
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import datetime as dt
from weboob.browser.pages import JsonPage, LoggedPage
from weboob.browser.elements import DictElement, ItemElement, method
from weboob.browser.filters.standard import CleanText, CleanDecimal, Currency
from weboob.browser.filters.json import Dict
from weboob.capabilities.bank import Recipient, Transfer, TransferBankError
from weboob.capabilities.base import NotAvailable
class MyRecipientItemElement(ItemElement):
def condition(self):
return Dict('eligibiliteCredit', default=False)
klass = Recipient
obj_id = Dict('numeroContratSouscrit')
obj_label = Dict('lib')
obj_iban = NotAvailable
obj_enabled_at = dt.date.today()
obj_category = 'Interne'
obj__index = Dict('index')
class RecipientsListPage(LoggedPage, JsonPage):
def get_rcpt_index(self, recipient):
if recipient.category == 'Externe':
for el in self.doc['listBeneficiaries']:
for rcpt in el:
# in this list, recipient iban is like FR111111111111111XXXXX
if rcpt['iban'][:-5] == recipient.iban[:-5] and rcpt['nom'] == recipient.label:
return rcpt['index']
return recipient._index
class TransferInfoPage(LoggedPage, JsonPage):
def get_numbers(self):
# If account information is not available when asking for the
# recipients (server error for ex.), return an empty dictionary
# that will be filled later after being returned the json of the
# account page (containing the accounts IDs too).
if 'listCompteTitulaireCotitulaire' not in self.doc and 'exception' in self.doc:
return {}
ret = {}
ret.update({
d['index']: d['numeroContratSouscrit']
for d in self.doc['listCompteTitulaireCotitulaire']
})
ret.update({
d['index']: d['numeroContratSouscrit']
for p in self.doc['listCompteMandataire'].values()
for d in p
})
ret.update({
d['index']: d['numeroContratSouscrit']
for p in self.doc['listCompteLegalRep'].values()
for d in p
})
return ret
@method
class iter_titu_accounts(DictElement):
item_xpath = 'listCompteTitulaireCotitulaire'
class item(MyRecipientItemElement):
pass
@method
class iter_manda_accounts(DictElement):
item_xpath = 'listCompteMandataire/*'
class item(MyRecipientItemElement):
pass
@method
class iter_legal_rep_accounts(DictElement):
item_xpath = 'listCompteLegalRep/*'
class item(MyRecipientItemElement):
pass
@method
class iter_external_recipients(DictElement):
item_xpath = 'listBeneficiaries'
class item(ItemElement):
klass = Recipient
obj_id = obj_iban = Dict('iban')
obj_label = Dict('nom')
obj_category = 'Externe'
obj_enabled_at = dt.date.today()
obj__index = Dict('index')
def condition(self):
return Dict('actif', default=False)(self)
class TransferPage(LoggedPage, JsonPage):
def on_load(self):
if self.doc.get('exception') and not self.doc.get('debitAccountOwner'):
if Dict('exception/type')(self.doc) == 1:
# technical error
assert False, 'Error with code %s occured during init_transfer: %s' % \
(Dict('exception/code')(self.doc), Dict('exception/message')(self.doc))
elif Dict('exception/type')(self.doc) == 2:
# user error
TransferBankError(message=Dict('exception/message')(self.doc))
def handle_transfer(self, account, recipient, amount, reason, exec_date):
transfer = Transfer()
transfer.amount = CleanDecimal(Dict('amount'))(self.doc)
transfer.currency = Currency(Dict('codeDevise'))(self.doc)
transfer.label = reason
if exec_date:
transfer.exec_date = dt.date.fromtimestamp(int(Dict('date')(self.doc))//1000)
transfer.account_id = account.id
transfer.account_label = CleanText(Dict('debitAccountLabel'))(self.doc)
transfer.account_balance = CleanDecimal(Dict('debitAccountBalance'))(self.doc)
transfer.recipient_id = recipient.id
transfer.recipient_iban = recipient.iban
transfer.recipient_label = CleanText(Dict('creditAccountOwner'))(self.doc)
return transfer
def get_transfer_confirm_id(self):
return self.doc.get('numeroOperation')
This diff is collapsed.
# -*- coding: utf-8 -*-
# Copyright(C) 2012-2019 Budget Insight
from weboob.browser import AbstractBrowser
class NetfincaBrowser(AbstractBrowser):
PARENT = 'netfinca'
BASEURL = 'https://www.cabourse.credit-agricole.fr'
...@@ -22,19 +22,23 @@ ...@@ -22,19 +22,23 @@
from decimal import Decimal from decimal import Decimal
import re import re
import json import json
import dateutil
from weboob.browser.pages import HTMLPage, JsonPage, LoggedPage from weboob.browser.pages import HTMLPage, JsonPage, LoggedPage
from weboob.exceptions import BrowserUnavailable from weboob.exceptions import ActionNeeded
from weboob.capabilities import NotAvailable from weboob.capabilities import NotAvailable
from weboob.capabilities.bank import ( from weboob.capabilities.bank import (
Account, AccountOwnerType, Account, AccountOwnerType, Transaction, Investment,
) )
from weboob.capabilities.profile import Person, Company
from weboob.capabilities.contact import Advisor
from weboob.browser.elements import DictElement, ItemElement, method from weboob.browser.elements import DictElement, ItemElement, method
from weboob.browser.filters.standard import ( from weboob.browser.filters.standard import (
CleanText, CleanDecimal, Currency as CleanCurrency, Format, Field, Map, Eval CleanText, CleanDecimal, Currency as CleanCurrency, Format, Field, Map, Eval, Env, Regexp, Date,
) )
from weboob.browser.filters.html import Attr
from weboob.browser.filters.json import Dict from weboob.browser.filters.json import Dict
from weboob.tools.capabilities.bank.investments import is_isin_valid
def float_to_decimal(f): def float_to_decimal(f):
...@@ -54,7 +58,7 @@ def get_keypad_id(self): ...@@ -54,7 +58,7 @@ def get_keypad_id(self):
class LoginPage(HTMLPage): class LoginPage(HTMLPage):
def get_login_form(self, username, keypad_password, keypad_id): def get_login_form(self, username, keypad_password, keypad_id):
form = self.get_form(id="loginForm") form = self.get_form(id="loginForm")
form['j_username'] = username form['j_username'] = username[:11]
form['j_password'] = keypad_password form['j_password'] = keypad_password
form['keypadId'] = keypad_id form['keypadId'] = keypad_id
return form return form
...@@ -64,9 +68,12 @@ class LoggedOutPage(HTMLPage): ...@@ -64,9 +68,12 @@ class LoggedOutPage(HTMLPage):
def is_here(self): def is_here(self):
return self.doc.xpath('//b[text()="FIN DE CONNEXION"]') return self.doc.xpath('//b[text()="FIN DE CONNEXION"]')
class FirstConnectionPage(LoggedPage, HTMLPage):
def on_load(self): def on_load(self):
self.logger.warning('We have been logged out!') message = CleanText('//p[contains(text(), "votre première visite")]')(self.doc)
raise BrowserUnavailable() if message:
raise ActionNeeded(message)
class SecurityPage(JsonPage): class SecurityPage(JsonPage):
...@@ -74,6 +81,11 @@ def get_accounts_url(self): ...@@ -74,6 +81,11 @@ def get_accounts_url(self):
return Dict('url')(self.doc) return Dict('url')(self.doc)
class TokenPage(LoggedPage, JsonPage):
def get_token(self):
return Dict('token')(self.doc)
class ContractsPage(LoggedPage, HTMLPage): class ContractsPage(LoggedPage, HTMLPage):
pass pass
...@@ -102,12 +114,15 @@ class ContractsPage(LoggedPage, HTMLPage): ...@@ -102,12 +114,15 @@ class ContractsPage(LoggedPage, HTMLPage):
'DAV TIGERE': Account.TYPE_SAVINGS, 'DAV TIGERE': Account.TYPE_SAVINGS,
'CPTEXCPRO': Account.TYPE_SAVINGS, 'CPTEXCPRO': Account.TYPE_SAVINGS,
'CPTEXCENT': Account.TYPE_SAVINGS, 'CPTEXCENT': Account.TYPE_SAVINGS,
'DAT': Account.TYPE_SAVINGS,
'PRET PERSO': Account.TYPE_LOAN, 'PRET PERSO': Account.TYPE_LOAN,
'P. ENTREPR': Account.TYPE_LOAN, 'P. ENTREPR': Account.TYPE_LOAN,
'P. HABITAT': Account.TYPE_LOAN, 'P. HABITAT': Account.TYPE_LOAN,
'PRET 0%': Account.TYPE_LOAN, 'PRET 0%': Account.TYPE_LOAN,
'INV PRO': Account.TYPE_LOAN, 'INV PRO': Account.TYPE_LOAN,
'TRES. PRO': Account.TYPE_LOAN, 'TRES. PRO': Account.TYPE_LOAN,
'CT ATT HAB': Account.TYPE_LOAN,
'PRET CEL': Account.TYPE_LOAN,
'PEA': Account.TYPE_PEA, 'PEA': Account.TYPE_PEA,
'PEAP': Account.TYPE_PEA, 'PEAP': Account.TYPE_PEA,
'DAV PEA': Account.TYPE_PEA, 'DAV PEA': Account.TYPE_PEA,
...@@ -165,8 +180,10 @@ class get_main_account(ItemElement): ...@@ -165,8 +180,10 @@ class get_main_account(ItemElement):
obj_balance = Eval(float_to_decimal, Dict('comptePrincipal/solde')) obj_balance = Eval(float_to_decimal, Dict('comptePrincipal/solde'))
obj_currency = CleanCurrency(Dict('comptePrincipal/idDevise')) obj_currency = CleanCurrency(Dict('comptePrincipal/idDevise'))
obj__index = Dict('comptePrincipal/index') obj__index = Dict('comptePrincipal/index')
obj__category = None # Main accounts have no category obj__category = Dict('comptePrincipal/grandeFamilleProduitCode', default=None)
obj__id_element_contrat = CleanText(Dict('comptePrincipal/idElementContrat')) obj__id_element_contrat = CleanText(Dict('comptePrincipal/idElementContrat'))
obj__fam_product_code = CleanText(Dict('comptePrincipal/codeFamilleProduitBam'))
obj__fam_contract_code = CleanText(Dict('comptePrincipal/codeFamilleContratBam'))
def obj_type(self): def obj_type(self):
_type = Map(CleanText(Dict('comptePrincipal/libelleUsuelProduit')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN)(self) _type = Map(CleanText(Dict('comptePrincipal/libelleUsuelProduit')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN)(self)
...@@ -174,22 +191,6 @@ def obj_type(self): ...@@ -174,22 +191,6 @@ def obj_type(self):
self.logger.warning('We got an untyped account: please add "%s" to ACCOUNT_TYPES.' % CleanText(Dict('comptePrincipal/libelleUsuelProduit'))(self)) self.logger.warning('We got an untyped account: please add "%s" to ACCOUNT_TYPES.' % CleanText(Dict('comptePrincipal/libelleUsuelProduit'))(self))
return _type return _type
class obj__cards(DictElement):
item_xpath = 'comptePrincipal/cartesDD'
class item(ItemElement):
klass = Account
def obj_id(self):
return CleanText(Dict('idCarte'))(self).replace(' ', '')
obj_label = Format('Carte %s %s', Field('id'), CleanText(Dict('titulaire')))
obj_type = Account.TYPE_CARD
obj_coming = Eval(float_to_decimal, Dict('encoursCarteM'))
obj_balance = CleanDecimal(0)
obj__index = Dict('index')
obj__category = None
@method @method
class iter_accounts(DictElement): class iter_accounts(DictElement):
item_xpath = 'grandesFamilles/*/elementsContrats' item_xpath = 'grandesFamilles/*/elementsContrats'
...@@ -199,18 +200,28 @@ class item(ItemElement): ...@@ -199,18 +200,28 @@ class item(ItemElement):
klass = Account klass = Account
obj_id = CleanText(Dict('numeroCompteBam')) def obj_id(self):
obj_number = CleanText(Dict('numeroCompteBam')) # Loan ids may be duplicated so we use the contract number for now:
if Field('type')(self) == Account.TYPE_LOAN:
return CleanText(Dict('idElementContrat'))(self)
return CleanText(Dict('numeroCompte'))(self)
obj_number = CleanText(Dict('numeroCompte'))
obj_label = CleanText(Dict('libelleProduit')) obj_label = CleanText(Dict('libelleProduit'))
obj_currency = CleanCurrency(Dict('idDevise')) obj_currency = CleanCurrency(Dict('idDevise'))
obj__index = Dict('index') obj__index = Dict('index')
obj__category = Dict('grandeFamilleProduitCode', default=None) obj__category = Dict('grandeFamilleProduitCode', default=None)
obj__id_element_contrat = CleanText(Dict('idElementContrat')) obj__id_element_contrat = CleanText(Dict('idElementContrat'))
obj__fam_product_code = CleanText(Dict('codeFamilleProduitBam'))
obj__fam_contract_code = CleanText(Dict('codeFamilleContratBam'))
def obj_type(self): def obj_type(self):
if CleanText(Dict('libelleUsuelProduit'))(self) in ('HABITATION',):
# No need to log warning for "assurance" accounts
return NotAvailable
_type = Map(CleanText(Dict('libelleUsuelProduit')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN)(self) _type = Map(CleanText(Dict('libelleUsuelProduit')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN)(self)
if _type == Account.TYPE_UNKNOWN: if _type == Account.TYPE_UNKNOWN:
self.logger.warning('We got an untyped account: please add "%s" to ACCOUNT_TYPES.' % CleanText(Dict('libelleUsuelProduit'))(self)) self.logger.warning('There is an untyped account: please add "%s" to ACCOUNT_TYPES.' % CleanText(Dict('libelleUsuelProduit'))(self))
return _type return _type
def obj_balance(self): def obj_balance(self):
...@@ -227,22 +238,30 @@ def condition(self): ...@@ -227,22 +238,30 @@ def condition(self):
class AccountDetailsPage(LoggedPage, JsonPage): class AccountDetailsPage(LoggedPage, JsonPage):
def get_account_balances(self): def get_account_balances(self):
# We use the 'idElementContrat' key because it is unique
# whereas the account id may not be unique for Loans
account_balances = {} account_balances = {}
for el in self.doc: for el in self.doc:
# Insurances have no balance, we skip them
if el.get('typeProduit') == 'assurance':
continue
value = el.get('solde', el.get('encoursActuel', el.get('valorisationContrat', el.get('montantRestantDu', el.get('capitalDisponible'))))) value = el.get('solde', el.get('encoursActuel', el.get('valorisationContrat', el.get('montantRestantDu', el.get('capitalDisponible')))))
assert value is not None, 'Could not find the account balance' if value is None:
account_balances[Dict('numeroCompte')(el)] = float_to_decimal(value) continue
account_balances[Dict('idElementContrat')(el)] = float_to_decimal(value)
return account_balances return account_balances
def get_loan_ids(self): def get_loan_ids(self):
# We use the 'idElementContrat' key because it is unique
# whereas the account id may not be unique for Loans
loan_ids = {} loan_ids = {}
for el in self.doc: for el in self.doc:
if el.get('numeroCredit'): if el.get('numeroCredit'):
# Loans # Loans
loan_ids[Dict('numeroCompte')(el)] = Dict('numeroCredit')(el) loan_ids[Dict('idElementContrat')(el)] = Dict('numeroCredit')(el)
elif el.get('numeroContrat'): elif el.get('numeroContrat'):
# Revolving credits # Revolving credits
loan_ids[Dict('numeroCompte')(el)] = Dict('numeroContrat')(el) loan_ids[Dict('idElementContrat')(el)] = Dict('numeroContrat')(el)
return loan_ids return loan_ids
...@@ -252,12 +271,191 @@ def get_iban(self): ...@@ -252,12 +271,191 @@ def get_iban(self):
class HistoryPage(LoggedPage, JsonPage): class HistoryPage(LoggedPage, JsonPage):
pass def has_next_page(self):
return Dict('hasNext')(self.doc)
def get_next_index(self):
return Dict('nextSetStartIndex')(self.doc)
class InvestmentPage(LoggedPage, JsonPage): @method
pass class iter_history(DictElement):
item_xpath = 'listeOperations'
class item(ItemElement):
TRANSACTION_TYPES = {
'PAIEMENT PAR CARTE': Transaction.TYPE_CARD,
'REMISE CARTE': Transaction.TYPE_CARD,
'PRELEVEMENT CARTE': Transaction.TYPE_CARD_SUMMARY,
'RETRAIT AU DISTRIBUTEUR': Transaction.TYPE_WITHDRAWAL,
"RETRAIT MUR D'ARGENT": Transaction.TYPE_WITHDRAWAL,
'FRAIS': Transaction.TYPE_BANK,
'COTISATION': Transaction.TYPE_BANK,
'VIREMENT': Transaction.TYPE_TRANSFER,
'VIREMENT EN VOTRE FAVEUR': Transaction.TYPE_TRANSFER,
'VIREMENT EMIS': Transaction.TYPE_TRANSFER,
'CHEQUE EMIS': Transaction.TYPE_CHECK,
'REMISE DE CHEQUE': Transaction.TYPE_DEPOSIT,
'PRELEVEMENT': Transaction.TYPE_ORDER,
'PRELEVT': Transaction.TYPE_ORDER,
'PRELEVMNT': Transaction.TYPE_ORDER,
'REMBOURSEMENT DE PRET': Transaction.TYPE_LOAN_PAYMENT,
}
klass = Transaction
obj_raw = Format('%s %s %s', CleanText(Dict('libelleTypeOperation')), CleanText(Dict('libelleOperation')), CleanText(Dict('libelleComplementaire')))
obj_label = Format('%s %s', CleanText(Dict('libelleTypeOperation')), CleanText(Dict('libelleOperation')))
obj_amount = Eval(float_to_decimal, Dict('montant'))
obj_type = Map(CleanText(Dict('libelleTypeOperation')), TRANSACTION_TYPES, Transaction.TYPE_UNKNOWN)
def obj_date(self):
return dateutil.parser.parse(Dict('dateValeur')(self))
def obj_rdate(self):
return dateutil.parser.parse(Dict('dateOperation')(self))
class CardsPage(LoggedPage, JsonPage):
@method
class iter_card_parents(DictElement):
item_xpath = 'comptes'
class iter_cards(DictElement):
item_xpath = 'listeCartes'
def parse(self, el):
self.env['parent_id'] = Dict('idCompte')(el)
class item(ItemElement):
klass = Account
def obj_id(self):
return CleanText(Dict('idCarte'))(self).replace(' ', '')
def condition(self):
assert CleanText(Dict('codeTypeDebitPaiementCarte'))(self) in ('D', 'I')
return CleanText(Dict('codeTypeDebitPaiementCarte'))(self)=='D'
obj_label = Format('Carte %s %s', Field('id'), CleanText(Dict('titulaire')))
obj_type = Account.TYPE_CARD
obj_coming = Eval(lambda x: -float_to_decimal(x), Dict('encoursCarteM'))
obj_balance = CleanDecimal(0)
obj__parent_id = Env('parent_id')
obj__index = Dict('index')
obj__id_element_contrat = None
class CardHistoryPage(LoggedPage, JsonPage):
@method
class iter_card_history(DictElement):
item_xpath = None
class item(ItemElement):
klass = Transaction
obj_raw = CleanText(Dict('libelleOperation'))
obj_label = CleanText(Dict('libelleOperation'))
obj_amount = Eval(float_to_decimal, Dict('montant'))
obj_type = Transaction.TYPE_DEFERRED_CARD
def obj_date(self):
return dateutil.parser.parse(Dict('datePrelevement')(self))
def obj_rdate(self):
return dateutil.parser.parse(Dict('dateOperation')(self))
class NetfincaRedirectionPage(LoggedPage, HTMLPage):
def get_url(self):
return Regexp(Attr('//body', 'onload'), r'document.location="([^"]+)"')(self.doc)
class PredicaRedirectionPage(LoggedPage, HTMLPage):
def on_load(self):
form = self.get_form()
form.submit()
class PredicaInvestmentsPage(LoggedPage, JsonPage):
@method
class iter_investments(DictElement):
item_xpath = 'listeSupports/support'
class item(ItemElement):
klass = Investment
obj_label = CleanText(Dict('lcspt'))
obj_valuation = Eval(float_to_decimal, Dict('mtvalspt'))
def obj_portfolio_share(self):
portfolio_share = Dict('txrpaspt', default=None)(self)
if portfolio_share:
return Eval(lambda x: float_to_decimal(x / 100), portfolio_share)(self)
return NotAvailable
def obj_unitvalue(self):
unit_value = Dict('mtliqpaaspt', default=None)(self)
if unit_value:
return Eval(float_to_decimal, unit_value)(self)
return NotAvailable
def obj_quantity(self):
quantity = Dict('qtpaaspt', default=None)(self)
if quantity:
return Eval(float_to_decimal, quantity)(self)
return NotAvailable
def obj_code(self):
code = Dict('cdsptisn')(self)
if is_isin_valid(code):
return code
return NotAvailable
def obj_code_type(self):
if is_isin_valid(Field('code')(self)):
return Investment.CODE_TYPE_ISIN
return NotAvailable
class ProfilePage(LoggedPage, JsonPage): class ProfilePage(LoggedPage, JsonPage):
pass @method
\ No newline at end of file class get_user_profile(ItemElement):
klass = Person
obj_name = CleanText(Dict('displayName', default=NotAvailable))
obj_phone = CleanText(Dict('branchPhone', default=NotAvailable))
obj_birth_date = Date(Dict('birthdate', default=NotAvailable))
@method
class get_company_profile(ItemElement):
klass = Company
obj_name = CleanText(Dict('displayName', default=NotAvailable))
obj_phone = CleanText(Dict('branchPhone', default=NotAvailable))
obj_registration_date = Date(Dict('birthdate', default=NotAvailable))
@method
class get_advisor(ItemElement):
klass = Advisor
def obj_name(self):
# If no advisor is displayed, we return the agency advisor.
if Dict('advisorGivenName')(self) and Dict('advisorFamilyName')(self):
return Format('%s %s', CleanText(Dict('advisorGivenName')), CleanText(Dict('advisorFamilyName')))(self)
return Format('%s %s', CleanText(Dict('branchManagerGivenName')), CleanText(Dict('branchManagerFamilyName')))(self)
class ProfileDetailsPage(LoggedPage, HTMLPage):
@method
class fill_profile(ItemElement):
obj_email = CleanText('//p[contains(@class, "Data mail")]', default=NotAvailable)
obj_address = CleanText('//p[strong[contains(text(), "Adresse")]]/text()[2]', default=NotAvailable)
@method
class fill_advisor(ItemElement):
obj_phone = CleanText('//div[@id="blockConseiller"]//a[contains(@class, "advisorNumber")]', default=NotAvailable)
class ProProfileDetailsPage(ProfileDetailsPage):
pass
...@@ -84,7 +84,7 @@ class CragrModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapContact ...@@ -84,7 +84,7 @@ class CragrModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapContact
}.items())]) }.items())])
CONFIG = BackendConfig(Value('website', label=u'Région', choices=website_choices), CONFIG = BackendConfig(Value('website', label=u'Région', choices=website_choices),
ValueBackendPassword('login', label=u'N° de compte', masked=False), ValueBackendPassword('login', label=u'N° de compte', masked=False, regexp=r'\d+'),
ValueBackendPassword('password', label=u'Code personnel', regexp=r'\d{6}')) ValueBackendPassword('password', label=u'Code personnel', regexp=r'\d{6}'))
BROWSER = ProxyBrowser BROWSER = ProxyBrowser
...@@ -98,7 +98,8 @@ def create_default_browser(self): ...@@ -98,7 +98,8 @@ def create_default_browser(self):
site_conf = self.COMPAT_DOMAINS.get(site_conf, site_conf) site_conf = self.COMPAT_DOMAINS.get(site_conf, site_conf)
return self.create_browser(site_conf, return self.create_browser(site_conf,
self.config['login'].get(), self.config['login'].get(),
self.config['password'].get()) self.config['password'].get(),
weboob=self.weboob)
def iter_accounts(self): def iter_accounts(self):
return self.browser.get_accounts_list() return self.browser.get_accounts_list()
...@@ -114,15 +115,17 @@ def to_date(obj): ...@@ -114,15 +115,17 @@ def to_date(obj):
return obj.date() return obj.date()
return obj return obj
for tr in self.browser.get_history(account): for tr in self.browser.get_history(account, coming):
tr_coming = to_date(tr.date) > today tr_coming = to_date(tr.date) > today
if coming == tr_coming: if coming == tr_coming:
yield tr yield tr
elif coming:
break
def iter_history(self, account): def iter_history(self, account):
if account.type == Account.TYPE_CARD: if account.type == Account.TYPE_CARD:
return self._history_filter(account, False) return self._history_filter(account, False)
return self.browser.get_history(account) return self.browser.get_history(account, False)
def iter_coming(self, account): def iter_coming(self, account):
if account.type == Account.TYPE_CARD: if account.type == Account.TYPE_CARD:
......
...@@ -412,7 +412,7 @@ def market_accounts_matching(self, accounts_list, market_accounts_list): ...@@ -412,7 +412,7 @@ def market_accounts_matching(self, accounts_list, market_accounts_list):
account.balance = self.page.get_pea_balance() account.balance = self.page.get_pea_balance()
@need_login @need_login
def get_history(self, account): def get_history(self, account, coming=False):
if account.type in (Account.TYPE_MARKET, Account.TYPE_PEA, Account.TYPE_LIFE_INSURANCE, Account.TYPE_PERP): if account.type in (Account.TYPE_MARKET, Account.TYPE_PEA, Account.TYPE_LIFE_INSURANCE, Account.TYPE_PERP):
self.logger.warning('This account is not supported') self.logger.warning('This account is not supported')
raise NotImplementedError() raise NotImplementedError()
......
...@@ -83,7 +83,7 @@ def iter_history(self, account): ...@@ -83,7 +83,7 @@ def iter_history(self, account):
yield tr yield tr
def iter_coming(self, account): def iter_coming(self, account):
account = self.get_account(account.id) account = self.get_account_for_history(account.id)
for tr in self.browser.get_history(account, coming=True): for tr in self.browser.get_history(account, coming=True):
if tr._is_coming: if tr._is_coming:
yield tr yield tr
......
...@@ -312,8 +312,11 @@ def list_operations(self, page, account): ...@@ -312,8 +312,11 @@ def list_operations(self, page, account):
form.pop(k, None) form.pop(k, None)
form.submit() form.submit()
# IndexError when form xpath returns [], StopIteration if next called on empty iterable # IndexError when form xpath returns [], StopIteration if next called on empty iterable
except (IndexError, StopIteration, FormNotFound): except (StopIteration, FormNotFound):
self.logger.warning('Could not get history on new website') self.logger.warning('Could not get history on new website')
except IndexError:
# 6 months history is not available
pass
while self.page: while self.page:
try: try:
......
...@@ -907,7 +907,11 @@ def obj_type(self): ...@@ -907,7 +907,11 @@ def obj_type(self):
def obj_original_amount(self): def obj_original_amount(self):
m = re.search(r'(([\s-]\d+)+,\d+)', CleanText(TableCell('commerce'))(self)) m = re.search(r'(([\s-]\d+)+,\d+)', CleanText(TableCell('commerce'))(self))
if m and not 'FRAIS' in CleanText(TableCell('commerce'))(self): if m and not 'FRAIS' in CleanText(TableCell('commerce'))(self):
return Decimal(m.group(1).replace(',', '.').replace(' ', '')).quantize(Decimal('0.01')) matched_text = m.group(1)
submatch = re.search(r'\d+-(.*)', matched_text)
if submatch:
matched_text = submatch.group(1)
return Decimal(matched_text.replace(',', '.').replace(' ', '')).quantize(Decimal('0.01'))
return NotAvailable return NotAvailable
def obj_original_currency(self): def obj_original_currency(self):
...@@ -979,7 +983,11 @@ def obj_type(self): ...@@ -979,7 +983,11 @@ def obj_type(self):
def obj_original_amount(self): def obj_original_amount(self):
m = re.search(r'(([\s-]\d+)+,\d+)', CleanText(TableCell('commerce'))(self)) m = re.search(r'(([\s-]\d+)+,\d+)', CleanText(TableCell('commerce'))(self))
if m and not 'FRAIS' in CleanText(TableCell('commerce'))(self): if m and not 'FRAIS' in CleanText(TableCell('commerce'))(self):
return Decimal(m.group(1).replace(',', '.').replace(' ', '')).quantize(Decimal('0.01')) matched_text = m.group(1)
submatch = re.search(r'\d+-(.*)', matched_text)
if submatch:
matched_text = submatch.group(1)
return Decimal(matched_text.replace(',', '.').replace(' ', '')).quantize(Decimal('0.01'))
return NotAvailable return NotAvailable
def obj_original_currency(self): def obj_original_currency(self):
...@@ -1105,11 +1113,11 @@ def obj_commission(self): ...@@ -1105,11 +1113,11 @@ def obj_commission(self):
@method @method
class iter_investment(TableElement): class iter_investment(TableElement):
item_xpath = '//table[has-class("liste")]/tbody/tr[count(td)>=7]' item_xpath = '//table[has-class("liste") and not (@summary="Avances")]/tbody/tr[count(td)>=7]'
head_xpath = '//table[has-class("liste")]/thead/tr/th' head_xpath = '//table[has-class("liste") and not (@summary="Avances")]/thead/tr/th'
col_label = 'Support' col_label = 'Support'
col_unitprice = re.compile(r"^Prix d'achat moyen") col_unitprice = re.compile(r'Prix')
col_vdate = re.compile(r'Date de cotation') col_vdate = re.compile(r'Date de cotation')
col_unitvalue = 'Valeur de la part' col_unitvalue = 'Valeur de la part'
col_quantity = 'Nombre de parts' col_quantity = 'Nombre de parts'
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
from .pages.login import LoginPage, UnavailablePage from .pages.login import LoginPage, UnavailablePage
from .pages.accounts_list import ( from .pages.accounts_list import (
AccountsList, AccountHistoryPage, CardHistoryPage, InvestmentHistoryPage, PeaHistoryPage, LoanPage, ProfilePage, ProfilePageCSV, SecurityPage, AccountsList, AccountHistoryPage, CardHistoryPage, InvestmentHistoryPage, PeaHistoryPage, LoanPage, ProfilePage, ProfilePageCSV, SecurityPage, FakeActionPage,
) )
from .pages.transfer import ( from .pages.transfer import (
RegisterTransferPage, ValidateTransferPage, ConfirmTransferPage, RecipientsPage, RecipientSMSPage RegisterTransferPage, ValidateTransferPage, ConfirmTransferPage, RecipientsPage, RecipientSMSPage
...@@ -84,7 +84,7 @@ class Fortuneo(LoginBrowser, StatesMixin): ...@@ -84,7 +84,7 @@ class Fortuneo(LoginBrowser, StatesMixin):
r'fr/prive/mes-comptes/compte-courant/.*/init-confirmer-saisie-virement.jsp', r'fr/prive/mes-comptes/compte-courant/.*/init-confirmer-saisie-virement.jsp',
r'/fr/prive/mes-comptes/compte-courant/.*/confirmer-saisie-virement.jsp', r'/fr/prive/mes-comptes/compte-courant/.*/confirmer-saisie-virement.jsp',
ConfirmTransferPage) ConfirmTransferPage)
fake_action_page = URL(r'fr/prive/mes-comptes/synthese-globale/synthese-mes-comptes.jsp', FakeActionPage)
profile = URL(r'/fr/prive/informations-client.jsp', ProfilePage) profile = URL(r'/fr/prive/informations-client.jsp', ProfilePage)
profile_csv = URL(r'/PdfStruts\?*', ProfilePageCSV) profile_csv = URL(r'/PdfStruts\?*', ProfilePageCSV)
...@@ -160,7 +160,17 @@ def get_accounts_list(self): ...@@ -160,7 +160,17 @@ def get_accounts_list(self):
self.process_action_needed() self.process_action_needed()
assert self.accounts_page.is_here() assert self.accounts_page.is_here()
return self.page.get_list() accounts_list = self.page.get_list()
if self.fake_action_page.is_here():
# A false action needed is present, it's a choice to make Fortuno your main bank.
# To avoid it, we need to first detect it on the account_page
# Then make a post request to mimic the click on choice 'later'
# And to finish we must to reload the page with a POST to get the accounts
# before going on the accounts_page, which will have the data.
self.location(self.absurl('ReloadContext?action=1&', base=True), method='POST')
self.accounts_page.go()
accounts_list = self.page.get_list()
return accounts_list
def process_action_needed(self): def process_action_needed(self):
# we have to go in an iframe to know if there are CGUs # we have to go in an iframe to know if there are CGUs
......