Skip to content
Commits on Source (7)
......@@ -23,7 +23,7 @@
import re
from weboob.browser.pages import LoggedPage, JsonPage
from weboob.browser.pages import LoggedPage, JsonPage, HTMLPage
from weboob.browser.elements import method, DictElement, ItemElement
from weboob.browser.filters.json import Dict
from weboob.browser.filters.standard import (
......@@ -216,3 +216,16 @@ class fill_account_coming(ItemElement):
class AccountInfoPage(LoggedPage, JsonPage):
def get_iban(self):
return self.doc['iban'].replace(' ', '')
class RedirectOldPage(LoggedPage, HTMLPage):
# We land here when going away from bourse website.
# bourse.ing.fr -> secure.ing.fr (here) -> to whatever the form points
def on_load(self):
self.get_form(name='module').submit()
class BourseLandingPage(LoggedPage, HTMLPage):
# when going to bourse space, we land on this page, which is logged
# that's all what this class is for: know we're logged
pass
# -*- coding: utf-8 -*-
# Copyright(C) 2009-2014 Florent Fourcot, Romain Bignon
# Copyright(C) 2020 budget-insight
#
# This file is part of weboob.
#
......@@ -21,37 +21,39 @@
from __future__ import unicode_literals
from weboob.exceptions import ActionNeeded
from weboob.browser.pages import HTMLPage, LoggedPage
from weboob.browser.filters.standard import CleanText
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 (
Format, BrowserURL, Env,
)
from weboob.capabilities.bill import Document, DocumentTypes
class ActionNeededPage(HTMLPage):
def on_load(self):
if self.doc.xpath('//form//h1[1][contains(text(), "Accusé de reception du chéquier")]'):
form = self.get_form(name='Alert')
form['command'] = 'validateAlertMessage'
form['radioValide_1_2_40003039944'] = 'Non'
form.submit()
elif self.doc.xpath('//p[@class="cddErrorMessage"]'):
error_message = CleanText('//p[@class="cddErrorMessage"]')(self.doc)
# TODO python2 handles unicode exceptions badly, fix when passing to python3
raise ActionNeeded(error_message.encode('ascii', 'replace'))
else:
raise ActionNeeded(CleanText('//form//h1[1]')(self.doc))
class StatementsPage(LoggedPage, JsonPage):
@method
class iter_documents(DictElement):
item_xpath = '*/statementsMetadata'
class StopPage(HTMLPage):
pass
class item(ItemElement):
klass = Document
obj_type = DocumentTypes.STATEMENT
obj_format = 'pdf'
class ReturnPage(LoggedPage, HTMLPage):
def on_load(self):
self.get_form(name='retoursso').submit()
obj_id = Format('%s.%s', Env('subscription'), Dict('id'))
def obj_date(self):
# does the "day" key actually exist?
return datetime.date(self.el['year'], self.el['month'], self.el.get('day', 1))
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()
obj_label = Format('%s', obj_date)
obj_url = BrowserURL(
'statement_dl',
account_uid=Env('subscription'),
year=Dict('year'),
month=Dict('month')
)
......@@ -23,8 +23,10 @@
from io import BytesIO
from weboob.browser.pages import JsonPage
from weboob.browser.pages import JsonPage, HTMLPage
from weboob.browser.filters.json import Dict
from weboob.browser.filters.standard import CleanText
from weboob.exceptions import ActionNeeded
from .transfer_page import TransferINGVirtKeyboard
......@@ -54,3 +56,22 @@ def get_password_coord(self, img, password):
def get_keypad_url(self):
return Dict('keyPadUrl')(self.doc)
class ActionNeededPage(HTMLPage):
def on_load(self):
if self.doc.xpath('//form//h1[1][contains(text(), "Accusé de reception du chéquier")]'):
form = self.get_form(name='Alert')
form['command'] = 'validateAlertMessage'
form['radioValide_1_2_40003039944'] = 'Non'
form.submit()
elif self.doc.xpath('//p[@class="cddErrorMessage"]'):
error_message = CleanText('//p[@class="cddErrorMessage"]')(self.doc)
# TODO python2 handles unicode exceptions badly, fix when passing to python3
raise ActionNeeded(error_message.encode('ascii', 'replace'))
else:
raise ActionNeeded(CleanText('//form//h1[1]')(self.doc))
class StopPage(HTMLPage):
pass
......@@ -21,7 +21,7 @@
from __future__ import unicode_literals
from weboob.browser.pages import LoggedPage, JsonPage
from weboob.browser.pages import LoggedPage, JsonPage, HTMLPage
from weboob.browser.filters.json import Dict
from weboob.browser.filters.standard import CleanText, Format
from weboob.browser.elements import ItemElement, method
......@@ -49,3 +49,10 @@ class get_profile(ItemElement):
Dict('mailingAddress/postCode'),
Dict('mailingAddress/country')
))
class UselessProfilePage(LoggedPage, HTMLPage):
# We land here after going away from bourse website.
# We are redirected to this, we can't choose to land on accounts list, only here.
# This page is just for staying logged.
pass
......@@ -49,16 +49,16 @@ class TransferINGVirtKeyboard(SimpleVirtualKeyboard):
}
symbols = {
'0': 'e3e62175aa1a5ef8dc67639194caa880',
'1': '80245727e4e5f123fd64bbb1fa80dde0',
'2': '62cfc40429652190c996db741ac90830',
'3': 'bb2f87d32f688679745fe95ac31b80fd',
'4': 'a4b5e16c64817deb12ca6311cb98e59a',
'5': '56a8f3b4f068f9e2f93c4daa3a53dc17',
'6': 'b50f7e4a375153b9f6b029dc9b0a7e64',
'7': 'd52320c62c6157d0cadbb7a186153628',
'8': 'dd3fb25fc7f0765610b0ffe47da85330',
'9': 'ca55399a5b36da3fedcd1dbb73d72a2f',
'0': ('178b23cc890c258bd5594665f2df31c5', '9229a326c21320282f604c2e2d026c2b'),
'1': 'd4a68e94d6267de3fa0c426aba0b8dc6',
'2': '4a17f9e4088ef7d1a499a80bd7b56718',
'3': 'f7f6364000813aec31e3d2df0dde8194',
'4': '4f3161c7dacb0f8981dc8ad8321b7d22',
'5': '6210d53a580d26fdbbf1e5ba62dc5f3d',
'6': 'f748b7a25f12cc8b87deb22e33eff4a5',
'7': '04a0f83158133ab5eeb69163f08c918f',
'8': '859b2ad7dd70f429c761db4d625e3b57',
'9': 'f249afdd16cf98e441e71d7a9dae5359',
}
# Clean image
......
This diff is collapsed.
# -*- coding: utf-8 -*-
# Copyright(C) 2012-2020 Budget Insight
#
# 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 <http://www.gnu.org/licenses/>.
# flake8: compatible
from __future__ import unicode_literals
from weboob.browser import AbstractBrowser, URL, need_login
from .boursedirect_pages import MarketOrdersPage, MarketOrderDetailsPage
class BourseDirectBrowser(AbstractBrowser):
PARENT = 'boursedirect'
BASEURL = 'https://bourse.ing.fr'
market_orders = URL(r'/priv/compte.php\?ong=7', MarketOrdersPage)
market_orders_details = URL(r'/priv/detailOrdre.php', MarketOrderDetailsPage)
@need_login
def iter_market_orders(self, account):
# 'Bourse Direct' space of ING still uses the old navigation for Market Orders
if account.type not in (account.TYPE_PEA, account.TYPE_MARKET):
return
self.pre_invests.go(nc=account._select)
self.market_orders.go()
for order in self.page.iter_market_orders():
if order.url:
self.location(order.url)
if self.market_orders_details.is_here():
self.page.fill_market_order(obj=order)
else:
self.logger.warning('Unknown details URL for market order %s.', order.label)
else:
self.logger.warning('Market order %s has no details URL.', order.label)
yield order
# -*- coding: utf-8 -*-
# Copyright(C) 2009-2011 Romain Bignon, Florent Fourcot
# Copyright(C) 2012-2020 Budget Insight
#
# This file is part of a weboob module.
#
......@@ -19,28 +19,18 @@
# flake8: compatible
from .accounts_list import (
AccountsList, TitreDetails, ASVInvest, DetailFondsPage, IbanPage,
ProfilePage, LoanTokenPage, LoanDetailPage,
)
from .login import StopPage, ActionNeededPage, ReturnPage, ApiRedirectionPage
from .bills import BillsPage
from .titre import (
NetissimaPage, TitrePage, TitreHistory, TitreValuePage, ASVHistory,
MarketOrdersPage, MarketOrderDetailsPage,
)
class AccountPrelevement(AccountsList):
pass
__all__ = [
'AccountsList', 'NetissimaPage', 'TitreDetails',
'AccountPrelevement', 'BillsPage', 'StopPage',
'TitrePage', 'TitreHistory', 'IbanPage',
'TitreValuePage', 'ASVHistory', 'ASVInvest', 'DetailFondsPage',
'ActionNeededPage', 'ReturnPage', 'ProfilePage', 'LoanTokenPage',
'LoanDetailPage', 'ApiRedirectionPage', 'MarketOrdersPage',
'MarketOrderDetailsPage',
]
from __future__ import unicode_literals
from weboob.browser.pages import AbstractPage
class MarketOrdersPage(AbstractPage):
PARENT = 'boursedirect'
PARENT_URL = 'market_orders'
BROWSER_ATTR = 'package.browser.BoursedirectBrowser'
class MarketOrderDetailsPage(AbstractPage):
PARENT = 'boursedirect'
PARENT_URL = 'market_orders_details'
BROWSER_ATTR = 'package.browser.BoursedirectBrowser'
This diff is collapsed.
......@@ -28,7 +28,7 @@
from weboob.capabilities.bank import CapBankTransferAddRecipient, Account, AccountNotFound, RecipientNotFound
from weboob.capabilities.wealth import CapBankWealth
from weboob.capabilities.bill import (
CapDocument, Bill, Subscription,
CapDocument, Document, Subscription,
SubscriptionNotFound, DocumentNotFound, DocumentTypes,
)
from weboob.capabilities.profile import CapProfile
......@@ -61,7 +61,8 @@ def create_default_browser(self):
return self.create_browser(
self.config['login'].get(),
self.config['password'].get(),
birthday=self.config['birthday'].get()
birthday=self.config['birthday'].get(),
weboob=self.weboob,
)
def iter_resources(self, objs, split_path):
......@@ -74,7 +75,10 @@ def iter_resources(self, objs, split_path):
############# CapBank #############
def iter_accounts(self):
return self.browser.iter_matching_accounts()
ignored_types = (Account.TYPE_LOAN,)
for account in self.browser.iter_accounts():
if account.type not in ignored_types:
yield account
def get_account(self, _id):
return find_object(self.iter_accounts(), id=_id, error=AccountNotFound)
......@@ -167,7 +171,7 @@ def get_subscription(self, _id):
return find_object(self.browser.get_subscriptions(), id=_id, error=SubscriptionNotFound)
def get_document(self, _id):
subscription = self.get_subscription(_id.split('-')[0])
subscription = self.get_subscription(_id.split('.')[0])
return find_object(self.browser.get_documents(subscription), id=_id, error=DocumentNotFound)
def iter_documents(self, subscription):
......@@ -175,11 +179,11 @@ def iter_documents(self, subscription):
subscription = self.get_subscription(subscription)
return self.browser.get_documents(subscription)
def download_document(self, bill):
if not isinstance(bill, Bill):
bill = self.get_document(bill)
def download_document(self, document):
if not isinstance(document, Document):
document = self.get_document(document)
return self.browser.download_document(bill).content
return self.browser.download_document(document)
############# CapProfile #############
def get_profile(self):
......
# -*- coding: utf-8 -*-
# Copyright(C) 2009-2014 Florent Fourcot, Romain Bignon
#
# 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 <http://www.gnu.org/licenses/>.
# flake8: compatible
from __future__ import unicode_literals
from datetime import date, timedelta
import datetime
import re
from weboob.capabilities.bank import Account, Loan
from weboob.capabilities.wealth import Investment
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.profile import Person
from weboob.browser.pages import HTMLPage, LoggedPage, JsonPage
from weboob.browser.elements import ListElement, TableElement, ItemElement, method, DataError
from weboob.browser.filters.standard import (
CleanText, CleanDecimal, Filter, Field, MultiFilter, Date,
Lower, Async, AsyncLoad, Format, Env,
Regexp,
)
from weboob.browser.filters.json import Dict
from weboob.browser.filters.html import Attr, Link, TableCell
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
class Transaction(FrenchTransaction):
PATTERNS = [
(
re.compile(r'^retrait dab (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{4}) (?P<text>.*)'),
FrenchTransaction.TYPE_WITHDRAWAL,
),
# Withdrawal in foreign currencies will look like "retrait 123 currency"
(re.compile(r'^retrait (?P<text>.*)'), FrenchTransaction.TYPE_WITHDRAWAL),
(
re.compile(r'^paiement par carte (?P<dd>\d{2})/(?P<mm>\d{2})/(?P<yy>\d{4}) (?P<text>.*)'),
FrenchTransaction.TYPE_CARD,
),
(re.compile(r'^virement (sepa )?(emis vers|recu|emis)? (?P<text>.*)'), FrenchTransaction.TYPE_TRANSFER),
(re.compile(r'^remise cheque(?P<text>.*)'), FrenchTransaction.TYPE_DEPOSIT),
(re.compile(r'^cheque (?P<text>.*)'), FrenchTransaction.TYPE_CHECK),
(re.compile(r'^prelevement (?P<text>.*)'), FrenchTransaction.TYPE_ORDER),
(re.compile(r'^prlv sepa (?P<text>.*)'), FrenchTransaction.TYPE_ORDER),
(re.compile(r'^prélèvement sepa en faveur de (?P<text>.*)'), FrenchTransaction.TYPE_ORDER),
(re.compile(r'^commission sur (?P<text>.*)'), FrenchTransaction.TYPE_BANK),
]
class AddPref(MultiFilter):
prefixes = {
u'Courant': u'CC-', u'Livret A': 'LA-', u'Orange': 'LEO-',
u'Durable': u'LDD-', u"Titres": 'TITRE-', u'PEA': u'PEA-',
}
def filter(self, values):
el, label = values
for key, pref in self.prefixes.items():
if key in label:
return pref + el
return el
class AddType(Filter):
types = {
'Courant': Account.TYPE_CHECKING,
'Livret A': Account.TYPE_SAVINGS,
'Orange': Account.TYPE_SAVINGS,
'Durable': Account.TYPE_SAVINGS,
'Titres': Account.TYPE_MARKET,
'PEA': Account.TYPE_PEA,
'Direct Vie': Account.TYPE_LIFE_INSURANCE,
'Assurance Vie': Account.TYPE_LIFE_INSURANCE,
'Crédit Immobilier': Account.TYPE_LOAN,
'Prêt Personnel': Account.TYPE_LOAN,
}
def filter(self, label):
for key, acc_type in self.types.items():
if key in label:
return acc_type
return Account.TYPE_UNKNOWN
class PreHashmd5(MultiFilter):
def filter(self, values):
concat = ''
for value in values:
if type(value) is datetime.date:
concat += value.strftime('%d/%m/%Y')
else:
concat += u'%s' % value
return concat.encode('utf-8')
class INGDate(Date):
monthvalue = {
'janv.': '01', 'févr.': '02', 'mars': '03', 'avr.': '04',
'mai': '05', 'juin': '06', 'juil.': '07', 'août': '08',
'sept.': '09', 'oct.': '10', 'nov.': '11', 'déc.': '12',
}
def filter(self, txt):
if txt == 'hier':
return date.today() - timedelta(days=1)
elif txt == "aujourd'hui":
return date.today()
elif txt == 'demain':
return date.today() + timedelta(days=1)
frenchmonth = txt.split(' ')[1]
month = self.monthvalue[frenchmonth]
txt = txt.replace(' ', '')
txt = txt.replace(frenchmonth, '/%s/' % month)
return super(INGDate, self).filter(txt)
class INGCategory(Filter):
catvalue = {
'virt': "Virement", 'autre': "Autre",
'plvt': 'Prélèvement', 'cb_ret': u"Carte retrait",
'cb_ach': 'Carte achat', 'chq': 'Chèque',
'frais': 'Frais bancaire', 'sepaplvt': 'Prélèvement',
}
def filter(self, txt):
txt = txt.split('-')[0].lower()
try:
return self.catvalue[txt]
except KeyError:
return txt
class AccountsList(LoggedPage, HTMLPage):
i = 0
def has_error(self):
return len(self.doc.xpath('//div[has-class("alert-warning")]')) > 0
def has_link(self):
return len(self.doc.xpath('//a[contains(@href, "goTo")]'))
def get_card_list(self):
card_list = []
card_elements = self.doc.xpath('//div[has-class("ccinc_cards")]/div[has-class("accordion")]')
for card in card_elements:
card_properties = {}
# Regexp parse the text to extract the card number that may be in different formats
card_properties['number'] = Regexp(CleanText('.'), r'(\d+[\s|*]+\d+)', default=NotAvailable)(card)
debit_info = (CleanText('.//div[@class="debit-info"]', default='')(card))
is_deferred = u'Débit différé' in debit_info
is_immediate = u'Débit immédiat' in debit_info
if is_immediate:
card_properties['kind'] = self.browser.IMMEDIATE_CB
elif is_deferred:
card_properties['kind'] = self.browser.DEFERRED_CB
else:
raise DataError("Cannot tell if the card {} is deferred or immediate".format(card_properties['number']))
card_list.append(card_properties)
return card_list
@method
class get_list(ListElement):
item_xpath = '//div[@id="bloc-menu-comptes"]//a[@class="mainclic"]'
class item(ItemElement):
klass = Account
obj_currency = u'EUR'
obj_label = CleanText('./span[@class="title"]')
obj_id = AddPref(Field('_id'), Field('label'))
obj_type = AddType(Field('label'))
obj__jid = Attr('//input[@name="javax.faces.ViewState"]', 'value')
def obj_balance(self):
balance = CleanDecimal('./span[@class="solde"]/label', replace_dots=True)(self)
if Field('type')(self) == Account.TYPE_LOAN:
balance = -abs(balance)
return balance
def obj__id(self):
return CleanText('./span[@class="account-number"]')(self)
def condition(self):
# do not return accounts in application state
# they are not displayed on new website
return not CleanText('./span[@class="life-insurance-application"]')(self)
@method
class get_detailed_loans(ListElement):
item_xpath = '//div[@class="mainclic"]'
class item(ItemElement):
klass = Loan
obj_currency = u'EUR'
obj_label = CleanText('.//span[@class="title"]')
obj_id = AddPref(Field('_id'), Field('label'))
obj_type = AddType(Field('label'))
obj__jid = Attr('//input[@name="javax.faces.ViewState"]', 'value')
obj__id = CleanText('.//span[@class="account-number"]')
def obj_balance(self):
balance = CleanDecimal('.//div/span[@class="solde"]/label', replace_dots=True)(self)
return -abs(balance)
class generic_transactions(ListElement):
class item(ItemElement):
klass = Transaction
obj_id = None # will be overwrited by the browser
# we use lower for compatibility with the old website
obj_amount = CleanDecimal('.//td[starts-with(@class, "amount")]', replace_dots=True)
obj_date = INGDate(CleanText('.//td[@class="date"]'), dayfirst=True)
obj_rdate = Field('date')
obj__hash = PreHashmd5(Field('date'), Field('raw'), Field('amount'))
obj_category = INGCategory(Attr('.//td[@class="picto"]/span', 'class'))
def obj_raw(self):
return (
Transaction.Raw(Lower('.//td[@class="lbl"]'))(self)
or Format('%s %s', Field('date'), Field('amount'))(self)
)
def condition(self):
date_field = self.el.find('.//td[@class="date"]')
if date_field is None or 'À venir' in CleanText().filter(date_field):
return False
if 'index' in self.env and self.env['index'] > 0 and self.page.i < self.env['index']:
self.page.i += 1
return False
return True
@method
class get_coming(generic_transactions):
item_xpath = '//div[@class="transactions cc future"]//table'
@method
class get_transactions_cc(generic_transactions):
item_xpath = '//div[@class="temporaryTransactionList"]//table'
@method
class get_transactions_others(generic_transactions):
item_xpath = '//table'
def get_history_jid(self):
span = Attr('//*[starts-with(@id, "index:j_id")]', 'id')(self.doc)
jid = span.split(':')[1]
return jid
def get_asv_jid(self):
return self.doc.xpath('//input[@id="javax.faces.ViewState"]/@value')[0]
def islast(self):
havemore = self.doc.xpath('//*[has-class("show-more-transactions")]')
if len(havemore) == 0:
return True
nomore = self.doc.xpath('//*[has-class("no-more-transactions")]')
return len(nomore) > 0
@property
def is_asv(self):
span = self.doc.xpath('//span[@id="index:panelASV"]')
return len(span) > 0
@property
def asv_has_detail(self):
ap = self.doc.xpath('//a[@id="index:asvInclude:goToAsvPartner"] | //p[contains(text(), "Gestion Libre")]')
return len(ap) > 0
@property
def asv_is_other(self):
a = self.doc.xpath('//a[@id="index:asvInclude:goToAsvPartner"]')
return len(a) > 0
def submit(self):
form = self.get_form(name="follow_link")
form['follow_link:j_idcl'] = "follow_link:goToAsvPartner"
form.submit()
def get_multispace(self):
multispace = []
for a in self.doc.xpath('//a[contains(@id, "mainMenu")]'):
space = {}
name = CleanText('.')(a)
if 'Vos comptes' not in name:
space['name'] = name
else:
space['name'] = CleanText('//div[@class="print-content"]/h1')(a)
space['id'] = Regexp(Attr('.', 'id'), r'mainMenu:(.*)')(a)
space['form'] = Attr('.', 'onclick')(a)
space['is_active'] = 'active' in CleanText('./@class')(a)
multispace.append(space)
return multispace
def fillup_form(self, form, regexp, string):
# fill form depending on JS
link = re.search(regexp, string).group(1)
parts = link.split(',')
for p in parts:
f = p.split("':'")
form[f[0].replace("'", '')] = f[1].replace("'", '')
def change_space(self, space):
form = self.get_form(id='mainMenu')
self.fillup_form(form, r"':\{(.*)\}\s\}", space['form'])
form['AJAXREQUEST'] = '_viewRoot'
form.submit()
def load_space_page(self):
# The accounts page exists in two forms: with the spaces list and without
# When having the spaceless page, a form must be submit to access the space page
form = self.get_form(id='header-menu')
on_click = Attr('//a[@class="home"]', 'onclick')(self.doc)
self.fillup_form(form, r"\),\{(.*)\},'", on_click)
form.submit()
def is_multispace_page(self):
return self.doc.xpath('//a[contains(@name, "mainMenu")]')
class IbanPage(LoggedPage, HTMLPage):
def get_iban(self):
iban = CleanText('//tr[td[1]//text()="IBAN"]/td[2]')(self.doc).strip().replace(' ', '')
if not iban or 'null' in iban:
return NotAvailable
return iban
class LoanTokenPage(LoggedPage, HTMLPage):
def on_load(self):
form = self.get_form()
form.submit()
class LoanDetailPage(LoggedPage, JsonPage):
def getdetails(self, loan):
loan.total_amount = CleanDecimal(Dict('amount'))(self.doc)
loan.maturity_date = Date(Dict('loanEndDate'))(self.doc)
loan.duration = Dict('loanDuration')(self.doc)
loan.rate = CleanDecimal(Dict('variableInterestRate'))(self.doc) / 100
loan.nb_payments_left = Dict('remainingMonth')(self.doc)
loan.last_payment_date = Date(Dict('lastRefundDate'))(self.doc)
loan.next_payment_date = Date(Dict('nextRefundDate'))(self.doc)
loan.next_payment_amount = CleanDecimal(Dict('monthlyRefund'))(self.doc)
class TitreDetails(LoggedPage, HTMLPage):
def submit(self):
form = self.get_form()
form.submit()
class ASVInvest(LoggedPage, HTMLPage):
@method
class iter_investments(TableElement):
# Ignore the first line:
# <tr>
# <td colspan="5" class="enteteTableau metaEnteteTableau enteteTableauFirstCol metaEnteteTableauFirstCol">R&eacute;partition de
# l'investissement
# </td>
# <td colspan="3" class="enteteTableau metaEnteteTableau enteteTableauFirstCol metaEnteteTableauFirstCol">
# Plus/moins-values&nbsp;(**)
# </td>
# </tr>
# Then, there is the line of column heads.
# Ignore also information lines like that:
# <tr>
# <td colspan="8" class="liTableau" align="left">Gestion Pilotée
# <td>
# </tr>
item_xpath = '//table[@class="Tableau"]//tr[position()>2 and count(./td) >= 8]'
head_xpath = '//table[@class="Tableau"]//tr[position()=2]/td'
col_label = u'Support(s)'
col_vdate = re.compile('Date')
col_unitvalue = u'Valeur de part'
col_quantity = u'Nombre de parts'
col_valuation = u'Contre-valeur'
col_unitprice = [u'Prix revient', u'PAM']
col_diff = u'Montant'
col_diff_percent = u'%'
class item(ItemElement):
klass = Investment
# Euro funds links are like that:
# <td class="lpTableau lpTableauFirstCol"><a href="javascript:alert('Les performances de ce fond ne sont pas consultables.')" onclick="">Eurossima
# </a></td>
# So ignore them.
load_details = (
Link('.//td[1]//a') & Regexp(pattern='^((?!javascript:).*)', default=NotAvailable)
& AsyncLoad
)
def obj_code(self):
val = (
Async('details')
& CleanText('//td[@class="libelle-normal" and contains(.,"CodeISIN")]', default=NotAvailable)
)(self)
if val:
return val.split('CodeISIN : ')[1]
else:
return NotAvailable
def obj_diff_ratio(self):
diff = CleanDecimal(TableCell('diff_percent'), replace_dots=True, default=NotAvailable)(self)
if not diff:
return diff
return diff / 100
obj_label = CleanText(TableCell('label'))
obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True)
obj_unitvalue = CleanDecimal(TableCell('unitvalue'), replace_dots=True, default=NotAvailable)
obj_quantity = CleanDecimal(TableCell('quantity'), default=NotAvailable)
obj_valuation = CleanDecimal(TableCell('valuation'), replace_dots=True)
obj_unitprice = CleanDecimal(TableCell('unitprice', default=None), replace_dots=True, default=NotAvailable)
obj_diff = CleanDecimal(TableCell('diff'), replace_dots=True, default=NotAvailable)
class DetailFondsPage(LoggedPage, HTMLPage):
def get_isin_code(self):
return CleanText('//td[contains(text(), "CodeISIN")]/b', default=NotAvailable)(self.doc)
def MyInput(*args, **kwargs):
args = (u'//input[contains(@name, "%s")]' % args[0], 'value',)
kwargs.update(default=NotAvailable)
return Attr(*args, **kwargs)
def MySelect(*args, **kwargs):
args = (u'//select[contains(@name, "%s")]/option[@selected]' % args[0],)
kwargs.update(default=NotAvailable)
return CleanText(*args, **kwargs)
class ProfilePage(LoggedPage, HTMLPage):
@method
class get_profile(ItemElement):
klass = Person
obj_name = CleanText('//a[has-class("hme-pm")]/span[@title]')
obj_address = CleanText('//ul[@class="newPostAdress"]//dd[@class="withMessage"]')
obj_country = CleanText('//dt[label[contains(text(), "Pays")]]/following-sibling::dd')
obj_email = CleanText('//dt[contains(text(), "Email")]/following-sibling::dd/text()')
obj_phone = Env('phone')
obj_mobile = Env('mobile')
def parse(self, el):
pattern = '//dt[contains(text(), "%s")]/following-sibling::dd/label'
phone = CleanText(pattern % "professionnel")(self)
mobile = CleanText(pattern % "portable")(self)
self.env['phone'] = phone or mobile
self.env['mobile'] = mobile
@method
class update_profile(ItemElement):
obj_job = MyInput('category_pro')
obj_job_contract_type = MySelect('contractType')
obj_company_name = MyInput('category_empl')
obj_socioprofessional_category = MySelect('personal_form:csp')
def obj_job_activity_area(self):
return MySelect('business_sector')(self) or NotAvailable
def obj_main_bank(self):
return MySelect('present_bank')(self) or NotAvailable
def obj_housing_status(self):
return MySelect('housingType')(self) or NotAvailable
def obj_job_start_date(self):
month = MySelect('seniority_Month')(self)
year = MySelect('seniority_Year')(self)
if month and year:
return Date(default=NotAvailable).filter('01/%s/%s' % (month, year))
return NotAvailable
def obj_birth_date(self):
birth_date = self.page.browser.birthday
return Date().filter("%s/%s/%s" % (birth_date[2:4], birth_date[:2], birth_date[-4:]))
# -*- coding: utf-8 -*-
# Copyright(C) 2009-2014 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 <http://www.gnu.org/licenses/>.
# flake8: compatible
from __future__ import unicode_literals
from weboob.capabilities.bill import DocumentTypes, Bill, Subscription
from weboob.browser.pages import HTMLPage, LoggedPage, pagination, Form
from weboob.browser.filters.standard import Filter, CleanText, Format, Field, Env, Date
from weboob.browser.filters.html import Attr
from weboob.browser.elements import ListElement, ItemElement, method
from weboob.tools.date import parse_french_date
class FormId(Filter):
def filter(self, txt):
formid = txt.split("parameters")[1]
formid = formid.split("'")[2]
return formid
class MyForm(Form):
def submit(self, **kwargs):
"""
Submit the form but keep current browser.page
"""
kwargs.setdefault('data_encoding', self.page.encoding)
return self.page.browser.open(self.request, **kwargs)
class BillsPage(LoggedPage, HTMLPage):
def build_doc(self, data):
self.encoding = self.response.encoding
return super(BillsPage, self).build_doc(data)
@method
class iter_subscriptions(ListElement):
item_xpath = '//ul[@class="unstyled striped"]/li'
class item(ItemElement):
klass = Subscription
obj__javax = Attr("//form[@id='accountsel_form']/input[@name='javax.faces.ViewState']", 'value')
obj_id = Attr('input', 'value')
obj_label = CleanText('label')
obj__formid = FormId(Attr('input', 'onclick'))
def get_selected_year(self):
return int(CleanText('//form[@id="years_form"]//ul/li[@class="rich-list-item selected"]')(self.doc))
def go_to_year(self, year):
if year == self.get_selected_year():
return
ref = Attr('//form[@id="years_form"]//ul//a[text()="%s"]' % year, 'id')(self.doc)
self.FORM_CLASS = Form
form = self.get_form(name='years_form')
form.pop('years_form:j_idcl')
form.pop('years_form:_link_hidden_')
form['AJAXREQUEST'] = 'years_form:year_region'
form[ref] = ref
return form.submit()
def download_document(self, bill):
# MyForm do open, and not location to keep html page as self.page, to reduce number of request on this html page
self.FORM_CLASS = MyForm
_id = bill._localid.split("'")[3]
form = self.get_form(name='downpdf_form')
form['statements_form'] = 'statements_form'
form['statements_form:j_idcl'] = _id
return form.submit()
@pagination
@method
class iter_documents(ListElement):
flush_at_end = True
item_xpath = '//ul[@id="statements_form:statementsel"]/li'
def next_page(self):
lis = self.page.doc.xpath('//form[@name="years_form"]//li')
selected = False
ref = None
for li in lis:
if 'rich-list-item selected' in li.attrib['class']:
selected = True
else:
if selected:
ref = li.find('a').attrib['id']
break
if ref is None:
return
form = self.page.get_form(name='years_form')
form.pop('years_form:j_idcl')
form.pop('years_form:_link_hidden_')
form['AJAXREQUEST'] = 'years_form:year_region'
form[ref] = ref
return form.request
def flush(self):
for obj in reversed(self.objects.values()):
yield obj
class item(ItemElement):
klass = Bill
obj_label = CleanText('a[1]', replace=[(' ', '-')])
obj_id = Format('%s-%s', Env('subid'), Field('label'))
# Force first day of month as label is in form "janvier 2016"
obj_date = Format('1 %s', Field('label')) & Date(parse_func=parse_french_date)
obj_format = 'pdf'
obj_type = DocumentTypes.STATEMENT
obj__localid = Attr('a[2]', 'onclick')
def condition(self):
return (
not ('tous les relev' in CleanText('a[1]')(self.el))
and not ('annuel' in CleanText('a[1]')(self.el))
)
def obj__year(self):
return int(CleanText('a[1]')(self).split(' ')[1])
# -*- coding: utf-8 -*-
# Copyright(C) 2013-2014 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 <http://www.gnu.org/licenses/>.
# flake8: compatible
from __future__ import unicode_literals
import re
from decimal import Decimal
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.wealth import (
Investment, MarketOrder, MarketOrderDirection, MarketOrderType,
MarketOrderPayment,
)
from weboob.browser.pages import RawPage, HTMLPage, LoggedPage, pagination
from weboob.browser.elements import ListElement, TableElement, ItemElement, method
from weboob.browser.filters.standard import (
CleanDecimal, CleanText, Date, Regexp, Env, Map, Eval, Base, MapIn,
)
from weboob.browser.filters.html import Link, Attr, TableCell
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
from weboob.tools.capabilities.bank.investments import create_french_liquidity, IsinCode
class NetissimaPage(HTMLPage):
pass
class Transaction(FrenchTransaction):
pass
class TitreValuePage(LoggedPage, HTMLPage):
def get_isin(self):
# redirection page with a url which contains the ISIN
# example: https://bourse.ing.fr/fr/marche/euronext-paris/<label>-<ISIN>-UG-EUR-XPAR/seance?headerless=true
return IsinCode(Regexp(CleanText('//script'), r'-([A-Z]{2}[A-Z0-9]{9}\d)-'), default=NotAvailable)(self.doc)
class TitrePage(LoggedPage, RawPage):
def build_doc(self, content):
return content.decode(self.encoding)
def get_balance(self):
return CleanDecimal(default=None, replace_dots=True).filter(self.doc.split('{')[0])
def iter_investments(self, account):
# We did not get some html, but something like that (XX is a quantity, YY a price):
# "message='<total> &euro;{<total> &euro;{0,01 &euro;{<liquidity> &euro;{0,00{{05/17{{03/05/2017{11:06{-XX &euro;{710TI81000029397EUR{XX &euro;{XX &euro;{|OPHTHOTECH(NASDAQ)#cotationValeur.php?val=OPHT&amp;pl=11&amp;nc=2&amp;
# popup=2{6{E:ALO{PAR{{reel{695{380{ALSTOM REGROUPT#XX#YY,YY &euro;#YY,YY &euro;#1 YYY,YY &euro;#-YYY,YY &euro;#-42,42%#-0,98 %#42,42 %#|1|AXA#cotationValeur.php?val=E:CS&amp;pl=6&amp;nc=1&amp;
# popup=2{6{E:CS{PAR{{reel{695{380{AXA#XX#YY,YY &euro;#YY,YYY &euro;#YYY,YY &euro;#YY,YY &euro;#3,70%#42,42 %#42,42 %#|1|blablablab #cotationValeur.php?val=P:CODE&amp;pl=6&amp;nc=1&amp;
# [...]
data = self.browser.cache["investments_data"].get(account.id, self.doc)
lines = data.split("|1|")
message = lines[0]
if len(lines) > 1:
start = 1
lines[0] = lines[0].split("|")[1]
else:
start = 0
lines = data.split("popup=2")
lines.pop(0)
invests = []
for line in lines:
_id, _pl = None, None
columns = line.split('#')
if columns[1] != '':
_pl = columns[start].split('{')[1]
_id = columns[start].split('{')[2]
invest = Investment()
# If the link with the label and ISIN code is present we use it to fill the label and code.
# If not, the label can still be found in the first column of the row but the ISIN is unavailable.
invest.label = columns[start].split('{')[-1] or columns[0]
invest.code = _id or NotAvailable
if invest.code and ':' in invest.code:
invest.code = self.browser.titrevalue.open(val=invest.code, pl=_pl).get_isin()
# The code we got is not a real ISIN code.
if invest.code and not re.match(r'^[A-Z]{2}[\d]{10}$|^[A-Z]{2}[\d]{5}[A-Z]{1}[\d]{4}$', invest.code):
m = re.search(r'\{([A-Z]{2}[\d]{10})\{|\{([A-Z]{2}[\d]{5}[A-Z]{1}[\d]{4})\{', line)
if m:
invest.code = m.group(1) or m.group(2)
for x, attr in enumerate(['quantity', 'unitprice', 'unitvalue', 'valuation', 'diff'], 1):
currency = FrenchTransaction.Currency().filter(columns[start + x])
amount = CleanDecimal(default=NotAvailable).filter(FrenchTransaction.clean_amount(columns[start + x]))
if currency and currency != account.currency:
invest.original_currency = currency
attr = "original_" + attr
setattr(invest, attr, amount)
# valuation is not nullable, use 0 as default value
if not invest.valuation:
invest.valuation = Decimal('0')
# On some case we have a multine investment with a total column
# for now we have only see this on 2 lines, we will need to adapt it when o
col_num = 0
if start == 0:
col_num = 9
if columns[col_num] == '|Total' and _id == 'fichevaleur':
prev_inv = invest
invest = invests.pop(-1)
if prev_inv.quantity:
invest.quantity = invest.quantity + prev_inv.quantity
if prev_inv.valuation:
invest.valuation = invest.valuation + prev_inv.valuation
if prev_inv.diff:
invest.diff = invest.diff + prev_inv.diff
invests.append(invest)
# There is no investment on life insurance in the process to be created.
if len(message.split('&')) >= 4:
# We also have to get the liquidity as an investment.
valuation = CleanDecimal(None, True).filter(message.split('&')[3].replace('euro;{', '').strip())
invests.append(create_french_liquidity(valuation))
for invest in invests:
yield invest
class TitreHistory(LoggedPage, HTMLPage):
@method
class iter_history(ListElement):
item_xpath = '//table[@class="datas retour"]/tr'
class item(ItemElement):
klass = Transaction
obj_raw = Transaction.Raw('td[4] | td[3]')
obj_date = Date(CleanText('td[2]'), dayfirst=True)
obj_amount = CleanDecimal('td[7]', replace_dots=True)
def condition(self):
return len(self.el.xpath('td[@class="impaire"]')) > 0
MARKET_ORDER_DIRECTIONS = {
'Achat': MarketOrderDirection.BUY,
'Vente': MarketOrderDirection.SALE,
}
MARKET_ORDER_TYPES = {
'marché': MarketOrderType.MARKET,
'limit': MarketOrderType.LIMIT,
'déclenchement': MarketOrderType.TRIGGER,
}
MARKET_ORDER_PAYMENT_METHODS = {
'Cpt': MarketOrderPayment.CASH,
}
class MarketOrdersPage(LoggedPage, HTMLPage):
@method
class iter_market_orders(TableElement):
item_xpath = '//table//tr[td[@class="celluleI2T4C"]]/following-sibling::tr'
head_xpath = '//table//td[@class="celluleI2T4C"]'
col_direction = 'Sens'
col_label = 'Valeur'
col_quantity = 'Quantité'
col_ordervalue = 'Limite'
col_state = 'Etat'
col_unitprice = 'Cours Exec'
col_validity_date = 'Validité'
col__details_link = 'Détail'
class item(ItemElement):
def condition(self):
return "Pas d'ordre" not in CleanText('.')(self)
klass = MarketOrder
obj_direction = Map(
CleanText(TableCell('direction')),
MARKET_ORDER_DIRECTIONS,
MarketOrderDirection.UNKNOWN
)
obj_label = CleanText(TableCell('label'))
obj_quantity = Eval(lambda x: abs(x), CleanDecimal.French(TableCell('quantity')))
obj_ordervalue = CleanDecimal.French(TableCell('ordervalue'))
obj_state = Regexp(CleanText(TableCell('state')), r'([^\(]+)(?: \(|$)')
obj_payment_method = MapIn(
CleanText(TableCell('state')),
MARKET_ORDER_PAYMENT_METHODS,
MarketOrderPayment.UNKNOWN
)
obj_unitprice = CleanDecimal.French(TableCell('unitprice'), default=NotAvailable)
obj_validity_date = Date(CleanText(TableCell('validity_date')), dayfirst=True)
obj__details_link = Base(
TableCell('_details_link'),
Regexp(Link('./a', default=None), r"ouvrePopup\('(.+?)'\)", default=NotAvailable)
)
class MarketOrderDetailsPage(LoggedPage, HTMLPage):
@method
class fill_market_order(ItemElement):
obj_id = CleanText('//td[contains(text(), "Référence")]/following-sibling::td[1]', default=NotAvailable)
obj_code = IsinCode(
Regexp(
CleanText('//td[contains(text(), "Valeur")]/following-sibling::td[1]'),
r'\((.*?)\)',
default=NotAvailable
),
default=NotAvailable
)
obj_order_type = MapIn(
CleanText('//td[contains(text(), "Limite")]/following-sibling::td[1]'),
MARKET_ORDER_TYPES,
MarketOrderType.UNKNOWN
)
obj_execution_date = Date(
Regexp(
CleanText('//td[contains(text(), "Date exécuté")]/following-sibling::td[1]'),
r'(.*?) ',
default=NotAvailable
),
dayfirst=True,
default=NotAvailable
)
obj_date = Date(
Regexp(
CleanText('//td[contains(text(), "Création")]/following-sibling::td[1]'),
r'(.*?) ',
default=NotAvailable
),
dayfirst=True,
default=NotAvailable
)
class ASVHistory(LoggedPage, HTMLPage):
@method
class get_investments(TableElement):
item_xpath = '//table[@class="Tableau"]/tr[td[not(has-class("enteteTableau"))]]'
head_xpath = '//table[@class="Tableau"]/tr[td[has-class("enteteTableau")]]/td'
col_label = 'Support(s)'
col_vdate = 'Date de valeur'
col_unitvalue = 'Valeur de part'
col_quantity = ['(*) Nb de parts', 'Nb de parts']
col_valuation = ['Montant', 'Montant versé']
class item(ItemElement):
klass = Investment
obj_label = CleanText(TableCell('label'))
obj_quantity = CleanDecimal(TableCell('quantity'), replace_dots=True, default=NotAvailable)
obj_unitvalue = CleanDecimal(TableCell('unitvalue'), replace_dots=True, default=NotAvailable)
obj_valuation = CleanDecimal(TableCell('valuation'), replace_dots=True)
obj_vdate = Date(CleanText(TableCell('vdate')), dayfirst=True)
obj__code_url = Regexp(Attr('./td/a', 'onclick', default=""), r'PageExterne\(\'([^\']+)', default=None)
@pagination
@method
class iter_history(TableElement):
item_xpath = '//table[@class="Tableau"]/tr[td[not(has-class("enteteTableau"))]]'
head_xpath = '//table[@class="Tableau"]/tr[td[has-class("enteteTableau")]]/td'
col_date = 'Date d\'effet'
col_raw = 'Nature du mouvement'
col_amount = 'Montant brut'
next_page = Link('//a[contains(@href, "PageSuivante")]', default=None)
class item(ItemElement):
klass = Transaction
obj_date = Date(CleanText(TableCell('date')), dayfirst=True)
obj_raw = Transaction.Raw(TableCell('raw'))
obj_amount = CleanDecimal(TableCell('amount'), replace_dots=True)
obj__detail = Env('detail')
def obj_id(self):
try:
return Regexp(Link('./td/a', default=None), r'numMvt=(\d+)', default=None)(self)
except TypeError:
return NotAvailable
def parse(self, el):
link = Link('./td/a', default=None)(self)
page = None
if link:
page = self.page.browser.async_open(link)
self.env['detail'] = page