diff --git a/modules/binck/__init__.py b/modules/binck/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..faf9cce5ea1728dd824629d88a48512aa7d71814
--- /dev/null
+++ b/modules/binck/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Arthur Huillet
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+
+from .module import BinckModule
+
+
+__all__ = ['BinckModule']
diff --git a/modules/binck/browser.py b/modules/binck/browser.py
new file mode 100644
index 0000000000000000000000000000000000000000..8168b543f4e08d9e270aa88d2fe8b767802949fd
--- /dev/null
+++ b/modules/binck/browser.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Arthur Huillet
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+
+from weboob.browser import LoginBrowser, URL, need_login
+from weboob.exceptions import BrowserIncorrectPassword
+
+from .pages import LoginPage, AccountsList, TransactionHistoryJSON, AccountSelectionBar, InvestmentListJSON
+
+__all__ = ['Binck']
+
+
+class Binck(LoginBrowser):
+ BASEURL = 'https://web.binck.fr/'
+
+ # login and account overview are easy. Other pages are stateful and use the
+ # current account that is sent via a POST request that includes a one-time
+ # verification token
+ login_page = URL('/logon$', LoginPage)
+ accounts_page = URL('/AccountsOverview/Index', AccountsList)
+ transaction_history_json = URL('/TransactionsOverview/GetTransactions', TransactionHistoryJSON)
+ investment_list_json = URL('/PortfolioOverview/GetPortfolioOverview', InvestmentListJSON)
+ account_history_page = URL('/TransactionsOverview/FilteredOverview', AccountSelectionBar)
+ investment_page = URL('/PortfolioOverview/Index', AccountSelectionBar)
+ generic_page_with_account_selector = URL('/Home/Index', AccountSelectionBar)
+
+ def __init__(self, *args, **kwargs):
+ super(Binck, self).__init__(*args, **kwargs)
+ self.current_account = None
+ self.verification_token = None
+
+ def do_login(self):
+ self.login_page.stay_or_go()
+ self.page.login(self.username, self.password)
+
+ if self.login_page.is_here():
+ raise BrowserIncorrectPassword()
+
+ @need_login
+ def get_accounts_list(self):
+ self.accounts_page.stay_or_go()
+ return self.page.get_list()
+
+ def make_account_current(self, account):
+ self.token = self.page.get_account_selector_verification_token()
+ self.location('/Header/SwitchAccount', data={'__RequestVerificationToken': self.token, 'accountNumber': account.id})
+ self.token = self.page.get_account_selector_verification_token()
+ self.current_account = account
+
+ @need_login
+ def iter_investment(self, account):
+ self.investment_page.stay_or_go()
+ self.make_account_current(account)
+ self.location('/PortfolioOverview/GetPortfolioOverview', data={'grouping': 'SecurityCategory'},
+ headers={'__RequestVerificationToken': self.token})
+ return self.page.iter_investment()
+
+ @need_login
+ def iter_history(self, account):
+ # Not very useful as Binck's history page sucks (descriptions make no sense)
+ self.account_history_page.stay_or_go()
+ self.make_account_current(account)
+ # XXX handle other currencies than EUR
+ self.location('/TransactionsOverview/GetTransactions',
+ data={'currencyCode': 'EUR', 'mutationGroup': '', 'startDate': '', 'endDate': '', 'isFilteredOverview': True},
+ headers={'__RequestVerificationToken': self.token})
+
+ return self.page.iter_history()
diff --git a/modules/binck/favicon.png b/modules/binck/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..81f069f79025e6b9d77ecc09ae0220ae4f5e88bf
Binary files /dev/null and b/modules/binck/favicon.png differ
diff --git a/modules/binck/module.py b/modules/binck/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..21c7779eeda06af450bddad1c72a7b466deee692
--- /dev/null
+++ b/modules/binck/module.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Arthur Huillet
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+
+from weboob.capabilities.base import find_object
+from weboob.capabilities.bank import CapBankWealth, AccountNotFound
+from weboob.tools.backend import Module, BackendConfig
+from weboob.tools.value import ValueBackendPassword
+
+from .browser import Binck
+
+
+__all__ = ['BinckModule']
+
+
+class BinckModule(Module, CapBankWealth):
+ NAME = 'binck'
+ MAINTAINER = 'Arthur Huillet'
+ EMAIL = 'arthur.huillet+weboob@free.fr'
+ VERSION = '1.4'
+ LICENSE = 'AGPLv3+'
+ DESCRIPTION = u'Binck'
+ CONFIG = BackendConfig(
+ ValueBackendPassword('login', label='Identifiant', masked=False, required=True),
+ ValueBackendPassword('password', label='Mot de passe', required=True))
+ BROWSER = Binck
+
+ def create_default_browser(self):
+ return self.create_browser(
+ self.config['login'].get(),
+ self.config['password'].get()
+ )
+
+ def get_account(self, id):
+ """
+ Get an account from its ID.
+
+ :param id: ID of the account
+ :type id: :class:`str`
+ :rtype: :class:`Account`
+ :raises: :class:`AccountNotFound`
+ """
+ return find_object(self.iter_accounts(), id=id, error=AccountNotFound)
+
+ def iter_accounts(self):
+ """
+ Iter accounts.
+
+ :rtype: iter[:class:`Account`]
+ """
+ return self.browser.get_accounts_list()
+
+ def iter_investment(self, account):
+ return self.browser.iter_investment(account)
+
+ def iter_history(self, account):
+ return self.browser.iter_history(account)
diff --git a/modules/binck/pages.py b/modules/binck/pages.py
new file mode 100644
index 0000000000000000000000000000000000000000..6925df24de9ae5c7852d301815b5c5c8948852f1
--- /dev/null
+++ b/modules/binck/pages.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Arthur Huillet
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from __future__ import unicode_literals
+
+from weboob.browser.filters.html import Attr
+from weboob.browser.filters.standard import CleanText, CleanDecimal, Currency, Date
+from weboob.browser.filters.json import Dict
+
+from weboob.capabilities import NotAvailable
+from weboob.capabilities.bank import Account, Transaction, Investment
+from weboob.browser.pages import HTMLPage, LoggedPage, JsonPage
+from weboob.browser.elements import method, ItemElement, DictElement
+
+import lxml
+
+
+class LoginPage(HTMLPage):
+ def login(self, login, passwd):
+
+ form = self.get_form(id='loginForm')
+ form['UserName'] = login
+ form['Password'] = passwd
+ form.submit()
+
+
+class AccountSelectionBar(LoggedPage, HTMLPage):
+ def get_account_selector_verification_token(self):
+ return self.doc.xpath("//div[@class='accounts']//input[@name='__RequestVerificationToken']/@value")[0]
+
+
+class AccountsList(AccountSelectionBar):
+
+ def get_list(self):
+ accounts = []
+
+ for account_category in self.doc.xpath('//div[@id="AccountGroups"]//h1'):
+ type_mappings = {'PEA' : Account.TYPE_PEA,
+ 'Compte-titres' : Account.TYPE_MARKET,
+ 'Livret' : Account.TYPE_SAVINGS,
+ # XXX PEA-PME?
+ }
+
+ try:
+ current_type = type_mappings[account_category.text]
+ except KeyError:
+ self.logger.warning("Unknown account type for " + CleanText().filter(account_category))
+ current_type = Account.TYPE_UNKNOWN
+
+ for cpt in account_category.xpath('..//tr[@data-accountnumber]'):
+ account = Account()
+ account.type = current_type
+ account.id = Attr(".", "data-accountnumber")(cpt)
+ account.label = CleanText("./td[1]")(cpt)
+ account.balance = CleanDecimal("./td[2]", replace_dots=True)(cpt)
+ account.iban = NotAvailable # XXX need IBAN
+ accounts.append(account)
+ return accounts
+
+
+class TransactionHistoryJSON(JsonPage):
+ @method
+ class iter_history(DictElement):
+ item_xpath = 'Transactions'
+ '''
+{u'CurrentPage': 0,
+ u'EndOfData': True,
+ u'LastSequenceNumber': 1,
+ u'MutationGroupIntroductionText': u'Historique des transactions',
+ u'MutationGroupIntroductionTitle': u'Toutes les transactions',
+ u'Pages': [24],
+ u'Transactions': [{u'Amount': u'0,00 \u20ac',
+ u'Date': u'06/04/2017',
+ u'Description': u'\xe0 xxxx',
+ u'Mutation': u'-345,99 \u20ac',
+ u'Number': 24,
+ u'TransactionId': 20333,
+ u'Type': u'Virement interne',
+ u'ValueDate': u'06/04/2017'},
+ {u'Amount': u'345,99 \u20ac',
+ u'Date': u'05/04/2017',
+ u'Description': u"'Frais remboursement'",
+ u'Mutation': u'5,00 \u20ac',
+ u'Number': 23,
+ u'TransactionId': 2031111,
+ u'Type': u'Commissions',
+ u'ValueDate': u'05/04/2017'},
+}
+'''
+ class item(ItemElement):
+ klass = Transaction
+ obj_id = Dict('TransactionId')
+ obj_date = Date(Dict('Date'), dayfirst=True, default=NotAvailable)
+ obj_label = CleanText(Dict('Description'))
+ obj_amount = CleanDecimal(Dict('Mutation'), replace_dots=True, default=NotAvailable)
+ obj__account_balance = CleanDecimal(Dict('Amount'), replace_dots=True, default=NotAvailable)
+ obj__num_oper = Dict('Number')
+ obj__transaction_id = Dict('TransactionId')
+ # XXX this page has types that do not make a lot of sense, and
+ # crappy descriptions, and weboob does not have the types we want
+ # anyway. Don't bother filling in the type.
+ obj_type = Transaction.TYPE_BANK
+ obj_vdate = Date(Dict('ValueDate'), dayfirst=True, default=NotAvailable)
+
+ def obj_original_currency(self):
+ return Account.get_currency(Dict('Mutation')(self))
+
+ def obj__transaction_detail(self):
+ return
+ 'https://web.binck.fr/TransactionDetails/TransactionDetails?transactionSequenceNumber=%d¤cyCode=%s' % \
+ (self.obj._num_oper, self.obj.original_currency)
+
+
+class InvestmentListJSON(JsonPage):
+ '''
+{
+ "PortfolioOverviewGroups": [
+ {
+ "GroupName": "ETF",
+ "Items": [
+ {
+ "CurrencyCode": "EUR",
+ "SecurityId": 4019272,
+ "SecurityName": "Amundi ETF MSCI Emg Markets UCITS EUR C",
+ "Quantity": "5\u00a0052",
+ "QuantityValue": 5052.0,
+ "Quote": "4,1265",
+ "ValueInEuro": "20\u00a0847,08 \u20ac",
+ "ValueInEuroRaw": 20847.08,
+ "HistoricQuote": "3,68289",
+ "HistoricValue": "18\u00a0605,95 \u20ac",
+ "HistoricValueInEuro": "18\u00a0605,95 \u20ac",
+ "ResultValueInEuro": "2\u00a0241,12 \u20ac",
+ "ResultPercentageInEuro": "12,05 %",
+ "ValueInSecurityCurrency": "20\u00a0847,08 \u20ac",
+ "Difference": "-0,0285",
+ "DifferencePercentage": "-0,69 %",
+ "ResultValueInSecurityCurrency": "2\u00a0241,12 \u20ac",
+ "ResultPercentageInSecurityCurrency": "12,05 %",
+ "DayResultInEuro": "-166,72 \u20ac",
+ "LowestPrice": "4,1294",
+ "HighestPrice": "4,141",
+ "OpeningPrice": "4,1305",
+ "ClosingPrice": "4,1595",
+ "IsinCode": "LU1681045370",
+ "SecurityCategory": "ETF",
+ },
+ '''
+ @method
+ class iter_investment(DictElement):
+ item_xpath = 'PortfolioOverviewGroups/*/Items'
+
+ class item(ItemElement):
+ klass = Investment
+ obj_id = Dict('SecurityId')
+ obj_label = Dict('SecurityName')
+ obj_code = Dict('IsinCode')
+ obj_code_type = Investment.CODE_TYPE_ISIN
+ obj_quantity = CleanDecimal(Dict('QuantityValue'))
+ obj_unitprice = CleanDecimal(Dict('HistoricQuote'), replace_dots=True)
+ obj_unitvalue = CleanDecimal(Dict('Quote'), replace_dots=True)
+ obj_valuation = CleanDecimal(Dict('ValueInEuroRaw'))
+ obj_diff = CleanDecimal(Dict('ResultValueInEuro'), replace_dots=True)
+ obj_diff_percent = CleanDecimal(Dict('ResultPercentageInEuro'), replace_dots=True)
+ obj_original_currency = Currency(Dict('CurrencyCode'))
+ obj_original_valuation = CleanDecimal(Dict('ValueInSecurityCurrency'), replace_dots=True)
+ obj_original_diff = CleanDecimal(Dict('ResultValueInSecurityCurrency'), replace_dots=True)
+
diff --git a/modules/binck/test.py b/modules/binck/test.py
new file mode 100644
index 0000000000000000000000000000000000000000..56a9f4910eae4bbc98510845f36195218e48fa9f
--- /dev/null
+++ b/modules/binck/test.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Arthur Huillet
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+from __future__ import unicode_literals
+
+
+from weboob.tools.test import BackendTest
+
+
+class BinckTest(BackendTest):
+ MODULE = 'binck'
+ def test_binck(self):
+ l = list(self.backend.iter_accounts())
+ self.assertTrue(len(l) > 0)
+# a = l[0]
+# list(self.backend.iter_history(a))
diff --git a/tools/py3-compatible.modules b/tools/py3-compatible.modules
index 6d6dfaa829710e231b692ee2a07bdaa5fc08ff76..6da804819aefc080d8d0dd43adc55fcc2bd2d81c 100644
--- a/tools/py3-compatible.modules
+++ b/tools/py3-compatible.modules
@@ -20,6 +20,7 @@ becm
bforbank
bibliothequesparis
billetreduc
+binck
biplan
blablacar
blogspot