Source code for piecash.core.account

from __future__ import unicode_literals

import uuid
from enum import Enum

from sqlalchemy import Column, VARCHAR, ForeignKey, INTEGER
from sqlalchemy.orm import relation, validates

from .._common import CallableList, GncConversionError
from .._declbase import DeclarativeBaseGuid
from ..sa_extra import mapped_to_slot_property

root_types = {"ROOT"}
asset_types = {"RECEIVABLE", "MUTUAL", "CASH", "ASSET", "BANK", "STOCK"}
liability_types = {"CREDIT", "LIABILITY", "PAYABLE"}
income_types = {"INCOME"}
expense_types = {"EXPENSE"}
trading_types = {"TRADING"}
equity_types = {"EQUITY"}
# : the different types of accounts
ACCOUNT_TYPES = (
    equity_types
    | income_types
    | expense_types
    | asset_types
    | liability_types
    | root_types
    | trading_types
)


[docs]class AccountType(Enum): root = "ROOT" receivable = "RECEIVABLE" mutual = "MUTUAL" cash = "CASH" asset = "ASSET" bank = "BANK" stock = "STOCK" credit = "CREDIT" liability = "LIABILITY" payable = "PAYABLE" income = "INCOME" expense = "EXPENSE" trading = "TRADING" equity = "EQUITY"
# types that are compatible with other types incexp_types = income_types | expense_types assetliab_types = asset_types | liability_types # types according to the sign of their balance positive_types = asset_types | expense_types | trading_types negative_types = liability_types | income_types | equity_types def _is_parent_child_types_consistent(type_parent, type_child, control_mode): """ Return True if the child account is consistent with the parent account in terms of types, i.e.: 1) if the parent is a root account, child can be anything but a root account 2) if the child is a root account, it must have no parent account 3) both parent and child are of the same family (asset, equity, income&expense, trading) Arguments type_parent(str): the type of the parent account type_child(str): the type of the child account Returns True if both accounts are consistent, False otherwise """ if type_parent in root_types: if "allow-root-subaccounts" in control_mode: return type_child in ACCOUNT_TYPES else: return type_child in (ACCOUNT_TYPES - root_types) if type_child in root_types: return (type_parent is None) or ("allow-root-subaccounts" in control_mode) for acc_types in (assetliab_types, equity_types, incexp_types, trading_types): if (type_child in acc_types) and (type_parent in acc_types): return True return False
[docs]class Account(DeclarativeBaseGuid): """ A GnuCash Account which is specified by its name, type and commodity. Attributes: type (str): type of the Account sign (int): 1 for accounts with positive balances, -1 for accounts with negative balances code (str): code of the Account commodity (:class:`piecash.core.commodity.Commodity`): the commodity of the account commodity_scu (int): smallest currency unit for the account non_std_scu (int): 1 if the scu of the account is NOT the same as the commodity description (str): description of the account name (str): name of the account fullname (str): full name of the account (including name of parent accounts separated by ':') placeholder (int): 1 if the account is a placeholder (should not be involved in transactions) hidden (int): 1 if the account is hidden is_template (bool): True if the account is a template account (ie commodity=template/template) parent (:class:`Account`): the parent account of the account (None for the root account of a book) children (list of :class:`Account`): the list of the children accounts splits (list of :class:`piecash.core.transaction.Split`): the list of the splits linked to the account lots (list of :class:`piecash.business.Lot`): the list of lots to which the account is linked book (:class:`piecash.core.book.Book`): the book if the account is the root account (else None) budget_amounts (list of :class:`piecash.budget.BudgetAmount`): list of budget amounts of the account scheduled_transaction (:class:`piecash.core.transaction.ScheduledTransaction`): scheduled transaction linked to the account """ __tablename__ = "accounts" __table_args__ = {} # column definitions guid = Column( "guid", VARCHAR(length=32), primary_key=True, nullable=False, default=lambda: uuid.uuid4().hex, ) name = Column("name", VARCHAR(length=2048), nullable=False) type = Column("account_type", VARCHAR(length=2048), nullable=False) commodity_guid = Column( "commodity_guid", VARCHAR(length=32), ForeignKey("commodities.guid") ) _commodity_scu = Column("commodity_scu", INTEGER(), nullable=False) _non_std_scu = Column("non_std_scu", INTEGER(), nullable=False) @property def non_std_scu(self): return self._non_std_scu @property def commodity_scu(self): return self._commodity_scu @commodity_scu.setter def commodity_scu(self, value): if value is None: self._non_std_scu = 0 if self.commodity: value = self.commodity.fraction else: value = 0 else: self._non_std_scu = 1 self._commodity_scu = value parent_guid = Column("parent_guid", VARCHAR(length=32), ForeignKey("accounts.guid")) code = Column("code", VARCHAR(length=2048)) description = Column("description", VARCHAR(length=2048)) hidden = Column("hidden", INTEGER()) _placeholder = Column("placeholder", INTEGER()) placeholder = mapped_to_slot_property( _placeholder, slot_name="placeholder", slot_transform=lambda v: "true" if v else None, ) # relation definitions commodity = relation("Commodity", back_populates="accounts") children = relation( "Account", back_populates="parent", cascade="all, delete-orphan", collection_class=CallableList, ) parent = relation( "Account", back_populates="children", remote_side=guid, ) splits = relation( "Split", back_populates="account", cascade="all, delete-orphan", collection_class=CallableList, ) lots = relation( "Lot", back_populates="account", cascade="all, delete-orphan", collection_class=CallableList, ) budget_amounts = relation( "BudgetAmount", back_populates="account", cascade="all, delete-orphan", collection_class=CallableList, ) scheduled_transaction = relation( "ScheduledTransaction", back_populates="template_account", cascade="all, delete-orphan", uselist=False, ) def __init__( self, name, type, commodity, parent=None, description="", commodity_scu=None, hidden=0, placeholder=0, code="", book=None, children=None, ): book = book or (commodity and commodity.book) or (parent and parent.book) if not book: raise ValueError("Could not find a book to attach the account to") book.add(self) self.name = name self.commodity = commodity self.type = type self.parent = parent self.description = description self.hidden = hidden self.placeholder = placeholder self.code = code self.commodity_scu = commodity_scu if children: self.children[:] = children
[docs] def object_to_validate(self, change): if change[-1] != "deleted": yield self
[docs] def validate(self): if self.type not in ACCOUNT_TYPES: raise ValueError( "Account_type '{}' is not in {}".format(self.type, ACCOUNT_TYPES) ) if self.parent: if not _is_parent_child_types_consistent( self.parent.type, self.type, self.book.control_mode ): raise ValueError( "Child type '{}' is not consistent with parent type {}".format( self.type, self.parent.type ) ) for acc in self.parent.children: if acc.name == self.name and acc != self: raise ValueError( "{} has two children with the same name {} : {} and {}".format( self.parent, self.name, self, acc ) ) else: if self.type in root_types: if self.name not in ["Template Root", "Root Account"]: raise ValueError( "{} is a root account but has a name = '{}'".format( self, self.name ) ) else: raise ValueError( "{} has no parent but is not a root account".format(self) )
[docs] @validates("commodity") def observe_commodity(self, key, value): """ Ensure update of commodity_scu when commodity is changed """ if value and (self.commodity_scu is None or self.non_std_scu == 0): self.commodity_scu = value.fraction return value
@property def fullname(self): if self.parent: pfn = self.parent.fullname if pfn: return "{}:{}".format(pfn, self.name) else: return self.name else: return ""
[docs] def get_balance( self, recurse=True, commodity=None, natural_sign=True, at_date=None ): """ Returns the balance of the account (including its children accounts if recurse=True) expressed in account's commodity/currency. If this is a stock/fund account, it will return the number of shares held. If this is a currency account, it will be in account's currency. In case of recursion, the commodity of children accounts will be transformed to the commodity of the father account using the latest price (if no price is available to convert , it is considered as 0). If natural_sign is True, the sign of the balance is reverted for the account with type {'LIABILITY', 'PAYABLE', 'CREDIT', 'INCOME', 'EQUITY'} Attributes: recurse (bool, optional): True if the balance should include children accounts (default to True) commodity (:class:`piecash.core.commodity.Commodity`): the currency into which to get the balance (default to None, i.e. the currency of the account) natural_sign (bool, optional): True if the balance sign is reversed for accounts of type {'LIABILITY', 'PAYABLE', 'CREDIT', 'INCOME', 'EQUITY'} (default to True) at_date (:class:`datetime.datetime`): the sum() balance of the account at a given date based on transaction post date Returns: the balance of the account """ if commodity is None: commodity = self.commodity if at_date is None: balance = sum([sp.quantity for sp in self.splits]) else: balance = sum( [ sp.quantity for sp in self.splits if sp.transaction.post_date <= at_date ] ) if commodity != self.commodity: try: # conversion is done directly from self.commodity to commodity (if possible) factor = self.commodity.currency_conversion(commodity) balance = balance * factor except GncConversionError: # conversion is done from self.commodity to self.parent.commodity and then to commodity factor1 = self.commodity.currency_conversion(self.parent.commodity) factor2 = self.parent.commodity.currency_conversion(commodity) factor = factor1 * factor2 balance = balance * factor if recurse and self.children: balance += sum( acc.get_balance( recurse=recurse, commodity=commodity, natural_sign=False, at_date=at_date, ) for acc in self.children ) if natural_sign: return balance * self.sign else: return balance
@property def sign(self): return 1 if (self.type in positive_types) else -1 @property def is_template(self): return self.commodity.namespace == "template" def __str__(self): if self.commodity: return "Account<{acc.fullname}[{acc.commodity.mnemonic}]>".format(acc=self) else: return "Account<{acc.fullname}>".format(acc=self)