# -*- coding: utf-8 -*-
# Copyright(C) 2010-2016 Romain Bignon
#
# This file is part of weboob.
#
# weboob 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.
#
# 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from binascii import crc32
import re
from weboob.capabilities.base import (
BaseObject, Field, StringField, DecimalField, IntField,
UserError, Currency, NotAvailable, EnumField, Enum,
empty, find_object
)
from weboob.capabilities.date import DateField
from weboob.capabilities.collection import CapCollection
from weboob.tools.compat import unicode
__all__ = [
'CapBank', 'BaseAccount', 'Account', 'Loan', 'Transaction', 'AccountNotFound',
'AccountType', 'AccountOwnership',
]
class ObjectNotFound(UserError):
pass
class AccountNotFound(ObjectNotFound):
"""
Raised when an account is not found.
"""
def __init__(self, msg='Account not found'):
super(AccountNotFound, self).__init__(msg)
class BaseAccount(BaseObject, Currency):
"""
Generic class aiming to be parent of :class:`Recipient` and
:class:`Account`.
"""
label = StringField('Pretty label')
currency = StringField('Currency', default=None)
bank_name = StringField('Bank Name', mandatory=False)
def __init__(self, id='0', url=None):
super(BaseAccount, self).__init__(id, url)
@property
def currency_text(self):
return Currency.currency2txt(self.currency)
@property
def ban(self):
""" Bank Account Number part of IBAN"""
if not self.iban:
return NotAvailable
return self.iban[4:]
class AccountType(Enum):
UNKNOWN = 0
CHECKING = 1
"Transaction, everyday transactions"
SAVINGS = 2
"Savings/Deposit, can be used for every banking"
DEPOSIT = 3
"Term of Fixed Deposit, has time/amount constraints"
LOAN = 4
"Loan account"
MARKET = 5
"Stock market or other variable investments"
JOINT = 6
"Joint account"
CARD = 7
"Card account"
LIFE_INSURANCE = 8
"Life insurances"
PEE = 9
"Employee savings PEE"
PERCO = 10
"Employee savings PERCO"
ARTICLE_83 = 11
"Article 83"
RSP = 12
"Employee savings RSP"
PEA = 13
"Share savings"
CAPITALISATION = 14
"Life Insurance capitalisation"
PERP = 15
"Retirement savings"
MADELIN = 16
"Complementary retirement savings"
MORTGAGE = 17
"Mortgage"
CONSUMER_CREDIT = 18
"Consumer credit"
REVOLVING_CREDIT = 19
"Revolving credit"
PER = 20
"Pension plan PER"
REAL_ESTATE = 21
"Real estate investment such as SCPI, OPCI, SCI"
class AccountOwnerType(object):
"""
Specifies the usage of the account
"""
PRIVATE = u'PRIV'
"""private personal account"""
ORGANIZATION = u'ORGA'
"""professional account"""
ASSOCIATION = u'ASSO'
"""association account"""
class AccountOwnership(object):
"""
Relationship between the credentials owner (PSU) and the account
"""
OWNER = u'owner'
"""The PSU is the account owner"""
CO_OWNER = u'co-owner'
"""The PSU is the account co-owner"""
ATTORNEY = u'attorney'
"""The PSU is the account attorney"""
class Account(BaseAccount):
"""
Bank account.
"""
TYPE_UNKNOWN = AccountType.UNKNOWN
TYPE_CHECKING = AccountType.CHECKING
TYPE_SAVINGS = AccountType.SAVINGS
TYPE_DEPOSIT = AccountType.DEPOSIT
TYPE_LOAN = AccountType.LOAN
TYPE_MARKET = AccountType.MARKET
TYPE_JOINT = AccountType.JOINT
TYPE_CARD = AccountType.CARD
TYPE_LIFE_INSURANCE = AccountType.LIFE_INSURANCE
TYPE_PEE = AccountType.PEE
TYPE_PERCO = AccountType.PERCO
TYPE_ARTICLE_83 = AccountType.ARTICLE_83
TYPE_RSP = AccountType.RSP
TYPE_PEA = AccountType.PEA
TYPE_CAPITALISATION = AccountType.CAPITALISATION
TYPE_PERP = AccountType.PERP
TYPE_MADELIN = AccountType.MADELIN
TYPE_MORTGAGE = AccountType.MORTGAGE
TYPE_CONSUMER_CREDIT = AccountType.CONSUMER_CREDIT
TYPE_REVOLVING_CREDIT = AccountType.REVOLVING_CREDIT
TYPE_PER = AccountType.PER
TYPE_REAL_ESTATE = AccountType.REAL_ESTATE
type = EnumField('Type of account', AccountType, default=TYPE_UNKNOWN)
owner_type = StringField('Usage of account') # cf AccountOwnerType class
balance = DecimalField('Balance on this bank account')
coming = DecimalField('Sum of coming movements')
iban = StringField('International Bank Account Number', mandatory=False)
ownership = StringField('Relationship between the credentials owner (PSU) and the account') # cf AccountOwnership class
# card attributes
paydate = DateField('For credit cards. When next payment is due.')
paymin = DecimalField('For credit cards. Minimal payment due.')
cardlimit = DecimalField('For credit cards. Credit limit.')
number = StringField('Shown by the bank to identify your account ie XXXXX7489')
# Wealth accounts (market, life insurance...)
valuation_diff = DecimalField('+/- values total')
valuation_diff_ratio = DecimalField('+/- values ratio')
# Employee savings (PERP, PERCO, Article 83...)
company_name = StringField('Name of the company of the stock - only for employee savings')
# parent account
# - A checking account parent of a card account
# - A checking account parent of a recurring loan account
# - An investment account parent of a liquidity account
# - ...
parent = Field('Parent account', BaseAccount)
opening_date = DateField('Date when the account contract was created on the bank')
def __repr__(self):
return "<%s id=%r label=%r>" % (type(self).__name__, self.id, self.label)
# compatibility alias
@property
def valuation_diff_percent(self):
return self.valuation_diff_ratio
@valuation_diff_percent.setter
def valuation_diff_percent(self, value):
self.valuation_diff_ratio = value
class Loan(Account):
"""
Account type dedicated to loans and credits.
"""
name = StringField('Person name')
account_label = StringField('Label of the debited account')
insurance_label = StringField('Label of the insurance')
total_amount = DecimalField('Total amount loaned')
available_amount = DecimalField('Amount available') # only makes sense for revolving credit
used_amount = DecimalField('Amount already used') # only makes sense for revolving credit
subscription_date = DateField('Date of subscription of the loan')
maturity_date = DateField('Estimated end date of the loan')
duration = IntField('Duration of the loan given in months')
rate = DecimalField('Monthly rate of the loan')
nb_payments_left = IntField('Number of payments still due')
nb_payments_done = IntField('Number of payments already done')
nb_payments_total = IntField('Number total of payments')
last_payment_amount = DecimalField('Amount of the last payment done')
last_payment_date = DateField('Date of the last payment done')
next_payment_amount = DecimalField('Amount of next payment')
next_payment_date = DateField('Date of the next payment')
class TransactionType(Enum):
UNKNOWN = 0
TRANSFER = 1
ORDER = 2
CHECK = 3
DEPOSIT = 4
PAYBACK = 5
WITHDRAWAL = 6
CARD = 7
LOAN_PAYMENT = 8
BANK = 9
CASH_DEPOSIT = 10
CARD_SUMMARY = 11
DEFERRED_CARD = 12
class Transaction(BaseObject):
"""
Bank transaction.
"""
TYPE_UNKNOWN = TransactionType.UNKNOWN
TYPE_TRANSFER = TransactionType.TRANSFER
TYPE_ORDER = TransactionType.ORDER
TYPE_CHECK = TransactionType.CHECK
TYPE_DEPOSIT = TransactionType.DEPOSIT
TYPE_PAYBACK = TransactionType.PAYBACK
TYPE_WITHDRAWAL = TransactionType.WITHDRAWAL
TYPE_CARD = TransactionType.CARD
TYPE_LOAN_PAYMENT = TransactionType.LOAN_PAYMENT
TYPE_BANK = TransactionType.BANK
TYPE_CASH_DEPOSIT = TransactionType.CASH_DEPOSIT
TYPE_CARD_SUMMARY = TransactionType.CARD_SUMMARY
TYPE_DEFERRED_CARD = TransactionType.DEFERRED_CARD
date = DateField('Debit date on the bank statement')
rdate = DateField('Real date, when the payment has been made; usually extracted from the label or from credit card info')
vdate = DateField('Value date, or accounting date; usually for professional accounts')
bdate = DateField('Bank date, when the transaction appear on website (usually extracted from column date)')
type = EnumField('Type of transaction, use TYPE_* constants', TransactionType, default=TYPE_UNKNOWN)
raw = StringField('Raw label of the transaction')
category = StringField('Category of the transaction')
label = StringField('Pretty label')
amount = DecimalField('Net amount of the transaction, used to compute account balance')
card = StringField('Card number (if any)')
commission = DecimalField('Commission part on the transaction (in account currency)')
gross_amount = DecimalField('Amount of the transaction without the commission')
# International
original_amount = DecimalField('Original net amount (in another currency)')
original_currency = StringField('Currency of the original amount')
country = StringField('Country of transaction')
original_commission = DecimalField('Original commission (in another currency)')
original_commission_currency = StringField('Currency of the original commission')
original_gross_amount = DecimalField('Original gross amount (in another currency)')
# Financial arbitrations
investments = Field('List of investments related to the transaction', list, default=[])
def __repr__(self):
return "" % (self.date, self.label, self.amount)
def unique_id(self, seen=None, account_id=None):
"""
Get an unique ID for the transaction based on date, amount and raw.
:param seen: if given, the method uses this dictionary as a cache to
prevent several transactions with the same values to have the same
unique ID.
:type seen: :class:`dict`
:param account_id: if given, add the account ID in data used to create
the unique ID. Can be useful if you want your ID to be unique across
several accounts.
:type account_id: :class:`str`
:returns: an unique ID encoded in 8 length hexadecimal string (for example ``'a64e1bc9'``)
:rtype: :class:`str`
"""
crc = crc32(unicode(self.date).encode('utf-8'))
crc = crc32(unicode(self.amount).encode('utf-8'), crc)
if not empty(self.raw):
label = self.raw
else:
label = self.label
crc = crc32(re.sub('[ ]+', ' ', label).encode("utf-8"), crc)
if account_id is not None:
crc = crc32(unicode(account_id).encode('utf-8'), crc)
if seen is not None:
while crc in seen:
crc = crc32(b"*", crc)
seen.add(crc)
return "%08x" % (crc & 0xffffffff)
class CapBank(CapCollection):
"""
Capability of bank websites to see accounts and transactions.
"""
def iter_resources(self, objs, split_path):
"""
Iter resources.
Default implementation of this method is to return on top-level
all accounts (by calling :func:`iter_accounts`).
:param objs: type of objects to get
:type objs: tuple[:class:`BaseObject`]
:param split_path: path to discover
:type split_path: :class:`list`
:rtype: iter[:class:`BaseObject`]
"""
if Account in objs:
self._restrict_level(split_path)
return self.iter_accounts()
def iter_accounts(self):
"""
Iter accounts.
:rtype: iter[:class:`Account`]
"""
raise NotImplementedError()
def get_account(self, id):
"""
Get an account from its ID.
:param id: ID of the account
:type id: :class:`str`
:rtype: :class:`Account`
:raises: :class:`AccountNotFound`
"""
return find_object(self.iter_accounts(), id=id, error=AccountNotFound)
def iter_history(self, account):
"""
Iter history of transactions on a specific account.
:param account: account to get history
:type account: :class:`Account`
:rtype: iter[:class:`Transaction`]
:raises: :class:`AccountNotFound`
"""
raise NotImplementedError()
def iter_coming(self, account):
"""
Iter coming transactions on a specific account.
:param account: account to get coming transactions
:type account: :class:`Account`
:rtype: iter[:class:`Transaction`]
:raises: :class:`AccountNotFound`
"""
raise NotImplementedError()