diff --git a/modules/bolden/__init__.py b/modules/bolden/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e42ccdbf4371f87d47a2c34381d55d9185e1b0c
--- /dev/null
+++ b/modules/bolden/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Vincent A
+#
+# 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 BoldenModule
+
+
+__all__ = ['BoldenModule']
diff --git a/modules/bolden/browser.py b/modules/bolden/browser.py
new file mode 100644
index 0000000000000000000000000000000000000000..b76a339afa798413c190a3913c0bf68a508373ff
--- /dev/null
+++ b/modules/bolden/browser.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Vincent A
+#
+# 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 datetime import timedelta, datetime
+
+from weboob.browser import LoginBrowser, need_login, URL
+from weboob.capabilities.bill import Document
+
+from .pages import (
+ LoginPage, HomeLendPage, PortfolioPage, OperationsPage, MAIN_ID, ProfilePage,
+)
+
+
+class BoldenBrowser(LoginBrowser):
+ BASEURL = 'https://bolden.fr/'
+
+ login = URL(r'/connexion', LoginPage)
+ home_lend = URL(r'/tableau-de-bord-investisseur', HomeLendPage)
+ profile = URL(r'/mon-profil', ProfilePage)
+ portfolio = URL(r'/InvestorDashboard/GetPortfolio', PortfolioPage)
+ operations = URL(r'/InvestorDashboard/GetOperations\?startDate=(?P[\d-]+)&endDate=(?P[\d-]+)', OperationsPage)
+
+ def do_login(self):
+ self.login.go()
+ self.page.do_login(self.username, self.password)
+
+ if self.login.is_here():
+ self.page.check_error()
+ assert False, 'should not be on login page'
+
+ @need_login
+ def iter_accounts(self):
+ self.portfolio.go()
+ return self.page.iter_accounts()
+
+ @need_login
+ def iter_history(self, account):
+ if account.id != MAIN_ID:
+ return []
+ return self._iter_all_history()
+
+ def _iter_all_history(self):
+ end = datetime.now()
+ while True:
+ start = end - timedelta(days=365)
+
+ self.operations.go(start=start.strftime('%Y-%m-%d'), end=end.strftime('%Y-%m-%d'))
+ transactions = list(self.page.iter_history())
+ if not transactions:
+ break
+
+ last_with_date = None
+ for tr in transactions:
+ if tr.date is None:
+ tr.date = last_with_date.date
+ tr.label = '%s %s' % (last_with_date.label, tr.label)
+ else:
+ last_with_date = tr
+
+ yield tr
+
+ end = start
+
+ @need_login
+ def get_profile(self):
+ self.profile.go()
+ return self.page.get_profile()
+
+ @need_login
+ def iter_documents(self):
+ for acc in self.iter_accounts():
+ if acc.id == MAIN_ID:
+ continue
+
+ doc = Document()
+ doc.id = acc.id
+ doc.url = acc._docurl
+ doc.label = 'Contrat %s' % acc.label
+ doc.type = 'other'
+ doc.format = 'pdf'
+ yield doc
diff --git a/modules/bolden/module.py b/modules/bolden/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..866b4b3d57758a7bfb2e2f9da057ce2c58a800a8
--- /dev/null
+++ b/modules/bolden/module.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Vincent A
+#
+# 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.backend import Module, BackendConfig
+from weboob.tools.value import ValueBackendPassword
+from weboob.capabilities.bank import CapBank, Account
+from weboob.capabilities.base import find_object
+from weboob.capabilities.bill import (
+ CapDocument, Subscription, SubscriptionNotFound, DocumentNotFound, Document,
+)
+from weboob.capabilities.profile import CapProfile
+
+from .browser import BoldenBrowser
+
+
+__all__ = ['BoldenModule']
+
+
+class BoldenModule(Module, CapBank, CapDocument, CapProfile):
+ NAME = 'bolden'
+ DESCRIPTION = 'Bolden'
+ MAINTAINER = 'Vincent A'
+ EMAIL = 'dev@indigo.re'
+ LICENSE = 'AGPLv3+'
+ VERSION = '1.4'
+
+ BROWSER = BoldenBrowser
+
+ CONFIG = BackendConfig(
+ ValueBackendPassword('login', label='Email', masked=False),
+ ValueBackendPassword('password', label='Mot de passe'),
+ )
+
+ def create_default_browser(self):
+ return self.create_browser(self.config['login'].get(), self.config['password'].get())
+
+ def iter_accounts(self):
+ return self.browser.iter_accounts()
+
+ def iter_history(self, account):
+ return self.browser.iter_history(account)
+
+ def get_profile(self):
+ return self.browser.get_profile()
+
+ def iter_subscription(self):
+ sub = Subscription()
+ sub.id = '_bolden_'
+ sub.subscriber = self.get_profile().name
+ sub.label = 'Bolden %s' % sub.subscriber
+ return [sub]
+
+ def get_subscription(self, _id):
+ if _id == '_bolden_':
+ return self.iter_subscription()[0]
+ raise SubscriptionNotFound()
+
+ def iter_documents(self, sub):
+ if not isinstance(sub, Subscription):
+ sub = self.get_subscription(sub)
+ return self.browser.iter_documents()
+
+ def get_document(self, id):
+ return find_object(self.browser.iter_documents(), id=id, error=DocumentNotFound)
+
+ def download_document(self, doc):
+ if not isinstance(doc, Document):
+ doc = self.get_document(doc)
+ return self.browser.open(doc.url).content
+
+ def iter_resources(self, objs, split_path):
+ if Account in objs:
+ self._restrict_level(split_path)
+ return self.iter_accounts()
+ if Subscription in objs:
+ self._restrict_level(split_path)
+ return self.iter_subscription()
diff --git a/modules/bolden/pages.py b/modules/bolden/pages.py
new file mode 100644
index 0000000000000000000000000000000000000000..9f9ee35f20d787ac6e5f5ad67ff5e860fa4eaca8
--- /dev/null
+++ b/modules/bolden/pages.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2018 Vincent A
+#
+# 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 decimal import Decimal
+
+from weboob.browser.elements import ListElement, ItemElement, method, TableElement
+from weboob.browser.filters.html import TableCell, Link, Attr
+from weboob.browser.filters.standard import (
+ CleanText, CleanDecimal, Slugify, Date, Field, Format,
+)
+from weboob.browser.pages import HTMLPage, LoggedPage
+from weboob.capabilities.bank import Account, Transaction
+from weboob.capabilities.profile import Profile
+from weboob.exceptions import BrowserIncorrectPassword
+from weboob.tools.compat import urljoin
+
+
+MAIN_ID = '_bolden_'
+
+class LoginPage(HTMLPage):
+ def do_login(self, username, password):
+ form = self.get_form(id='loginform')
+ form['Email'] = username
+ form['Password'] = password
+ form.submit()
+
+ def check_error(self):
+ msg = CleanText('//div[has-class("validation-summary-errors")]')(self.doc)
+ if 'Tentative de connexion invalide' in msg:
+ raise BrowserIncorrectPassword(msg)
+
+
+class HomeLendPage(LoggedPage, HTMLPage):
+ pass
+
+
+class PortfolioPage(LoggedPage, HTMLPage):
+ @method
+ class iter_accounts(ListElement):
+ class get_main(ItemElement):
+ klass = Account
+
+ obj_id = MAIN_ID
+ obj_label = 'Compte Bolden'
+ obj_type = Account.TYPE_CHECKING
+ obj_currency = 'EUR'
+ obj_balance = CleanDecimal('//div[p[has-class("investor-state") and contains(text(),"Fonds disponibles :")]]/p[has-class("investor-status")]', replace_dots=True)
+ #obj_coming = CleanDecimal('//div[p[has-class("investor-state") and contains(text(),"Capital restant dû :")]]/p[has-class("investor-status")]', replace_dots=True)
+
+ class iter_lends(TableElement):
+ head_xpath = '//div[@class="tab-wallet"]/table/thead//td'
+
+ col_label = 'Emprunteur'
+ col_coming = 'Capital restant dû'
+ col_doc = 'Contrat'
+
+ item_xpath = '//div[@class="tab-wallet"]/table/tbody/tr'
+
+ class item(ItemElement):
+ klass = Account
+
+ obj_label = CleanText(TableCell('label'))
+ obj_id = Slugify(Field('label'))
+ obj_type = Account.TYPE_SAVINGS
+ obj_currency = 'EUR'
+ obj_coming = CleanDecimal(TableCell('coming'), replace_dots=True)
+ obj_balance = Decimal('0')
+
+ def obj__docurl(self):
+ return urljoin(self.page.url, Link('.//a')(TableCell('doc')(self)[0]))
+
+
+class OperationsPage(LoggedPage, HTMLPage):
+ @method
+ class iter_history(TableElement):
+ head_xpath = '//div[@class="tab-wallet"]/table/thead//td'
+
+ col_date = 'Date'
+ col_label = 'Opération'
+ col_amount = 'Montant'
+
+ item_xpath = '//div[@class="tab-wallet"]/table/tbody/tr'
+
+ class item(ItemElement):
+ klass = Transaction
+
+ def condition(self):
+ return not Field('label')(self).startswith('dont ')
+
+ obj_label = CleanText(TableCell('label'))
+
+ def obj_amount(self):
+ v = CleanDecimal(TableCell('amount'), replace_dots=True)(self)
+ if Field('label')(self).startswith('Investissement'):
+ v = -v
+ return v
+
+ obj_date = Date(CleanText(TableCell('date')), dayfirst=True, default=None)
+
+
+class ProfilePage(LoggedPage, HTMLPage):
+ @method
+ class get_profile(ItemElement):
+ klass = Profile
+
+ obj_name = Format(
+ '%s %s',
+ Attr('//input[@id="SubModel_FirstName"]', 'value'),
+ Attr('//input[@id="SubModel_LastName"]', 'value'),
+ )
+ obj_phone = Attr('//input[@id="SubModel_Phone"]', 'value')
+ obj_address = Format(
+ '%s %s %s %s %s',
+ Attr('//input[@id="SubModel_Address_Street"]', 'value'),
+ Attr('//input[@id="SubModel_Address_Suplement"]', 'value'),
+ Attr('//input[@id="SubModel_Address_PostalCode"]', 'value'),
+ Attr('//input[@id="SubModel_Address_City"]', 'value'),
+ CleanText('//select[@id="SubModel_Address_Country"]/option[@selected]'),
+ )
diff --git a/tools/py3-compatible.modules b/tools/py3-compatible.modules
index 94b274d68fc974f1199ffc47c4234ca6c7915c1f..71b7e2d0a95abe43b5ecb83da6085a552c4a272b 100644
--- a/tools/py3-compatible.modules
+++ b/tools/py3-compatible.modules
@@ -28,6 +28,7 @@ blablacar
blogspot
bnporc
bnppere
+bolden
boursorama
bp
bred