Attached is a patch that adds three example python bindings scripts. - simple_sqlite_create.py shows a simple creation of an sqlite3 gnucash file - account_analysis.py is an auditing tool that shows the debits and credits on an account in specified periods in CSV format - new_book_with_opening_balances.py creates a new book from an original with the same account structure and creates opening balances
Apply with -p0. Mark Jenkins
Index: src/optional/python-bindings/example_scripts/simple_sqlite_create.py =================================================================== --- src/optional/python-bindings/example_scripts/simple_sqlite_create.py (.../trunk) (revision 0) +++ src/optional/python-bindings/example_scripts/simple_sqlite_create.py (.../branches/python-bindings) (revision 528) @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +from gnucash import Session, Account +from os.path import abspath +from gnucash.gnucash_core_c import ACCT_TYPE_ASSET + +s = Session('sqlite3://%s' % abspath('test.blob'), True) +# this seems to make a difference in more complex cases +s.save() + +book = s.book +root = book.get_root_account() +a = Account(book) +root.append_child(a) +a.SetName('wow') +a.SetType(ACCT_TYPE_ASSET) + +commod_table = book.get_table() +a.SetCommodity( commod_table.lookup('CURRENCY', 'CAD') ) +s.save() + +s.end() Index: src/optional/python-bindings/example_scripts/account_analysis.py =================================================================== --- src/optional/python-bindings/example_scripts/account_analysis.py (.../trunk) (revision 0) +++ src/optional/python-bindings/example_scripts/account_analysis.py (.../branches/python-bindings) (revision 528) @@ -0,0 +1,258 @@ +#!/usr/bin/env python + +# account_analysis.py -- Output all the credits and debits on an account +# +# Copyright (C) 2009, 2010 ParIT Worker Co-operative <transpare...@parit.ca> +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact: +# Free Software Foundation Voice: +1-617-542-5942 +# 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 +# Boston, MA 02110-1301, USA g...@gnu.org +# +# @author Mark Jenkins, ParIT Worker Co-operative <m...@parit.ca> + + +# python imports +from sys import argv, stdout +from datetime import date, timedelta +from bisect import bisect_right +from decimal import Decimal +from math import log10 +import csv + +# gnucash imports +from gnucash import Session, GncNumeric, Split + +# Invoke this script like the following example +# $ gnucash-env python account_analysis.py gnucash_file.gnucash \ +# 2010 1 monthly 12 \ +# debits-show credits-show Assets 'Test Account' +# +# That will do an analysis on the account 'Assets:Test Account' from +# gnucash_file.xac, all of the debits and all of the credits will be shown +# and summed on for 12 monthly periods starting from January (1st month) 2010 +# +# if you just want to see the credit and debit sums for each period, use +# the debits-noshow and credits-noshow argument +# +# The output goes to stdout and is in csv format. +# +# Account path arugments are space separated, so you need to quote parts of +# the account path with spaces in them +# +# available period types are monthly quarterly and yearly +# +# At the moment this script only supports GnuCash files of the sqllite3 type +# its an easy edit to switch to xml: etc... + + +# a dictionary with a period name as key, and number of months in that +# kind of period as the value +PERIODS = {"monthly": 1, + "quarterly": 3, + "yearly": 12 } + +NUM_MONTHS = 12 + +ONE_DAY = timedelta(days=1) + +DEBITS_SHOW, CREDITS_SHOW = ("debits-show", "credits-show") + +ZERO = Decimal(0) + +def gnc_numeric_to_python_Decimal(numeric): + negative = numeric.negative_p() + if negative: + sign = 1 + else: + sign = 0 + copy = GncNumeric(numeric.num(), numeric.denom()) + result = copy.to_decimal(None) + if not result: + raise Exception("gnc numeric value %s can't be converted to deciaml" % + copy.to_string() ) + digit_tuple = tuple( int(char) + for char in str(copy.num()) + if char != '-' ) + denominator = copy.denom() + exponent = int(log10(denominator)) + assert( (10 ** exponent) == denominator ) + return Decimal( (sign, digit_tuple, -exponent) ) + + +def next_period_start(start_year, start_month, period_type): + # add numbers of months for the period length + end_month = start_month + PERIODS[period_type] + # use integer division to find out if the new end month is in a different + # year, what year it is, and what the end month number should be changed + # to. + # Because this depends on modular arithmatic, we have to curvert the month + # values from 1-12 to 0-11 by subtracting 1 and putting it back after + # + # the really cool part is that this whole thing is implemented without + # any branching; if end_month > NUM_MONTHS + # + # A the super nice thing is that you can add all kinds of period lengths + # to PERIODS + end_year = start_year + ( (end_month-1) / NUM_MONTHS ) + end_month = ( (end_month-1) % NUM_MONTHS ) + 1 + + return end_year, end_month + + +def period_end(start_year, start_month, period_type): + if period_type not in PERIODS: + raise Exception("%s is not a valid period, should be %s" % ( + period_type, str(PERIODS.keys()) ) ) + + end_year, end_month = next_period_start(start_year, start_month, + period_type) + + # last step, the end date is day back from the start of the next period + # so we get a period end like + # 2010-03-31 for period starting 2010-01 instead of 2010-04-01 + return date(end_year, end_month, 1) - ONE_DAY + + +def generate_period_boundaries(start_year, start_month, period_type, periods): + for i in xrange(periods): + yield ( date(start_year, start_month, 1), + period_end(start_year, start_month, period_type) ) + start_year, start_month = next_period_start(start_year, start_month, + period_type) + +def account_from_path(top_account, account_path, original_path=None): + if original_path==None: original_path = account_path + account, account_path = account_path[0], account_path[1:] + account = top_account.lookup_by_name(account) + if account.get_instance() == None: + raise Exception( + "path " + ''.join(original_path) + " could not be found") + if len(account_path) > 0 : + return account_from_path(account, account_path, original_path) + else: + return account + + +def main(): + (gnucash_file, start_year, start_month, period_type, periods, + debits_show, credits_show) = argv[1:8] + start_year, start_month, periods = [int(blah) + for blah in (start_year, start_month, + periods) ] + + debits_show = debits_show == DEBITS_SHOW + credits_show = credits_show == CREDITS_SHOW + + account_path = argv[8:] + + gnucash_session = Session("sqlite3://%s" % gnucash_file, is_new=False) + root_account = gnucash_session.book.get_root_account() + account_of_interest = account_from_path(root_account, account_path) + + # a list of all the periods of interest, for each period + # keep the start date, end date, a list to store debits and credits, + # and sums for tracking the sum of all debits and sum of all credits + period_list = [ + [start_date, end_date, + [], # debits + [], # credits + ZERO, # debits sum + ZERO, # credits sum + ] + for start_date, end_date in generate_period_boundaries( + start_year, start_month, period_type, periods) + ] + # a copy of the above list with just the period start dates + period_starts = [e[0] for e in period_list ] + + # insert and add all splits in the periods of interest + for split in account_of_interest.GetSplitList(): + split = Split(instance=split) + trans = split.parent + trans_date = date.fromtimestamp(trans.GetDate()) + + # use binary search to find the period that starts before or on + # the transaction date + period_index = bisect_right( period_starts, trans_date ) - 1 + + # ignore transactions with a date before the matching period start + # (after subtracting 1 above start_index would be -1) + # and after the last period_end + if period_index >= 0 and \ + trans_date <= period_list[len(period_list)-1][1]: + + # get the period bucket appropriate for the split in question + period = period_list[period_index] + + # more specifically, we'd expect the transaction date + # to be on or after the period start, and before or on the + # period end, assuming the binary search (bisect_right) + # assumptions from above are are right.. + # + # in other words, we assert our use of binary search + # and the filtered results from the above if provide all the + # protection we need + assert( trans_date>= period[0] and trans_date <= period[1] ) + + split_amount = gnc_numeric_to_python_Decimal(split.GetAmount()) + + # if the amount is negative, this is a credit + if split_amount < ZERO: + debit_credit_offset = 1 + # else a debit + else: + debit_credit_offset = 0 + + # store the debit or credit Split with its transaction, using the + # above offset to get in the right bucket + # + # if we wanted to be really cool we'd keep the transactions + period[2+debit_credit_offset].append( (trans, split) ) + + # add the debit or credit to the sum, using the above offset + # to get in the right bucket + period[4+debit_credit_offset] += split_amount + + csv_writer = csv.writer(stdout) + csv_writer.writerow( ('period start', 'period end', 'debits', 'credits') ) + + def generate_detail_rows(values): + return ( + ('', '', '', '', trans.GetDescription(), + gnc_numeric_to_python_Decimal(split.GetAmount())) + for trans, split in values ) + + + for start_date, end_date, debits, credits, debit_sum, credit_sum in \ + period_list: + csv_writer.writerow( (start_date, end_date, debit_sum, credit_sum) ) + + if debits_show and len(debits) > 0: + csv_writer.writerow( + ('DEBITS', '', '', '', 'description', 'value') ) + csv_writer.writerows( generate_detail_rows(debits) ) + csv_writer.writerow( () ) + if credits_show and len(credits) > 0: + csv_writer.writerow( + ('CREDITS', '', '', '', 'description', 'value') ) + csv_writer.writerows( generate_detail_rows(credits) ) + csv_writer.writerow( () ) + + # no save needed, we're just reading.. + gnucash_session.end() + + +if __name__ == "__main__": main() + + Index: src/optional/python-bindings/example_scripts/new_book_with_opening_balances.py =================================================================== --- src/optional/python-bindings/example_scripts/new_book_with_opening_balances.py (.../trunk) (revision 0) +++ src/optional/python-bindings/example_scripts/new_book_with_opening_balances.py (.../branches/python-bindings) (revision 528) @@ -0,0 +1,313 @@ +#!/usr/bin/env python + +# new_book_with_opening_balances.py -- Replicate the account structure of a +# book and apply basis opening balances from the original +# +# Copyright (C) 2009, 2010 ParIT Worker Co-operative <transpare...@parit.ca> +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact: +# Free Software Foundation Voice: +1-617-542-5942 +# 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 +# Boston, MA 02110-1301, USA g...@gnu.org +# +# @author Mark Jenkins, ParIT Worker Co-operative <m...@parit.ca> + +from gnucash import Session, Account, Transaction, Split, GncNumeric +from gnucash.gnucash_core_c import \ + GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT, \ + ACCT_TYPE_ASSET, ACCT_TYPE_BANK, ACCT_TYPE_CASH, ACCT_TYPE_CHECKING, \ + ACCT_TYPE_CREDIT, ACCT_TYPE_EQUITY, ACCT_TYPE_EXPENSE, ACCT_TYPE_INCOME, \ + ACCT_TYPE_LIABILITY, ACCT_TYPE_MUTUAL, ACCT_TYPE_PAYABLE, \ + ACCT_TYPE_RECEIVABLE, ACCT_TYPE_STOCK, ACCT_TYPE_ROOT, ACCT_TYPE_TRADING + +from sys import argv +from os.path import abspath +from datetime import date + +# This script takes a gnucash url +# and creates a new file/db at a second url that has the same +# account tree and an equivilent opening balance on all the simple balance +# sheet accounts (not for income and expense accounts) +# +# This is done a per currency basis, one opening balance account for each +# +# For non-simple balance sheet accounts (like payable, recievable, stock, +# mutual, and trading, you'll have to put the opening balance in yourself +# +# Invocation examples: +# gnucash-env python new_book_with_opening_balances.py \ +# 'sqlite3:///home/mark/test.gnucash' +# 'sqlite3:///home/mark/new_test.gnucash' +# +# gnucash-env python new_book_with_opening_balances.py \ +# 'sqlite3:///home/mark/test.gnucash' \ +# 'xml:///crypthome/mark/parit-financial-system/new_test.gnucashxml' +# +# Remember that the gnucash python package has to be in your PYTHONPATH +# if you're installed GnuCash in a non-standard location, you'll have to do +# something like this +# export PYTHONPATH=gnucash_install_path/lib/python2.x/site-packages/ + + +OPENING_DATE = (1, 1, 2011) # day, month, year + +# possible acccount types of interest for opening balances +ACCOUNT_TYPES_TO_OPEN = set( ( + ACCT_TYPE_BANK, + ACCT_TYPE_CASH, + ACCT_TYPE_CREDIT, + ACCT_TYPE_ASSET, + ACCT_TYPE_LIABILITY, + ACCT_TYPE_STOCK, + ACCT_TYPE_MUTUAL, + ACCT_TYPE_INCOME, + ACCT_TYPE_EXPENSE, + ACCT_TYPE_EQUITY, + ACCT_TYPE_RECEIVABLE, + ACCT_TYPE_PAYABLE, + ACCT_TYPE_TRADING, +)) + +# You don't need an opening balance for income and expenses, past income +# and expenses should be in Equity->retained earnings +# so we remove them from the above set +ACCOUNT_TYPES_TO_OPEN = ACCOUNT_TYPES_TO_OPEN.difference( set(( + ACCT_TYPE_INCOME, + ACCT_TYPE_EXPENSE, + )) ) + +# This script isn't capable of properly creating the lots required for +# STOCK, MUTUAL, RECEIVABLE, and PAYABLE -- you'll have to create opening +# balances for them manually; so they are not included in the set for +# opening balances +ACCOUNT_TYPES_TO_OPEN = ACCOUNT_TYPES_TO_OPEN.difference( set(( + ACCT_TYPE_STOCK, + ACCT_TYPE_MUTUAL, + ACCT_TYPE_RECEIVABLE, + ACCT_TYPE_PAYABLE, + )) ) + +# this script isn't capable of properly setting up the transactions for +# ACCT_TYPE_TRADING, you'll have to create opening balances for them mannually; +# so, they are not included in the set of accounts used for opening balances +ACCOUNT_TYPES_TO_OPEN.remove(ACCT_TYPE_TRADING) + +OPENING_BALANCE_ACCOUNT = ( 'Equity', 'Opening Balances') + +# if possible, this program will try to use the account above for the +# currency listed below, and a variation of the above +# Equity->"Opening Balances Symbol" for all other currencies +PREFERED_CURRENCY_FOR_SIMPLE_OPENING_BALANCE = ("CURRENCY", "CAD") + +def initialize_split(book, value, account, trans): + split = Split(book) + split.SetValue(value) + split.SetAccount(account) + split.SetParent(trans) + return split + + +def record_opening_balance(original_account, new_account, new_book, + opening_balance_per_currency, commodity_tuple + ): + # create an opening balance if the account type is right + if new_account.GetType() in ACCOUNT_TYPES_TO_OPEN: + final_balance = original_account.GetBalance() + if final_balance.num() != 0: + # if there is a new currency type, associate with the currency + # a Transaction which will be the opening transaction for that + # currency and a GncNumeric value which will be the opening + # balance acccount amount + if commodity_tuple not in opening_balance_per_currency: + trans = Transaction(new_book) + opening_balance_per_currency[commodity_tuple] = ( + Transaction(new_book), GncNumeric(0, 1) ) + trans, total = opening_balance_per_currency[commodity_tuple] + + new_total = total.sub( + final_balance, + GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT ) + + initialize_split( + new_book, + final_balance, + new_account, trans) + opening_balance_per_currency[commodity_tuple] = \ + (trans, new_total ) + +def recursivly_build_account_tree(original_parent_account, + new_parent_account, + new_book, + new_commodity_table, + opening_balance_per_currency, + account_types_to_open ): + + for child in original_parent_account.get_children(): + original_account = Account(instance=child) + new_account = Account(new_book) + # attach new account to its parent + new_parent_account.append_child(new_account) + + # copy simple attributes + for attribute in ('Name', 'Type', 'Description', 'Notes', + 'Code', 'TaxRelated', 'Placeholder'): + # new_account.SetAttribute( original_account.GetAttribute() ) + getattr(new_account, 'Set' + attribute)( + getattr(original_account, 'Get' + attribute)() ) + + # copy commodity + orig_commodity = original_account.GetCommodity() + namespace = orig_commodity.get_namespace() + mnemonic = orig_commodity.get_mnemonic() + new_commodity = new_commodity_table.lookup(namespace, mnemonic) + assert(new_commodity.get_instance() != None ) + new_account.SetCommodity(new_commodity) + + record_opening_balance( original_account, new_account, + new_book, opening_balance_per_currency, + (namespace, mnemonic), + ) + + recursivly_build_account_tree(original_account, + new_account, + new_book, + new_commodity_table, + opening_balance_per_currency, + account_types_to_open) + +def reconstruct_account_name_with_mnemonic(account_tuple, mnemonic): + opening_balance_account_pieces = list(account_tuple) + opening_balance_account_pieces[ + len(opening_balance_account_pieces) - 1 ] += " - " + mnemonic + return opening_balance_account_pieces + +def find_or_make_account(account_tuple, root_account, book, + currency ): + current_account_name, account_path = account_tuple[0], account_tuple[1:] + current_account = root_account.lookup_by_name(current_account_name) + if current_account.get_instance() == None: + current_account = Account(book) + current_account.SetName(current_account_name) + current_account.SetCommodity(currency) + root_account.append_child(current_account) + + if len(account_path) > 0: + return find_or_make_account(account_path, current_account, book, + currency) + else: + account_commod = current_account.GetCommodity() + if (account_commod.get_mnemonic(), + account_commod.get_namespace() ) == \ + (currency.get_mnemonic(), + currency.get_namespace()) : + return current_account + else: + return None + +def choke_on_none_for_no_account(opening_account, extra_string ): + if opening_account == None: + raise Exception("account currency and name mismatch, " + extra_string) + +def create_opening_balance_transaction(commodtable, namespace, mnemonic, + new_book_root, new_book, + opening_trans, opening_amount, + simple_opening_name_used): + currency = commodtable.lookup(namespace, mnemonic) + assert( currency.get_instance() != None ) + + if simple_opening_name_used: + account_pieces = reconstruct_account_name_with_mnemonic( + OPENING_BALANCE_ACCOUNT, + mnemonic) + opening_account = find_or_make_account( + account_pieces, new_book_root, new_book, currency ) + choke_on_none_for_no_account(opening_account, + ', '.join(account_pieces) ) + else: + opening_account = find_or_make_account(OPENING_BALANCE_ACCOUNT, + new_book_root, new_book, + currency ) + simple_opening_name_used = True + if opening_account == None: + account_pieces = reconstruct_account_name_with_mnemonic( + OPENING_BALANCE_ACCOUNT, + mnemonic) + opening_account = find_or_make_account( + account_pieces, new_book_root, new_book, currency ) + choke_on_none_for_no_account(opening_account, + ', '.join(account_pieces) ) + + # we don't need to use the opening balance account at all if all + # the accounts being given an opening balance balance out + if opening_amount.num() != 0: + initialize_split(new_book, opening_amount, opening_account, + opening_trans) + + opening_trans.SetDate( *OPENING_DATE ) + opening_trans.SetCurrency(currency) + opening_trans.SetDescription("Opening Balance") + + return simple_opening_name_used + +def main(): + original_book_session = Session(argv[1], False) + new_book_session = Session(argv[2], True) + new_book = new_book_session.get_book() + new_book_root = new_book.get_root_account() + + commodtable = new_book.get_table() + # we discovered that if we didn't have this save early on, there would + # be trouble later + new_book_session.save() + + opening_balance_per_currency = {} + recursivly_build_account_tree( + original_book_session.get_book().get_root_account(), + new_book_root, + new_book, + commodtable, + opening_balance_per_currency, + ACCOUNT_TYPES_TO_OPEN + ) + + (namespace, mnemonic) = PREFERED_CURRENCY_FOR_SIMPLE_OPENING_BALANCE + if (namespace, mnemonic) in opening_balance_per_currency: + opening_trans, opening_amount = opening_balance_per_currency[ + (namespace, mnemonic)] + simple_opening_name_used = create_opening_balance_transaction( + commodtable, namespace, mnemonic, + new_book_root, new_book, + opening_trans, opening_amount, + False ) + del opening_balance_per_currency[ + PREFERED_CURRENCY_FOR_SIMPLE_OPENING_BALANCE] + else: + simple_opening_name_used = False + + for (namespace, mnemonic), (opening_trans, opening_amount) in \ + opening_balance_per_currency.iteritems() : + simple_opening_name_used = create_opening_balance_transaction( + commodtable, namespace, mnemonic, + new_book_root, new_book, + opening_trans, opening_amount, + simple_opening_name_used ) + + new_book_session.save() + new_book_session.end() + original_book_session.end() + + +if __name__ == "__main__": + main() + + Property changes on: src/optional/python-bindings/example_scripts/new_book_with_opening_balances.py ___________________________________________________________________ Added: svn:mergeinfo Index: src/optional/python-bindings/Makefile.am =================================================================== --- src/optional/python-bindings/Makefile.am (.../trunk) (revision 528) +++ src/optional/python-bindings/Makefile.am (.../branches/python-bindings) (revision 528) @@ -52,6 +52,10 @@ example_scripts/simple_book.py \ example_scripts/simple_session.py \ example_scripts/simple_test.py \ + example_scripts/simple_sqlite_create.py \ + example_scripts/change_tax_code.py \ + example_scripts/account_analysis.py \ + example_scripts/ new_book_with_opening_balances.py \ glib.i MAINTAINERCLEANFILES = gnucash-core.c
_______________________________________________ gnucash-devel mailing list gnucash-devel@gnucash.org https://lists.gnucash.org/mailman/listinfo/gnucash-devel