GnuCash 2.4.99
account_analysis.py
Go to the documentation of this file.
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 
 All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Defines