...
 
Commits (82)
......@@ -21,7 +21,9 @@ from __future__ import unicode_literals
from datetime import date
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.browser.browsers import ClientError
......@@ -85,6 +87,11 @@ class AmazonBrowser(LoginBrowser, StatesMixin):
self.location('/ap/signin', data=res_form, headers=self.otp_headers)
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"]'):
self.page.send_code()
......
......@@ -45,9 +45,20 @@ class PanelPage(LoggedPage, 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):
message = self.doc.xpath('//div[@class="a-box-inner"]/p')
return message[0] if message else None
return CleanText('//div[@class="a-box-inner"]/p')(self.doc)
def send_code(self):
form = self.get_form()
......
......@@ -61,8 +61,8 @@ class PortfolioPage(LoggedPage, HTMLPage):
obj_label = 'Compte Bolden'
obj_type = Account.TYPE_MARKET
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_valuation_diff = CleanDecimal('//p[has-class("rent-amount strong dashboard-text")]', 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.French('//div[has-class("rent-total")]')
@method
class iter_investments(TableElement):
......@@ -93,7 +93,7 @@ class PortfolioPage(LoggedPage, HTMLPage):
return urljoin(self.page.url, Link('.//a')(TableCell('doc')(self)[0]))
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):
......
......@@ -74,7 +74,10 @@ class BPBrowser(LoginBrowser, StatesMixin):
'/voscomptes/canalXHTML/pret/encours/detaillerOffrePretConsoListe-encoursPrets.ea',
'/voscomptes/canalXHTML/pret/creditRenouvelable/init-consulterCreditRenouvelable.ea',
'/voscomptes/canalXHTML/pret/encours/rechercherPret-encoursPrets.ea',
'/voscomptes/canalXHTML/sso/commun/init-integration.ea\?partenaire',
'/voscomptes/canalXHTML/sso/lbpf/souscriptionCristalFormAutoPost.jsp',
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.*',
'/voscomptes/canalXHTML/comptesCommun/imprimerRIB/init-selection_rib.ea', AccountRIB)
......@@ -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>.*)',
'/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_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 = 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\?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',
'/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>.*)',
'/voscomptes/canalXHTML/CNE/releveCNE/releveCNE-releve_cne.ea', AccountHistory)
......@@ -218,6 +222,10 @@ class BPBrowser(LoginBrowser, StatesMixin):
@need_login
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:
accounts = []
to_check = []
......@@ -237,7 +245,7 @@ class BPBrowser(LoginBrowser, StatesMixin):
for account in self.page.iter_accounts():
if account.type == Account.TYPE_LOAN:
self.location(account.url)
if 'CreditRenouvelable' not in account.url:
if 'initSSO' not in account.url:
for loan in self.page.iter_loans():
loan.currency = account.currency
accounts.append(loan)
......@@ -248,9 +256,11 @@ class BPBrowser(LoginBrowser, StatesMixin):
student_loan.currency = account.currency
accounts.append(student_loan)
else:
for loan in self.page.iter_revolving_loans():
loan.currency = account.currency
accounts.append(loan)
# The main revolving page is not accessible, we can reach it by this new way
self.location(self.absurl('/voscomptes/canalXHTML/sso/lbpf/souscriptionCristalFormAutoPost.jsp'))
self.page.go_revolving()
revolving_loan = self.page.get_revolving_attributes(account)
accounts.append(revolving_loan)
page.go()
elif account.type == Account.TYPE_PERP:
......@@ -308,7 +318,7 @@ class BPBrowser(LoginBrowser, StatesMixin):
self.go_linebourse(account)
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 []
if account.type == Account.TYPE_CARD:
......
......@@ -30,7 +30,7 @@ from weboob.capabilities.profile import Person
from weboob.browser.elements import ListElement, ItemElement, method, TableElement
from weboob.browser.pages import LoggedPage, RawPage, PartialHTMLPage, HTMLPage
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.tools.compat import urljoin, unicode
......@@ -58,6 +58,8 @@ class item_account_generic(ItemElement):
def obj_url(self):
url = Link(u'./a', default=NotAvailable)(self)
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 url
......@@ -74,9 +76,9 @@ class item_account_generic(ItemElement):
has_coming = False
coming = 0
self.page.browser.open(Field('url')(self))
coming_operations = self.page.browser.open(
BrowserURL('par_account_checking_coming', accountId=Field('id'))(self))
details_page = self.page.browser.open(Field('url')(self))
coming_op_link = Regexp(Link(u'//a[contains(text(), "Opérations à venir")]'), r'../(.*)')(details_page.page.doc)
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):
has_coming = True
......@@ -91,10 +93,11 @@ class item_account_generic(ItemElement):
return NotAvailable
def obj_iban(self):
response = self.page.browser.open(
'/voscomptes/canalXHTML/comptesCommun/imprimerRIB/init-imprimer_rib.ea?numeroCompte=%s' % Field('id')(
self))
return response.page.get_iban()
rib_link = Link('//a[abbr[contains(text(), "RIB")]]', default=NotAvailable)(self.el)
if rib_link:
response = self.page.browser.open(rib_link)
return response.page.get_iban()
return NotAvailable
def obj_type(self):
types = {'comptes? bancaires?': Account.TYPE_CHECKING,
......@@ -137,6 +140,10 @@ class AccountList(LoggedPage, MyHTMLPage):
raise BrowserUnavailable()
def go_revolving(self):
form = self.get_form()
form.submit()
@property
def no_accounts(self):
return len(self.doc.xpath('//iframe[contains(@src, "/comptes_contrats/sans_")] |\
......@@ -156,6 +163,22 @@ class AccountList(LoggedPage, MyHTMLPage):
def 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
class iter_revolving_loans(ListElement):
item_xpath = '//div[@class="bloc Tmargin"]//dl'
......
......@@ -333,7 +333,7 @@ class CaisseEpargne(LoginBrowser, StatesMixin):
days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
month = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
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():
msg = self.page.loan_unavailable_msg()
if msg:
......@@ -597,7 +597,7 @@ class CaisseEpargne(LoginBrowser, StatesMixin):
@need_login
def get_investment(self, account):
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()
if account.type == Account.TYPE_PEA and account.label == 'PEA NUMERAIRE':
......@@ -622,7 +622,7 @@ class CaisseEpargne(LoginBrowser, StatesMixin):
yield investment
return
elif account.type == Account.TYPE_LIFE_INSURANCE:
elif account.type in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_CAPITALISATION):
if "MILLEVIE" in account.label:
self.page.go_life_insurance(account)
label = account.label.split()[-1]
......
This diff is collapsed.
......@@ -19,9 +19,9 @@
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.base import find_object
from weboob.capabilities.base import find_object, strict_find_object
from weboob.capabilities.profile import CapProfile
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import Value, ValueBackendPassword
......@@ -33,7 +33,7 @@ from .pro.browser import CmsoProBrowser
__all__ = ['CmsoModule']
class CmsoModule(Module, CapBankWealth, CapContact, CapProfile):
class CmsoModule(Module, CapBankTransfer, CapBankWealth, CapContact, CapProfile):
NAME = 'cmso'
MAINTAINER = 'Romain Bignon'
EMAIL = 'romain@weboob.org'
......@@ -69,6 +69,39 @@ class CmsoModule(Module, CapBankWealth, CapContact, CapProfile):
def iter_investment(self, 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):
if self.config['website'].get() != "par":
raise NotImplementedError()
......
......@@ -22,6 +22,7 @@ from __future__ import unicode_literals
import re
import json
from datetime import date
from functools import wraps
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
......@@ -33,8 +34,9 @@ from weboob.tools.capabilities.bank.transactions import sorted_transactions
from .pages import (
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):
......@@ -92,7 +94,11 @@ class CmsoParBrowser(LoginBrowser, StatesMixin):
'https://www.*/domiweb/prive/particulier', MarketPage)
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)
......@@ -158,7 +164,7 @@ class CmsoParBrowser(LoginBrowser, StatesMixin):
seen = {}
self.recipients.go(data='{"beneficiaryType":"INTERNATIONAL"}', headers=self.json_headers)
self.transfer_info.go(json={"beneficiaryType":"INTERNATIONAL"})
numbers = self.page.get_numbers()
# First get all checking accounts...
......@@ -305,6 +311,74 @@ class CmsoParBrowser(LoginBrowser, StatesMixin):
return []
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))
@need_login
def get_advisor(self):
......
......@@ -135,6 +135,8 @@ class AccountsPage(LoggedPage, JsonPage):
# Iban is available without last 5 numbers, or by sms
obj_iban = NotAvailable
obj__index = Dict('index')
# to know if we can do transfer on account
obj__eligible_debit = Dict('eligibiliteDebit', default=False)
def obj_balance(self):
balance = CleanDecimal(Dict('soldeEuro', default="0"))(self)
......@@ -194,6 +196,8 @@ class AccountsPage(LoggedPage, JsonPage):
obj_coming = CleanDecimal(Dict('AVenir', default=None), default=NotAvailable)
obj__index = Dict('index')
obj__owner = Dict('nomTitulaire')
# to know if we can do transfer on account
obj__eligible_debit = Dict('eligibiliteDebit', default=False)
def obj_id(self):
type = Field('type')(self)
......@@ -549,35 +553,6 @@ class AdvisorPage(LoggedPage, JsonPage):
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):
# 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'
This diff is collapsed.
......@@ -84,7 +84,7 @@ class CragrModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapContact
}.items())])
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}'))
BROWSER = ProxyBrowser
......@@ -98,7 +98,8 @@ class CragrModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapContact
site_conf = self.COMPAT_DOMAINS.get(site_conf, site_conf)
return self.create_browser(site_conf,
self.config['login'].get(),
self.config['password'].get())
self.config['password'].get(),
weboob=self.weboob)
def iter_accounts(self):
return self.browser.get_accounts_list()
......@@ -114,15 +115,17 @@ class CragrModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapContact
return obj.date()
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
if coming == tr_coming:
yield tr
elif coming:
break
def iter_history(self, account):
if account.type == Account.TYPE_CARD:
return self._history_filter(account, False)
return self.browser.get_history(account)
return self.browser.get_history(account, False)
def iter_coming(self, account):
if account.type == Account.TYPE_CARD:
......
......@@ -412,7 +412,7 @@ class Cragr(LoginBrowser, StatesMixin):
account.balance = self.page.get_pea_balance()
@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):
self.logger.warning('This account is not supported')
raise NotImplementedError()
......
......@@ -83,7 +83,7 @@ class CreditDuNordModule(Module, CapBankWealth, CapProfile):
yield tr
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):
if tr._is_coming:
yield tr
......
......@@ -312,8 +312,11 @@ class CreditMutuelBrowser(LoginBrowser, StatesMixin):
form.pop(k, None)
form.submit()
# 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')
except IndexError:
# 6 months history is not available
pass
while self.page:
try:
......
......@@ -907,7 +907,11 @@ class CardPage2(CardPage, HTMLPage, XMLPage):
def obj_original_amount(self):
m = re.search(r'(([\s-]\d+)+,\d+)', 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
def obj_original_currency(self):
......@@ -979,7 +983,11 @@ class CardPage2(CardPage, HTMLPage, XMLPage):
def obj_original_amount(self):
m = re.search(r'(([\s-]\d+)+,\d+)', 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
def obj_original_currency(self):
......@@ -1105,11 +1113,11 @@ class LIAccountsPage(LoggedPage, HTMLPage):
@method
class iter_investment(TableElement):
item_xpath = '//table[has-class("liste")]/tbody/tr[count(td)>=7]'
head_xpath = '//table[has-class("liste")]/thead/tr/th'
item_xpath = '//table[has-class("liste") and not (@summary="Avances")]/tbody/tr[count(td)>=7]'
head_xpath = '//table[has-class("liste") and not (@summary="Avances")]/thead/tr/th'
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_unitvalue = 'Valeur de la part'
col_quantity = 'Nombre de parts'
......
......@@ -32,7 +32,7 @@ from weboob.tools.value import Value
from .pages.login import LoginPage, UnavailablePage
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 (
RegisterTransferPage, ValidateTransferPage, ConfirmTransferPage, RecipientsPage, RecipientSMSPage
......@@ -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/.*/confirmer-saisie-virement.jsp',
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_csv = URL(r'/PdfStruts\?*', ProfilePageCSV)
......@@ -160,7 +160,17 @@ class Fortuneo(LoginBrowser, StatesMixin):
self.process_action_needed()
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):
# we have to go in an iframe to know if there are CGUs
......
......@@ -452,6 +452,9 @@ class AccountsList(LoggedPage, HTMLPage):
'| //div[@id="as_renouvellementMotDePasse.do_"]//p[contains(text(), "votre mot de passe")]'
'| //div[@id="as_afficherSecuriteForteOTPIdentification.do_"]//span[contains(text(), "Pour valider ")]')
if global_error_message:
if "Et si vous faisiez de Fortuneo votre banque principale" in CleanText(global_error_message)(self):
self.browser.location('/ReloadContext', data={'action': 4})
return
raise ActionNeeded(CleanText('.')(global_error_message[0]))
local_error_message = page.doc.xpath('//div[@id="error"]/p[@class="erreur_texte1"]')
if local_error_message:
......@@ -501,6 +504,9 @@ class AccountsList(LoggedPage, HTMLPage):
return accounts
class FakeActionPage(LoggedPage, HTMLPage):
pass
class LoanPage(LoggedPage, HTMLPage):
def get_balance(self):
return CleanText(u'//p[@id="c_montantRestant"]//strong')(self.doc)
......
......@@ -160,7 +160,7 @@ class AccountsPage(GenericLandingPage):
klass = Account
def condition(self):
return len(self.el.xpath('./td')) > 2
return len(self.el.xpath('./td')) > 2 and "en opposition" not in CleanText('./td[1]')(self)
# Some accounts have no <a> in the first <td>
def obj_label(self):
......
This diff is collapsed.
......@@ -16,6 +16,8 @@
#
# 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 re
from datetime import timedelta
......@@ -27,10 +29,9 @@ from weboob.capabilities.bill import (
SubscriptionNotFound, DocumentNotFound, DocumentTypes,
)
from weboob.capabilities.profile import CapProfile
from weboob.capabilities.base import find_object, NotAvailable
from weboob.capabilities.base import find_object
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword, ValueDate
from weboob.browser.exceptions import ServerError
from .browser import IngBrowser
......@@ -39,13 +40,13 @@ __all__ = ['INGModule']
class INGModule(Module, CapBankWealth, CapBankTransfer, CapDocument, CapProfile):
NAME = 'ing'
MAINTAINER = u'Florent Fourcot'
MAINTAINER = 'Florent Fourcot'
EMAIL = 'weboob@flo.fourcot.fr'
VERSION = '1.5'
LICENSE = 'AGPLv3+'
DESCRIPTION = 'ING Direct'
CONFIG = BackendConfig(ValueBackendPassword('login',
label=u'Numéro client',
label='Numéro client',
masked=False),
ValueBackendPassword('password',
label='Code secret',
......@@ -89,7 +90,7 @@ class INGModule(Module, CapBankWealth, CapBankTransfer, CapDocument, CapProfile)
def init_transfer(self, transfer, **params):
self.logger.info('Going to do a new transfer')
transfer.label = ' '.join(w for w in re.sub('[^0-9a-zA-Z/\-\?:\(\)\.,\'\+ ]+', '', transfer.label).split()).upper()
transfer.label = ' '.join(w for w in re.sub(r'[^0-9a-zA-Z/\-\?:\(\)\.,\'\+ ]+', '', transfer.label).split()).upper()
if transfer.account_iban:
account = find_object(self.iter_accounts(), iban=transfer.account_iban, error=AccountNotFound)
else:
......@@ -138,13 +139,8 @@ class INGModule(Module, CapBankWealth, CapBankTransfer, CapDocument, CapProfile)
def download_document(self, bill):
if not isinstance(bill, Bill):
bill = self.get_document(bill)
self.get_document(bill.id)
try:
self.browser.predownload(bill)
except ServerError:
return NotAvailable
assert(self.browser.response.headers['content-type'] in ["application/pdf", "application/download"])
return self.browser.response.content
return self.browser.download_document(bill).content
def get_profile(self):
return self.browser.get_profile()
......@@ -16,9 +16,11 @@
#
# 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
from weboob.capabilities.bill import DocumentTypes, Bill, Subscription
from weboob.browser.pages import HTMLPage, LoggedPage, pagination
from weboob.browser.pages import HTMLPage, LoggedPage, pagination, Form
from weboob.browser.filters.standard import Filter, CleanText, Format, Field, Env, Date
from weboob.browser.filters.html import Attr
from weboob.browser.elements import ListElement, ItemElement, method
......@@ -32,29 +34,60 @@ class FormId(Filter):
return formid
class MyForm(Form):
def submit(self, **kwargs):
"""
Submit the form but keep current browser.page
"""
kwargs.setdefault('data_encoding', self.page.encoding)
return self.page.browser.open(self.request, **kwargs)
class BillsPage(LoggedPage, HTMLPage):
@method
class iter_account(ListElement):
class iter_subscriptions(ListElement):
item_xpath = '//ul[@class="unstyled striped"]/li'
class item(ItemElement):
klass = Subscription
obj__javax = Attr("//form[@id='accountsel_form']/input[@name='javax.faces.ViewState']", 'value')
obj_id = Attr('input', "value")
obj_id = Attr('input', 'value')
obj_label = CleanText('label')
obj__formid = FormId(Attr('input', 'onclick'))
def postpredown(self, _id):
_id = _id.split("'")[3]
form = self.get_form(name="downpdf_form")
def get_selected_year(self):
return int(CleanText('//form[@id="years_form"]//ul/li[@class="rich-list-item selected"]')(self.doc))
def go_to_year(self, year):
if year == self.get_selected_year():
return
ref = Attr('//form[@id="years_form"]//ul//a[text()="%s"]' % year, 'id')(self.doc)
self.FORM_CLASS = Form
form = self.get_form(name='years_form')
form.pop('years_form:j_idcl')
form.pop('years_form:_link_hidden_')
form['AJAXREQUEST'] = 'years_form:year_region'
form[ref] = ref
return form.submit()
def download_document(self, bill):
# MyForm do open, and not location to keep html page as self.page, to reduce number of request on this html page
self.FORM_CLASS = MyForm
_id = bill._localid.split("'")[3]
form = self.get_form(name='downpdf_form')
form['statements_form'] = 'statements_form'
form['statements_form:j_idcl'] = _id
form.submit()
return form.submit()
@pagination
@method
class iter_documents(ListElement):
flush_at_end = True
item_xpath = '//ul[@id="statements_form:statementsel"]/li'
def next_page(self):
......@@ -62,7 +95,7 @@ class BillsPage(LoggedPage, HTMLPage):
selected = False
ref = None
for li in lis:
if "rich-list-item selected" in li.attrib['class']:
if 'rich-list-item selected' in li.attrib['class']:
selected = True
else:
if selected:
......@@ -70,22 +103,29 @@ class BillsPage(LoggedPage, HTMLPage):
break
if ref is None:
return
form = self.page.get_form(name="years_form")
form = self.page.get_form(name='years_form')
form.pop('years_form:j_idcl')
form.pop('years_form:_link_hidden_')
form['AJAXREQUEST'] = "years_form:year_region"
form['AJAXREQUEST'] = 'years_form:year_region'
form[ref] = ref
return form.request
def flush(self):
for obj in reversed(self.objects.values()):
yield obj
class item(ItemElement):
klass = Bill
condition = lambda self: not (u"tous les relev" in CleanText('a[1]')(self.el)) and not (u'annuel' in CleanText('a[1]')(self.el))
condition = lambda self: not ('tous les relev' in CleanText('a[1]')(self.el)) and not ('annuel' in CleanText('a[1]')(self.el))
obj_label = CleanText('a[1]', replace=[(' ', '-')])
obj_id = Format(u"%s-%s", Env('subid'), Field('label'))
obj_id = Format('%s-%s', Env('subid'), Field('label'))
# Force first day of month as label is in form "janvier 2016"
obj_date = Format("1 %s", Field('label')) & Date(parse_func=parse_french_date)
obj_format = u"pdf"
obj_date = Format('1 %s', Field('label')) & Date(parse_func=parse_french_date)
obj_format = 'pdf'
obj_type = DocumentTypes.STATEMENT
obj__localid = Attr('a[2]', 'onclick')
def obj__year(self):
return int(CleanText('a[1]')(self).split(' ')[1])
......@@ -73,7 +73,7 @@ class TransferPage(LoggedPage, HTMLPage):
def parse(self, el):
_id = Attr('.', 'data-acct-number')(self)
accounts = [acc for acc in self.page.browser.get_accounts_list(get_iban=False, space=self.env['origin']._space) if _id in acc.id]
accounts = [acc for acc in self.page.browser.get_accounts_list(fill_account=False, space=self.env['origin']._space) if _id in acc.id]
assert len(accounts) == 1
account = accounts[0]
self.env['id'] = account.id
......
......@@ -27,7 +27,7 @@ from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
from weboob.browser.exceptions import ServerError
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.bank import Account, AddRecipientBankError, AddRecipientStep, Recipient
from weboob.capabilities.bank import Account, AddRecipientBankError, AddRecipientStep, Recipient, AccountOwnerType
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
......@@ -126,6 +126,7 @@ class LCLBrowser(LoginBrowser, StatesMixin):
self.accounts_list = None
self.current_contract = None
self.contracts = None
self.owner_type = AccountOwnerType.PRIVATE
def load_state(self, state):
super(LCLBrowser, self).load_state(state)
......@@ -284,6 +285,9 @@ class LCLBrowser(LoginBrowser, StatesMixin):
self.get_accounts()
else:
self.get_accounts()
for account in self.accounts_list:
account.owner_type = self.owner_type
return iter(self.accounts_list)
def get_bourse_accounts_ids(self):
......@@ -540,6 +544,7 @@ class LCLProBrowser(LCLBrowser):
def __init__(self, *args, **kwargs):
super(LCLProBrowser, self).__init__(*args, **kwargs)
self.session.cookies.set("lclgen", "professionnels", domain=urlsplit(self.BASEURL).hostname)
self.owner_type = AccountOwnerType.ORGANIZATION
class ELCLBrowser(LCLBrowser):
......
......@@ -20,6 +20,7 @@
from weboob.browser import LoginBrowser, URL, need_login
from weboob.exceptions import BrowserIncorrectPassword
from weboob.capabilities.bank import AccountOwnerType
from .pages import LoginPage, MovementsPage, ProfilePage, PassExpiredPage
......@@ -37,6 +38,8 @@ class LCLEnterpriseBrowser(LoginBrowser):
def __init__(self, *args, **kwargs):
super(LCLEnterpriseBrowser, self).__init__(*args, **kwargs)
self.accounts = None
self.owner_type = AccountOwnerType.ORGANIZATION
def deinit(self):
if self.page and self.page.logged:
......@@ -57,7 +60,10 @@ class LCLEnterpriseBrowser(LoginBrowser):
def get_accounts_list(self):
if not self.accounts:
self.accounts = list(self.movements.go().iter_accounts())
return self.accounts
for account in self.accounts:
account.owner_type = self.owner_type
yield account
@need_login
def get_history(self, account):
......
......@@ -139,6 +139,13 @@ class Number26Browser(DomainBrowser):
@need_login
def _internal_get_transactions(self, categories, filter_func):
TYPES = {
'PT': Transaction.TYPE_CARD,
'AA': Transaction.TYPE_CARD,
'CT': Transaction.TYPE_TRANSFER,
'WEE': Transaction.TYPE_BANK,
}
transactions = self.request('/api/smrt/transactions?limit=1000')
for t in transactions:
......@@ -167,12 +174,7 @@ class Number26Browser(DomainBrowser):
if "originalAmount" in t:
new.original_amount = Decimal(str(t["originalAmount"]))
if t["type"] == 'PT':
new.type = Transaction.TYPE_CARD
elif t["type"] == 'CT':
new.type = Transaction.TYPE_TRANSFER
elif t["type"] == 'WEE':
new.type = Transaction.TYPE_BANK
new.type = TYPES.get(t["type"], Transaction.TYPE_UNKNOWN)
if t["category"] in categories:
new.category = categories[t["category"]]
......
This diff is collapsed.
......@@ -73,18 +73,11 @@ class SocieteGeneraleModule(Module, CapBankWealth, CapBankTransferAddRecipient,
def iter_coming(self, account):
if hasattr(self.browser, 'get_cb_operations'):
transactions = list(self.browser.get_cb_operations(account))
else:
transactions = [tr for tr in self.browser.iter_history(account) if tr._coming]
transactions = sorted_transactions(transactions)
return transactions
return sorted_transactions(transactions)
return self.browser.iter_coming(account)
def iter_history(self, account):
if hasattr(self.browser, 'get_cb_operations'):
transactions = list(self.browser.iter_history(account))
else:
transactions = [tr for tr in self.browser.iter_history(account) if not tr._coming]
transactions = sorted_transactions(transactions)
return transactions
return self.browser.iter_history(account)
def iter_investment(self, account):
return self.browser.iter_investment(account)
......@@ -107,7 +100,7 @@ class SocieteGeneraleModule(Module, CapBankWealth, CapBankTransferAddRecipient,
return self.browser.iter_recipients(origin_account)