Using GNewCash

GnuCashFile

GnuCashFile is responsible for reading data from and writing data to GnuCash files.

To read a GnuCash file:

from gnewcash import GnuCashFile
my_file = GnuCashFile.read_file('/path/to/my/file.gnucash')

To write a GnuCash file (recommend backing up beforehand if overwriting an existing file):

from gnewcash import GnuCashFile, Book
my_book = Book()
my_file = GnuCashFile([my_book])
my_file.build_file('/path/to/my/file.gnucash')

The build_file function accepts two arguments to modify how the file is written: prettify_xml and use_gzip. Both options are turned off by default.

  • prettify_xml will prettify the XML before writing to disk. Helpful if the XML is to be reviewed on disk.
  • use_gzip will write the file to disk using GZip compression (GnuCash’s default).

Book

Book is a GnuCash entry that contains transactions, accounts, and their associated commodity.

When you read an existing GnuCash file, the book object(s) are automatically generated from the XML. To create a book object:

from gnewcash import Book, Account, AccountType, TransactionManager, Commodity
my_root_account = Account()
my_root_account.type = AccountType.ROOT
my_commodity = Commodity('USD', 'ISO4217')
my_transaction_manager = TransactionManager()
my_book = Book(root_account=my_root_account, transactions=my_transaction_manager, commodities=[my_commodity])

Commodity

Commodity is a GnuCash entry that defines the type of currency used for the book/account/transaction.

You can find more information on commodities and their implementations here.

There is a long list of available commodities in GnuCash. Implementing each one isn’t practical, so I’d recommend opening an uncompressed GnuCash file and view which commodity you’re using in the XML.

Here’s an example:

<gnc:commodity version="2.0.0">
  <cmdty:space>ISO4217</cmdty:space>
  <cmdty:id>USD</cmdty:id>
  <cmdty:get_quotes/>
  <cmdty:quote_source>currency</cmdty:quote_source>
  <cmdty:quote_tz/>
</gnc:commodity>

Account

Account is a GnuCash entry that’s best described from the GnuCash documentation itself.

It’s essentially a “bucket” that you can assign transaction splits to. Accounts can also have sub-accounts.

Retrieving Accounts

When you read in an existing GnuCash file, the accounts and their subaccounts are loaded into the root_account property of the Book. You can retrieve subaccounts by either accessing the children property of the root account (not recommended) or using the get_account method on the Book object (recommended).

Using get_account is pretty straightforward. Each account name from the root account should be an argument to get_account.

For example, if your hierarchy structure in GnuCash is:

  • Root Account
    • Assets
      • Current Assets
        • Checking Account
        • Credit Card
    • Expenses
      • Bills
        • Rent
        • Phone

And you want to get the “Checking Account” account, this call will retrieve it for you:

checking_account = my_book.get_account('Assets', 'Current Assets', 'Checking Account')

Creating Accounts

There isn’t a default “loader” for accounts, as everyone’s requirements (or preferences) are different. To create it purely in code, you’d do the following:

from gnewcash import Account, AccountType
my_root_account = Account()
my_root_account.type = AccountType.ROOT

assets_account = Account()
assets_account.type = AccountType.ASSET
assets_account.name = 'Assets'
assets_account.parent = my_root_account

current_assets_account = Account()
current_assets_account.type = AccountType.ASSET
current_assets_account.name = 'Current Assets'
current_assets_account.parent = assets_account

checking_account = Account()
checking_account.type = AccountType.BANK
checking_account.name = 'Checking Account'
checking_account.parent = current_assets_account

credit_card_account = Account()
credit_card_account.type = AccountType.CREDIT
credit_card_account.name = 'Credit Card'
credit_card_account.parent = current_assets_account

As you can tell, the code above is unwieldy. GNewCash does support shortcut accounts that cuts down on the lines of code. Here’s the same example using shortcut accounts.

from gnewcash import Account, AccountType, AssetAccount, BankAccount, CreditAccount
my_root_account = Account()
my_root_account.type = AccountType.ROOT

assets_account = AssetAccount()
assets_account.name = 'Assets'
assets_account.parent = my_root_account

current_assets_account = AssetAccount()
current_assets_account.name = 'Current Assets'
current_assets_account.parent = assets_account

checking_account = BankAccount()
checking_account.name = 'Checking Account'
checking_account.parent = current_assets_account

credit_card_account = CreditAccount()
credit_card_account.name = 'Credit Card'
credit_card_account.parent = current_assets_account

Better, but still a bit much. Here’s a JSON loader I wrote for personal usage:

import json

from gnewcash import Account, BankAccount, IncomeAccount, AssetAccount, CreditAccount, ExpenseAccount, EquityAccount, LiabilityAccount

def load_accounts_from_json(json_file):
    with open(json_file, 'r') as account_data_file:
        account_data = json.load(account_data_file)

    accounts = load_account_and_subaccounts(account_data)
    return accounts


def load_account_and_subaccounts(account_object, account_parent=None, current_path=None):
    account_lookup = dict()
    account_class = get_account_type(account_object['type'])

    account_class_object = account_class()
    if account_object['type'].upper() == 'ROOT':
        account_class_object.type = AccountType.ROOT
    else:
        account_class_object.name = account_object['name']
        account_class_object.parent = account_parent
    if not current_path:
        account_lookup['/'] = account_class_object
    else:
        account_lookup[current_path + account_object['path']] = account_class_object

    for account in account_object['subaccounts']:
        if not current_path:
            account_lookup.update(load_account_and_subaccounts(account, account_class_object, '/'))
        else:
            account_lookup.update(load_account_and_subaccounts(account, account_class_object, current_path +
                                                               account_object['path'] + '/'))
    return account_lookup

def get_account_type(account_type_string):
    account_type_mapping = {
        'ROOT': Account,
        'BANK': BankAccount,
        'INCOME': IncomeAccount,
        'ASSET': AssetAccount,
        'CREDIT': CreditAccount,
        'EXPENSE': ExpenseAccount,
        'EQUITY': EquityAccount,
        'LIABILITY': LiabilityAccount
    }
    return account_type_mapping[account_type_string]

Passing in a JSON file into load_accounts_from_json with this structure:

{
  "name": "Root Account",
  "path": "",
  "type": "ROOT",
  "subaccounts": [
    {
      "name": "Expenses",
      "path": "expenses",
      "type": "EXPENSE",
      "subaccounts": [
         {
          "name": "Bills",
          "path": "bills",
          "type": "EXPENSE",
          "subaccounts": [
            {
              "name": "Rent",
              "path": "rent",
              "type": "EXPENSE",
              "subaccounts": []
            },
            {
              "name": "Phone",
              "path": "phone",
              "type": "EXPENSE",
              "subaccounts": []
            }
          ]
        }
      ]
    },
    {
      "name": "Assets",
      "path": "assets",
      "type": "ASSET",
      "subaccounts": [
        {
          "name": "Current Assets",
          "path": "current_assets",
          "type": "ASSET",
          "subaccounts": [
            {
              "name": "Checking Account",
              "path": "checking_account",
              "type": "BANK",
              "subaccounts": []
            },
            {
              "name": "Credit Card",
              "path": "credit_card",
              "type": "CREDIT",
              "subaccounts": []
            }
          ]
        }
      ]
    }
  ]
}

Will yield the following dict:

{
    '/': root_account,
    '/assets': assets_account,
    '/assets/current_assets': current_assets_account,
    '/assets/current_assets/checking_account': checking_account,
    '/assets/current_assets/credit_card': credit_card_account,
    '/expenses': expenses_account,
    '/expenses/bills': bills_account,
    '/expenses/bills/rent': rent_account,
    '/expenses/bills/phone': phone_account
}

Feel free to use or modify that for your own usage!

Interest Accounts

Interest accounts are special accounts that actually aren’t used inside GnuCash. Trying to add one of the special accounts to a GnuCash file would result in an error.

The purpose of an interest account is to calculate balances and payment schedules for loans that accumulate interest.

There are two types of interest accounts: InterestAccount and InterestAccountWithSubaccounts.

For most purposes you just need the InterestAccount class. For loans where your overall loan is comprised of several smaller loans, you’d use the InterestAccountWithSubaccounts.

Here’s the general usage of an interest account:

from datetime import datetime
from decimal import Decimal

from gnewcash import InterestAccount

my_loan = InterestAccount(starting_balance=Decimal('1000'),
                          starting_date=datetime(2019, 1, 1),
                          interest_percentage=Decimal('0.05'),  # 5% APR
                          payment_amount=Decimal('50'))
my_loan.get_info_at_date(datetime(2019, 7, 1))

# LoanStatus(iterator_balance=Decimal('722.15'), iterator_date=datetime.datetime(2019, 7, 1, 0, 0), interest=Decimal('3.21'), amount_to_capital=Decimal('46.79'))

my_loan.get_all_payments()

# Probably should be converted to a namedtuple, but fields are: date, balance before payment, amount to principal
# [(datetime.datetime(2019, 2, 1, 0, 0), Decimal('1000'), Decimal('45.83')),
#  (datetime.datetime(2019, 3, 1, 0, 0), Decimal('954.17'), Decimal('46.02')),
#  (datetime.datetime(2019, 4, 1, 0, 0), Decimal('908.15'), Decimal('46.21')),
#  (datetime.datetime(2019, 5, 1, 0, 0), Decimal('861.94'), Decimal('46.40')),
#  (datetime.datetime(2019, 6, 1, 0, 0), Decimal('815.54'), Decimal('46.60')),
#  (datetime.datetime(2019, 7, 1, 0, 0), Decimal('768.94'), Decimal('46.79')),
#  (datetime.datetime(2019, 8, 1, 0, 0), Decimal('722.15'), Decimal('46.99')),
#  (datetime.datetime(2019, 9, 1, 0, 0), Decimal('675.16'), Decimal('47.18')),
#  (datetime.datetime(2019, 10, 1, 0, 0), Decimal('627.98'), Decimal('47.38')),
#  (datetime.datetime(2019, 11, 1, 0, 0), Decimal('580.60'), Decimal('47.58')),
#  (datetime.datetime(2019, 12, 1, 0, 0), Decimal('533.02'), Decimal('47.77')),
#  (datetime.datetime(2020, 1, 1, 0, 0), Decimal('485.25'), Decimal('47.97')),
#  (datetime.datetime(2020, 2, 1, 0, 0), Decimal('437.28'), Decimal('48.17')),
#  (datetime.datetime(2020, 3, 1, 0, 0), Decimal('389.11'), Decimal('48.37')),
#  (datetime.datetime(2020, 4, 1, 0, 0), Decimal('340.74'), Decimal('48.58')),
#  (datetime.datetime(2020, 5, 1, 0, 0), Decimal('292.16'), Decimal('48.78')),
#  (datetime.datetime(2020, 6, 1, 0, 0), Decimal('243.38'), Decimal('48.98')),
#  (datetime.datetime(2020, 7, 1, 0, 0), Decimal('194.40'), Decimal('49.18')),
#  (datetime.datetime(2020, 8, 1, 0, 0), Decimal('145.22'), Decimal('49.39')),
#  (datetime.datetime(2020, 9, 1, 0, 0), Decimal('95.83'), Decimal('49.60')),
#  (datetime.datetime(2020, 10, 1, 0, 0), Decimal('46.23'), Decimal('49.80'))]

Interest accounts also take the following constructor parameters:

  • additional_payments

    List of LoanExtraPayment objects.

    Note: Additional payments are assumed to have no interest collected on them.

  • skip_payment_dates

    List of datetime objects for dates that payments should be skipped.

  • interest_start_date

    datetime object that designates when interest starts incurring on the loan. (InterestAccount only)

Transaction

Transaction is a GnuCash object that represents a real-world transaction; for example, a credit/debit card purchase or a money transfer.

Transactions contain Splits that indicate how much money was added or removed from a particular account for the transaction. You can find more information on splits here.

By default, all transactions in GNewCash are “split transactions”, although there are plans to add a SimpleTransaction class for easier usage.

Retrieving Transactions

When you load an existing GnuCash file via the GnuCashFile.read_file method, the transactions in the document are loaded into a TransactionManager object. You can retrieve transactions for a given account like so:

my_file = GnuCashFile.read_file('/path/to/my/file.gnucash')
my_book = my_file.books[0]
checking_account = my_book.get_account('Assets', 'Current Assets', 'Checking Account')
checking_transactions = list(my_book.transactions.get_transactions(checking_account))

get_transactions returns a generator, so you can iterate over transactions in a memory-efficient way.

Creating Transactions

Like accounts, transactions can be unwieldy without some sort of loader (which depends on your implementation).

Creating a transaction can be done like so:

from datetime import datetime
from decimal import Decimal

from gnewcash import Transaction, Split

from pytz import timezone


my_new_transaction = Transaction()

# Date that the transaction takes place
my_new_transaction.date_posted = datetime(2019, 7, 5, 0, 0, 0, 0, tzinfo=timezone('US/Eastern'))

# Date that the transaction was created
my_new_transaction.date_entered = datetime.now(tz=timezone('US/Eastern'))

# Description for the transaction
my_new_transaction.description = 'My First Transaction'

# Memo for the transaction (appears in the "Num" field in GnuCash)
my_new_transaction.memo = 'My First Memo'

# Splits define what amount of money goes where. There should be at least 2 splits in a transaction.
my_new_transaction.splits = [
    Split(checking_account, Decimal('-50.00')),
    Split(phone_bill, Decimal('50.00')),
]

To add your new transaction to the TransactionManager, simply call:

my_book.transactions.add(my_new_transaction)

Transactions have an additional property called cleared, which returns a bool indicating if all splits are in the “cleared” state.

Transactions also have an additional method called mark_transaction_cleared, which sets the reconciled_state of all splits on the transaction to “c” (for cleared).

For more information on reconciliation states, please see the GnuCash documentation.

Creating Transactions (Simplified)

As of version 1.0.2, GNewCash provides a class called SimpleTransaction. The purpose of this class is to simplify creating transactions that consist of only two splits: “from” and “to”.

When using SimpleTransaction, the code changes to this:

from datetime import datetime
from decimal import Decimal

from gnewcash import SimpleTransaction

from pytz import timezone


my_new_transaction = SimpleTransaction()

# Date that the transaction takes place
my_new_transaction.date_posted = datetime(2019, 7, 5, 0, 0, 0, 0, tzinfo=timezone('US/Eastern'))

# Date that the transaction was created
my_new_transaction.date_entered = datetime.now(tz=timezone('US/Eastern'))

# Description for the transaction
my_new_transaction.description = 'My First Transaction'

# Memo for the transaction (appears in the "Num" field in GnuCash)
my_new_transaction.memo = 'My First Memo'

# Define the dollar amount
my_new_transaction.amount = Decimal('50.00')

# Define where it's coming from
my_new_transaction.from_account = checking_account

# Define where it's going to
my_new_transaction.to_account = phone_bill

Transaction Manager

The Transaction Manager is a class used to maintain transactions in the GnuCash file.

  • add
    Adds the transaction to the manager. By default, the manager will maintain sort order based on date_posted. You can disable this functionality by either setting the disable_sort property on the manager to False, or by passing sort_transactions=False when calling GnuCashFile.read_file. Some functions inside GNewCash rely on the transactions being sorted, so be careful when turning this setting off.
  • remove
    Removes the transaction from the manager. No magic behind the scenes here.
  • get_account_ending_balance
    Retrieves the final balance for the provided account, based on transactions in the manager.
  • get_account_starting_balance
    Retrieves the starting balance (dollar amount of first transaction by posted date) for the provided account, based on transactions in the manager.
  • get_balance_at_date
    Retrieves the account balance for the specified account at a certain date. If the provided date is None, it will retrieve the ending balance.
  • get_transactions
    Generator function that retrieves transactions for a specified account. If no account is provided, all transactions will be returned by the generator.
  • minimum_balance_past_date
    Retrieves the minimum balance past a certain date for the given account. It returns a tuple of the date that the account is at the minimum balance, and the minimum balance itself.

Questions/Comments/Concerns?

That should be all you need to start using GNewCash. If you have any questions, comments, or concerns with the documentation or implementation of GNewCash itself, please submit an issue on our issue tracker.

Happy programming!