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)