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