From a2dda0154bdfc6668c5aee8bd91397ffe291f1dc Mon Sep 17 00:00:00 2001 From: Vincent A Date: Sun, 22 Jul 2018 11:38:43 +0200 Subject: [PATCH] [bolden] new CapBank/CapDocument module for crowlending site --- modules/bolden/__init__.py | 26 +++++++ modules/bolden/browser.py | 99 +++++++++++++++++++++++++ modules/bolden/module.py | 96 ++++++++++++++++++++++++ modules/bolden/pages.py | 137 +++++++++++++++++++++++++++++++++++ tools/py3-compatible.modules | 1 + 5 files changed, 359 insertions(+) create mode 100644 modules/bolden/__init__.py create mode 100644 modules/bolden/browser.py create mode 100644 modules/bolden/module.py create mode 100644 modules/bolden/pages.py diff --git a/modules/bolden/__init__.py b/modules/bolden/__init__.py new file mode 100644 index 0000000000..6e42ccdbf4 --- /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 0000000000..b76a339afa --- /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 0000000000..866b4b3d57 --- /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 0000000000..9f9ee35f20 --- /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 94b274d68f..71b7e2d0a9 100644 --- a/tools/py3-compatible.modules +++ b/tools/py3-compatible.modules @@ -28,6 +28,7 @@ blablacar blogspot bnporc bnppere +bolden boursorama bp bred -- GitLab