# -*- coding: utf-8 -*- # Copyright(C) 2018 Célande Adrien # # 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 import re from datetime import date from decimal import Decimal from weboob.tools.date import parse_french_date from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded, ParseError from weboob.capabilities.base import find_object from weboob.browser.pages import JsonPage, LoggedPage, HTMLPage from weboob.capabilities import NotAvailable from weboob.capabilities.bank import Account from weboob.capabilities.profile import Person from weboob.browser.filters.standard import CleanText from weboob.tools.capabilities.bank.transactions import FrenchTransaction class Transaction(FrenchTransaction): PATTERNS = [(re.compile('^.*Virement (?P.*)'), FrenchTransaction.TYPE_TRANSFER), (re.compile(u'PRELEV SEPA (?P.*)'), FrenchTransaction.TYPE_ORDER), (re.compile(u'.*Prélèvement.*'), FrenchTransaction.TYPE_ORDER), (re.compile(u'^(REGL|Rgt)(?P.*)'), FrenchTransaction.TYPE_ORDER), (re.compile('^(?P.*) Carte \d+\s+ LE (?P
\d{2})/(?P\d{2})/(?P\d{2})'), FrenchTransaction.TYPE_CARD), (re.compile(u'^Débit mensuel.*'), FrenchTransaction.TYPE_CARD_SUMMARY), (re.compile(u"^Retrait d'espèces à un DAB (?P.*) CARTE [X\d]+ LE (?P
\d{2})/(?P\d{2})/(?P\d{2})"), FrenchTransaction.TYPE_WITHDRAWAL), (re.compile(u'^Paiement de chèque (?P.*)'), FrenchTransaction.TYPE_CHECK), (re.compile(u'^(Cotisation|Intérêts) (?P.*)'), FrenchTransaction.TYPE_BANK), (re.compile(u'^(Remise Chèque|Remise de chèque)\s*(?P.*)'), FrenchTransaction.TYPE_DEPOSIT), (re.compile('^Versement (?P.*)'), FrenchTransaction.TYPE_DEPOSIT), ] class MyJsonPage(LoggedPage, JsonPage): def get_content(self): return self.doc.get('content', {}) class HomePage(LoggedPage, HTMLPage): pass class LoginPage(LoggedPage, HTMLPage): pass class UniversePage(MyJsonPage): def get_universes(self): universe_data = self.get_content() universes = {} universes[universe_data['universKey']] = universe_data['title'] for universe in universe_data.get('menus', {}): universes[universe['universKey']] = universe['title'] return universes class TokenPage(MyJsonPage): pass class MoveUniversePage(LoggedPage, HTMLPage): pass class SwitchPage(LoggedPage, JsonPage): pass class LoansPage(MyJsonPage): def iter_loans(self, current_univers): for content in self.get_content(): a = Account() a.id = "%s.%s" % (content['comptePrets'].strip(), content['numeroDossier'].strip()) a.type = Account.TYPE_LOAN a.label = ' '.join([content['intitule'].strip(), content['libellePrets'].strip()]) a.balance = -Decimal(str(content['montantCapitalDu']['valeur'])) a.currency = content['montantCapitalDu']['monnaie']['code'].strip() a._univers = current_univers yield a class AccountsPage(MyJsonPage): ACCOUNT_TYPES = { '000': Account.TYPE_CHECKING, # Compte à vue '011': Account.TYPE_CARD, # Carte bancaire '020': Account.TYPE_SAVINGS, # Compte sur livret '021': Account.TYPE_SAVINGS, '023': Account.TYPE_SAVINGS, # LDD Solidaire '025': Account.TYPE_SAVINGS, # Livret Fidélis '027': Account.TYPE_SAVINGS, # Livret A '037': Account.TYPE_SAVINGS, '077': Account.TYPE_SAVINGS, # Livret Bambino '078': Account.TYPE_SAVINGS, # Livret jeunes '080': Account.TYPE_SAVINGS, # Plan épargne logement '081': Account.TYPE_SAVINGS, '097': Account.TYPE_CHECKING, # Solde en devises '730': Account.TYPE_DEPOSIT, # Compte à terme Optiplus '999': Account.TYPE_MARKET, # no label, we use 'Portefeuille Titres' if needed } def iter_accounts(self, accnum, current_univers): seen = set() accounts_list = [] for content in self.get_content(): if accnum != '00000000000' and content['numero'] != accnum: continue for poste in content['postes']: a = Account() a._number = content['numeroLong'] a._nature = poste['codeNature'] a._codeSousPoste = poste['codeSousPoste'] if 'codeSousPoste' in poste else None a._consultable = poste['consultable'] a._univers = current_univers a.id = '%s.%s' % (a._number, a._nature) if a.id in seen: # some accounts like "compte à terme fidélis" have the same _number and _nature # but in fact are kind of closed, so worthless... self.logger.warning('ignored account id %r (%r) because it is already used', a.id, poste.get('numeroDossier')) continue seen.add(a.id) a.type = self.ACCOUNT_TYPES.get(poste['codeNature'], Account.TYPE_UNKNOWN) if a.type == Account.TYPE_UNKNOWN: self.logger.warning("unknown type %s" % poste['codeNature']) if a.type == Account.TYPE_CARD: a.parent = find_object(accounts_list, _number=a._number, type=Account.TYPE_CHECKING) if 'numeroDossier' in poste and poste['numeroDossier']: a._file_number = poste['numeroDossier'] a.id += '.%s' % a._file_number if poste['postePortefeuille']: a.label = u'Portefeuille Titres' a.balance = Decimal(str(poste['montantTitres']['valeur'])) a.currency = poste['montantTitres']['monnaie']['code'].strip() if not a.balance and not a.currency and 'dateTitres' not in poste: continue accounts_list.append(a) if 'libelle' not in poste: continue a.label = ' '.join([content['intitule'].strip(), poste['libelle'].strip()]) a.balance = Decimal(str(poste['solde']['valeur'])) a.currency = poste['solde']['monnaie']['code'].strip() # Some accounts may have balance currency if 'Solde en devises' in a.label and a.currency != u'EUR': a.id += str(poste['monnaie']['codeSwift']) accounts_list.append(a) return accounts_list class IbanPage(MyJsonPage): def set_iban(self, account): iban_response = self.get_content() account.iban = iban_response.get('iban', NotAvailable) class LifeInsurancesPage(MyJsonPage): def iter_life_insurances(self, current_univers): for content in self.get_content(): a = Account() a.id = str(content['avoirs']['contrats'][0]['numero']) a._number = content['avoirs']['contrats'][0]['cptRattachement'].rstrip('0') a.type = Account.TYPE_LIFE_INSURANCE a.label = ' '.join([content['titulaire'].strip(), content['avoirs']['contrats'][0]['libelleProduit'].strip()]) a.balance = Decimal(str(content['avoirs']['valeur'])) a.currency = 'EUR' a._univers = current_univers # The investment list for each life insurance is available here: a._investments = [inv for inv in content['avoirs']['contrats'][0]['allocations']] a._consultable = False yield a class SearchPage(LoggedPage, JsonPage): def get_transaction_list(self): result = self.doc if int(result['erreur']['code']) != 0: raise BrowserUnavailable("API sent back an error code") return result['content']['operations'] def iter_history(self, account, operation_list, seen, today, coming): transactions = [] for op in reversed(operation_list): t = Transaction() t.id = op['id'] if op['id'] in seen: raise ParseError('There are several transactions with the same ID, probably an infinite loop') seen.add(t.id) d = date.fromtimestamp(op.get('dateDebit', op.get('dateOperation'))/1000) op['details'] = [re.sub('\s+', ' ', i).replace('\x00', '') for i in op['details'] if i] # sometimes they put "null" elements... label = re.sub('\s+', ' ', op['libelle']).replace('\x00', '') raw = ' '.join([label] + op['details']) t.rdate = date.fromtimestamp(op.get('dateOperation', op.get('dateDebit'))/1000) vdate = date.fromtimestamp(op.get('dateValeur', op.get('dateDebit', op.get('dateOperation')))/1000) t.parse(d, raw, vdate=vdate) t.amount = Decimal(str(op['montant'])) if 'categorie' in op: t.category = op['categorie'] t.label = label t._coming = op['intraday'] if t._coming: # coming transactions have a random uuid id (inconsistent between requests) t.id = '' t._coming |= (t.date > today) if t.type == Transaction.TYPE_CARD and account.type == Account.TYPE_CARD: t.type = Transaction.TYPE_DEFERRED_CARD transactions.append(t) return transactions class ProfilePage(MyJsonPage): def get_profile(self): profile = Person() content = self.get_content() profile.name = content['prenom'] + ' ' + content['nom'] profile.address = content['adresse'] + ' ' + content['codePostal'] + ' ' + content['ville'] profile.country = content['pays'] profile.birth_date = parse_french_date(content['dateNaissance']).date() return profile class EmailsPage(MyJsonPage): def set_email(self, profile): content = self.get_content() profile.email = content['emailPart'] class ErrorPage(LoggedPage, HTMLPage): def on_load(self): if 'gestion-des-erreurs/erreur-pwd' in self.url: raise BrowserIncorrectPassword(CleanText('//h3')(self.doc)) if 'gestion-des-erreurs/opposition' in self.url: # need a case to retrieve the error message raise BrowserIncorrectPassword('Votre compte a été désactivé') if '/pages-gestion-des-erreurs/erreur-technique' in self.url: errmsg = CleanText('//h4')(self.doc) raise BrowserUnavailable(errmsg) if '/pages-gestion-des-erreurs/message-tiers-oppose' in self.url: # need a case to retrieve the error message raise ActionNeeded("Impossible de se connecter au compte car l'identification en 2 étapes a été activée")