diff --git a/modules/ing/api/__init__.py b/modules/ing/api/__init__.py
index 64d0d9c9de305f1a7a5e6711b05d15ce90850a56..ff74abf0bea532e1e697c0df904217c1e4105ec6 100644
--- a/modules/ing/api/__init__.py
+++ b/modules/ing/api/__init__.py
@@ -5,19 +5,25 @@
# 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
+# it under the terms of the GNU Lesser 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.
+# GNU Lesser General Public License for more details.
#
-# You should have received a copy of the GNU Affero General Public License
+# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .login import LoginPage
+from .accounts_page import AccountsPage, HistoryPage, ComingPage
+from .transfer_page import DebitAccountsPage, CreditAccountsPage
+from .profile_page import ProfilePage
-__all__ = ['LoginPage', ]
+__all__ = ['LoginPage', 'AccountsPage',
+ 'HistoryPage', 'ComingPage',
+ 'DebitAccountsPage', 'CreditAccountsPage',
+ 'ProfilePage']
diff --git a/modules/ing/api/accounts_page.py b/modules/ing/api/accounts_page.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd91e8225b7806ef8bbebbd1126b99adb7dbdafb
--- /dev/null
+++ b/modules/ing/api/accounts_page.py
@@ -0,0 +1,75 @@
+# -*- 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 Lesser 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+from weboob.browser.pages import LoggedPage, JsonPage
+from weboob.browser.elements import method, DictElement, ItemElement
+from weboob.browser.filters.json import Dict
+from weboob.browser.filters.standard import (
+ CleanText, CleanDecimal, Date, Eval,
+)
+from weboob.capabilities.bank import Account, Transaction
+
+
+class AccountsPage(LoggedPage, JsonPage):
+ @method
+ class iter_accounts(DictElement):
+ item_xpath = 'accounts'
+
+ class item(ItemElement):
+ klass = Account
+
+ obj_id = Dict('uid')
+ obj_label = Dict('type/label')
+ obj_number = CleanText(Dict('label'), replace=[(' ', '')])
+
+ def obj_balance(self):
+ if not Dict('hasPositiveBalance')(self):
+ return -CleanDecimal(Dict('ledgerBalance'))(self)
+ return CleanDecimal(Dict('ledgerBalance'))(self)
+
+
+class HistoryPage(LoggedPage, JsonPage):
+ def is_empty_page(self):
+ return len(self.doc) == 0
+
+ @method
+ class iter_history(DictElement):
+ class item(ItemElement):
+ klass = Transaction
+
+ obj_id = Eval(str, Dict('id'))
+ obj_label = CleanText(Dict('detail'))
+ obj_amount = CleanDecimal(Dict('amount'))
+ obj_date = Date(Dict('effectiveDate'))
+
+
+class ComingPage(LoggedPage, JsonPage):
+ @method
+ class iter_coming(DictElement):
+ item_xpath = 'futureOperations'
+
+ class item(ItemElement):
+ klass = Transaction
+
+ obj_label = Dict('label')
+ obj_amount = CleanDecimal(Dict('amount'))
+ obj_date = Date(Dict('effectiveDate'))
+ obj_vdate = Date(Dict('operationDate'))
diff --git a/modules/ing/api/login.py b/modules/ing/api/login.py
index eb1f8995ed6616973c6fd24aa41cbb6c7fe129d4..ac99f42a91d0131269b9ceb95d81c08c2e91ce29 100644
--- a/modules/ing/api/login.py
+++ b/modules/ing/api/login.py
@@ -5,16 +5,16 @@
# 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
+# it under the terms of the GNU Lesser 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.
+# GNU Lesser General Public License for more details.
#
-# You should have received a copy of the GNU Affero General Public License
+# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from io import BytesIO
@@ -105,7 +105,3 @@ def get_password_coord(self, img, password):
password_radom_coords = vk.password_tiles_coord(password)
# pin positions (website side) start at 1, our positions start at 0
return [password_radom_coords[index-1] for index in pin_position]
-
- def get_error(self):
- if 'error' in self.doc:
- return (Dict('error/code')(self.doc), Dict('error/message')(self.doc))
diff --git a/modules/ing/api/profile_page.py b/modules/ing/api/profile_page.py
new file mode 100644
index 0000000000000000000000000000000000000000..4472f386bfcd925a77af8b2b74ad6eee59ac9a9c
--- /dev/null
+++ b/modules/ing/api/profile_page.py
@@ -0,0 +1,49 @@
+# -*- 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 Lesser 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+from weboob.browser.pages import LoggedPage, JsonPage
+from weboob.browser.filters.json import Dict
+from weboob.browser.filters.standard import CleanText, Format
+from weboob.browser.elements import ItemElement, method
+from weboob.capabilities.profile import Profile
+from weboob.capabilities.base import NotAvailable
+
+
+class ProfilePage(LoggedPage, JsonPage):
+ @method
+ class get_profile(ItemElement):
+ klass = Profile
+
+ obj_name = Format('%s %s', Dict('name/firstName'), Dict('name/lastName'))
+ obj_country = Dict('mailingAddress/country')
+ obj_phone = Dict('phones/0/number', default=NotAvailable)
+ obj_email = Dict('emailAddress')
+
+ obj_address = CleanText(Format(
+ '%s %s %s %s %s %s %s',
+ Dict('mailingAddress/address1'),
+ Dict('mailingAddress/address2'),
+ Dict('mailingAddress/address3'),
+ Dict('mailingAddress/address4'),
+ Dict('mailingAddress/city'),
+ Dict('mailingAddress/postCode'),
+ Dict('mailingAddress/country')
+ ))
diff --git a/modules/ing/api/transfer_page.py b/modules/ing/api/transfer_page.py
new file mode 100644
index 0000000000000000000000000000000000000000..47ee658daf5fa12d12c90a761f254bf2b290fddb
--- /dev/null
+++ b/modules/ing/api/transfer_page.py
@@ -0,0 +1,61 @@
+# -*- 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 Lesser 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+from datetime import datetime
+
+from weboob.browser.pages import LoggedPage, JsonPage
+from weboob.browser.elements import method, DictElement, ItemElement
+from weboob.browser.filters.json import Dict
+from weboob.browser.filters.standard import (
+ Env, Field,
+)
+from weboob.capabilities.bank import Recipient
+
+
+class DebitAccountsPage(LoggedPage, JsonPage):
+ def get_debit_accounts_uid(self):
+ return [Dict('uid')(recipient) for recipient in self.doc]
+
+
+class CreditAccountsPage(LoggedPage, JsonPage):
+ @method
+ class iter_recipients(DictElement):
+ class item(ItemElement):
+ def condition(self):
+ return Dict('uid')(self) != Env('acc_uid')(self)
+
+ klass = Recipient
+
+ def obj__is_internal_recipient(self):
+ return bool(Dict('ledgerBalance', default=None)(self))
+
+ obj_id = Dict('uid')
+ obj_enabled_at = datetime.now().replace(microsecond=0)
+
+ def obj_label(self):
+ if Field('_is_internal_recipient')(self):
+ return Dict('type/label')(self)
+ return Dict('owner')(self)
+
+ def obj_category(self):
+ if Field('_is_internal_recipient')(self):
+ return 'Interne'
+ return 'Externe'
diff --git a/modules/ing/api_browser.py b/modules/ing/api_browser.py
index 91a2416a15b7b80474e65e2dcf98d18b816bec78..315d0120d430c37c7fc57600a2b4f08f974402a7 100644
--- a/modules/ing/api_browser.py
+++ b/modules/ing/api_browser.py
@@ -5,32 +5,74 @@
# 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
+# it under the terms of the GNU Lesser 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.
+# GNU Lesser General Public License for more details.
#
-# You should have received a copy of the GNU Affero General Public License
+# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
-from __future__ import unicode_literals
+from __future__ import unicode_literals
+import json
from collections import OrderedDict
+from functools import wraps
-from weboob.browser import LoginBrowser, URL, need_login
-from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
+from weboob.browser import LoginBrowser, URL
+from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded
from weboob.browser.exceptions import ClientError
-from .api import LoginPage
+from .api import (
+ LoginPage, AccountsPage, HistoryPage, ComingPage,
+ DebitAccountsPage, CreditAccountsPage,
+ ProfilePage,
+)
from .web import StopPage, ActionNeededPage
from .browser import IngBrowser
+def need_login(func):
+ @wraps(func)
+ def inner(self, *args, **kwargs):
+ browser_conditions = (
+ getattr(self, 'logged', False),
+ getattr(self.old_browser, 'logged', False)
+ )
+ page_conditions = (
+ (getattr(self, 'page', False) and self.page.logged),
+ (getattr(self.old_browser, 'page', False) and self.old_browser.page.logged)
+ )
+ if not any(browser_conditions) and not any(page_conditions):
+ self.do_login()
+
+ if self.logger.settings.get('export_session'):
+ self.logger.debug('logged in with session: %s', json.dumps(self.export_session()))
+ return func(self, *args, **kwargs)
+
+ return inner
+
+
+def need_to_be_on_website(website):
+ assert website in ('web', 'api')
+
+ def decorator(func):
+ @wraps(func)
+ def wrapper(self, *args, **kwargs):
+ if website == 'web' and self.is_on_new_website:
+ self.redirect_to_old_browser()
+ elif website == 'api' and not self.is_on_new_website:
+ self.redirect_to_api_browser()
+ return func(self, *args, **kwargs)
+ return wrapper
+ return decorator
+
+
class IngAPIBrowser(LoginBrowser):
BASEURL = 'https://m.ing.fr'
@@ -45,12 +87,36 @@ class IngAPIBrowser(LoginBrowser):
actioneeded = URL(r'https://secure.ing.fr/general\?command=displayTRAlertMessage',
r'https://secure.ing.fr/protected/pages/common/eco1/moveMoneyForbidden.jsf', ActionNeededPage)
+ # bank
+ history = URL(r'/secure/api-v1/accounts/(?P.*)/transactions/after/(?P\d+)/limit/50', HistoryPage)
+ coming = URL(r'/secure/api-v1/accounts/(?P.*)/futureOperations', ComingPage)
+ accounts = URL(r'/secure/api-v1/accounts', AccountsPage)
+
+ # transfer
+ credit_accounts = URL(r'/secure/api-v1/transfers/debitAccounts/(?P.*)/creditAccounts', CreditAccountsPage)
+ debit_accounts = URL(r'/secure/api-v1/transfers/debitAccounts', DebitAccountsPage)
+
+ # profile
+ informations = URL(r'/secure/api-v1/customer/info', ProfilePage)
+
def __init__(self, *args, **kwargs):
self.birthday = kwargs.pop('birthday')
super(IngAPIBrowser, self).__init__(*args, **kwargs)
self.old_browser = IngBrowser(*args, **kwargs)
+ def handle_login_error(self, r):
+ error_page = r.response.json()
+ assert 'error' in error_page, "Something went wrong in login"
+ error = error_page['error']
+
+ if error['code'] == 'AUTHENTICATION.INVALID_PIN_CODE':
+ raise BrowserIncorrectPassword(error['message'])
+ elif error['code'] == 'AUTHENTICATION.ACCOUNT_INACTIVE':
+ raise ActionNeeded(error['message'])
+ assert error['code'] != 'INPUT_INVALID', error['message']
+ raise BrowserUnavailable(error['message'])
+
def do_login(self):
assert self.password.isdigit()
assert self.birthday.isdigit()
@@ -63,7 +129,10 @@ def do_login(self):
('birthDate', self.birthday),
('cif', self.username),
])
- self.login.go(json=data)
+ try:
+ self.login.go(json=data)
+ except ClientError as e:
+ self.handle_login_error(e)
data = '{"keyPadSize":{"width":3800,"height":1520},"mode":""}'
self.keypad.go(data=data, headers={'Content-Type': 'application/json'})
@@ -75,28 +144,24 @@ def do_login(self):
try:
self.pin_page.go(json=data, headers={'Referer': 'https://m.ing.fr/secure/login/pin'})
- except ClientError:
- # handle error later
- pass
-
- error = self.page.get_error()
- if not self.page.is_logged:
- assert error
- if error[0] == 'AUTHENTICATION.INVALID_PIN_CODE':
- raise BrowserIncorrectPassword(error[1])
- assert error[0] != 'INPUT_INVALID', '%s' % error[1]
- raise BrowserUnavailable(error[1])
+ except ClientError as e:
+ self.handle_login_error(e)
self.auth_token = self.page.response.headers['Ingdf-Auth-Token']
self.session.headers['Ingdf-Auth-Token'] = self.auth_token
self.session.cookies['ingdfAuthToken'] = self.auth_token
- # Go on old website because new website is not stable
- self.redirect_to_old_browser()
+ # to be on logged page, to avoid relogin
+ self.accounts.go()
+
+ def deinit(self):
+ self.old_browser.deinit()
+ super(IngAPIBrowser, self).deinit()
def redirect_to_old_browser(self):
+ self.logger.info('Go on old website')
token = self.location(
- 'https://m.ing.fr/secure/api-v1/sso/exit?context={"originatingApplication":"SECUREUI"}&targetSystem=INTERNET',
+ '/secure/api-v1/sso/exit?context={"originatingApplication":"SECUREUI"}&targetSystem=INTERNET',
method='POST'
).content
data = {
@@ -110,41 +175,136 @@ def redirect_to_old_browser(self):
self.location('https://secure.ing.fr', data=data, headers={'Referer': 'https://secure.ing.fr'})
self.old_browser.session.cookies.update(self.session.cookies)
- def deinit(self):
- super(IngAPIBrowser, self).deinit()
- self.old_browser.deinit()
+ def redirect_to_api_browser(self):
+ self.logger.info('Go on new website')
+ self.old_browser.redirect_to_api_browser()
+ self.session.cookies.update(self.old_browser.session.cookies)
+ self.accounts.go()
- @need_login
- def get_accounts_list(self):
+ @property
+ def is_on_new_website(self):
+ return self.BASEURL in self.url
+
+ ############# CapBank #############
+ @need_to_be_on_website('web')
+ def get_web_accounts(self):
+ """iter accounts on old website"""
return self.old_browser.get_accounts_list()
+ @need_to_be_on_website('api')
+ def get_api_accounts(self):
+ """iter accounts on new website"""
+ self.accounts.stay_or_go()
+ return self.page.iter_accounts()
+
@need_login
- def get_account(self, _id):
- raise BrowserUnavailable()
+ def iter_matching_accounts(self):
+ """Do accounts matching for old and new website"""
+
+ api_accounts = [acc for acc in self.get_api_accounts()]
+
+ # go on old website because new website have only cheking and card account information
+ for web_acc in self.get_web_accounts():
+ for api_acc in api_accounts:
+ if web_acc.id[-4:] == api_acc.number[-4:]:
+ web_acc._uid = api_acc.id
+ yield web_acc
+ break
+ else:
+ assert False, 'There should be same account in web and api website'
+
+ @need_to_be_on_website('web')
+ def get_web_history(self, account):
+ """iter history on old website"""
+ return self.old_browser.get_history(account)
+
+ @need_to_be_on_website('api')
+ def get_api_history(self, account):
+ """iter history on new website"""
+
+ # first request transaction id is 0 to get the most recent transaction
+ first_transaction_id = 0
+ request_number_security = 0
+
+ while request_number_security < 200:
+ request_number_security += 1
+
+ # first_transaction_id is 0 for the first request, then
+ # it will decreasing after first_transaction_id become the last transaction id of the list
+ self.history.go(account_uid=account._uid, tr_id=first_transaction_id)
+ if self.page.is_empty_page():
+ # empty page means that there are no more transactions
+ break
+
+ for tr in self.page.iter_history():
+ # transaction id is decreasing
+ first_transaction_id = int(tr.id)
+ yield tr
+
+ # like website, add 1 to the last transaction id of the list to get next transactions page
+ first_transaction_id +=1
@need_login
- def get_coming(self, account):
- raise BrowserUnavailable()
+ def iter_history(self, account):
+ """History switch"""
+
+ if account.type not in (account.TYPE_CHECKING, ):
+ return self.get_web_history(account)
+ else:
+ return self.get_api_history(account)
+
+ @need_to_be_on_website('web')
+ def get_web_coming(self, account):
+ """iter coming on old website"""
+ return self.old_browser.get_coming(account)
+
+ @need_to_be_on_website('api')
+ def get_api_coming(self, account):
+ """iter coming on new website"""
+ self.coming.go(account_uid=account._uid)
+ return self.page.iter_coming()
@need_login
- def get_history(self, account):
- raise BrowserUnavailable()
+ def iter_coming(self, account):
+ """Incoming switch"""
+
+ if account.type not in (account.TYPE_CHECKING, ):
+ return self.get_web_coming(account)
+ else:
+ return self.get_api_coming(account)
+
+ ############# CapWealth #############
+ @need_login
+ def get_investments(self, account):
+ if account.type not in (account.TYPE_MARKET, account.TYPE_LIFE_INSURANCE, account.TYPE_PEA):
+ return []
+
+ # can't use `need_to_be_on_website`
+ # because if return without iter invest on old website,
+ # previous page is not handled by new website
+ if self.is_on_new_website:
+ self.redirect_to_old_browser()
+ return self.old_browser.get_investments(account)
+ ############# CapTransfer #############
@need_login
+ @need_to_be_on_website('api')
def iter_recipients(self, account):
- raise BrowserUnavailable()
+ self.debit_accounts.go()
+ if account._uid not in self.page.get_debit_accounts_uid():
+ return
+
+ self.credit_accounts.go(account_uid=account._uid)
+ for recipient in self.page.iter_recipients(acc_uid=account._uid):
+ yield recipient
@need_login
def init_transfer(self, account, recipient, transfer):
- raise BrowserUnavailable()
+ raise NotImplementedError()
@need_login
def execute_transfer(self, transfer):
- raise BrowserUnavailable()
-
- @need_login
- def get_investments(self, account):
- raise BrowserUnavailable()
+ raise NotImplementedError()
############# CapDocument #############
@need_login
@@ -161,4 +321,5 @@ def download_document(self, bill):
############# CapProfile #############
@need_login
def get_profile(self):
- raise BrowserUnavailable()
+ self.informations.go()
+ return self.page.get_profile()
diff --git a/modules/ing/browser.py b/modules/ing/browser.py
index ef94f812918eec9839a1121c415c027ccc9084ad..2b39d9532a8c133c8cef55e60ad788d802b2ceee 100644
--- a/modules/ing/browser.py
+++ b/modules/ing/browser.py
@@ -30,13 +30,13 @@
from weboob.browser.exceptions import ServerError
from weboob.capabilities.bank import Account, AccountNotFound
from weboob.capabilities.base import find_object, NotAvailable
-from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from .web import (
AccountsList, NetissimaPage, TitrePage,
- TitreHistory, TransferPage, BillsPage, StopPage, TitreDetails,
+ TitreHistory, BillsPage, StopPage, TitreDetails,
TitreValuePage, ASVHistory, ASVInvest, DetailFondsPage, IbanPage,
ActionNeededPage, ReturnPage, ProfilePage, LoanTokenPage, LoanDetailPage,
+ ApiRedirectionPage,
)
__all__ = ['IngBrowser']
@@ -86,6 +86,7 @@ class IngBrowser(LoginBrowser):
ibanpage = URL(r'/protected/pages/common/rib/initialRib.jsf', IbanPage)
loantokenpage = URL(r'general\?command=goToConsumerLoanCommand&redirectUrl=account-details', LoanTokenPage)
loandetailpage = URL(r'https://subscribe.ing.fr/consumerloan/consumerloan-v1/consumer/details', LoanDetailPage)
+
# CapBank-Market
netissima = URL(r'/data/asv/fiches-fonds/fonds-netissima.html', NetissimaPage)
starttitre = URL(r'/general\?command=goToAccount&zone=COMPTE', TitrePage)
@@ -97,12 +98,16 @@ class IngBrowser(LoginBrowser):
r'https://ingdirectvie.ing.fr/b2b2c/epargne/CoeDetMvt', ASVHistory)
asv_invest = URL(r'https://ingdirectvie.ing.fr/b2b2c/epargne/CoeDetCon', ASVInvest)
detailfonds = URL(r'https://ingdirectvie.ing.fr/b2b2c/fonds/PerDesFac\?codeFonds=(.*)', DetailFondsPage)
+
+
# CapDocument
billpage = URL(r'/protected/pages/common/estatement/eStatement.jsf', BillsPage)
+
# CapProfile
profile = URL(r'/protected/pages/common/profil/(?P\w+).jsf', ProfilePage)
- transfer = URL(r'/protected/pages/common/virement/index.jsf', TransferPage)
+ # New website redirection
+ api_redirection_url = URL(r'/general\?command=goToSecureUICommand&redirectUrl=transfers', ApiRedirectionPage)
__states__ = ['where']
@@ -126,6 +131,11 @@ def __init__(self, *args, **kwargs):
def do_login(self):
pass
+ def redirect_to_api_browser(self):
+ # get form to be redirected on transfer page
+ self.api_redirection_url.go()
+ self.page.go_new_website()
+
@need_login
def set_multispace(self):
self.where = 'start'
@@ -151,6 +161,8 @@ def change_space(self, space):
self.page.change_space(space)
self.current_space = space
+ else:
+ self.accountspage.go()
def is_same_space(self, a, b):
return (
@@ -307,15 +319,16 @@ def go_account_page(self, account):
def get_coming(self, account):
self.change_space(account._space)
- if account.type != Account.TYPE_CHECKING and\
- account.type != Account.TYPE_SAVINGS:
- raise NotImplementedError()
+ # checking accounts are handled on api website
+ if account.type != Account.TYPE_SAVINGS:
+ return []
+
account = self.get_account(account.id, space=account._space)
self.go_account_page(account)
jid = self.page.get_history_jid()
if jid is None:
self.logger.info('There is no history for this account')
- return
+ return []
return self.page.get_coming()
@need_login
@@ -328,31 +341,23 @@ def get_history(self, account):
yield result
return
- elif account.type != Account.TYPE_CHECKING and\
- account.type != Account.TYPE_SAVINGS:
- raise NotImplementedError()
+ # checking accounts are handled on api website
+ elif account.type != Account.TYPE_SAVINGS:
+ return
+
account = self.get_account(account.id, space=account._space)
self.go_account_page(account)
jid = self.page.get_history_jid()
- only_deferred_cb = self.only_deferred_cards.get(account._id)
if jid is None:
self.logger.info('There is no history for this account')
return
- if account.type == Account.TYPE_CHECKING:
- history_function = AccountsList.get_transactions_cc
- index = -1 # disable the index. It works without it on CC
- else:
- history_function = AccountsList.get_transactions_others
- index = 0
+ index = 0
hashlist = set()
while True:
i = index
- for transaction in history_function(self.page, index=index):
- if only_deferred_cb and transaction.type == FrenchTransaction.TYPE_CARD:
- transaction.type = FrenchTransaction.TYPE_DEFERRED_CARD
-
+ for transaction in AccountsList.get_transactions_others(self.page, index=index):
transaction.id = hashlib.md5(transaction._hash).hexdigest()
while transaction.id in hashlist:
transaction.id = hashlib.md5((transaction.id + "1").encode('ascii')).hexdigest()
@@ -373,33 +378,6 @@ def get_history(self, account):
}
self.accountspage.go(data=data)
- @need_login
- @start_with_main_site
- def iter_recipients(self, account):
- self.change_space(account._space)
-
- self.transfer.go()
- if not self.page.able_to_transfer(account):
- return iter([])
-
- self.page.go_to_recipient_selection(account)
- return self.page.get_recipients(origin=account)
-
- @need_login
- @start_with_main_site
- def init_transfer(self, account, recipient, transfer):
- self.change_space(account._space)
-
- self.transfer.go()
- self.page.do_transfer(account, recipient, transfer)
- return self.page.recap(account, recipient, transfer)
-
- @need_login
- @start_with_main_site
- def execute_transfer(self, transfer):
- self.page.confirm(self.password)
- return transfer
-
def go_on_asv_detail(self, account, link):
try:
if self.page.asv_is_other:
@@ -484,6 +462,10 @@ def get_investments(self, account):
inv.portfolio_share = shares[inv.label]
yield inv
+ # return on old ing website
+ assert self.asv_invest.is_here(), "Should be on ING generali website"
+ self.lifeback.go()
+
def get_history_titre(self, account):
self.go_investments(account)
@@ -571,11 +553,3 @@ def download_document(self, bill):
self._go_to_subscription(self.cache['subscriptions'][subid])
self.page.go_to_year(bill._year)
return self.page.download_document(bill)
-
- ############# CapProfile #############
- @start_with_main_site
- @need_login
- def get_profile(self):
- profile = self.profile.go(page='coordonnees').get_profile()
- self.profile.go(page='infosperso').update_profile(profile)
- return profile
diff --git a/modules/ing/module.py b/modules/ing/module.py
index b6d7f24f2f7bc7f01853e03662b34c3803bb3236..6d85a17edf1b6fe5ec332faad1c0f130e8665f75 100644
--- a/modules/ing/module.py
+++ b/modules/ing/module.py
@@ -19,11 +19,7 @@
from __future__ import unicode_literals
-import re
-from datetime import timedelta
-from decimal import Decimal
-
-from weboob.capabilities.bank import CapBankWealth, CapBankTransfer, Account, AccountNotFound, RecipientNotFound
+from weboob.capabilities.bank import CapBankWealth, CapBankTransfer, Account, AccountNotFound
from weboob.capabilities.bill import (
CapDocument, Bill, Subscription,
SubscriptionNotFound, DocumentNotFound, DocumentTypes,
@@ -72,55 +68,42 @@ def iter_resources(self, objs, split_path):
self._restrict_level(split_path)
return self.iter_subscription()
+ ############# CapBank #############
def iter_accounts(self):
- return self.browser.get_accounts_list()
+ return self.browser.iter_matching_accounts()
def get_account(self, _id):
- return self.browser.get_account(_id)
+ return find_object(self.iter_accounts(), id=_id, error=AccountNotFound)
def iter_history(self, account):
if not isinstance(account, Account):
account = self.get_account(account)
- return self.browser.get_history(account)
+ return self.browser.iter_history(account)
- def iter_transfer_recipients(self, account):
+ def iter_coming(self, account):
if not isinstance(account, Account):
account = self.get_account(account)
- return self.browser.iter_recipients(account)
-
- def init_transfer(self, transfer, **params):
- self.logger.info('Going to do a new transfer')
- 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:
- account = find_object(self.iter_accounts(), id=transfer.account_id, error=AccountNotFound)
-
- if transfer.recipient_iban:
- recipient = find_object(self.iter_transfer_recipients(account.id), iban=transfer.recipient_iban, error=RecipientNotFound)
- else:
- recipient = find_object(self.iter_transfer_recipients(account.id), id=transfer.recipient_id, error=RecipientNotFound)
-
- transfer.amount = Decimal(transfer.amount).quantize(Decimal('.01'))
-
- return self.browser.init_transfer(account, recipient, transfer)
-
- def execute_transfer(self, transfer, **params):
- return self.browser.execute_transfer(transfer)
-
- def transfer_check_exec_date(self, old_exec_date, new_exec_date):
- return old_exec_date <= new_exec_date <= old_exec_date + timedelta(days=4)
+ return self.browser.iter_coming(account)
+ ############# CapWealth #############
def iter_investment(self, account):
if not isinstance(account, Account):
account = self.get_account(account)
return self.browser.get_investments(account)
- def iter_coming(self, account):
+ ############# CapTransfer #############
+ def iter_transfer_recipients(self, account):
if not isinstance(account, Account):
account = self.get_account(account)
- return self.browser.get_coming(account)
+ return self.browser.iter_recipients(account)
+
+ def init_transfer(self, transfer, **params):
+ raise NotImplementedError()
+
+ def execute_transfer(self, transfer, **params):
+ raise NotImplementedError()
+ ############# CapDocument #############
def iter_subscription(self):
return self.browser.get_subscriptions()
@@ -142,5 +125,6 @@ def download_document(self, bill):
return self.browser.download_document(bill).content
+ ############# CapProfile #############
def get_profile(self):
return self.browser.get_profile()
diff --git a/modules/ing/web/__init__.py b/modules/ing/web/__init__.py
index 2bf74acc928c84b823ab5eb2b8f77ce309ac1b8c..bc5d0692208e3f1f4afcab2ac78267123ffa1b14 100644
--- a/modules/ing/web/__init__.py
+++ b/modules/ing/web/__init__.py
@@ -22,8 +22,7 @@
AccountsList, TitreDetails, ASVInvest, DetailFondsPage, IbanPage,
ProfilePage, LoanTokenPage, LoanDetailPage,
)
-from .login import StopPage, ActionNeededPage, ReturnPage
-from .transfer import TransferPage
+from .login import StopPage, ActionNeededPage, ReturnPage, ApiRedirectionPage
from .bills import BillsPage
from .titre import NetissimaPage, TitrePage, TitreHistory, TitreValuePage, ASVHistory
@@ -33,8 +32,8 @@ class AccountPrelevement(AccountsList):
__all__ = ['AccountsList', 'NetissimaPage','TitreDetails',
- 'AccountPrelevement', 'TransferPage',
+ 'AccountPrelevement',
'BillsPage', 'StopPage', 'TitrePage', 'TitreHistory', 'IbanPage',
'TitreValuePage', 'ASVHistory', 'ASVInvest','DetailFondsPage',
'ActionNeededPage', 'ReturnPage', 'ProfilePage', 'LoanTokenPage',
- 'LoanDetailPage']
+ 'LoanDetailPage', 'ApiRedirectionPage']
diff --git a/modules/ing/web/login.py b/modules/ing/web/login.py
index a0ef10a7475351f8888a7ee0cdeb2c7167e031d8..4d0234009d71dc338a74cf2c5eff2c8eb09e6d2d 100644
--- a/modules/ing/web/login.py
+++ b/modules/ing/web/login.py
@@ -5,16 +5,16 @@
# 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
+# it under the terms of the GNU Lesser 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.
+# GNU Lesser General Public License for more details.
#
-# You should have received a copy of the GNU Affero General Public License
+# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
@@ -45,3 +45,10 @@ class StopPage(HTMLPage):
class ReturnPage(LoggedPage, HTMLPage):
def on_load(self):
self.get_form(name='retoursso').submit()
+
+
+class ApiRedirectionPage(LoggedPage, HTMLPage):
+ def go_new_website(self):
+ form = self.get_form(name="module")
+ form.request.headers['Referer'] = "https://secure.ing.fr"
+ form.submit()
diff --git a/modules/ing/web/transfer.py b/modules/ing/web/transfer.py
deleted file mode 100644
index 33b52c0c26186dbca562491b886c6e72b5001c0c..0000000000000000000000000000000000000000
--- a/modules/ing/web/transfer.py
+++ /dev/null
@@ -1,225 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright(C) 2009-2014 Romain Bignon, Florent Fourcot
-#
-# This file is part of a weboob module.
-#
-# This weboob module is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This weboob module 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 Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this weboob module. If not, see .
-
-from datetime import datetime
-
-from weboob.capabilities.bank import Recipient, Transfer, TransferInvalidAmount
-from weboob.capabilities import NotAvailable
-from weboob.browser.pages import HTMLPage, LoggedPage
-from weboob.browser.elements import ListElement, ItemElement, method
-from weboob.browser.filters.standard import CleanText, CleanDecimal, Env
-from weboob.browser.filters.html import Attr
-from weboob.tools.capabilities.bank.transactions import FrenchTransaction
-from weboob.tools.capabilities.bank.iban import is_iban_valid
-from weboob.tools.date import parse_french_date
-
-from ..api.login import INGVirtKeyboard
-
-
-class MyRecipient(ItemElement):
- klass = Recipient
-
- def obj_enabled_at(self):
- return datetime.now().replace(microsecond=0)
-
-
-class TransferPage(LoggedPage, HTMLPage):
- def able_to_transfer(self, origin):
- return [div for div in self.doc.xpath('//div[@id="internalAccounts"]//div[@data-acct-number]')
- if Attr('.', 'data-acct-number')(div) in origin.id and 'disabled' not in div.attrib['class']]
-
- @method
- class get_recipients(ListElement):
- class ExternalRecipients(ListElement):
- item_xpath = '//tr[@id="externalAccountsIsotopeWrapper"]//div[not(has-class("disabled")) and @data-acct-number]'
-
- class item(MyRecipient):
-
- obj_id = Attr('.', 'data-acct-number')
- obj_label = CleanText('.//span[@class="title"]')
- obj_category = u'Externe'
- obj_bank_name = CleanText(Attr('.//span[@class="bankname"]', 'title'))
-
- def obj_iban(self):
- return self.obj_id(self) if is_iban_valid(self.obj_id(self)) else NotAvailable
-
- class InternalRecipients(ListElement):
- item_xpath = '//div[@id="internalAccounts"]//td/div[not(has-class("disabled"))]'
-
- class item(MyRecipient):
-
- obj_category = u'Interne'
- obj_currency = FrenchTransaction.Currency('.//span[@class="solde"]/label')
- obj_id = Env('id')
- obj_label = Env('label')
- obj_iban = Env('iban')
- obj_bank_name = u'ING'
-
- def parse(self, el):
- _id = Attr('.', 'data-acct-number')(self)
- 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
- self.env['label'] = account.label
- self.env['iban'] = account.iban
-
- def get_origin_account_id(self, origin):
- return [Attr('.', 'data-acct-number')(div) for div in self.doc.xpath('//div[@id="internalAccounts"]//div[@data-acct-number]')
- if Attr('.', 'data-acct-number')(div) in origin.id][0]
-
- def update_origin_account_estimated_balance(self, origin):
- for div in self.doc.xpath('//div[@id="internalAccounts"]//div[@data-acct-number]'):
- if Attr('.', 'data-acct-number')(div) in origin.id:
- origin._estimated_balance = CleanDecimal('.//span[@class="solde"]', replace_dots=True, default=NotAvailable)(div)
-
- def update_origin_account_label(self, origin):
- # 'Compte Courant Joint' can become 'Compte Courant'
- # search for the account label used to do transfer
- for div in self.doc.xpath('//div[@id="internalAccounts"]//div[@data-acct-number]'):
- if Attr('.', 'data-acct-number')(div) in origin.id:
- origin._account_label = CleanText('.//span[@class="title"]', default=NotAvailable)(div)
-
- def update_recipient_account_label(self, recipient):
- # 'Compte Courant Joint' can become 'Compte Courant'
- # search for the account label used to do transfer
- for div in self.doc.xpath('//div[@id="internalAccounts"]//div[@data-acct-number]'):
- if Attr('.', 'data-acct-number')(div) in recipient.id:
- recipient._account_label = CleanText('.//span[@class="title"]', default=NotAvailable)(div)
-
- def get_transfer_form(self, txt):
- form = self.get_form(xpath='//form[script[contains(text(), "%s")]]' % txt)
- form['AJAXREQUEST'] = '_viewRoot'
- form['AJAX:EVENTS_COUNT'] = '1'
- param = Attr('//form[script[contains(text(), "RenderTransferDetail")]]/script[contains(text(), "%s")]' % txt, 'id')(self.doc)
- form[param] = param
- return form
-
- def go_to_recipient_selection(self, origin):
- form = self.get_transfer_form('SetScreenStep')
- form['screenStep'] = '1'
- form.submit()
-
- # update account estimated balance and account label for the origin account check on summary page
- self.update_origin_account_estimated_balance(origin)
- self.update_origin_account_label(origin)
- # Select debit account
- form = self.get_transfer_form('SetDebitAccount')
- form['selectedDebitAccountNumber'] = self.get_origin_account_id(origin)
- form.submit()
-
- # Render available accounts
- form = self.get_transfer_form('ReRenderAccountList')
- form.submit()
-
- def do_transfer(self, account, recipient, transfer):
- self.go_to_recipient_selection(account)
-
- # update recipient account label for the recipient check on summary page
- self.update_recipient_account_label(recipient)
- form = self.get_transfer_form('SetScreenStep')
- form['screenStep'] = '2'
- form.submit()
-
- form = self.get_transfer_form('SetCreditAccount')
- # intern id is like XX-XXXXXXXXXXXX but in request, only the second part is necessary
- form['selectedCreditAccountNumber'] = recipient.id.split('-')[-1]
- form.submit()
-
- form = self.get_transfer_form('ReRenderAccountList')
- form.submit()
-
- form = self.get_transfer_form('ReRenderStepTwo')
- form.submit()
-
- form = self.get_form()
- keys = [k for k in form if '_link_hidden' in k or 'j_idcl' in k]
- for k in keys:
- form.pop(k)
- form['AJAXREQUEST'] = "_viewRoot"
- form['AJAX:EVENTS_COUNT'] = "1"
- form["transfer_form:transferAmount"] = str(transfer.amount)
- form["transfer_form:validateDoTransfer"] = "needed"
- form['transfer_form:transferMotive'] = transfer.label
- form['transfer_form:ipt-date-exec'] = transfer.exec_date.strftime('%d/%m/%Y')
- form['transfer_form'] = 'transfer_form'
- form['transfer_form:valide'] = 'transfer_form:valide'
- form.submit()
-
- def continue_transfer(self, password):
- form = self.get_form(xpath='//form[h2[contains(text(), "Saisissez votre code secret pour valider la transaction")]]')
- vk = INGVirtKeyboard(self)
- for k in form:
- if 'mrltransfer' in k:
- form[k] = vk.get_coordinates(password)
- form.submit()
-
- def confirm(self, password):
- vk = INGVirtKeyboard(self)
-
- form = self.get_form(xpath='//form[h2[contains(text(), "Saisissez votre code secret pour valider la transaction")]]')
- for elem in form:
- if "_link_hidden_" in elem or "j_idcl" in elem:
- form.pop(elem)
-
- form['AJAXREQUEST'] = '_viewRoot'
- form['%s:mrgtransfer' % form.name] = '%s:mrgtransfer' % form.name
- form['%s:mrltransfer' % form.name] = vk.get_coordinates(password)
- form.submit()
-
- def recap(self, origin, recipient, transfer):
- error = CleanText(u'//div[@id="transfer_form:moveMoneyDetailsBody"]//span[@class="error"]', default=None)(self.doc) or \
- CleanText(u'//p[contains(text(), "Nous sommes désolés. Le solde de votre compte ne doit pas être inférieur au montant de votre découvert autorisé. Veuillez saisir un montant inférieur.")]', default=None)(self.doc)
- if error:
- raise TransferInvalidAmount(message=error)
-
- t = Transfer()
- t.label = transfer.label
- t.amount = CleanDecimal('//div[@id="transferSummary"]/div[@id="virementLabel"]\
- //label[@class="digits positive"]', replace_dots=True)(self.doc)
- t.currency = FrenchTransaction.Currency('//div[@id="transferSummary"]/div[@id="virementLabel"]\
- //label[@class="digits positive"]')(self.doc)
-
- # check origin account balance
- origin_balance = CleanDecimal('//div[@id="transferSummary"]/div[has-class("debit")]\
- //label[has-class("digits")]', replace_dots=True)(self.doc)
- assert (origin_balance == origin.balance) or (origin_balance == origin._estimated_balance)
- t.account_balance = origin.balance
-
- # check account label for origin and recipient
- origin_label = CleanText('//div[@id="transferSummary"]/div[has-class("debit")]\
- //span[@class="title"]')(self.doc)
- recipient_label = CleanText('//div[@id="transferSummary"]/div[has-class("credit")]\
- //span[@class="title"]')(self.doc)
- assert (origin.label == origin_label) or (origin._account_label == origin_label)
- assert (recipient.label == recipient_label) or (recipient._account_label == recipient_label)
-
- t.account_label = origin.label
- t.account_iban = origin.iban
- t.account_id = origin.id
-
- t.recipient_label = recipient.label
- t.recipient_iban = recipient.iban
- t.recipient_id = recipient.id
-
- t.exec_date = parse_french_date(CleanText('//p[has-class("exec-date")]', children=False,
- replace=[('le', ''), (u'exécuté', ''), ('demain', ''), ('(', ''), (')', ''),
- ("aujourd'hui", '')])(self.doc)).date()
-
- return t