Skip to content
Commits on Source (55)
......@@ -19,12 +19,13 @@
from __future__ import unicode_literals
from weboob.browser.pages import HTMLPage, LoggedPage, FormNotFound, PartialHTMLPage
from weboob.browser.pages import HTMLPage, LoggedPage, FormNotFound, PartialHTMLPage, pagination
from weboob.browser.elements import ItemElement, ListElement, method
from weboob.browser.filters.html import Link
from weboob.browser.filters.standard import (
CleanText, CleanDecimal, Env, Regexp, Format,
Field, Currency, RegexpError, Date, Async, AsyncLoad
Field, Currency, RegexpError, Date, Async, AsyncLoad,
Coalesce,
)
from weboob.capabilities.bill import DocumentTypes, Bill, Subscription
from weboob.capabilities.base import NotAvailable
......@@ -130,10 +131,14 @@ def obj_label(self):
class DocumentsPage(LoggedPage, HTMLPage):
@pagination
@method
class iter_documents(ListElement):
item_xpath = '//div[contains(@class, "order") and contains(@class, "a-box-group")]'
def next_page(self):
return Link('//ul[@class="a-pagination"]/li[@class="a-last"]/a')(self)
class item(ItemElement):
klass = Bill
load_details = Field('_pre_url') & AsyncLoad
......@@ -146,20 +151,27 @@ class item(ItemElement):
obj_type = DocumentTypes.BILL
def obj_date(self):
date = Date(CleanText('.//div[has-class("a-span4") and not(has-class("recipient"))]/div[2]'),
parse_func=parse_french_date, dayfirst=True, default=NotAvailable)(self)
if date is NotAvailable:
return Date(CleanText('.//div[has-class("a-span3") and not(has-class("recipient"))]/div[2]'),
parse_func=parse_french_date, dayfirst=True)(self)
return date
# The date xpath changes depending on the kind of order
return Coalesce(
Date(CleanText('.//div[has-class("a-span4") and not(has-class("recipient"))]/div[2]'), parse_func=parse_french_date, dayfirst=True, default=NotAvailable),
Date(CleanText('.//div[has-class("a-span3") and not(has-class("recipient"))]/div[2]'), parse_func=parse_french_date, dayfirst=True, default=NotAvailable),
Date(CleanText('.//div[has-class("a-span2") and not(has-class("recipient"))]/div[2]'), parse_func=parse_french_date, dayfirst=True, default=NotAvailable),
)(self)
def obj_price(self):
# Some orders, audiobooks for example, are paid using "audio credits", they have no price or currency
currency = Env('currency')(self)
return CleanDecimal('.//div[has-class("a-col-left")]//span[has-class("value") and contains(., "%s")]' % currency, replace_dots=currency == u'EUR')(self)
return CleanDecimal(
'.//div[has-class("a-col-left")]//span[has-class("value") and contains(., "%s")]' % currency,
replace_dots=currency == 'EUR', default=NotAvailable
)(self)
def obj_currency(self):
currency = Env('currency')(self)
return Currency('.//div[has-class("a-col-left")]//span[has-class("value") and contains(., "%s")]' % currency)(self)
return Currency(
'.//div[has-class("a-col-left")]//span[has-class("value") and contains(., "%s")]' % currency,
default=NotAvailable
)(self)
def obj_url(self):
async_page = Async('details').loaded_page(self)
......
......@@ -27,8 +27,9 @@
from .pages import (
LoginPage, AccountsPage, AccountHistoryPage, AmundiInvestmentsPage, AllianzInvestmentPage,
EEInvestmentPage, InvestmentPerformancePage, InvestmentDetailPage, EEProductInvestmentPage,
EresInvestmentPage, CprInvestmentPage, BNPInvestmentPage, BNPInvestmentApiPage, AxaInvestmentPage,
EpsensInvestmentPage, EcofiInvestmentPage, SGGestionInvestmentPage, SGGestionPerformancePage,
EresInvestmentPage, CprInvestmentPage, CprPerformancePage, BNPInvestmentPage, BNPInvestmentApiPage,
AxaInvestmentPage, EpsensInvestmentPage, EcofiInvestmentPage, SGGestionInvestmentPage,
SGGestionPerformancePage,
)
......@@ -54,6 +55,7 @@ class AmundiBrowser(LoginBrowser):
eres_investments = URL(r'https://www.eres-group.com/eres/new_fiche_fonds.php', EresInvestmentPage)
# CPR asset management investments
cpr_investments = URL(r'https://www.cpr-am.fr/particuliers/product/view', CprInvestmentPage)
cpr_performance = URL(r'https://www.cpr-am.fr/particuliers/ezjscore', CprPerformancePage)
# BNP Paribas Epargne Retraite Entreprises
bnp_investments = URL(r'https://www.epargne-retraite-entreprises.bnpparibas.com/entreprises/fonds', BNPInvestmentPage)
bnp_investment_api = URL(r'https://www.epargne-retraite-entreprises.bnpparibas.com/api2/funds/overview/(?P<fund_id>.*)', BNPInvestmentApiPage)
......@@ -150,7 +152,6 @@ def fill_investment_details(self, inv):
# Pages with asset category & recommended period
elif (self.eres_investments.is_here() or
self.cpr_investments.is_here() or
self.ee_product_investments.is_here() or
self.epsens_investments.is_here() or
self.ecofi_investments.is_here()):
......@@ -178,14 +179,17 @@ def fill_investment_details(self, inv):
if complete_performance_history:
inv.performance_history = complete_performance_history
elif self.sg_gestion_investments.is_here():
elif (self.sg_gestion_investments.is_here() or
self.cpr_investments.is_here()):
# Fetch asset category & recommended period
self.page.fill_investment(obj=inv)
# Fetch all performances on the details page
performance_url = self.page.get_performance_url()
if performance_url:
self.location(performance_url)
inv.performance_history = self.page.get_performance_history()
complete_performance_history = self.page.get_performance_history()
if complete_performance_history:
inv.performance_history = complete_performance_history
elif self.bnp_investments.is_here():
# We fetch the fund ID and get the attributes directly from the BNP-ERE API
......
......@@ -120,7 +120,19 @@ def condition(self):
obj__details_url = Dict('urlFicheFonds', default=None)
obj_code = IsinCode(Dict('codeIsin', default=NotAvailable), default=NotAvailable)
obj_code_type = IsinType(Dict('codeIsin', default=NotAvailable))
obj_diff = CleanDecimal.SI(Dict('mtPMV', default=None), default=NotAvailable)
def obj_diff(self):
diff = CleanDecimal.SI(Dict('mtPMV', default=None), default=NotAvailable)(self)
# Some invests have no diff value but the website fills the json field with the valuation.
if diff == Field('valuation')(self):
return NotAvailable
return diff
def obj_portfolio_share(self):
portfolio_share_percent = CleanDecimal.SI(Dict('pourcentageSupport', default=None), default=None)(self)
if portfolio_share_percent is None:
return NotAvailable
return portfolio_share_percent / 100
def obj_srri(self):
srri = Dict('SRRI')(self)
......@@ -225,11 +237,16 @@ def get_performance_url(self):
class InvestmentPerformancePage(LoggedPage, HTMLPage):
'''
Note: this class is used to parse a pop-up that contains
investment details for the regular Amundi website,
as well as the SG Gestion and the CPR spaces.
'''
def get_performance_history(self):
# The positions of the columns depend on the age of the investment fund.
# For example, if the fund is younger than 5 years, there will be not '5 ans' column.
durations = [CleanText('.')(el) for el in self.doc.xpath('//div[h2[contains(text(), "Performances glissantes")]]//th')]
values = [CleanText('.')(el) for el in self.doc.xpath('//div[h2[contains(text(), "Performances glissantes")]]//tr[td[text()="Fonds"]]//td')]
durations = [CleanText('.')(el) for el in self.doc.xpath('//div[contains(@class, "fpPerfglissanteclassique")]//th')]
values = [CleanText('.')(el) for el in self.doc.xpath('//div[contains(@class, "fpPerfglissanteclassique")]//tr[td[text()="Fonds"]]//td')]
matches = dict(zip(durations, values))
# We do not fill the performance dictionary if no performance is available,
# otherwise it will overwrite the data obtained from the JSON with empty values.
......@@ -243,12 +260,20 @@ def get_performance_history(self):
return perfs
class SGGestionPerformancePage(InvestmentPerformancePage):
pass
class CprPerformancePage(InvestmentPerformancePage):
pass
class InvestmentDetailPage(LoggedPage, HTMLPage):
def get_recommended_period(self):
return Title(CleanText('//label[contains(text(), "Durée minimum de placement")]/following-sibling::span', default=NotAvailable))(self.doc)
def get_asset_category(self):
return CleanText('//label[contains(text(), "Classe d\'actifs")]/following-sibling::span', default=NotAvailable)(self.doc)
return CleanText('(//label[contains(text(), "Classe d\'actifs")])[1]/following-sibling::span', default=NotAvailable)(self.doc)
class EEProductInvestmentPage(LoggedPage, HTMLPage):
......@@ -294,6 +319,13 @@ class fill_investment(ItemElement):
obj_asset_category = Title('//div[contains(text(), "Classe d\'actifs") or contains(text(), "Asset class")]//strong', default=NotAvailable)
obj_recommended_period = Title('//div[contains(text(), "Durée recommandée") or contains(text(), "Recommended duration")]//strong', default=NotAvailable)
def get_performance_url(self):
js_script = CleanText('//script[@language="javascript"]')(self.doc) # beurk
# Extract performance URL from a string such as 'Product.init(false,"/particuliers..."'
m = re.search(r'(/particuliers[^\"]+)', js_script)
if m:
return 'https://www.cpr-am.fr' + m.group(1)
class BNPInvestmentPage(LoggedPage, HTMLPage):
def get_fund_id(self):
......@@ -343,7 +375,3 @@ class fill_investment(ItemElement):
def get_performance_url(self):
return Attr('(//li[@role="presentation"])[1]//a', 'data-href', default=None)(self.doc)
class SGGestionPerformancePage(InvestmentPerformancePage):
pass
......@@ -35,6 +35,7 @@
class AvivaBrowser(LoginBrowser):
TIMEOUT = 120
BASEURL = 'https://www.aviva.fr'
validation = BrowserParamURL(r'/conventions/acceptation\?backurl=/(?P<browser_subsite>[^/]+)/Accueil', ValidationPage)
......
......@@ -75,7 +75,8 @@ class AXABrowser(LoginBrowser):
)
def do_login(self):
# due to the website change, login changed too, this is for don't try to login with the wrong login
# Due to the website change, login changed too.
# This is for avoiding to log-in with the wrong login
if self.username.isdigit() and len(self.username) > 7:
raise ActionNeeded()
......@@ -99,6 +100,12 @@ def do_login(self):
if not self.password.isdigit() or self.page.check_error():
raise BrowserIncorrectPassword()
url = self.page.get_url()
if 'bank-otp' in url:
# The SCA is Cross-Browser so the user can do the SMS validation on the website
# and then try to synchronize the connection again.
raise ActionNeeded('Vous devez réaliser la double authentification sur le portail internet')
# home page to finish login
self.location('https://espaceclient.axa.fr/')
......
......@@ -98,6 +98,9 @@ class LoginPage(JsonPage):
def check_error(self):
return (not Dict('errors')(self.doc)) is False
def get_url(self):
return CleanText(Dict('datas/url', default=''))(self.doc)
class ChangepasswordPage(HTMLPage):
def on_load(self):
......
......@@ -496,7 +496,7 @@ def get_token(self):
headers = {'Referer': self.url}
# Sometime, the page is a 302 and redirect to a page where there are no information that we need,
# so we try with 2 others url to further fetch token when empty page
# so we try with 3 others url to further fetch token when empty page
r = self.browser.open(url, data='taskId=aUniversMesComptes', params={'vary': vary}, headers=headers)
if not int(r.headers.get('Content-Length', 0)):
......@@ -505,6 +505,9 @@ def get_token(self):
if not int(r.headers.get('Content-Length', 0)):
r = self.browser.open(url, data={'taskId': 'equipementDom'}, params={'vary': vary}, headers=headers)
if not int(r.headers.get('Content-Length', 0)):
r = self.browser.open(url)
doc = r.page.doc
date = None
for script in doc.xpath('//script'):
......
......@@ -18,30 +18,15 @@
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from weboob.browser.pages import HTMLPage, LoggedPage
from weboob.browser.pages import HTMLPage, LoggedPage, AbstractPage
from weboob.browser.elements import method, ItemElement
from weboob.browser.filters.standard import CleanText, Format
from weboob.capabilities import NotAvailable
from weboob.exceptions import BrowserIncorrectPassword
class LoginPage(HTMLPage):
REFRESH_MAX = 10.0
def login(self, login, passwd):
form = self.get_form(xpath='//form[contains(@name, "ident")]')
form['_cm_user'] = login
form['_cm_pwd'] = passwd
form.submit()
@property
def logged(self):
return self.doc.xpath('//div[@id="e_identification_ok"]')
def on_load(self):
error_msg_xpath = '//div[has-class("err")]//p[contains(text(), "votre mot de passe est faux")]'
if self.doc.xpath(error_msg_xpath):
raise BrowserIncorrectPassword(CleanText(error_msg_xpath)(self.doc))
class LoginPage(AbstractPage):
PARENT = 'creditmutuel'
PARENT_URL = 'login'
class AdvisorPage(LoggedPage, HTMLPage):
......
......@@ -55,6 +55,8 @@ def search_events(self, q):
self.search.go()
self.page.search(q)
day_events = []
for event in self.page.iter_events(date=date):
for h, m in event._date_hours:
event = event.copy()
......@@ -62,7 +64,10 @@ def search_events(self, q):
self.set_id_end(event)
if event.start_date >= original_start:
yield event
day_events.append(event)
day_events.sort(key=lambda ev: ev.start_date)
for event in day_events:
yield event
def get_event(self, _id):
try:
......
......@@ -17,7 +17,6 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from .module import BNPorcModule
__all__ = ['BNPorcModule']
......@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
# yapf-compatible
from __future__ import unicode_literals
from datetime import date, timedelta
......@@ -28,7 +30,6 @@
from .pages import LoginPage, AccountsPage, HistoryPage
__all__ = ['BNPCompany']
......@@ -49,7 +50,6 @@ def do_login(self):
self.page.login(self.username, self.password)
@need_login
def iter_accounts(self):
self.accounts.go()
......@@ -74,10 +74,11 @@ def get_transactions(self, id_account, typeReleve, dateMin, dateMax='null'):
@need_login
def iter_history(self, account):
return self.get_transactions(account.id,
'Comptable',
(date.today() - timedelta(days=90)).strftime('%Y%m%d'),
date.today().strftime('%Y%m%d'))
return self.get_transactions(
account.id,
'Comptable', (date.today() - timedelta(days=90)).strftime('%Y%m%d'),
date.today().strftime('%Y%m%d')
)
@need_login
def iter_documents(self, subscription):
......@@ -89,9 +90,7 @@ def iter_subscription(self):
@need_login
def iter_coming_operations(self, account):
return self.get_transactions(account.id,
'Previsionnel',
(date.today().strftime('%Y%m%d')))
return self.get_transactions(account.id, 'Previsionnel', (date.today().strftime('%Y%m%d')))
@need_login
def iter_investment(self, account):
......
......@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
# yapf-compatible
from __future__ import unicode_literals
from io import BytesIO
......@@ -32,19 +34,20 @@
class BNPVirtKeyboard(MappedVirtKeyboard):
symbols = {'0': 'ff069462836e30a39c911034048f5bb3',
'1': '7969f04e4e82eaefa2ce7a9a23c26178',
'2': '1e6020f97ca1c3ce3da4f39ded15d67d',
'3': 'f84284b40aea93c24814e23e14e76cc8',
'4': '88bab262d4b344c0ef8f06ddd01adbcf',
'5': '0a270764fc5d8334bcb55053432b26cb',
'6': 'e6a4444a6c752cd3e655f2883e530080',
'7': '933d4ca5df6b2b3df2dea00a21a3fed6',
'8': ['f28b918777d21a5fde2bffb9899e2138', 'a97e6e27159084d50f8ef00548b70252'],
'9': 'be751b77af0d998ab4c2cfd38455b2a6',
}
color=(0,0,0)
symbols = {
'0': 'ff069462836e30a39c911034048f5bb3',
'1': '7969f04e4e82eaefa2ce7a9a23c26178',
'2': '1e6020f97ca1c3ce3da4f39ded15d67d',
'3': 'f84284b40aea93c24814e23e14e76cc8',
'4': '88bab262d4b344c0ef8f06ddd01adbcf',
'5': '0a270764fc5d8334bcb55053432b26cb',
'6': 'e6a4444a6c752cd3e655f2883e530080',
'7': '933d4ca5df6b2b3df2dea00a21a3fed6',
'8': ['f28b918777d21a5fde2bffb9899e2138', 'a97e6e27159084d50f8ef00548b70252'],
'9': 'be751b77af0d998ab4c2cfd38455b2a6',
}
color = (0, 0, 0)
def __init__(self, basepage):
img = basepage.doc.xpath('//img[@id="gridpass_img"]')[0]
......
......@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
# yapf-compatible
from __future__ import unicode_literals
from datetime import datetime
......@@ -36,27 +38,37 @@
PasswordExpiredPage, TransactionPage, MarketPage, InvestPage,
)
__all__ = ['BNPEnterprise']
class BNPEnterprise(LoginBrowser):
BASEURL = 'https://secure1.entreprises.bnpparibas.net'
login = URL('/sommaire/jsp/identification.jsp',
'/sommaire/generateImg', LoginPage)
login = URL('/sommaire/jsp/identification.jsp', '/sommaire/generateImg', LoginPage)
auth = URL('/sommaire/PseMenuServlet', AuthPage)
accounts = URL('/NCCPresentationWeb/e10_soldes/liste_soldes.do', AccountsPage)
account_history_view = URL('/NCCPresentationWeb/e10_soldes/init.do\?nccIdSelected=NCC_Soldes',
'/NCCPresentationWeb/e11_releve_op/init.do\?identifiant=(?P<identifiant>)'
'&typeSolde=(?P<type_solde>)&typeReleve=(?P<type_releve>)&typeDate=(?P<type_date>)'
'&dateMin=(?P<date_min>)&dateMax=(?P<date_max>)&ajax=true',
'/NCCPresentationWeb/e11_releve_op/init.do', AccountHistoryViewPage)
account_coming_view = URL('/NCCPresentationWeb/m04_selectionCompteGroupe/init.do\?type=compte&identifiant=(?P<identifiant>)', AccountHistoryViewPage)
account_history = URL('/NCCPresentationWeb/e11_releve_op/listeOperations.do\?identifiant=(?P<identifiant>)&typeSolde=(?P<type_solde>)&typeReleve=(?P<type_releve>)&typeDate=(?P<type_date>)&dateMin=(?P<date_min>)&dateMax=(?P<date_max>)&ajax=true',
'/NCCPresentationWeb/e11_releve_op/listeOperations.do', AccountHistoryPage)
account_coming = URL('/NCCPresentationWeb/e12_rep_cat_op/listOperations.do\?periode=date_valeur&identifiant=(?P<identifiant>)',
'/NCCPresentationWeb/e12_rep_cat_op/listOperations.do', AccountHistoryPage)
account_history_view = URL(
'/NCCPresentationWeb/e10_soldes/init.do\?nccIdSelected=NCC_Soldes',
'/NCCPresentationWeb/e11_releve_op/init.do\?identifiant=(?P<identifiant>)'
'&typeSolde=(?P<type_solde>)&typeReleve=(?P<type_releve>)&typeDate=(?P<type_date>)'
'&dateMin=(?P<date_min>)&dateMax=(?P<date_max>)&ajax=true',
'/NCCPresentationWeb/e11_releve_op/init.do',
AccountHistoryViewPage
)
account_coming_view = URL(
'/NCCPresentationWeb/m04_selectionCompteGroupe/init.do\?type=compte&identifiant=(?P<identifiant>)',
AccountHistoryViewPage
)
account_history = URL(
'/NCCPresentationWeb/e11_releve_op/listeOperations.do\?identifiant=(?P<identifiant>)&typeSolde=(?P<type_solde>)&typeReleve=(?P<type_releve>)&typeDate=(?P<type_date>)&dateMin=(?P<date_min>)&dateMax=(?P<date_max>)&ajax=true',
'/NCCPresentationWeb/e11_releve_op/listeOperations.do',
AccountHistoryPage
)
account_coming = URL(
'/NCCPresentationWeb/e12_rep_cat_op/listOperations.do\?periode=date_valeur&identifiant=(?P<identifiant>)',
'/NCCPresentationWeb/e12_rep_cat_op/listOperations.do',
AccountHistoryPage
)
transaction_detail = URL(r'/NCCPresentationWeb/e21/getOptBDDF.do', TransactionPage)
invest = URL(r'/opcvm/lister-composition/afficher.do', InvestPage)
......@@ -139,7 +151,11 @@ def _iter_history_base(self, account):
# To avoid duplicated transactions we exit as soon a transaction is not within the expected timeframe
for date in rrule(MONTHLY, dtstart=(datetime.now() - relativedelta(months=11)), until=datetime.now())[::-1]:
params = dict(identifiant=account.iban, type_solde='C', type_releve='Previsionnel', type_date='O',
params = dict(
identifiant=account.iban,
type_solde='C',
type_releve='Previsionnel',
type_date='O',
date_min=(date + relativedelta(days=1) - relativedelta(months=1)).strftime(dformat),
date_max=date.strftime(dformat)
)
......@@ -153,8 +169,9 @@ def _iter_history_base(self, account):
continue
if transaction.date > date:
self.logger.debug('transaction not within expected timeframe, stop iterating history: %r',
transaction.to_dict())
self.logger.debug(
'transaction not within expected timeframe, stop iterating history: %r', transaction.to_dict()
)
return
yield transaction
......
......@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
# yapf-compatible
from __future__ import unicode_literals
import re
......@@ -39,22 +41,24 @@
from weboob.capabilities import NotAvailable
from weboob.exceptions import BrowserPasswordExpired, BrowserForbidden
def fromtimestamp(milliseconds):
return datetime.fromtimestamp(milliseconds/1000)
return datetime.fromtimestamp(milliseconds / 1000)
class BNPVirtKeyboard(MappedVirtKeyboard):
symbols = {'0': '8adee734aaefb163fb008d26bb9b3a42',
'1': 'dad45ef18a75200030073ab102155e2f',
'2': '6cb4c69361f5ce32b68b477db98dd0fb',
'3': 'aa9f2d90c8112b84805d908938eefff7',
'4': '5aa9329aceab4318c2c96130915e87b7',
'5': 'd9fbfdf531ad888a9d79855536905d23',
'6': '50ce19be233ac07bebb59a16a3b9d4a7',
'7': '3a1f932237aab949fa6c59565823218b',
'8': 'd46cf28408db75caa915edb871ea573a',
'9': '87686fd75d283905d7651e1098db0882',
}
symbols = {
'0': '8adee734aaefb163fb008d26bb9b3a42',
'1': 'dad45ef18a75200030073ab102155e2f',
'2': '6cb4c69361f5ce32b68b477db98dd0fb',
'3': 'aa9f2d90c8112b84805d908938eefff7',
'4': '5aa9329aceab4318c2c96130915e87b7',
'5': 'd9fbfdf531ad888a9d79855536905d23',
'6': '50ce19be233ac07bebb59a16a3b9d4a7',
'7': '3a1f932237aab949fa6c59565823218b',
'8': 'd46cf28408db75caa915edb871ea573a',
'9': '87686fd75d283905d7651e1098db0882',
}
color = (0, 0, 0)
......@@ -106,8 +110,8 @@ def on_load(self):
class AccountsPage(LoggedPage, JsonPage):
TYPES = {
'Compte chèque': Account.TYPE_CHECKING,
'Compte à vue': Account.TYPE_CHECKING,
'Compte chèque': Account.TYPE_CHECKING,
'Compte à vue': Account.TYPE_CHECKING,
}
@method
......@@ -121,9 +125,7 @@ def obj_id(self):
return CleanText(Dict('numeroCompte'))(self)[2:]
obj_balance = Eval(
lambda x, y: x / 10**y,
CleanDecimal(Dict('soldeComptable')),
CleanDecimal(Dict('decSoldeComptable'))
lambda x, y: x / 10 ** y, CleanDecimal(Dict('soldeComptable')), CleanDecimal(Dict('decSoldeComptable'))
)
obj_label = CleanText(Dict('libelleCompte'))
obj_currency = CleanText(Dict('deviseTenue'))
......@@ -133,9 +135,7 @@ def obj_type(self):
return self.page.TYPES.get(Dict('libelleType')(self), Account.TYPE_UNKNOWN)
def obj_coming(self):
page = self.page.browser.open(
BrowserURL('account_coming', identifiant=Field('iban'))(self)
).page
page = self.page.browser.open(BrowserURL('account_coming', identifiant=Field('iban'))(self)).page
nb_decimal = 0
if 'nb_dec' in Dict('infoOperationsAvenir/cumulTotal')(page.doc):
......@@ -144,7 +144,7 @@ def obj_coming(self):
nb_decimal = Dict('infoOperationsAvenir/cumulTotal/nbDec')
coming = Eval(
lambda x, y: x / 10**y,
lambda x, y: x / 10**y, # yapf: disable
CleanDecimal(Dict('infoOperationsAvenir/cumulTotal/montant', default='0')),
CleanDecimal(nb_decimal)
)(page.doc)
......@@ -161,7 +161,6 @@ class get_profile(ItemElement):
class BnpHistoryItem(ItemElement):
def obj_raw(self):
if self.el.get('nature.libelle') and self.el.get('libelle'):
return "%s %s" % (
......@@ -195,10 +194,12 @@ def load_details(self):
return
url = self.page.browser.transaction_detail.build()
return self.page.browser.open(url, is_async=True, data={
'type_mvt': self.detail_type_mvt,
'numero_mvt': Field('_trid')(self),
})
return self.page.browser.open(
url, is_async=True, data={
'type_mvt': self.detail_type_mvt,
'numero_mvt': Field('_trid')(self),
}
)
class AccountHistoryPage(LoggedPage, JsonPage):
......@@ -257,10 +258,10 @@ def obj_raw(self):
def obj_type(self):
type = self.page.TYPES.get(Dict('nature/codefamille')(self), Transaction.TYPE_UNKNOWN)
if (
(type == Transaction.TYPE_CARD and re.search(r' RELEVE DU \d+\.', Field('raw')(self))) or
(type == Transaction.TYPE_UNKNOWN and re.search(r'FACTURE CARTE AFFAIRES \w{16} SUIVANT RELEVE DU \d{2}.\d{2}.\d{4}', Field('raw')(self)))
):
if ((type == Transaction.TYPE_CARD and re.search(r' RELEVE DU \d+\.', Field('raw')(self))) or (
type == Transaction.TYPE_UNKNOWN and
re.search(r'FACTURE CARTE AFFAIRES \w{16} SUIVANT RELEVE DU \d{2}.\d{2}.\d{4}', Field('raw')(self))
)):
return Transaction.TYPE_CARD_SUMMARY
return type
......@@ -289,14 +290,9 @@ def obj_vdate(self):
return fromtimestamp(Dict('dateValeur')(self))
def obj_amount(self):
decimal_nb = Dict('montant/nbDec', default=None)(self)\
or Dict('montant/nb_dec')(self)
decimal_nb = (Dict('montant/nbDec', default=None)(self) or Dict('montant/nb_dec')(self))
return Eval(
lambda x, y: x / 10**y,
CleanDecimal(Dict('montant/montant')),
decimal_nb
)(self)
return Eval(lambda x, y: x / 10 ** y, CleanDecimal(Dict('montant/montant')), decimal_nb)(self)
obj__trid = Dict('id')
......@@ -328,11 +324,7 @@ def obj_amount(self):
decimal_nb = Dict('montantMvmt/nbDec', default=None)(self)\
or Dict('montantMvmt/nb_dec')(self)
return Eval(
lambda x, y: x / 10**y,
CleanDecimal(Dict('montantMvmt/montant')),
decimal_nb
)(self)
return Eval(lambda x, y: x / 10 ** y, CleanDecimal(Dict('montantMvmt/montant')), decimal_nb)(self)
obj__trid = Dict('idMouvement')
......@@ -343,14 +335,16 @@ class TransactionPage(LoggedPage, JsonPage):
class MarketPage(LoggedPage, HTMLPage):
TYPES = {
'comptes de titres': Account.TYPE_MARKET,
'comptes de titres': Account.TYPE_MARKET,
}
@method
class iter_market_accounts(TableElement):
def condition(self):
return not self.el.xpath('//table[@id="table-portefeuille"]//tr/td[contains(text(), "Aucun portefeuille à afficher") \
or contains(text(), "No portfolio to display")]')
return not self.el.xpath(
'//table[@id="table-portefeuille"]//tr/td[contains(text(), "Aucun portefeuille à afficher") \
or contains(text(), "No portfolio to display")]'
)
item_xpath = '//table[@id="table-portefeuille"]/tbody[@class="main-content"]/tr'
head_xpath = '//table[@id="table-portefeuille"]/thead/tr/th/label'
......@@ -384,7 +378,9 @@ def get_token(self):
def get_id(self, label):
id_simple = re.search(r'[0-9]+', label).group(0)
for options in self.doc.xpath('//div[@class="filterbox-content hide"]//select[@id="numero-compte-titre"]//option'):
for options in self.doc.xpath(
'//div[@class="filterbox-content hide"]//select[@id="numero-compte-titre"]//option'
):
if id_simple in CleanText(options)(self.doc):
return CleanText(options.xpath('./@value'))(self)
......@@ -404,7 +400,6 @@ class get_unique_market_account(ItemElement):
obj__parent = CleanText('//h3/span[span[@class="info-cheque"]]', children=False)
obj__unique = True
@method
class iter_investment(TableElement):
item_xpath = '//table[@class="csv-data-container hide"]//tr'
......@@ -416,13 +411,11 @@ class iter_investment(TableElement):
col_unitvalue = 'Valeur de la part'
col_valuation = 'Valorisation'
col_diff = '+/- value'
"""
Note: Pagination is not handled yet for investments, if we find a
customer with more than 10 invests we might have to handle clicking
on the button to get 50 invests per page or check if there is a link.
"""
class item(ItemElement):
klass = Investment
......@@ -432,7 +425,11 @@ class item(ItemElement):
obj_unitvalue = CleanDecimal(TableCell('unitvalue'), replace_dots=True)
obj_valuation = CleanDecimal(TableCell('valuation'), replace_dots=True)
obj_diff = CleanDecimal(TableCell('diff'), replace_dots=True)
obj_code_type = lambda self: Investment.CODE_TYPE_ISIN if Field('code')(self) is not NotAvailable else NotAvailable
def obj_code_type(self):
if Field('code')(self):
return Investment.CODE_TYPE_ISIN
return NotAvailable
def obj_code(self):
string = CleanText(TableCell('label'))(self)
......
......@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
# yapf-compatible
from __future__ import unicode_literals
import re
......@@ -42,11 +44,12 @@
from .company.browser import BNPCompany
from .pp.browser import BNPPartPro, HelloBank
__all__ = ['BNPorcModule']
class BNPorcModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapMessages, CapContact, CapProfile, CapDocument):
class BNPorcModule(
Module, CapBankWealth, CapBankTransferAddRecipient, CapMessages, CapContact, CapProfile, CapDocument
):
NAME = 'bnporc'
MAINTAINER = u'Romain Bignon'
EMAIL = 'romain@weboob.org'
......@@ -54,15 +57,22 @@ class BNPorcModule(Module, CapBankWealth, CapBankTransferAddRecipient, CapMessag
LICENSE = 'LGPLv3+'
DESCRIPTION = 'BNP Paribas'
CONFIG = BackendConfig(
ValueBackendPassword('login', label=u'Numéro client', masked=False),
ValueBackendPassword('password', label=u'Code secret', regexp='^(\d{6})$'),
ValueBool('rotating_password', label=u'Automatically renew password every 100 connections', default=False),
ValueBool('digital_key', label=u'User with digital key have to add recipient with digital key', default=False),
Value('website', label='Type de compte', default='pp',
choices={'pp': 'Particuliers/Professionnels',
'hbank': 'HelloBank',
'ent': 'Entreprises',
'ent2': 'Entreprises et PME (nouveau site)'}))
ValueBackendPassword('login', label=u'Numéro client', masked=False),
ValueBackendPassword('password', label=u'Code secret', regexp='^(\d{6})$'),
ValueBool('rotating_password', label=u'Automatically renew password every 100 connections', default=False),
ValueBool('digital_key', label=u'User with digital key have to add recipient with digital key', default=False),
Value(
'website',
label='Type de compte',
default='pp',
choices={
'pp': 'Particuliers/Professionnels',
'hbank': 'HelloBank',
'ent': 'Entreprises',
'ent2': 'Entreprises et PME (nouveau site)'
}
)
)
STORAGE = {'seen': []}
accepted_document_types = (
......@@ -141,7 +151,9 @@ def init_transfer(self, transfer, **params):
recipient = strict_find_object(self.iter_transfer_recipients(account.id), iban=transfer.recipient_iban)
if not recipient:
recipient = strict_find_object(self.iter_transfer_recipients(account.id), id=transfer.recipient_id, error=RecipientNotFound)
recipient = strict_find_object(
self.iter_transfer_recipients(account.id), id=transfer.recipient_id, error=RecipientNotFound
)
assert account.id.isdigit()
# quantize to show 2 decimals.
......
......@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
# yapf-compatible
from __future__ import unicode_literals
from datetime import datetime
......@@ -34,7 +36,6 @@
from weboob.capabilities.profile import ProfileMissing
from weboob.tools.decorators import retry
from weboob.tools.capabilities.bank.transactions import sorted_transactions
from weboob.tools.json import json
from weboob.browser.exceptions import ServerError
from weboob.browser.elements import DataError
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
......@@ -56,47 +57,35 @@
__all__ = ['BNPPartPro', 'HelloBank']
def JSON(data):
return ('json', data)
def isJSON(obj):
return type(obj) is tuple and obj and obj[0] == 'json'
class JsonBrowserMixin(object):
def open(self, *args, **kwargs):
if isJSON(kwargs.get('data')):
kwargs['data'] = json.dumps(kwargs['data'][1])
if 'headers' not in kwargs:
kwargs['headers'] = {}
kwargs['headers']['Content-Type'] = 'application/json'
return super(JsonBrowserMixin, self).open(*args, **kwargs)
class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser, StatesMixin):
class BNPParibasBrowser(LoginBrowser, StatesMixin):
TIMEOUT = 30.0
login = URL(r'identification-wspl-pres/identification\?acceptRedirection=true&timestamp=(?P<timestamp>\d+)',
r'SEEA-pa01/devServer/seeaserver',
r'https://mabanqueprivee.bnpparibas.net/fr/espace-prive/comptes-et-contrats\?u=%2FSEEA-pa01%2FdevServer%2Fseeaserver',
LoginPage)
login = URL(
r'identification-wspl-pres/identification\?acceptRedirection=true&timestamp=(?P<timestamp>\d+)',
r'SEEA-pa01/devServer/seeaserver',
r'https://mabanqueprivee.bnpparibas.net/fr/espace-prive/comptes-et-contrats\?u=%2FSEEA-pa01%2FdevServer%2Fseeaserver',
LoginPage
)
list_error_page = URL(r'https://mabanque.bnpparibas/rsc/contrib/document/properties/identification-fr-part-V1.json', ListErrorPage)
list_error_page = URL(
r'https://mabanque.bnpparibas/rsc/contrib/document/properties/identification-fr-part-V1.json', ListErrorPage
)
useless_page = URL(r'/fr/connexion/comptes-et-contrats', UselessPage)
con_threshold = URL(r'/fr/connexion/100-connexions',
r'/fr/connexion/mot-de-passe-expire',
r'/fr/espace-prive/100-connexions.*',
r'/fr/espace-pro/100-connexions-pro.*',
r'/fr/espace-pro/changer-son-mot-de-passe',
r'/fr/espace-client/100-connexions',
r'/fr/espace-prive/mot-de-passe-expire',
r'/fr/client/mdp-expire',
r'/fr/client/100-connexion',
r'/fr/systeme/page-indisponible', ConnectionThresholdPage)
con_threshold = URL(
r'/fr/connexion/100-connexions',
r'/fr/connexion/mot-de-passe-expire',
r'/fr/espace-prive/100-connexions.*',
r'/fr/espace-pro/100-connexions-pro.*',
r'/fr/espace-pro/changer-son-mot-de-passe',
r'/fr/espace-client/100-connexions',
r'/fr/espace-prive/mot-de-passe-expire',
r'/fr/client/mdp-expire',
r'/fr/client/100-connexion',
r'/fr/systeme/page-indisponible',
ConnectionThresholdPage
)
accounts = URL(r'udc-wspl/rest/getlstcpt', AccountsPage)
loan_details = URL(r'caraccomptes-wspl/rpc/(?P<loan_type>.*)', LoanDetailsPage)
ibans = URL(r'rib-wspl/rpc/comptes', AccountsIBANPage)
......@@ -109,7 +98,9 @@ class BNPParibasBrowser(JsonBrowserMixin, LoginBrowser, StatesMixin):
lifeinsurances_detail = URL(r'mefav-wspl/rest/detailMouvement', LifeInsurancesDetailPage)
natio_vie_pro = URL(r'/mefav-wspl/rest/natioViePro', NatioVieProPage)
capitalisation_page = URL(r'https://www.clients.assurance-vie.fr/servlets/helios.cinrj.htmlnav.runtime.FrontServlet', CapitalisationPage)
capitalisation_page = URL(
r'https://www.clients.assurance-vie.fr/servlets/helios.cinrj.htmlnav.runtime.FrontServlet', CapitalisationPage
)
market_list = URL(r'pe-war/rpc/SAVaccountDetails/get', MarketListPage)
market_syn = URL(r'pe-war/rpc/synthesis/get', MarketSynPage)
......@@ -184,7 +175,7 @@ def change_pass(self, oldpass, newpass):
@need_login
def get_profile(self):
self.profile.go(data=JSON({}))
self.profile.go(json={}, method='POST')
profile = self.page.get_profile()
if profile:
return profile
......@@ -192,13 +183,9 @@ def get_profile(self):
def is_loan(self, account):
return account.type in (
Account.TYPE_LOAN,
Account.TYPE_MORTGAGE,
Account.TYPE_CONSUMER_CREDIT,
Account.TYPE_REVOLVING_CREDIT
Account.TYPE_LOAN, Account.TYPE_MORTGAGE, Account.TYPE_CONSUMER_CREDIT, Account.TYPE_REVOLVING_CREDIT
)
@need_login
def iter_accounts(self):
if self.accounts_list is None:
......@@ -208,12 +195,12 @@ def iter_accounts(self):
ibans = self.page.get_ibans_dict() if self.ibans.is_here() else self.ibans.go().get_ibans_dict()
# This page might be unavailable.
try:
ibans.update(self.transfer_init.go(data=JSON({'modeBeneficiaire': '0'})).get_ibans_dict('Crediteur'))
ibans.update(self.transfer_init.go(json={'modeBeneficiaire': '0'}).get_ibans_dict('Crediteur'))
except (TransferAssertionError, AttributeError):
pass
accounts = list(self.accounts.go().iter_accounts(ibans=ibans))
self.market_syn.go(data=JSON({})) # do a post on the given URL
self.market_syn.go(json={}, method='POST') # do a post on the given URL
market_accounts = self.page.get_list() # get the list of 'Comptes Titres'
checked_accounts = set()
for account in accounts:
......@@ -282,30 +269,32 @@ def iter_history(self, account, coming=False):
return self.iter_lifeinsurance_history(account, coming)
elif account.type in (account.TYPE_MARKET, Account.TYPE_PEA) and not coming:
try:
self.market_list.go(data=JSON({}))
self.market_list.go(json={}, method='POST')
except ServerError:
self.logger.warning("An Internal Server Error occurred")
return iter([])
for market_acc in self.page.get_list():
if account.number[-4:] == market_acc['securityAccountNumber'][-4:]:
self.page = self.market_history.go(data=JSON({
"securityAccountNumber": market_acc['securityAccountNumber'],
}))
self.page = self.market_history.go(
json={
"securityAccountNumber": market_acc['securityAccountNumber'],
}
)
return self.page.iter_history()
return iter([])
else:
if not self.card_to_transaction_type:
self.list_detail_card.go()
self.card_to_transaction_type = self.page.get_card_to_transaction_type()
data = JSON({
data = {
"ibanCrypte": account.id,
"pastOrPending": 1,
"triAV": 0,
"startDate": (datetime.now() - relativedelta(years=1)).strftime('%d%m%Y'),
"endDate": datetime.now().strftime('%d%m%Y')
})
}
try:
self.history.go(data=data)
self.history.go(json=data)
except BrowserUnavailable:
# old url is still used for certain connections bu we don't know which one is,
# so the same HistoryPage is attained by the old url in another URL object
......@@ -320,17 +309,19 @@ def iter_history(self, account, coming=False):
@need_login
def iter_lifeinsurance_history(self, account, coming=False):
self.lifeinsurances_history.go(data=JSON({
self.lifeinsurances_history.go(json={
"ibanCrypte": account.id,
}))
})
for tr in self.page.iter_history(coming):
page = self.lifeinsurances_detail.go(data=JSON({
"ibanCrypte": account.id,
"idMouvement": tr._op.get('idMouvement'),
"ordreMouvement": tr._op.get('ordreMouvement'),
"codeTypeMouvement": tr._op.get('codeTypeMouvement'),
}))
page = self.lifeinsurances_detail.go(
json={
"ibanCrypte": account.id,
"idMouvement": tr._op.get('idMouvement'),
"ordreMouvement": tr._op.get('ordreMouvement'),
"codeTypeMouvement": tr._op.get('codeTypeMouvement'),
}
)
tr.investments = list(page.iter_investments())
yield tr
......@@ -357,14 +348,14 @@ def iter_investment(self, account):
else:
# No capitalisation contract has yet been found in the API:
assert account.type != account.TYPE_CAPITALISATION
self.lifeinsurances.go(data=JSON({
self.lifeinsurances.go(json={
"ibanCrypte": account.id,
}))
})
return self.page.iter_investments()
elif account.type in (account.TYPE_MARKET, account.TYPE_PEA):
try:
self.market_list.go(data=JSON({}))
self.market_list.go(json={}, method='POST')
except ServerError:
self.logger.warning("An Internal Server Error occurred")
return iter([])
......@@ -372,9 +363,9 @@ def iter_investment(self, account):
if account.number[-4:] == market_acc['securityAccountNumber'][-4:] and not account.iban:
# Sometimes generate an Internal Server Error ...
try:
self.market.go(data=JSON({
self.market.go(json={
"securityAccountNumber": market_acc['securityAccountNumber'],
}))
})
except ServerError:
self.logger.warning("An Internal Server Error occurred")
break
......@@ -385,7 +376,11 @@ def iter_investment(self, account):
@need_login
def iter_recipients(self, origin_account_id):
try:
if not origin_account_id in self.transfer_init.go(data=JSON({'modeBeneficiaire': '0'})).get_ibans_dict('Debiteur'):
if (
not origin_account_id in self.transfer_init.go(json={
'modeBeneficiaire': '0'
}).get_ibans_dict('Debiteur')
):
raise NotImplementedError()
except TransferAssertionError:
return
......@@ -398,7 +393,7 @@ def iter_recipients(self, origin_account_id):
yield recipient
if self.page.can_transfer_to_recipients(origin_account_id):
for recipient in self.recipients.go(data=JSON({'type': 'TOUS'})).iter_recipients():
for recipient in self.recipients.go(json={'type': 'TOUS'}).iter_recipients():
if recipient.iban not in seen:
seen.add(recipient.iban)
yield recipient
......@@ -425,7 +420,7 @@ def new_recipient(self, recipient, **params):
# need to be on recipient page send sms or mobile notification
# needed to get the phone number, enabling the possibility to send sms.
# all users with validated phone number can receive sms code
self.recipients.go(data=JSON({'type': 'TOUS'}))
self.recipients.go(json={'type': 'TOUS'})
# check type of recipient activation
type_activation = 'sms'
......@@ -439,10 +434,7 @@ def new_recipient(self, recipient, **params):
if type_activation == 'sms':
# post recipient data sending sms with same request
data['typeEnvoi'] = 'SMS'
recipient = self.add_recip.go(
data=json.dumps(data),
headers={'Content-Type': 'application/json'}
).get_recipient(recipient)
recipient = self.add_recip.go(json=data).get_recipient(recipient)
self.rcpt_transfer_id = recipient._transfer_id
self.need_reload_state = True
raise AddRecipientStep(recipient, Value('code', label='Saisissez le code reçu par SMS.'))
......@@ -451,7 +443,11 @@ def new_recipient(self, recipient, **params):
recipient.enabled_date = datetime.today()
raise AddRecipientStep(
recipient,
ValueBool('digital_key', label='Validez pour recevoir une demande sur votre application bancaire. La validation de votre bénéficiaire peut prendre plusieurs minutes.')
ValueBool(
'digital_key',
label=
'Validez pour recevoir une demande sur votre application bancaire. La validation de votre bénéficiaire peut prendre plusieurs minutes.'
)
)
@need_login
......@@ -464,7 +460,7 @@ def send_code(self, recipient, **params):
data['typeActivation'] = 1
data['codeActivation'] = params['code']
self.rcpt_transfer_id = None
return self.activate_recip_sms.go(data=json.dumps(data), headers={'Content-Type': 'application/json'}).get_recipient(recipient)
return self.activate_recip_sms.go(json=data).get_recipient(recipient)
@need_login
def new_recipient_digital_key(self, recipient, data):
......@@ -473,7 +469,7 @@ def new_recipient_digital_key(self, recipient, data):
"""
# post recipient data, sending app notification with same request
data['typeEnvoi'] = 'AF'
self.add_recip.go(data=json.dumps(data), headers={'Content-Type': 'application/json'})
self.add_recip.go(json=data)
recipient = self.page.get_recipient(recipient)
# prepare data for polling
......@@ -483,15 +479,12 @@ def new_recipient_digital_key(self, recipient, data):
polling_data['idTransaction'] = recipient._id_transaction
polling_data['typeActivation'] = 2
timeout = time.time() + 300.00 # float(second), like bnp website
timeout = time.time() + 300.00 # float(second), like bnp website
# polling
while time.time() < timeout:
time.sleep(5) # like website
self.activate_recip_digital_key.go(
data = json.dumps(polling_data),
headers = {'Content-Type': 'application/json'}
)
time.sleep(5) # like website
self.activate_recip_digital_key.go(json=polling_data)
if self.page.is_recipient_validated():
break
else:
......@@ -520,11 +513,11 @@ def init_transfer(self, account, recipient, amount, reason, exec_date):
raise TransferInvalidRecipient(message="Le bénéficiaire sélectionné n'est pas activé")
data = self.prepare_transfer(account, recipient, amount, reason, exec_date)
return self.validate_transfer.go(data=JSON(data)).handle_response(account, recipient, amount, reason)
return self.validate_transfer.go(json=data).handle_response(account, recipient, amount, reason)
@need_login
def execute_transfer(self, transfer):
self.register_transfer.go(data=JSON({'referenceVirement': transfer.id}))
self.register_transfer.go(json={'referenceVirement': transfer.id})
return self.page.handle_response(transfer)
@need_login
......@@ -575,7 +568,9 @@ def iter_documents(self, subscription):
data['ikpiPersonne'] = subscription._iduser
self.document_research.go(json=data)
for doc in self.page.iter_documents(sub_id=subscription.id, sub_number=subscription._number, baseurl=self.BASEURL):
for doc in self.page.iter_documents(
sub_id=subscription.id, sub_number=subscription._number, baseurl=self.BASEURL
):
if doc.id not in id_docs:
yield doc
......
......@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
# yapf-compatible
from __future__ import unicode_literals
import re
......@@ -73,13 +75,15 @@ def condition(self):
def obj_label(self):
if 'ibanCrypte' in self.el:
return '%s %s N° %s' % (Dict('dateDoc')(self), Dict('libelleSousFamille')(self), Dict('numeroCompteAnonymise')(self))
return '%s %s N° %s' % (
Dict('dateDoc')(self), Dict('libelleSousFamille')(self), Dict('numeroCompteAnonymise')(self)
)
else:
return '%s %s N° %s' % (Dict('dateDoc')(self), Dict('libelleSousFamille')(self), Dict('idContrat')(self))
def obj_url(self):
keys_to_copy = {
'idDocument' :'idDoc',
'idDocument': 'idDoc',
'dateDocument': 'dateDoc',
'idLocalisation': 'idLocalisation',
'viDocDocument': 'viDocDocument',
......
......@@ -17,6 +17,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
# yapf-compatible
from __future__ import unicode_literals
from collections import Counter
......@@ -60,7 +62,6 @@ class TransferAssertionError(Exception):
class ConnectionThresholdPage(HTMLPage):
NOT_REUSABLE_PASSWORDS_COUNT = 3
"""BNP disallows to reuse one of the three last used passwords."""
def make_date(self, yy, m, d):
current = datetime.now().year
if yy > current - 2000:
......@@ -109,8 +110,10 @@ def looks_legit(self, password):
def on_load(self):
msg = (
CleanText('//div[@class="confirmation"]//span[span]')(self.doc) or
CleanText('//p[contains(text(), "Vous avez atteint la date de fin de vie de votre code secret")]')(self.doc)
CleanText('//div[@class="confirmation"]//span[span]')(self.doc)
or CleanText('//p[contains(text(), "Vous avez atteint la date de fin de vie de votre code secret")]')(
self.doc
)
)
self.logger.warning('Password expired.')
......@@ -154,16 +157,18 @@ def cast(x, typ, default=None):
class BNPKeyboard(GridVirtKeyboard):
color = (0x1f, 0x27, 0x28)
margin = 3, 3
symbols = {'0': '43b2227b92e0546d742a1f087015e487',
'1': '2914e8cc694de26756096d0d0d4c6e0f',
'2': 'aac54304a7bb850805d29f54557be366',
'3': '0376d9f8419efee42e253d195a152547',
'4': '3719595f15b1ac1c5a73d84aa290b5f6',
'5': '617597f07a6530479927536671485439',
'6': '4f5dce7bd0d9213fdae54b79bb8dd33a',
'7': '49e07fa52b9bcee798f3a663f86e6cc1',
'8': 'c60b723b3d95a46416b34c2cbefba3ed',
'9': 'a13b8c3617a7bf854590833ddfb97f1f'}
symbols = {
'0': '43b2227b92e0546d742a1f087015e487',
'1': '2914e8cc694de26756096d0d0d4c6e0f',
'2': 'aac54304a7bb850805d29f54557be366',
'3': '0376d9f8419efee42e253d195a152547',
'4': '3719595f15b1ac1c5a73d84aa290b5f6',
'5': '617597f07a6530479927536671485439',
'6': '4f5dce7bd0d9213fdae54b79bb8dd33a',
'7': '49e07fa52b9bcee798f3a663f86e6cc1',
'8': 'c60b723b3d95a46416b34c2cbefba3ed',
'9': 'a13b8c3617a7bf854590833ddfb97f1f'
}
def __init__(self, browser, image):
symbols = list('%02d' % x for x in range(1, 11))
......@@ -191,7 +196,7 @@ def render_template(tmpl, **values):
@staticmethod
def generate_token(length=11):
chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'
return ''.join((chars[randint(0, len(chars)-1)] for _ in range(length)))
return ''.join((chars[randint(0, len(chars) - 1)] for _ in range(length)))
def build_doc(self, text):
try:
......@@ -222,7 +227,7 @@ def on_load(self):
websiteUnavailable_codes = [207, 1000, 1001]
if error in wrongpass_codes:
raise BrowserIncorrectPassword(msg)
elif error == 21: # "Ce service est momentanément indisponible. Veuillez renouveler votre demande ultérieurement." -> In reality, account is blocked because of too much wrongpass
elif error == 21: # "Ce service est momentanément indisponible. Veuillez renouveler votre demande ultérieurement." -> In reality, account is blocked because of too much wrongpass
raise ActionNeeded(u"Compte bloqué")
elif error in actionNeeded_codes:
raise ActionNeeded(msg)
......@@ -244,10 +249,12 @@ def login(self, username, password):
target = self.browser.BASEURL + 'SEEA-pa01/devServer/seeaserver'
user_agent = self.browser.session.headers.get('User-Agent') or ''
auth = self.render_template(self.get('data.authTemplate'),
idTelematique=username,
password=vk.get_string_code(password),
clientele=user_agent)
auth = self.render_template(
self.get('data.authTemplate'),
idTelematique=username,
password=vk.get_string_code(password),
clientele=user_agent
)
# XXX useless?
csrf = self.generate_token()
......@@ -292,8 +299,12 @@ def condition(self):
klass = Person
def parse(self, el):
if not Dict(self.item_path + 'etatCivil/prenom')(el).strip() and not Dict(self.item_path + 'etatCivil/nom')(el).strip():
if (
not Dict(self.item_path + 'etatCivil/prenom')(el).strip()
and not Dict(self.item_path + 'etatCivil/nom')(el).strip()
):
raise ProfileMissing()
obj_name = Format('%s %s', Dict(item_path + 'etatCivil/prenom'), Dict(item_path + 'etatCivil/nom'))
obj_spouse_name = Dict(item_path + 'etatCivil/nomMarital', default=NotAvailable)
obj_birth_date = Date(Dict(item_path + 'etatCivil/dateNaissance'), dayfirst=True)
......@@ -446,10 +457,15 @@ def on_load(self):
raise TransferAssertionError('%s, code=%s' % (message_code[0], message_code[1]))
def get_ibans_dict(self, account_type):
return dict([(a['ibanCrypte'], a['iban']) for a in self.path('data.infoVirement.listeComptes%s.*' % account_type)])
return dict([(a['ibanCrypte'], a['iban'])
for a in self.path('data.infoVirement.listeComptes%s.*' % account_type)])
def can_transfer_to_recipients(self, origin_account_id):
return next(a['eligibleVersBenef'] for a in self.path('data.infoVirement.listeComptesDebiteur.*') if a['ibanCrypte'] == origin_account_id) == '1'
return next(
a['eligibleVersBenef']
for a in self.path('data.infoVirement.listeComptesDebiteur.*')
if a['ibanCrypte'] == origin_account_id
) == '1'
@method
class transferable_on(DictElement):
......@@ -492,10 +508,15 @@ def obj_bank_name(self):
return Dict('nomBanque')(self) or NotAvailable
def obj_enabled_at(self):
return datetime.now().replace(microsecond=0) if Dict('libelleStatut')(self) == u'Activé' else (datetime.now() + timedelta(days=5)).replace(microsecond=0)
if Dict('libelleStatut')(self) == u'Activé':
return datetime.now().replace(microsecond=0)
return (datetime.now() + timedelta(days=5)).replace(microsecond=0)
def has_digital_key(self):
return Dict('data/infoBeneficiaire/authentForte')(self.doc) and Dict('data/infoBeneficiaire/nomDeviceAF', default=False)(self.doc)
return (
Dict('data/infoBeneficiaire/authentForte')(self.doc)
and Dict('data/infoBeneficiaire/nomDeviceAF', default=False)(self.doc)
)
class ValidateTransferPage(BNPPage):
......@@ -566,27 +587,45 @@ def handle_response(self, transfer):
class Transaction(FrenchTransaction):
PATTERNS = [(re.compile(u'^(?P<category>CHEQUE)(?P<text>.*)'), FrenchTransaction.TYPE_CHECK),
(re.compile('^(?P<category>FACTURE CARTE) DU (?P<dd>\d{2})(?P<mm>\d{2})(?P<yy>\d{2}) (?P<text>.*?)( CA?R?T?E? ?\d*X*\d*)?$'),
FrenchTransaction.TYPE_CARD),
(re.compile('^(?P<category>(PRELEVEMENT|TELEREGLEMENT|TIP)) (?P<text>.*)'),
FrenchTransaction.TYPE_ORDER),
(re.compile('^(?P<category>PRLV( EUROPEEN)? SEPA) (?P<text>.*?)( MDT/.*?)?( ECH/\d+)?( ID .*)?$'),
FrenchTransaction.TYPE_ORDER),
(re.compile('^(?P<category>ECHEANCEPRET)(?P<text>.*)'), FrenchTransaction.TYPE_LOAN_PAYMENT),
(re.compile('^(?P<category>RETRAIT DAB) (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{2})( (?P<HH>\d+)H(?P<MM>\d+))?( \d+)? (?P<text>.*)'),
FrenchTransaction.TYPE_WITHDRAWAL),
(re.compile('^(?P<category>VIR(EMEN)?T? (RECU |FAVEUR )?(TIERS )?)\w+ \d+/\d+ \d+H\d+ \w+ (?P<text>.*)$'),
FrenchTransaction.TYPE_TRANSFER),
(re.compile('^(?P<category>VIR(EMEN)?T? (EUROPEEN )?(SEPA )?(RECU |FAVEUR |EMIS )?(TIERS )?)(/FRM |/DE |/MOTIF |/BEN )?(?P<text>.*?)(/.+)?$'),
FrenchTransaction.TYPE_TRANSFER),
(re.compile('^(?P<category>REMBOURST) CB DU (?P<dd>\d{2})(?P<mm>\d{2})(?P<yy>\d{2}) (?P<text>.*)'),
FrenchTransaction.TYPE_PAYBACK),
(re.compile('^(?P<category>REMBOURST)(?P<text>.*)'), FrenchTransaction.TYPE_PAYBACK),
(re.compile('^(?P<category>COMMISSIONS)(?P<text>.*)'), FrenchTransaction.TYPE_BANK),
(re.compile('^(?P<text>(?P<category>REMUNERATION).*)'), FrenchTransaction.TYPE_BANK),
(re.compile('^(?P<category>REMISE CHEQUES)(?P<text>.*)'), FrenchTransaction.TYPE_DEPOSIT),
]
PATTERNS = [
(re.compile(u'^(?P<category>CHEQUE)(?P<text>.*)'), FrenchTransaction.TYPE_CHECK),
(
re.compile(
'^(?P<category>FACTURE CARTE) DU (?P<dd>\d{2})(?P<mm>\d{2})(?P<yy>\d{2}) (?P<text>.*?)( CA?R?T?E? ?\d*X*\d*)?$'
),
FrenchTransaction.TYPE_CARD
),
(re.compile('^(?P<category>(PRELEVEMENT|TELEREGLEMENT|TIP)) (?P<text>.*)'), FrenchTransaction.TYPE_ORDER),
(
re.compile('^(?P<category>PRLV( EUROPEEN)? SEPA) (?P<text>.*?)( MDT/.*?)?( ECH/\d+)?( ID .*)?$'),
FrenchTransaction.TYPE_ORDER
),
(re.compile('^(?P<category>ECHEANCEPRET)(?P<text>.*)'), FrenchTransaction.TYPE_LOAN_PAYMENT),
(
re.compile(
'^(?P<category>RETRAIT DAB) (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{2})( (?P<HH>\d+)H(?P<MM>\d+))?( \d+)? (?P<text>.*)'
),
FrenchTransaction.TYPE_WITHDRAWAL
),
(
re.compile('^(?P<category>VIR(EMEN)?T? (RECU |FAVEUR )?(TIERS )?)\w+ \d+/\d+ \d+H\d+ \w+ (?P<text>.*)$'),
FrenchTransaction.TYPE_TRANSFER
),
(
re.compile(
'^(?P<category>VIR(EMEN)?T? (EUROPEEN )?(SEPA )?(RECU |FAVEUR |EMIS )?(TIERS )?)(/FRM |/DE |/MOTIF |/BEN )?(?P<text>.*?)(/.+)?$'
),
FrenchTransaction.TYPE_TRANSFER
),
(
re.compile('^(?P<category>REMBOURST) CB DU (?P<dd>\d{2})(?P<mm>\d{2})(?P<yy>\d{2}) (?P<text>.*)'),
FrenchTransaction.TYPE_PAYBACK
),
(re.compile('^(?P<category>REMBOURST)(?P<text>.*)'), FrenchTransaction.TYPE_PAYBACK),
(re.compile('^(?P<category>COMMISSIONS)(?P<text>.*)'), FrenchTransaction.TYPE_BANK),
(re.compile('^(?P<text>(?P<category>REMUNERATION).*)'), FrenchTransaction.TYPE_BANK),
(re.compile('^(?P<category>REMISE CHEQUES)(?P<text>.*)'), FrenchTransaction.TYPE_DEPOSIT),
]
class HistoryPage(BNPPage):
......@@ -631,11 +670,15 @@ def iter_history(self):
'category': op.get('categorie'),
'amount': self.one('montant.montant', op),
})
tr.parse(raw=CleanText().filter(op.get('libelleOperation')),
date=parse_french_date(op.get('dateOperation')),
vdate=parse_french_date(self.one('montant.valueDate', op)))
raw_is_summary = re.match(r'FACTURE CARTE SELON RELEVE DU\b|FACTURE CARTE CARTE AFFAIRES \d{4}X{8}\d{4} SUIVANT\b', tr.raw)
tr.parse(
raw=CleanText().filter(op.get('libelleOperation')),
date=parse_french_date(op.get('dateOperation')),
vdate=parse_french_date(self.one('montant.valueDate', op))
)
raw_is_summary = re.match(
r'FACTURE CARTE SELON RELEVE DU\b|FACTURE CARTE CARTE AFFAIRES \d{4}X{8}\d{4} SUIVANT\b', tr.raw
)
if tr.type == Transaction.TYPE_CARD and raw_is_summary:
tr.type = Transaction.TYPE_CARD_SUMMARY
tr.deleted = True
......@@ -658,8 +701,7 @@ def iter_coming(self):
parse_with_patterns(tr.raw, tr, Transaction.PATTERNS)
if tr.type == Transaction.TYPE_CARD:
tr.type = self.browser.card_to_transaction_type.get(op.get('keyCarte'),
Transaction.TYPE_DEFERRED_CARD)
tr.type = self.browser.card_to_transaction_type.get(op.get('keyCarte'), Transaction.TYPE_DEFERRED_CARD)
yield tr
......@@ -708,11 +750,13 @@ def iter_history(self, coming):
'type': Transaction.TYPE_BANK,
'_state': op.get('statut'),
'amount': op.get('montantNet'),
})
})
tr.parse(date=parse_french_date(op.get('dateSaisie')),
vdate = parse_french_date(op.get('dateEffet')) if op.get('dateEffet') else None,
raw='%s %s' % (op.get('libelleMouvement'), op.get('canalSaisie') or ''))
tr.parse(
date=parse_french_date(op.get('dateSaisie')),
vdate=parse_french_date(op.get('dateEffet')) if op.get('dateEffet') else None,
raw='%s %s' % (op.get('libelleMouvement'), op.get('canalSaisie') or '')
)
tr._op = op
if not tr.amount:
......@@ -732,10 +776,10 @@ class NatioVieProPage(BNPPage):
# This form is required to go to the capitalisation contracts page.
def get_params(self):
params = {
'app': 'BNPNET',
'hageGroup': 'consultationBnpnet',
'init': 'true',
'multiInit': 'false',
'app': 'BNPNET',
'hageGroup': 'consultationBnpnet',
'init': 'true',
'multiInit': 'false',
}
params['a0'] = self.doc['data']['nationVieProInfos']['a0']
# The number of "p" keys may vary (p0, p1, p2 ... up to p13 or more)
......@@ -754,13 +798,13 @@ def has_contracts(self):
# To be completed with other account labels and types seen on the "Assurance Vie" space:
ACCOUNT_TYPES = {
'BNP Paribas Multiplacements': Account.TYPE_LIFE_INSURANCE,
'BNP Paribas Multihorizons': Account.TYPE_LIFE_INSURANCE,
'BNP Paribas Libertéa Privilège': Account.TYPE_LIFE_INSURANCE,
'BNP Paribas Avenir Retraite': Account.TYPE_LIFE_INSURANCE,
'BNP Paribas Multiciel Privilège': Account.TYPE_CAPITALISATION,
'Plan Epargne Retraite Particulier': Account.TYPE_PERP,
"Plan d'Épargne Retraite des Particuliers": Account.TYPE_PERP,
'BNP Paribas Multiplacements': Account.TYPE_LIFE_INSURANCE,
'BNP Paribas Multihorizons': Account.TYPE_LIFE_INSURANCE,
'BNP Paribas Libertéa Privilège': Account.TYPE_LIFE_INSURANCE,
'BNP Paribas Avenir Retraite': Account.TYPE_LIFE_INSURANCE,
'BNP Paribas Multiciel Privilège': Account.TYPE_CAPITALISATION,
'Plan Epargne Retraite Particulier': Account.TYPE_PERP,
"Plan d'Épargne Retraite des Particuliers": Account.TYPE_PERP,
}
@method
......@@ -812,7 +856,9 @@ def get_params(self, account):
# The investments vdate is out of the investments table and is the same for all investments:
def get_vdate(self):
return parse_french_date(CleanText('//table[tr[th[text()[contains(., "Date de valorisation")]]]]/tr[2]/td[2]')(self.doc))
return parse_french_date(
CleanText('//table[tr[th[text()[contains(., "Date de valorisation")]]]]/tr[2]/td[2]')(self.doc)
)
@method
class iter_investments(TableElement):
......@@ -832,6 +878,7 @@ class item(ItemElement):
obj_label = CleanText(TableCell('label'))
obj_valuation = CleanDecimal(TableCell('valuation'), replace_dots=True)
obj_portfolio_share = Eval(lambda x: x / 100, CleanDecimal(TableCell('portfolio_share'), replace_dots=True))
# There is no "unitvalue" information available on the "Assurances Vie" space.
def obj_quantity(self):
......@@ -889,7 +936,7 @@ def iter_history(self):
'amount': op.get('movementAmount'),
'date': datetime.fromtimestamp(op.get('movementDate') / 1000),
'label': op.get('operationName'),
})
})
tr.investments = []
inv = Investment()
......@@ -916,7 +963,9 @@ class item(ItemElement):
obj_mobile = CleanText(Dict('data/mobile'), replace=[(' ', '')])
obj_fax = CleanText(Dict('data/fax'), replace=[(' ', '')])
obj_agency = Dict('data/agence')
obj_address = Format('%s %s %s', Dict('data/adresseAgence'), Dict('data/codePostalAgence'), Dict('data/villeAgence'))
obj_address = Format(
'%s %s %s', Dict('data/adresseAgence'), Dict('data/codePostalAgence'), Dict('data/villeAgence')
)
class AddRecipPage(BNPPage):
......
......@@ -17,7 +17,6 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from weboob.tools.test import BackendTest
from random import choice
......
......@@ -22,8 +22,8 @@
from weboob.browser import AbstractBrowser, LoginBrowser, URL, need_login
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded
from .pages import (
LoginPage, ProfilePage, ErrorPage, AccountPage, InvestmentPage,
TermPage, UnexpectedPage, HistoryPage,
LoginPage, ProfilePage, ErrorPage, AccountPage, AccountSwitchPage,
InvestmentPage, TermPage, UnexpectedPage, HistoryPage,
)
......@@ -43,6 +43,7 @@ class VisiogoBrowser(LoginBrowser):
error_page2 = URL(r'https://authentication.bnpparibas.com/Error\?Code=500', UnexpectedPage)
term_page = URL(r'/Home/TermsOfUseApproval', TermPage)
account_page = URL(r'/GlobalView/Synthesis', AccountPage)
account_switch = URL(r'/Contract/_ChangeAffiliation', AccountSwitchPage)
investment_page = URL(r'/Saving/Details', InvestmentPage)
profile_page = URL(r'/en/Profile/EditContactDetails', ProfilePage)
history_page = URL(r'/en/Operation/History', HistoryPage)
......@@ -78,22 +79,35 @@ def iter_accounts(self):
# in order to handle their investments properly
if len(accounts_list) > 1:
self.multi_accounts = True
# In order to access an account's detail, we must determine its index
# in the list, but the order on investment_page is not the same as on
# account_page, so we must get the account indices on investment_page.
self.investment_page.go()
for account in accounts_list:
account._index = self.page.get_account_index(account._sublabel)
return accounts_list
def iter_investment(self, account):
if self.multi_accounts:
# No connection with multi-accounts containing investments yet
raise NotImplementedError()
# Access details of the right account
self.account_switch.go(
data={'index': account._index}
)
self.investment_page.go()
return self.page.iter_investments()
def iter_history(self, account):
if self.multi_accounts:
# Access details of the right account
self.account_switch.go(
data={'index': account._index}
)
self.history_page.go()
return self.page.iter_history()
def iter_pocket(self, account):
raise NotImplementedError()
def get_profile(self):
self.profile_page.go()
return self.page.get_profile()
def iter_history(self, account):
self.history_page.stay_or_go()
return self.page.iter_history(account)