diff --git a/modules/lendosphere/__init__.py b/modules/lendosphere/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8298ff613a0fc866ff919a31afb97186696d28d3
--- /dev/null
+++ b/modules/lendosphere/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2019 Vincent A
+#
+# 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 __future__ import unicode_literals
+
+
+from .module import LendosphereModule
+
+
+__all__ = ['LendosphereModule']
diff --git a/modules/lendosphere/browser.py b/modules/lendosphere/browser.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f5fd4bbbf9e04caab31da237f9e0d58c5beb640
--- /dev/null
+++ b/modules/lendosphere/browser.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2019 Vincent A
+#
+# 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 __future__ import unicode_literals
+
+import datetime
+
+from weboob.browser import LoginBrowser, URL, need_login
+from weboob.tools.capabilities.bank.investments import create_french_liquidity
+from weboob.capabilities.bank import Investment
+
+from .pages import (
+ LoginPage, SummaryPage, GSummaryPage, ProfilePage, ComingPage,
+)
+
+
+class AttrURL(URL):
+ def build(self, *args, **kwargs):
+ import re
+
+ for pattern in self.urls:
+ regex = re.compile(pattern)
+
+ for k in regex.groupindex:
+ if hasattr(self.browser, k) and k not in kwargs:
+ kwargs[k] = getattr(self.browser, k)
+
+ return super(AttrURL, self).build(*args, **kwargs)
+
+
+class LendosphereBrowser(LoginBrowser):
+ BASEURL = 'https://www.lendosphere.com'
+
+ login = URL(r'/membres/se-connecter', LoginPage)
+ dashboard = AttrURL(r'/membres/(?P[a-z0-9-]+)/tableau-de-bord', SummaryPage)
+ global_summary = AttrURL(r'/membres/(?P[a-z0-9-]+)/dashboard_global_info', GSummaryPage)
+ coming = AttrURL(r'/membres/(?P[a-z0-9-]+)/mes-echeanciers.csv', ComingPage)
+ profile = AttrURL(r'/membres/(?P[a-z0-9-]+)', ProfilePage)
+
+ def do_login(self):
+ self.login.go()
+ self.page.do_login(self.username, self.password)
+
+ if self.login.is_here():
+ self.page.raise_error()
+
+ self.user_id = self.page.params['user_id']
+
+ @need_login
+ def iter_accounts(self):
+ self.global_summary.go()
+ return [self.page.get_account()]
+
+ @need_login
+ def iter_investment(self, account):
+ today = datetime.date.today()
+
+ self.coming.go()
+
+ # unfortunately there doesn't seem to be a page indicating what's
+ # left to be repaid on each project, so let's sum...
+ valuations = {}
+ commissions = {}
+ for tr in self.page.iter_transactions():
+ if tr.date <= today:
+ continue
+
+ if tr.raw not in valuations:
+ valuations[tr.raw] = tr.amount
+ commissions[tr.raw] = tr.commission
+ else:
+ valuations[tr.raw] += tr.amount
+ commissions[tr.raw] += tr.commission
+
+ for label, value in valuations.items():
+ inv = Investment()
+ inv.label = label
+ inv.valuation = value
+ inv.diff = commissions[label]
+ yield inv
+
+ yield create_french_liquidity(account._liquidities)
diff --git a/modules/lendosphere/module.py b/modules/lendosphere/module.py
new file mode 100644
index 0000000000000000000000000000000000000000..4408689bc5850ca3b1f4fe73dd1522259badf837
--- /dev/null
+++ b/modules/lendosphere/module.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2019 Vincent A
+#
+# 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 __future__ import unicode_literals
+
+from weboob.tools.backend import Module, BackendConfig
+from weboob.tools.value import ValueBackendPassword
+from weboob.capabilities.bank import CapBankWealth
+
+from .browser import LendosphereBrowser
+
+
+__all__ = ['LendosphereModule']
+
+
+class LendosphereModule(Module, CapBankWealth):
+ NAME = 'lendosphere'
+ DESCRIPTION = 'Lendosphere'
+ MAINTAINER = 'Vincent A'
+ EMAIL = 'dev@indigo.re'
+ LICENSE = 'LGPLv3+'
+ VERSION = '1.6'
+
+ BROWSER = LendosphereBrowser
+
+ 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_investment(self, account):
+ return self.browser.iter_investment(account)
diff --git a/modules/lendosphere/pages.py b/modules/lendosphere/pages.py
new file mode 100644
index 0000000000000000000000000000000000000000..38ef213dd10d78161eb80f3185b59f7264264108
--- /dev/null
+++ b/modules/lendosphere/pages.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2019 Vincent A
+#
+# 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 __future__ import unicode_literals
+
+from weboob.browser.elements import ItemElement, method, DictElement
+from weboob.browser.filters.json import Dict
+from weboob.browser.filters.standard import (
+ CleanText, CleanDecimal, Date, Field,
+)
+from weboob.browser.pages import HTMLPage, CsvPage, LoggedPage
+from weboob.capabilities.base import NotAvailable
+from weboob.capabilities.bank import Account, Transaction
+from weboob.exceptions import BrowserIncorrectPassword
+
+
+MAIN_ID = '_lendosphere_'
+
+
+class LoginPage(HTMLPage):
+ def do_login(self, username, password):
+ form = self.get_form(id='new_user')
+ form['user[email]'] = username
+ form['user[password]'] = password
+ form.submit()
+
+ def raise_error(self):
+ msg = CleanText('//div[has-class("alert-danger")]')(self.doc)
+ if 'Votre email ou mot de passe est incorrect' in msg:
+ raise BrowserIncorrectPassword(msg)
+ assert False, 'unhandled error %r' % msg
+
+
+class SummaryPage(LoggedPage, HTMLPage):
+ pass
+
+
+class ProfilePage(LoggedPage, HTMLPage):
+ pass
+
+
+class ComingProjectPage(LoggedPage, HTMLPage):
+ def iter_projects(self):
+ return [value for value in self.doc.xpath('//select[@id="offer"]/option/@value') if value != '*']
+
+
+class ComingPage(LoggedPage, CsvPage):
+ HEADER = 1
+
+ @method
+ class iter_transactions(DictElement):
+ class item(ItemElement):
+ klass = Transaction
+
+ obj_type = Transaction.TYPE_BANK
+ obj_raw = Dict('Projet')
+ obj_date = Date(Dict('Date'), dayfirst=True)
+
+ obj_gross_amount = CleanDecimal.SI(Dict('Capital rembourse'))
+ obj_amount = CleanDecimal.SI(Dict('Montant brut'))
+ obj_commission = CleanDecimal.SI(Dict('Interets'))
+
+ obj__amount_left = CleanDecimal.SI(Dict('Capital restant du'))
+
+
+class GSummaryPage(LoggedPage, HTMLPage):
+ @method
+ class get_account(ItemElement):
+ klass = Account
+
+ obj_id = MAIN_ID
+ obj_currency = 'EUR'
+ obj_number = NotAvailable
+ obj_type = Account.TYPE_MARKET
+ obj_label = 'Lendosphere'
+
+ obj__liquidities = CleanDecimal.French('//div[div[@class="subtitle"][contains(text(),"Somme disponible")]]/div[@class="amount"]')
+ obj__invested = CleanDecimal.French('//tr[td[contains(text(),"Echéances brutes restantes*")]]/td[last()]')
+
+ def obj_balance(self):
+ return Field('_liquidities')(self) + Field('_invested')(self)