mirror of
https://github.com/Gnucash/gnucash.git
synced 2025-02-25 18:55:30 -06:00
It is split into - /libgnucash (for the non-gui bits) - /gnucash (for the gui) - /common (misc source files used by both) - /bindings (currently only holds python bindings) This is the first step in restructuring the code. It will need much more fine tuning later on.
279 lines
11 KiB
Python
279 lines
11 KiB
Python
#!/usr/bin/env python
|
|
|
|
# account_analysis.py -- Output all the credits and debits on an account
|
|
#
|
|
# Copyright (C) 2009, 2010 ParIT Worker Co-operative <transparency@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 gnu@gnu.org
|
|
#
|
|
# @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca>
|
|
|
|
## @file
|
|
# @brief Output all the credits and debits on an account
|
|
# @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca>
|
|
# @ingroup python_bindings_examples
|
|
|
|
# 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 arguments 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 arithmetic, 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 == 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():
|
|
|
|
if len(argv) < 10:
|
|
print 'not enough parameters'
|
|
print 'usage: account_analysis.py {book url} {start year} {start month, numeric} {period type: monthly, quarterly, or yearly} {number of periods to show, from start year and month} {whether to show debits: debits-show for true, all other values false} {whether to show credits: credits-show for true, all other values false} {space separated account path, as many nested levels as desired} '
|
|
print 'examples:\n'
|
|
print "The following example analyzes 12 months of 'Assets:Test Account' from /home/username/test.gnucash, starting in January of 2010, and shows both credits and debits"
|
|
print "gnucash-env python account_analysis.py '/home/username/test.gnucash' 2010 1 monthly 12 debits-show credits-show Assets 'Test Account'\n"
|
|
print "The following example analyzes 2 quarters of 'Liabilities:First Level:Second Level' from /home/username/test.gnucash, starting March 2011, and shows credits but not debits"
|
|
print "gnucash-env python account_analysis.py '/home/username/test.gnucash' 2011 3 quarterly 2 debits-noshow credits-show Liabilities 'First Level' 'Second Level"
|
|
return
|
|
|
|
try:
|
|
(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(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():
|
|
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()
|
|
except:
|
|
if "gnucash_session" in locals():
|
|
gnucash_session.end()
|
|
|
|
raise
|
|
|
|
if __name__ == "__main__": main()
|
|
|
|
|