diff --git a/weboob/capabilities/bank.py b/weboob/capabilities/bank.py deleted file mode 100644 index 1421aad7231b4c431a5d39fcbfe4855e7b3cddf9..0000000000000000000000000000000000000000 --- a/weboob/capabilities/bank.py +++ /dev/null @@ -1,1005 +0,0 @@ -# -*- 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 datetime import date, datetime -from binascii import crc32 -import re -from unidecode import unidecode - -from weboob.capabilities.base import empty, find_object -from weboob.exceptions import BrowserQuestion -from weboob.tools.capabilities.bank.iban import is_iban_valid -from weboob.tools.compat import unicode - -from .base import ( - BaseObject, Field, StringField, DecimalField, IntField, - UserError, Currency, NotAvailable, EnumField, Enum, - Capability, -) -from .date import DateField -from .collection import CapCollection - - -__all__ = [ - 'CapBank', 'BaseAccount', 'Account', 'Loan', 'Per', 'Transaction', 'AccountNotFound', - 'AccountType', 'AccountOwnership', 'Emitter', 'EmitterNumberType', - 'CapBankWealth', 'Investment', 'Pocket', - 'CapBankTransfer', 'Transfer', 'Recipient', - 'TransferError', 'TransferBankError', 'TransferInvalidAmount', 'TransferInsufficientFunds', - 'TransferInvalidCurrency', 'TransferInvalidLabel', - 'TransferInvalidEmitter', 'TransferInvalidOTP', 'TransferInvalidRecipient', - 'TransferCancelledByUser', 'TransferStep', - 'CapBankTransferAddRecipient', - 'RecipientNotFound', 'AddRecipientError', 'AddRecipientBankError', 'AddRecipientTimeout', - 'AddRecipientStep', 'RecipientInvalidIban', 'RecipientInvalidLabel', 'RecipientInvalidOTP', - 'Rate', 'CapCurrencyRate', 'BeneficiaryType', -] - - -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 RecipientNotFound(ObjectNotFound): - """ - Raised when a recipient is not found. - """ - - def __init__(self, msg='Recipient not found'): - super(RecipientNotFound, self).__init__(msg) - - -class TransferNotFound(ObjectNotFound): - def __init__(self, msg='Transfer not found'): - super(TransferNotFound, self).__init__(msg) - - -class TransferError(UserError): - """ - A transfer has failed. - """ - - code = 'transferError' - - def __init__(self, description=None, message=None): - """ - :param message: error message from the bank, if any - """ - - super(TransferError, self).__init__(message or description) - self.message = message - self.description = description - - -class TransferBankError(TransferError): - """The transfer was rejected by the bank with a message.""" - - code = 'bankMessage' - - -class TransferInvalidLabel(TransferError): - """The transfer label is invalid.""" - - code = 'invalidLabel' - - -class TransferInvalidEmitter(TransferError): - """The emitter account cannot be used for transfers.""" - - code = 'invalidEmitter' - - -class TransferInvalidRecipient(TransferError): - """The emitter cannot transfer to this recipient.""" - - code = 'invalidRecipient' - - -class TransferInvalidAmount(TransferError): - """This amount is not allowed.""" - - code = 'invalidAmount' - - -class TransferInvalidCurrency(TransferInvalidAmount): - """The transfer currency is invalid.""" - - code = 'invalidCurrency' - - -class TransferInsufficientFunds(TransferInvalidAmount): - """Not enough funds on emitter account.""" - - code = 'insufficientFunds' - - -class TransferInvalidDate(TransferError): - """This execution date cannot be used.""" - - code = 'invalidDate' - - -class TransferInvalidOTP(TransferError): - code = 'invalidOTP' - - -class TransferCancelledByUser(TransferError): - """The transfer is cancelled by the emitter or an authorized user""" - - code = 'cancelledByUser' - - -class AddRecipientError(UserError): - """ - Failed trying to add a recipient. - """ - - code = 'AddRecipientError' - - def __init__(self, description=None, message=None): - """ - :param message: error message from the bank, if any - """ - - super(AddRecipientError, self).__init__(message or description) - self.message = message - self.description = description - - -class AddRecipientBankError(AddRecipientError): - """The new recipient was rejected by the bank with a message.""" - - code = 'bankMessage' - - -class AddRecipientTimeout(AddRecipientError): - """Add new recipient request has timeout""" - - code = 'timeout' - - -class RecipientInvalidIban(AddRecipientError): - code = 'invalidIban' - - -class RecipientInvalidLabel(AddRecipientError): - code = 'invalidLabel' - - -class RecipientInvalidOTP(AddRecipientError): - code = 'invalidOTP' - - -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 Recipient(BaseAccount): - """ - Recipient of a transfer. - """ - enabled_at = DateField('Date of availability') - category = StringField('Recipient category') - iban = StringField('International Bank Account Number') - - # Needed for multispaces case - origin_account_id = StringField('Account id which recipient belong to') - - def __repr__(self): - return "<%s id=%r label=%r>" % (type(self).__name__, self.id, self.label) - - -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" - - -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 = 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 PerVersion(Enum): - PERIN = 'perin' # "PER individuel", subscribed by the account holder - PERCOL = 'percol' # "PER collectif", subscribed by the employer for all employees - PERCAT = 'percat' # "PER catégoriel", subscribed by the employer for a category of employees (for example managers) - - -class PerProviderType(Enum): - BANK = 'bank' - INSURER = 'insurer' - - -class Per(Account): - """ - Account type dedicated to PER retirement savings plans. - """ - - version = EnumField('Version of PER', PerVersion) - provider_type = EnumField('Type of account provider', PerProviderType) - - -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 Investment(BaseObject): - """ - Investment in a financial market. - """ - CODE_TYPE_ISIN = u'ISIN' - CODE_TYPE_AMF = u'AMF' - - label = StringField('Label of stocks') - code = StringField('Identifier of the stock') - code_type = StringField('Type of stock code (ISIN or AMF)') - description = StringField('Short description of the stock') - quantity = DecimalField('Quantity of stocks') - unitprice = DecimalField('Buy price of one stock') - unitvalue = DecimalField('Current value of one stock') - valuation = DecimalField('Total current valuation of the Investment') - vdate = DateField('Value date of the valuation amount') - diff = DecimalField('Difference between the buy cost and the current valuation') - diff_ratio = DecimalField('Difference in ratio (1 meaning 100%) between the buy cost and the current valuation') - portfolio_share = DecimalField('Ratio (1 meaning 100%) of the current amount relative to the total') - performance_history = Field('History of the performances of the stock (key=years, value=diff_ratio)', dict) - srri = IntField('Synthetic Risk and Reward Indicator of the stock (from 1 to 7)') - asset_category = StringField('Category of the stock') - recommended_period = StringField('Recommended investment period of the stock') - - # International - original_currency = StringField('Currency of the original amount') - original_valuation = DecimalField('Original valuation (in another currency)') - original_unitvalue = DecimalField('Original unitvalue (in another currency)') - original_unitprice = DecimalField('Original unitprice (in another currency)') - original_diff = DecimalField('Original diff (in another currency)') - - def __repr__(self): - return '' % (self.label, self.code, self.valuation) - - # compatibility alias - @property - def diff_percent(self): - return self.diff_ratio - - @diff_percent.setter - def diff_percent(self, value): - self.diff_ratio = value - - -class PocketCondition(Enum): - UNKNOWN = 0 - DATE = 1 - AVAILABLE = 2 - RETIREMENT = 3 - WEDDING = 4 - DEATH = 5 - INDEBTEDNESS = 6 - DIVORCE = 7 - DISABILITY = 8 - BUSINESS_CREATION = 9 - BREACH_EMPLOYMENT_CONTRACT = 10 - UNLOCKING_EXCEPTIONAL = 11 - THIRD_CHILD = 12 - EXPIRATION_UNEMPLOYMENT = 13 - PURCHASE_APARTMENT = 14 - - -class Pocket(BaseObject): - """ - Pocket - """ - CONDITION_UNKNOWN = PocketCondition.UNKNOWN - CONDITION_DATE = PocketCondition.DATE - CONDITION_AVAILABLE = PocketCondition.AVAILABLE - CONDITION_RETIREMENT = PocketCondition.RETIREMENT - CONDITION_WEDDING = PocketCondition.WEDDING - CONDITION_DEATH = PocketCondition.DEATH - CONDITION_INDEBTEDNESS = PocketCondition.INDEBTEDNESS - CONDITION_DIVORCE = PocketCondition.DIVORCE - CONDITION_DISABILITY = PocketCondition.DISABILITY - CONDITION_BUSINESS_CREATION = PocketCondition.BUSINESS_CREATION - CONDITION_BREACH_EMPLOYMENT_CONTRACT = PocketCondition.BREACH_EMPLOYMENT_CONTRACT - CONDITION_UNLOCKING_EXCEPTIONAL = PocketCondition.UNLOCKING_EXCEPTIONAL - CONDITION_THIRD_CHILD = PocketCondition.THIRD_CHILD - CONDITION_EXPIRATION_UNEMPLOYMENT = PocketCondition.EXPIRATION_UNEMPLOYMENT - CONDITION_PURCHASE_APARTMENT = PocketCondition.PURCHASE_APARTMENT - - label = StringField('Label of pocket') - amount = DecimalField('Amount of the pocket') - quantity = DecimalField('Quantity of stocks') - availability_date = DateField('Availability date of the pocket') - condition = EnumField('Withdrawal condition of the pocket', PocketCondition, default=CONDITION_UNKNOWN) - investment = Field('Reference to the investment of the pocket', Investment) - - -class TransferStep(BrowserQuestion): - def __init__(self, transfer, *values): - super(TransferStep, self).__init__(*values) - self.transfer = transfer - - -class AddRecipientStep(BrowserQuestion): - def __init__(self, recipient, *values): - super(AddRecipientStep, self).__init__(*values) - self.recipient = recipient - - -class BeneficiaryType(object): - RECIPIENT = 'recipient' - IBAN = 'iban' - PHONE_NUMBER = 'phone_number' - - -class TransferStatus(Enum): - UNKNOWN = 'unknown' - - INTERMEDIATE = 'intermediate' - """Transfer is not validated yet""" - - SCHEDULED = 'scheduled' - """Transfer to be executed later""" - - DONE = 'done' - """Transfer was executed""" - - CANCELLED = 'cancelled' - """Transfer was cancelled by the bank or by the user""" - - -class TransferFrequency(Enum): - UNKNOWN = 'unknown' - WEEKLY = 'weekly' - MONTHLY = 'monthly' - BIMONTHLY = 'bimonthly' - QUARTERLY = 'quarterly' - BIANNUAL = 'biannual' - YEARLY = 'yearly' - - -class TransferDateType(Enum): - FIRST_OPEN_DAY = 'first_open_day' - """Transfer to execute when possible (accounting opening days)""" - - INSTANT = 'instant' - """Transfer to execute immediately (not accounting opening days)""" - - DEFERRED = 'deferred' - """Transfer to execute on a chosen date""" - - PERIODIC = 'periodic' - """Transfer to execute periodically""" - - -class Transfer(BaseObject, Currency): - """ - Transfer from an account to a recipient. - """ - amount = DecimalField('Amount to transfer') - currency = StringField('Currency', default=None) - fees = DecimalField('Fees', default=None) - - exec_date = Field('Date of transfer', date, datetime) - label = StringField('Reason') - - account_id = StringField('ID of origin account') - account_iban = StringField('International Bank Account Number') - account_label = StringField('Label of origin account') - account_balance = DecimalField('Balance of origin account before transfer') - - # Information for beneficiary in recipient list - recipient_id = StringField('ID of recipient account') - recipient_iban = StringField('International Bank Account Number') - recipient_label = StringField('Label of recipient account') - - # Information for beneficiary not only in recipient list - # Like transfer to iban beneficiary - beneficiary_type = StringField('Transfer creditor number type', default=BeneficiaryType.RECIPIENT) - beneficiary_number = StringField('Transfer creditor number') - beneficiary_label = StringField('Transfer creditor label') - beneficiary_bic = StringField('Transfer creditor BIC') - - date_type = EnumField('Transfer execution date type', TransferDateType) - - frequency = EnumField('Frequency of periodic transfer', TransferFrequency) - first_due_date = DateField('Date of first transfer of periodic transfer') - last_due_date = DateField('Date of last transfer of periodic transfer') - - creation_date = DateField('Creation date of transfer') - status = EnumField('Transfer status', TransferStatus) - - cancelled_exception = Field('Transfer cancelled reason', TransferError) - - -class EmitterNumberType(Enum): - UNKNOWN = 'unknown' - IBAN = 'iban' - BBAN = 'bban' - - -class Emitter(BaseAccount): - """ - Transfer emitter account. - """ - number_type = EnumField('Account number type', EmitterNumberType, default=EmitterNumberType.UNKNOWN) - number = StringField('Account number value') - balance = DecimalField('Balance of emitter account') - - -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() - - -class CapBankWealth(CapBank): - """ - Capability of bank websites to see investments and pockets. - """ - - def iter_investment(self, account): - """ - Iter investment of a market account - - :param account: account to get investments - :type account: :class:`Account` - :rtype: iter[:class:`Investment`] - :raises: :class:`AccountNotFound` - """ - raise NotImplementedError() - - def iter_pocket(self, account): - """ - Iter pocket - - :param account: account to get pockets - :type account: :class:`Account` - :rtype: iter[:class:`Pocket`] - :raises: :class:`AccountNotFound` - """ - raise NotImplementedError() - - def iter_market_orders(self, account): - """ - Iter market orders - - :param account: account to get market orders - :type account: :class:`Account` - :rtype: iter[:class:`MarketOrder`] - :raises: :class:`AccountNotFound` - """ - raise NotImplementedError() - - -class CapTransfer(Capability): - accepted_beneficiary_types = (BeneficiaryType.RECIPIENT, ) - - accepted_execution_date_types = (TransferDateType.FIRST_OPEN_DAY, TransferDateType.DEFERRED) - - def iter_transfer_recipients(self, account): - """ - Iter recipients availables for a transfer from a specific account. - - :param account: account which initiate the transfer - :type account: :class:`Account` - :rtype: iter[:class:`Recipient`] - :raises: :class:`AccountNotFound` - """ - raise NotImplementedError() - - def init_transfer(self, transfer, **params): - """ - Initiate a transfer. - - :param :class:`Transfer` - :rtype: :class:`Transfer` - :raises: :class:`TransferError` - """ - raise NotImplementedError() - - def execute_transfer(self, transfer, **params): - """ - Execute a transfer. - - :param :class:`Transfer` - :rtype: :class:`Transfer` - :raises: :class:`TransferError` - """ - raise NotImplementedError() - - def confirm_transfer(self, transfer, **params): - """ - Transfer confirmation after multiple SCA from the Emitter. - This method is only used for PSD2 purpose. - Return the transfer with the new status. - - :param :class:`Transfer` - :rtype: :class:`Transfer` - :raises: :class:`TransferError` - """ - return self.get_transfer(transfer.id) - - def transfer(self, transfer, **params): - """ - Do a transfer from an account to a recipient. - - :param :class:`Transfer` - :rtype: :class:`Transfer` - :raises: :class:`TransferError` - """ - - transfer_not_check_fields = { - BeneficiaryType.RECIPIENT: ('id', 'beneficiary_number', 'beneficiary_label',), - BeneficiaryType.IBAN: ('id', 'recipient_id', 'recipient_iban', 'recipient_label',), - BeneficiaryType.PHONE_NUMBER: ('id', 'recipient_id', 'recipient_iban', 'recipient_label',), - } - - if not transfer.amount or transfer.amount <= 0: - raise TransferInvalidAmount('amount must be strictly positive') - - t = self.init_transfer(transfer, **params) - for key, value in t.iter_fields(): - if hasattr(transfer, key) and (key not in transfer_not_check_fields[transfer.beneficiary_type]): - transfer_val = getattr(transfer, key) - if hasattr(self, 'transfer_check_%s' % key): - assert getattr(self, 'transfer_check_%s' % key)(transfer_val, value), '%s changed during transfer processing (from "%s" to "%s")' % (key, transfer_val, value) - else: - assert transfer_val == value or empty(transfer_val), '%s changed during transfer processing (from "%s" to "%s")' % (key, transfer_val, value) - return self.execute_transfer(t, **params) - - def transfer_check_label(self, old, new): - old = re.sub(r'\s+', ' ', old).strip() - new = re.sub(r'\s+', ' ', new).strip() - return unidecode(old) == unidecode(new) - - def iter_transfers(self, account=None): - """ - Iter transfer transactions. - - :param account: account to get transfer history (or None for all accounts) - :type account: :class:`Account` - :rtype: iter[:class:`Transfer`] - :raises: :class:`AccountNotFound` - """ - raise NotImplementedError() - - def get_transfer(self, id): - """ - Get a transfer from its id. - - :param id: ID of the Transfer - :type id: :class:`str` - :rtype: :class:`Transfer` - """ - return find_object(self.iter_transfers(), id=id, error=TransferNotFound) - - def iter_emitters(self): - """ - Iter transfer emitter accounts. - - :rtype: iter[:class:`Emitter`] - """ - raise NotImplementedError() - - -class CapBankTransfer(CapBank, CapTransfer): - def account_to_emitter(self, account): - if isinstance(account, Account): - account = account.id - - return find_object(self.iter_emitters, id=account, error=ObjectNotFound) - - -class CapBankTransferAddRecipient(CapBankTransfer): - def new_recipient(self, recipient, **params): - raise NotImplementedError() - - def add_recipient(self, recipient, **params): - """ - Add a recipient to the connection. - - :param iban: iban of the new recipient. - :type iban: :class:`str` - :param label: label of the new recipient. - :type label: :class`str` - :raises: :class:`BrowserQuestion` - :raises: :class:`AddRecipientError` - :rtype: :class:`Recipient` - """ - if not is_iban_valid(recipient.iban): - raise RecipientInvalidIban('Iban is not valid.') - if not recipient.label: - raise RecipientInvalidLabel('Recipient label is mandatory.') - return self.new_recipient(recipient, **params) - - -class Rate(BaseObject, Currency): - """ - Currency exchange rate. - """ - - currency_from = StringField('The currency to which exchange rates are relative to. When converting 1 EUR to X HUF, currency_fom is EUR.)', default=None) - currency_to = StringField('The currency is converted to. When converting 1 EUR to X HUF, currency_to is HUF.)', default=None) - value = DecimalField('Exchange rate') - datetime = DateField('Collection date and time') - - -class CapCurrencyRate(CapBank): - """ - Capability of bank websites to get currency exchange rates. - """ - - def iter_currencies(self): - """ - Iter available currencies. - - :rtype: iter[:class:`Currency`] - """ - raise NotImplementedError() - - def get_rate(self, currency_from, currency_to): - """ - Get exchange rate. - - :param currency_from: currency to which exchange rate is relative to - :type currency_from: :class:`Currency` - :param currency_to: currency is converted to - :type currency_to: :class`Currency` - :rtype: :class:`Rate` - """ - raise NotImplementedError() diff --git a/weboob/capabilities/bank/__init__.py b/weboob/capabilities/bank/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c83542da810c415968d82f0f0636e3d28353a9eb --- /dev/null +++ b/weboob/capabilities/bank/__init__.py @@ -0,0 +1,118 @@ +# -*- 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 .transfer import ( + EmitterNumberType, + Emitter, + TransferFrequency, + TransferDateType, + TransferStatus, + TransferError, + TransferBankError, + TransferInvalidEmitter, + TransferInvalidRecipient, + TransferInvalidLabel, + TransferInvalidAmount, + TransferInvalidCurrency, + TransferInsufficientFunds, + TransferInvalidDate, + TransferInvalidOTP, + TransferCancelledByUser, + BeneficiaryType, + RecipientNotFound, + RecipientInvalidLabel, + Recipient, + Transfer, + TransferStep, + AddRecipientError, + AddRecipientBankError, + AddRecipientTimeout, + AddRecipientStep, + RecipientInvalidOTP, + RecipientInvalidIban, + CapTransfer, + CapBankTransfer, + CapBankTransferAddRecipient, +) +from .base import ( + AccountNotFound, + AccountType, + TransactionType, + AccountOwnerType, + Account, + Loan, + Transaction, + AccountOwnership, + CapBank, +) +from .rate import Rate, CapCurrencyRate +from .wealth import ( + Investment, + Per, + CapBankWealth, +) + + +__all__ = [ + 'EmitterNumberType', + 'Emitter', + 'TransferFrequency', + 'TransferDateType', + 'TransferStatus', + 'TransferError', + 'TransferBankError', + 'TransferInvalidEmitter', + 'TransferInvalidRecipient', + 'TransferInvalidLabel', + 'TransferInvalidAmount', + 'TransferInvalidCurrency', + 'TransferInsufficientFunds', + 'TransferInvalidDate', + 'TransferInvalidOTP', + 'TransferCancelledByUser', + 'BeneficiaryType', + 'RecipientNotFound', + 'RecipientInvalidLabel', + 'Recipient', + 'Transfer', + 'TransferStep', + 'AddRecipientError', + 'AddRecipientBankError', + 'AddRecipientTimeout', + 'AddRecipientStep', + 'RecipientInvalidOTP', + 'RecipientInvalidIban', + 'CapTransfer', + 'CapBankTransfer', + 'CapBankTransferAddRecipient', + 'AccountNotFound', + 'AccountType', + 'TransactionType', + 'AccountOwnerType', + 'Account', + 'Loan', + 'Transaction', + 'AccountOwnership', + 'CapBank', + 'Rate', + 'CapCurrencyRate', + 'Investment', + 'Per', + 'CapBankWealth', +] diff --git a/weboob/capabilities/bank/base.py b/weboob/capabilities/bank/base.py new file mode 100644 index 0000000000000000000000000000000000000000..1652f51a90417ff052dac08342a8b299ed70f526 --- /dev/null +++ b/weboob/capabilities/bank/base.py @@ -0,0 +1,403 @@ +# -*- 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" + + +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 = 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() diff --git a/weboob/capabilities/bank/rate.py b/weboob/capabilities/bank/rate.py new file mode 100644 index 0000000000000000000000000000000000000000..8c3ea349a9342539b5db17d114caace71564e64e --- /dev/null +++ b/weboob/capabilities/bank/rate.py @@ -0,0 +1,66 @@ +# -*- 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 weboob.capabilities.base import ( + BaseObject, StringField, DecimalField, Currency, + Capability, +) +from weboob.capabilities.date import DateField + + +__all__ = [ + 'Rate', 'CapCurrencyRate', +] + + +class Rate(BaseObject, Currency): + """ + Currency exchange rate. + """ + + currency_from = StringField('The currency to which exchange rates are relative to. When converting 1 EUR to X HUF, currency_fom is EUR.)', default=None) + currency_to = StringField('The currency is converted to. When converting 1 EUR to X HUF, currency_to is HUF.)', default=None) + value = DecimalField('Exchange rate') + datetime = DateField('Collection date and time') + + +class CapCurrencyRate(Capability): + """ + Capability of bank websites to get currency exchange rates. + """ + + def iter_currencies(self): + """ + Iter available currencies. + + :rtype: iter[:class:`Currency`] + """ + raise NotImplementedError() + + def get_rate(self, currency_from, currency_to): + """ + Get exchange rate. + + :param currency_from: currency to which exchange rate is relative to + :type currency_from: :class:`Currency` + :param currency_to: currency is converted to + :type currency_to: :class`Currency` + :rtype: :class:`Rate` + """ + raise NotImplementedError() diff --git a/weboob/capabilities/bank/transfer.py b/weboob/capabilities/bank/transfer.py new file mode 100644 index 0000000000000000000000000000000000000000..25da53ea553b02f6e633b42f9a88b02dd94248a7 --- /dev/null +++ b/weboob/capabilities/bank/transfer.py @@ -0,0 +1,448 @@ +# -*- 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 datetime import date, datetime +import re +from unidecode import unidecode + +from weboob.capabilities.base import ( + BaseObject, Field, StringField, DecimalField, + UserError, Currency, EnumField, Enum, + Capability, empty, find_object, +) +from weboob.capabilities.date import DateField +from weboob.exceptions import BrowserQuestion +from weboob.tools.capabilities.bank.iban import is_iban_valid + +from .base import ObjectNotFound, BaseAccount, CapBank, Account + + +__all__ = [ + 'AddRecipientBankError', 'AddRecipientError', 'AddRecipientStep', 'AddRecipientTimeout', + 'BeneficiaryType', + 'CapBankTransfer', 'CapBankTransferAddRecipient', + 'CapTransfer', 'Emitter', 'EmitterNumberType', 'Recipient', + 'RecipientInvalidIban', 'RecipientInvalidLabel', 'RecipientInvalidOTP', 'RecipientNotFound', + 'Transfer', + 'TransferBankError', 'TransferCancelledByUser', 'TransferDateType', 'TransferError', 'TransferFrequency', + 'TransferInsufficientFunds', 'TransferInvalidAmount', 'TransferInvalidCurrency', + 'TransferInvalidDate', 'TransferInvalidEmitter', 'TransferInvalidLabel', 'TransferInvalidOTP', 'TransferInvalidRecipient', + 'TransferNotFound', 'TransferStatus', 'TransferStep', +] + + +class RecipientNotFound(ObjectNotFound): + """ + Raised when a recipient is not found. + """ + + def __init__(self, msg='Recipient not found'): + super(RecipientNotFound, self).__init__(msg) + + +class TransferNotFound(ObjectNotFound): + def __init__(self, msg='Transfer not found'): + super(TransferNotFound, self).__init__(msg) + + +class TransferError(UserError): + """ + A transfer has failed. + """ + + code = 'transferError' + + def __init__(self, description=None, message=None): + """ + :param message: error message from the bank, if any + """ + + super(TransferError, self).__init__(message or description) + self.message = message + self.description = description + + +class TransferBankError(TransferError): + """The transfer was rejected by the bank with a message.""" + + code = 'bankMessage' + + +class TransferInvalidLabel(TransferError): + """The transfer label is invalid.""" + + code = 'invalidLabel' + + +class TransferInvalidEmitter(TransferError): + """The emitter account cannot be used for transfers.""" + + code = 'invalidEmitter' + + +class TransferInvalidRecipient(TransferError): + """The emitter cannot transfer to this recipient.""" + + code = 'invalidRecipient' + + +class TransferInvalidAmount(TransferError): + """This amount is not allowed.""" + + code = 'invalidAmount' + + +class TransferInvalidCurrency(TransferInvalidAmount): + """The transfer currency is invalid.""" + + code = 'invalidCurrency' + + +class TransferInsufficientFunds(TransferInvalidAmount): + """Not enough funds on emitter account.""" + + code = 'insufficientFunds' + + +class TransferInvalidDate(TransferError): + """This execution date cannot be used.""" + + code = 'invalidDate' + + +class TransferInvalidOTP(TransferError): + code = 'invalidOTP' + + +class TransferCancelledByUser(TransferError): + """The transfer is cancelled by the emitter or an authorized user""" + + code = 'cancelledByUser' + + +class AddRecipientError(UserError): + """ + Failed trying to add a recipient. + """ + + code = 'AddRecipientError' + + def __init__(self, description=None, message=None): + """ + :param message: error message from the bank, if any + """ + + super(AddRecipientError, self).__init__(message or description) + self.message = message + self.description = description + + +class AddRecipientBankError(AddRecipientError): + """The new recipient was rejected by the bank with a message.""" + + code = 'bankMessage' + + +class AddRecipientTimeout(AddRecipientError): + """Add new recipient request has timeout""" + + code = 'timeout' + + +class RecipientInvalidIban(AddRecipientError): + code = 'invalidIban' + + +class RecipientInvalidLabel(AddRecipientError): + code = 'invalidLabel' + + +class RecipientInvalidOTP(AddRecipientError): + code = 'invalidOTP' + + +class Recipient(BaseAccount): + """ + Recipient of a transfer. + """ + enabled_at = DateField('Date of availability') + category = StringField('Recipient category') + iban = StringField('International Bank Account Number') + + # Needed for multispaces case + origin_account_id = StringField('Account id which recipient belong to') + + def __repr__(self): + return "<%s id=%r label=%r>" % (type(self).__name__, self.id, self.label) + + +class TransferStep(BrowserQuestion): + def __init__(self, transfer, *values): + super(TransferStep, self).__init__(*values) + self.transfer = transfer + + +class AddRecipientStep(BrowserQuestion): + def __init__(self, recipient, *values): + super(AddRecipientStep, self).__init__(*values) + self.recipient = recipient + + +class BeneficiaryType(object): + RECIPIENT = 'recipient' + IBAN = 'iban' + PHONE_NUMBER = 'phone_number' + + +class TransferStatus(Enum): + UNKNOWN = 'unknown' + + INTERMEDIATE = 'intermediate' + """Transfer is not validated yet""" + + SCHEDULED = 'scheduled' + """Transfer to be executed later""" + + DONE = 'done' + """Transfer was executed""" + + CANCELLED = 'cancelled' + """Transfer was cancelled by the bank or by the user""" + + +class TransferFrequency(Enum): + UNKNOWN = 'unknown' + WEEKLY = 'weekly' + MONTHLY = 'monthly' + BIMONTHLY = 'bimonthly' + QUARTERLY = 'quarterly' + BIANNUAL = 'biannual' + YEARLY = 'yearly' + + +class TransferDateType(Enum): + FIRST_OPEN_DAY = 'first_open_day' + """Transfer to execute when possible (accounting opening days)""" + + INSTANT = 'instant' + """Transfer to execute immediately (not accounting opening days)""" + + DEFERRED = 'deferred' + """Transfer to execute on a chosen date""" + + PERIODIC = 'periodic' + """Transfer to execute periodically""" + + +class Transfer(BaseObject, Currency): + """ + Transfer from an account to a recipient. + """ + amount = DecimalField('Amount to transfer') + currency = StringField('Currency', default=None) + fees = DecimalField('Fees', default=None) + + exec_date = Field('Date of transfer', date, datetime) + label = StringField('Reason') + + account_id = StringField('ID of origin account') + account_iban = StringField('International Bank Account Number') + account_label = StringField('Label of origin account') + account_balance = DecimalField('Balance of origin account before transfer') + + # Information for beneficiary in recipient list + recipient_id = StringField('ID of recipient account') + recipient_iban = StringField('International Bank Account Number') + recipient_label = StringField('Label of recipient account') + + # Information for beneficiary not only in recipient list + # Like transfer to iban beneficiary + beneficiary_type = StringField('Transfer creditor number type', default=BeneficiaryType.RECIPIENT) + beneficiary_number = StringField('Transfer creditor number') + beneficiary_label = StringField('Transfer creditor label') + beneficiary_bic = StringField('Transfer creditor BIC') + + date_type = EnumField('Transfer execution date type', TransferDateType) + + frequency = EnumField('Frequency of periodic transfer', TransferFrequency) + first_due_date = DateField('Date of first transfer of periodic transfer') + last_due_date = DateField('Date of last transfer of periodic transfer') + + creation_date = DateField('Creation date of transfer') + status = EnumField('Transfer status', TransferStatus) + + cancelled_exception = Field('Transfer cancelled reason', TransferError) + + +class EmitterNumberType(Enum): + UNKNOWN = 'unknown' + IBAN = 'iban' + BBAN = 'bban' + + +class Emitter(BaseAccount): + """ + Transfer emitter account. + """ + number_type = EnumField('Account number type', EmitterNumberType, default=EmitterNumberType.UNKNOWN) + number = StringField('Account number value') + balance = DecimalField('Balance of emitter account') + + +class CapTransfer(Capability): + accepted_beneficiary_types = (BeneficiaryType.RECIPIENT, ) + + accepted_execution_date_types = (TransferDateType.FIRST_OPEN_DAY, TransferDateType.DEFERRED) + + def iter_transfer_recipients(self, account): + """ + Iter recipients availables for a transfer from a specific account. + + :param account: account which initiate the transfer + :type account: :class:`Account` + :rtype: iter[:class:`Recipient`] + :raises: :class:`AccountNotFound` + """ + raise NotImplementedError() + + def init_transfer(self, transfer, **params): + """ + Initiate a transfer. + + :param :class:`Transfer` + :rtype: :class:`Transfer` + :raises: :class:`TransferError` + """ + raise NotImplementedError() + + def execute_transfer(self, transfer, **params): + """ + Execute a transfer. + + :param :class:`Transfer` + :rtype: :class:`Transfer` + :raises: :class:`TransferError` + """ + raise NotImplementedError() + + def confirm_transfer(self, transfer, **params): + """ + Transfer confirmation after multiple SCA from the Emitter. + This method is only used for PSD2 purpose. + Return the transfer with the new status. + + :param :class:`Transfer` + :rtype: :class:`Transfer` + :raises: :class:`TransferError` + """ + return self.get_transfer(transfer.id) + + def transfer(self, transfer, **params): + """ + Do a transfer from an account to a recipient. + + :param :class:`Transfer` + :rtype: :class:`Transfer` + :raises: :class:`TransferError` + """ + + transfer_not_check_fields = { + BeneficiaryType.RECIPIENT: ('id', 'beneficiary_number', 'beneficiary_label',), + BeneficiaryType.IBAN: ('id', 'recipient_id', 'recipient_iban', 'recipient_label',), + BeneficiaryType.PHONE_NUMBER: ('id', 'recipient_id', 'recipient_iban', 'recipient_label',), + } + + if not transfer.amount or transfer.amount <= 0: + raise TransferInvalidAmount('amount must be strictly positive') + + t = self.init_transfer(transfer, **params) + for key, value in t.iter_fields(): + if hasattr(transfer, key) and (key not in transfer_not_check_fields[transfer.beneficiary_type]): + transfer_val = getattr(transfer, key) + if hasattr(self, 'transfer_check_%s' % key): + assert getattr(self, 'transfer_check_%s' % key)(transfer_val, value), '%s changed during transfer processing (from "%s" to "%s")' % (key, transfer_val, value) + else: + assert transfer_val == value or empty(transfer_val), '%s changed during transfer processing (from "%s" to "%s")' % (key, transfer_val, value) + return self.execute_transfer(t, **params) + + def transfer_check_label(self, old, new): + old = re.sub(r'\s+', ' ', old).strip() + new = re.sub(r'\s+', ' ', new).strip() + return unidecode(old) == unidecode(new) + + def iter_transfers(self, account=None): + """ + Iter transfer transactions. + + :param account: account to get transfer history (or None for all accounts) + :type account: :class:`Account` + :rtype: iter[:class:`Transfer`] + :raises: :class:`AccountNotFound` + """ + raise NotImplementedError() + + def get_transfer(self, id): + """ + Get a transfer from its id. + + :param id: ID of the Transfer + :type id: :class:`str` + :rtype: :class:`Transfer` + """ + return find_object(self.iter_transfers(), id=id, error=TransferNotFound) + + def iter_emitters(self): + """ + Iter transfer emitter accounts. + + :rtype: iter[:class:`Emitter`] + """ + raise NotImplementedError() + + +class CapBankTransfer(CapBank, CapTransfer): + def account_to_emitter(self, account): + if isinstance(account, Account): + account = account.id + + return find_object(self.iter_emitters, id=account, error=ObjectNotFound) + + +class CapBankTransferAddRecipient(CapBankTransfer): + def new_recipient(self, recipient, **params): + raise NotImplementedError() + + def add_recipient(self, recipient, **params): + """ + Add a recipient to the connection. + + :param iban: iban of the new recipient. + :type iban: :class:`str` + :param label: label of the new recipient. + :type label: :class`str` + :raises: :class:`BrowserQuestion` + :raises: :class:`AddRecipientError` + :rtype: :class:`Recipient` + """ + if not is_iban_valid(recipient.iban): + raise RecipientInvalidIban('Iban is not valid.') + if not recipient.label: + raise RecipientInvalidLabel('Recipient label is mandatory.') + return self.new_recipient(recipient, **params) diff --git a/weboob/capabilities/bank/wealth.py b/weboob/capabilities/bank/wealth.py new file mode 100644 index 0000000000000000000000000000000000000000..0bab04263800d9eb19eb874690862c1872c89005 --- /dev/null +++ b/weboob/capabilities/bank/wealth.py @@ -0,0 +1,232 @@ +# -*- 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 weboob.capabilities.base import ( + BaseObject, Field, StringField, DecimalField, IntField, + EnumField, Enum, +) +from weboob.capabilities.date import DateField + +from .base import Account, CapBank + +__all__ = [ + 'PerVersion', 'PerProviderType', 'Per', 'Investment', 'Pocket', + 'MarketOrderType', 'MarketOrderDirection', 'MarketOrderPayment', + 'MarketOrder', 'CapBankWealth', +] + + + +class PerVersion(Enum): + PERIN = 'perin' # "PER individuel", subscribed by the account holder + PERCOL = 'percol' # "PER collectif", subscribed by the employer for all employees + PERCAT = 'percat' # "PER catégoriel", subscribed by the employer for a category of employees (for example managers) + + +class PerProviderType(Enum): + BANK = 'bank' + INSURER = 'insurer' + + +class Per(Account): + """ + Account type dedicated to PER retirement savings plans. + """ + + version = EnumField('Version of PER', PerVersion) + provider_type = EnumField('Type of account provider', PerProviderType) + + +class Investment(BaseObject): + """ + Investment in a financial market. + """ + CODE_TYPE_ISIN = u'ISIN' + CODE_TYPE_AMF = u'AMF' + + label = StringField('Label of stocks') + code = StringField('Identifier of the stock') + code_type = StringField('Type of stock code (ISIN or AMF)') + description = StringField('Short description of the stock') + quantity = DecimalField('Quantity of stocks') + unitprice = DecimalField('Buy price of one stock') + unitvalue = DecimalField('Current value of one stock') + valuation = DecimalField('Total current valuation of the Investment') + vdate = DateField('Value date of the valuation amount') + diff = DecimalField('Difference between the buy cost and the current valuation') + diff_ratio = DecimalField('Difference in ratio (1 meaning 100%) between the buy cost and the current valuation') + portfolio_share = DecimalField('Ratio (1 meaning 100%) of the current amount relative to the total') + performance_history = Field('History of the performances of the stock (key=years, value=diff_ratio)', dict) + srri = IntField('Synthetic Risk and Reward Indicator of the stock (from 1 to 7)') + asset_category = StringField('Category of the stock') + recommended_period = StringField('Recommended investment period of the stock') + + # International + original_currency = StringField('Currency of the original amount') + original_valuation = DecimalField('Original valuation (in another currency)') + original_unitvalue = DecimalField('Original unitvalue (in another currency)') + original_unitprice = DecimalField('Original unitprice (in another currency)') + original_diff = DecimalField('Original diff (in another currency)') + + def __repr__(self): + return '' % (self.label, self.code, self.valuation) + + # compatibility alias + @property + def diff_percent(self): + return self.diff_ratio + + @diff_percent.setter + def diff_percent(self, value): + self.diff_ratio = value + + +class PocketCondition(Enum): + UNKNOWN = 0 + DATE = 1 + AVAILABLE = 2 + RETIREMENT = 3 + WEDDING = 4 + DEATH = 5 + INDEBTEDNESS = 6 + DIVORCE = 7 + DISABILITY = 8 + BUSINESS_CREATION = 9 + BREACH_EMPLOYMENT_CONTRACT = 10 + UNLOCKING_EXCEPTIONAL = 11 + THIRD_CHILD = 12 + EXPIRATION_UNEMPLOYMENT = 13 + PURCHASE_APARTMENT = 14 + + +class Pocket(BaseObject): + """ + Pocket + """ + CONDITION_UNKNOWN = PocketCondition.UNKNOWN + CONDITION_DATE = PocketCondition.DATE + CONDITION_AVAILABLE = PocketCondition.AVAILABLE + CONDITION_RETIREMENT = PocketCondition.RETIREMENT + CONDITION_WEDDING = PocketCondition.WEDDING + CONDITION_DEATH = PocketCondition.DEATH + CONDITION_INDEBTEDNESS = PocketCondition.INDEBTEDNESS + CONDITION_DIVORCE = PocketCondition.DIVORCE + CONDITION_DISABILITY = PocketCondition.DISABILITY + CONDITION_BUSINESS_CREATION = PocketCondition.BUSINESS_CREATION + CONDITION_BREACH_EMPLOYMENT_CONTRACT = PocketCondition.BREACH_EMPLOYMENT_CONTRACT + CONDITION_UNLOCKING_EXCEPTIONAL = PocketCondition.UNLOCKING_EXCEPTIONAL + CONDITION_THIRD_CHILD = PocketCondition.THIRD_CHILD + CONDITION_EXPIRATION_UNEMPLOYMENT = PocketCondition.EXPIRATION_UNEMPLOYMENT + CONDITION_PURCHASE_APARTMENT = PocketCondition.PURCHASE_APARTMENT + + label = StringField('Label of pocket') + amount = DecimalField('Amount of the pocket') + quantity = DecimalField('Quantity of stocks') + availability_date = DateField('Availability date of the pocket') + condition = EnumField('Withdrawal condition of the pocket', PocketCondition, default=CONDITION_UNKNOWN) + investment = Field('Reference to the investment of the pocket', Investment) + + +class CapBankWealth(CapBank): + """ + Capability of bank websites to see investments and pockets. + """ + + def iter_investment(self, account): + """ + Iter investment of a market account + + :param account: account to get investments + :type account: :class:`Account` + :rtype: iter[:class:`Investment`] + :raises: :class:`AccountNotFound` + """ + raise NotImplementedError() + + def iter_pocket(self, account): + """ + Iter pocket + + :param account: account to get pockets + :type account: :class:`Account` + :rtype: iter[:class:`Pocket`] + :raises: :class:`AccountNotFound` + """ + raise NotImplementedError() + + def iter_market_orders(self, account): + """ + Iter market orders + + :param account: account to get market orders + :type account: :class:`Account` + :rtype: iter[:class:`MarketOrder`] + :raises: :class:`AccountNotFound` + """ + raise NotImplementedError() + + +class MarketOrderType(Enum): + UNKNOWN = 0 + MARKET = 1 + """Order executed at the current market price""" + LIMIT = 2 + """Order executed with a maximum or minimum price limit""" + TRIGGER = 3 + """Order executed when the price reaches a specific value""" + + +class MarketOrderDirection(Enum): + UNKNOWN = 0 + BUY = 1 + SALE = 2 + + +class MarketOrderPayment(Enum): + UNKNOWN = 0 + CASH = 1 + DEFERRED = 2 + + +class MarketOrder(BaseObject): + """ + Market order + """ + + # Important: a Market Order always corresponds to one (and only one) investment + label = StringField('Label of the market order') + + # MarketOrder values + unitprice = DecimalField('Value of the stock at the moment of the market order') + unitvalue = DecimalField('Current value of the stock associated with the market order') + ordervalue = DecimalField('Limit value or trigger value, only relevant if the order type is LIMIT or TRIGGER') + currency = StringField('Currency of the market order - not always the same as account currency') + quantity = DecimalField('Quantity of stocks in the market order') + amount = DecimalField('Total amount that has been bought or sold') + + # MarketOrder additional information + order_type = EnumField('Type of market order', MarketOrderType, default=MarketOrderType.UNKNOWN) + direction = EnumField('Direction of the market order (buy or sale)', MarketOrderDirection, default=MarketOrderDirection.UNKNOWN) + payment_method = EnumField('Payment method of the market order', MarketOrderPayment, default=MarketOrderPayment.UNKNOWN) + date = DateField('Creation date of the market order') + validity_date = DateField('Validity date of the market order') + execution_date = DateField('Execution date of the market order (only for market orders that are completed)') + state = StringField('Current state of the market order (e.g. executed)') + code = StringField('Identifier of the stock related to the order') + stock_market = StringField('Stock market on which the order was executed') diff --git a/weboob/capabilities/wealth.py b/weboob/capabilities/wealth.py index bc162773ea10f55541cb16d4a26574e12e7ed834..64e6b797b3c26c5f7addafb4c34ff33cec60c159 100644 --- a/weboob/capabilities/wealth.py +++ b/weboob/capabilities/wealth.py @@ -20,66 +20,14 @@ from __future__ import unicode_literals # Temporary imports before moving these classes in this file -from weboob.capabilities.bank import ( - PerVersion, PerProviderType, Per, - Investment, Pocket, CapBankWealth, +from weboob.capabilities.bank.wealth import ( + PerVersion, PerProviderType, Per, Investment, Pocket, + MarketOrderType, MarketOrderDirection, MarketOrderPayment, + MarketOrder, CapBankWealth, ) -from .base import BaseObject, StringField, DecimalField, EnumField, Enum -from .date import DateField - __all__ = [ 'PerVersion', 'PerProviderType', 'Per', 'Investment', 'Pocket', 'MarketOrderType', 'MarketOrderDirection', 'MarketOrderPayment', 'MarketOrder', 'CapBankWealth', ] - - -class MarketOrderType(Enum): - UNKNOWN = 0 - MARKET = 1 - """Order executed at the current market price""" - LIMIT = 2 - """Order executed with a maximum or minimum price limit""" - TRIGGER = 3 - """Order executed when the price reaches a specific value""" - - -class MarketOrderDirection(Enum): - UNKNOWN = 0 - BUY = 1 - SALE = 2 - - -class MarketOrderPayment(Enum): - UNKNOWN = 0 - CASH = 1 - DEFERRED = 2 - - -class MarketOrder(BaseObject): - """ - Market order - """ - - # Important: a Market Order always corresponds to one (and only one) investment - label = StringField('Label of the market order') - - # MarketOrder values - unitprice = DecimalField('Value of the stock at the moment of the market order') - unitvalue = DecimalField('Current value of the stock associated with the market order') - ordervalue = DecimalField('Limit value or trigger value, only relevant if the order type is LIMIT or TRIGGER') - currency = StringField('Currency of the market order - not always the same as account currency') - quantity = DecimalField('Quantity of stocks in the market order') - amount = DecimalField('Total amount that has been bought or sold') - - # MarketOrder additional information - order_type = EnumField('Type of market order', MarketOrderType, default=MarketOrderType.UNKNOWN) - direction = EnumField('Direction of the market order (buy or sale)', MarketOrderDirection, default=MarketOrderDirection.UNKNOWN) - payment_method = EnumField('Payment method of the market order', MarketOrderPayment, default=MarketOrderPayment.UNKNOWN) - date = DateField('Creation date of the market order') - validity_date = DateField('Validity date of the market order') - execution_date = DateField('Execution date of the market order (only for market orders that are completed)') - state = StringField('Current state of the market order (e.g. executed)') - code = StringField('Identifier of the stock related to the order') - stock_market = StringField('Stock market on which the order was executed')