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)