From 61add2482512710d95a4532d785bd913fe23c4f8 Mon Sep 17 00:00:00 2001 From: Andrew Ruthven Date: Mon, 30 May 2011 21:44:38 +1200 Subject: [PATCH] Start of todo. --- find_todo.py | 261 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100755 find_todo.py diff --git a/find_todo.py b/find_todo.py new file mode 100755 index 0000000..2270426 --- /dev/null +++ b/find_todo.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python + +# account_analysis.py -- Output all the credits and debits on an account +# +# Copyright (C) 2009, 2010 ParIT Worker Co-operative +# 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 + +## @file +# @brief Output all the credits and debits on an account +# @author Mark Jenkins, ParIT Worker Co-operative +# @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 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(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() + + +if __name__ == "__main__": main() + + -- 2.30.2