|
GnuCash 2.4.99
|
00001 #!/usr/bin/env python 00002 00003 # account_analysis.py -- Output all the credits and debits on an account 00004 # 00005 # Copyright (C) 2009, 2010 ParIT Worker Co-operative <transparency@parit.ca> 00006 # This program is free software; you can redistribute it and/or 00007 # modify it under the terms of the GNU General Public License as 00008 # published by the Free Software Foundation; either version 2 of 00009 # the License, or (at your option) any later version. 00010 # 00011 # This program is distributed in the hope that it will be useful, 00012 # but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00014 # GNU General Public License for more details. 00015 # 00016 # You should have received a copy of the GNU General Public License 00017 # along with this program; if not, contact: 00018 # Free Software Foundation Voice: +1-617-542-5942 00019 # 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 00020 # Boston, MA 02110-1301, USA gnu@gnu.org 00021 # 00022 # @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca> 00023 00024 ## @file 00025 # @brief Output all the credits and debits on an account 00026 # @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca> 00027 # @ingroup python_bindings_examples 00028 00029 # python imports 00030 from sys import argv, stdout 00031 from datetime import date, timedelta 00032 from bisect import bisect_right 00033 from decimal import Decimal 00034 from math import log10 00035 import csv 00036 00037 # gnucash imports 00038 from gnucash import Session, GncNumeric, Split 00039 00040 # Invoke this script like the following example 00041 # $ gnucash-env python account_analysis.py gnucash_file.gnucash \ 00042 # 2010 1 monthly 12 \ 00043 # debits-show credits-show Assets 'Test Account' 00044 # 00045 # That will do an analysis on the account 'Assets:Test Account' from 00046 # gnucash_file.xac, all of the debits and all of the credits will be shown 00047 # and summed on for 12 monthly periods starting from January (1st month) 2010 00048 # 00049 # if you just want to see the credit and debit sums for each period, use 00050 # the debits-noshow and credits-noshow argument 00051 # 00052 # The output goes to stdout and is in csv format. 00053 # 00054 # Account path arugments are space separated, so you need to quote parts of 00055 # the account path with spaces in them 00056 # 00057 # available period types are monthly quarterly and yearly 00058 # 00059 # At the moment this script only supports GnuCash files of the sqllite3 type 00060 # its an easy edit to switch to xml: etc... 00061 00062 00063 # a dictionary with a period name as key, and number of months in that 00064 # kind of period as the value 00065 PERIODS = {"monthly": 1, 00066 "quarterly": 3, 00067 "yearly": 12 } 00068 00069 NUM_MONTHS = 12 00070 00071 ONE_DAY = timedelta(days=1) 00072 00073 DEBITS_SHOW, CREDITS_SHOW = ("debits-show", "credits-show") 00074 00075 ZERO = Decimal(0) 00076 00077 def gnc_numeric_to_python_Decimal(numeric): 00078 negative = numeric.negative_p() 00079 if negative: 00080 sign = 1 00081 else: 00082 sign = 0 00083 copy = GncNumeric(numeric.num(), numeric.denom()) 00084 result = copy.to_decimal(None) 00085 if not result: 00086 raise Exception("gnc numeric value %s can't be converted to deciaml" % 00087 copy.to_string() ) 00088 digit_tuple = tuple( int(char) 00089 for char in str(copy.num()) 00090 if char != '-' ) 00091 denominator = copy.denom() 00092 exponent = int(log10(denominator)) 00093 assert( (10 ** exponent) == denominator ) 00094 return Decimal( (sign, digit_tuple, -exponent) ) 00095 00096 00097 def next_period_start(start_year, start_month, period_type): 00098 # add numbers of months for the period length 00099 end_month = start_month + PERIODS[period_type] 00100 # use integer division to find out if the new end month is in a different 00101 # year, what year it is, and what the end month number should be changed 00102 # to. 00103 # Because this depends on modular arithmatic, we have to curvert the month 00104 # values from 1-12 to 0-11 by subtracting 1 and putting it back after 00105 # 00106 # the really cool part is that this whole thing is implemented without 00107 # any branching; if end_month > NUM_MONTHS 00108 # 00109 # A the super nice thing is that you can add all kinds of period lengths 00110 # to PERIODS 00111 end_year = start_year + ( (end_month-1) / NUM_MONTHS ) 00112 end_month = ( (end_month-1) % NUM_MONTHS ) + 1 00113 00114 return end_year, end_month 00115 00116 00117 def period_end(start_year, start_month, period_type): 00118 if period_type not in PERIODS: 00119 raise Exception("%s is not a valid period, should be %s" % ( 00120 period_type, str(PERIODS.keys()) ) ) 00121 00122 end_year, end_month = next_period_start(start_year, start_month, 00123 period_type) 00124 00125 # last step, the end date is day back from the start of the next period 00126 # so we get a period end like 00127 # 2010-03-31 for period starting 2010-01 instead of 2010-04-01 00128 return date(end_year, end_month, 1) - ONE_DAY 00129 00130 00131 def generate_period_boundaries(start_year, start_month, period_type, periods): 00132 for i in xrange(periods): 00133 yield ( date(start_year, start_month, 1), 00134 period_end(start_year, start_month, period_type) ) 00135 start_year, start_month = next_period_start(start_year, start_month, 00136 period_type) 00137 00138 def account_from_path(top_account, account_path, original_path=None): 00139 if original_path==None: original_path = account_path 00140 account, account_path = account_path[0], account_path[1:] 00141 00142 account = top_account.lookup_by_name(account) 00143 if account.get_instance() == None: 00144 raise Exception( 00145 "path " + ''.join(original_path) + " could not be found") 00146 if len(account_path) > 0 : 00147 return account_from_path(account, account_path, original_path) 00148 else: 00149 return account 00150 00151 00152 def main(): 00153 00154 if len(argv) < 10: 00155 print 'not enough parameters' 00156 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} ' 00157 print 'examples:\n' 00158 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" 00159 print "gnucash-env python account_analysis.py '/home/username/test.gnucash' 2010 1 monthly 12 debits-show credits-show Assets 'Test Account'\n" 00160 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" 00161 print "gnucash-env python account_analysis.py '/home/username/test.gnucash' 2011 3 quarterly 2 debits-noshow credits-show Liabilities 'First Level' 'Second Level" 00162 return 00163 00164 try: 00165 (gnucash_file, start_year, start_month, period_type, periods, 00166 debits_show, credits_show) = argv[1:8] 00167 start_year, start_month, periods = [int(blah) 00168 for blah in (start_year, start_month, 00169 periods) ] 00170 00171 debits_show = debits_show == DEBITS_SHOW 00172 credits_show = credits_show == CREDITS_SHOW 00173 00174 account_path = argv[8:] 00175 00176 gnucash_session = Session(gnucash_file, is_new=False) 00177 root_account = gnucash_session.book.get_root_account() 00178 account_of_interest = account_from_path(root_account, account_path) 00179 00180 # a list of all the periods of interest, for each period 00181 # keep the start date, end date, a list to store debits and credits, 00182 # and sums for tracking the sum of all debits and sum of all credits 00183 period_list = [ 00184 [start_date, end_date, 00185 [], # debits 00186 [], # credits 00187 ZERO, # debits sum 00188 ZERO, # credits sum 00189 ] 00190 for start_date, end_date in generate_period_boundaries( 00191 start_year, start_month, period_type, periods) 00192 ] 00193 # a copy of the above list with just the period start dates 00194 period_starts = [e[0] for e in period_list ] 00195 00196 # insert and add all splits in the periods of interest 00197 for split in account_of_interest.GetSplitList(): 00198 trans = split.parent 00199 trans_date = date.fromtimestamp(trans.GetDate()) 00200 00201 # use binary search to find the period that starts before or on 00202 # the transaction date 00203 period_index = bisect_right( period_starts, trans_date ) - 1 00204 00205 # ignore transactions with a date before the matching period start 00206 # (after subtracting 1 above start_index would be -1) 00207 # and after the last period_end 00208 if period_index >= 0 and \ 00209 trans_date <= period_list[len(period_list)-1][1]: 00210 00211 # get the period bucket appropriate for the split in question 00212 period = period_list[period_index] 00213 00214 # more specifically, we'd expect the transaction date 00215 # to be on or after the period start, and before or on the 00216 # period end, assuming the binary search (bisect_right) 00217 # assumptions from above are are right.. 00218 # 00219 # in other words, we assert our use of binary search 00220 # and the filtered results from the above if provide all the 00221 # protection we need 00222 assert( trans_date>= period[0] and trans_date <= period[1] ) 00223 00224 split_amount = gnc_numeric_to_python_Decimal(split.GetAmount()) 00225 00226 # if the amount is negative, this is a credit 00227 if split_amount < ZERO: 00228 debit_credit_offset = 1 00229 # else a debit 00230 else: 00231 debit_credit_offset = 0 00232 00233 # store the debit or credit Split with its transaction, using the 00234 # above offset to get in the right bucket 00235 # 00236 # if we wanted to be really cool we'd keep the transactions 00237 period[2+debit_credit_offset].append( (trans, split) ) 00238 00239 # add the debit or credit to the sum, using the above offset 00240 # to get in the right bucket 00241 period[4+debit_credit_offset] += split_amount 00242 00243 csv_writer = csv.writer(stdout) 00244 csv_writer.writerow( ('period start', 'period end', 'debits', 'credits') ) 00245 00246 def generate_detail_rows(values): 00247 return ( 00248 ('', '', '', '', trans.GetDescription(), 00249 gnc_numeric_to_python_Decimal(split.GetAmount())) 00250 for trans, split in values ) 00251 00252 00253 for start_date, end_date, debits, credits, debit_sum, credit_sum in \ 00254 period_list: 00255 csv_writer.writerow( (start_date, end_date, debit_sum, credit_sum) ) 00256 00257 if debits_show and len(debits) > 0: 00258 csv_writer.writerow( 00259 ('DEBITS', '', '', '', 'description', 'value') ) 00260 csv_writer.writerows( generate_detail_rows(debits) ) 00261 csv_writer.writerow( () ) 00262 if credits_show and len(credits) > 0: 00263 csv_writer.writerow( 00264 ('CREDITS', '', '', '', 'description', 'value') ) 00265 csv_writer.writerows( generate_detail_rows(credits) ) 00266 csv_writer.writerow( () ) 00267 00268 # no save needed, we're just reading.. 00269 gnucash_session.end() 00270 except: 00271 if not gnucash_session == None: 00272 gnucash_session.end() 00273 00274 raise 00275 00276 if __name__ == "__main__": main() 00277 00278
1.7.4