--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/gncmerge.py Sat May 23 22:34:03 2015 -0400
@@ -0,0 +1,228 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import gnucash
+import datetime
+import logging
+
+ZERO = gnucash.GncNumeric()
+
+def recurse_all_splits(account):
+ for acc in account.get_descendants():
+ for split in acc.GetSplitList():
+ yield split
+
+def check_in_date(start_date, end_date, d):
+ return start_date <= d and d <= end_date
+
+def check_split_date_func(start_date, end_date):
+ def f(split):
+ parent = split.GetParent()
+ if parent == None:
+ return False
+ d = datetime.date.fromtimestamp(split.GetParent().GetDate())
+ return start_date <= d and d <= end_date
+ return f
+
+def generate_splits(root, start, end):
+ f = check_split_date_func(start, end)
+ return (split for split in recurse_all_splits(root) if f(split))
+
+def generate_summary_amounts(root, start, end):
+ prevAccount = None
+ prevAccountName = "<None>"
+ zero = (0, ZERO, ZERO)
+ pos = zero
+ neg = zero
+ for split in generate_splits(root, start, end):
+ account = split.account
+ amount = split.GetAmount()
+ value = split.GetValue()
+ name = account.get_full_name()
+ is_neg = amount.negative_p()
+ if account.get_full_name() != prevAccountName:
+ if prevAccount != None:
+ yield (prevAccount,) + pos
+ yield (prevAccount,) + neg
+ prevAccount = account
+ prevAccountName = name
+ if is_neg:
+ pos = zero
+ neg = (1, amount, value)
+ else:
+ pos = (1, amount, value)
+ neg = zero
+ else:
+ tmp = neg if is_neg else pos
+ tmp = (tmp[0]+1,
+ tmp[1].add(amount, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT),
+ tmp[2].add(value, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT))
+ if is_neg:
+ neg = tmp
+ else:
+ pos = tmp
+ if prevAccount != None:
+ yield (prevAccount,) + pos
+ yield (prevAccount,) + neg
+
+def transaction_to_string(trans, currency):
+ result = str(datetime.date.fromtimestamp(trans.GetDate()))
+ result += " " + trans.GetDescription() + "\n"
+ for split in trans.GetSplitList():
+ result += "\t" + split.account.get_full_name() + " "
+ result += " "
+ c = split.account.GetCommodity()
+ a = split.GetAmount()
+ result += str(a.to_double()) + " " + c.get_nice_symbol()
+ if not currency.equal(c):
+ v = split.GetValue()
+ result += " => " + str(v.to_double()) + " " + currency.get_nice_symbol()
+ result += " ( " + str(v.div(a, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT).to_double())
+ result += " " + c.get_nice_symbol() + " )"
+ result += "\n"
+ imbl = trans.GetImbalanceValue()
+ result += "Balance: " + str(imbl.to_double()) + " " + currency.get_nice_symbol()
+ if not imbl.equal(ZERO):
+ for c, v in trans.GetImbalance():
+ result += "\n\t"
+ result += " => " + str(v.to_double()) + " " + c.get_nice_symbol()
+ return result
+
+def delete_transactions(root, start, end, filt):
+ removed = 0
+ for split in generate_splits(root, start, end):
+ t = split.GetParent()
+ if filt(t):
+ removed += t.CountSplits()
+ t.Destroy()
+ logging.info("%d splits removed.", removed)
+
+def transaction_not_equal_p(trans):
+ gtrans = trans.GetGUID()
+ def f(t):
+ return not gtrans.equal(t.GetGUID())
+ return f
+
+def generate_summary_splits(root, start, end):
+ book = root.get_book()
+ for account, count, amount, value in generate_summary_amounts(root, start, end):
+ if not amount.zero_p():
+ split = gnucash.Split(book)
+ split.SetAccount(account)
+ price = value.div(amount, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT)
+ split.SetSharePriceAndAmount(price, amount)
+ yield split, count
+
+def create_summary(root, start, end, currency, title):
+ logging.info("Creating summary %s for %s between %s and %s",
+ title, root.get_full_name(), start, end)
+ book = root.get_book()
+ trans = gnucash.Transaction(book)
+ trans.BeginEdit()
+ trans.SetDate(end.day, end.month, end.year)
+ trans.SetDescription(title)
+ trans.SetCurrency(currency)
+ total = 0
+ for split, count in generate_summary_splits(root, start, end):
+ split.SetParent(trans)
+ total += count
+ if trans.CountSplits() > 0:
+ logging.info("%d splits summary into one transaction '%s'.", total, title)
+ logging.debug(transaction_to_string(trans, currency))
+ assert(trans.IsBalanced())
+ trans.CommitEdit()
+ if total > 0:
+ delete_transactions(root, start, end, transaction_not_equal_p(trans))
+ else:
+ logging.info("No splits found for this date")
+
+def make_summary(root, currency, start, end, title):
+ create_summary(root, start, end, currency, title)
+
+def lookup_currency(book, currency):
+ return book.get_table().lookup("CURRENCY", currency)
+
+## Datetime support
+ONEDAY = datetime.timedelta(days=1)
+
+def first_day_of_month(d):
+ return datetime.date(d.year, d.month, 1)
+def first_day_of_year(d):
+ return datetime.date(d.year, 1, 1)
+
+def next_day(d):
+ return d + ONEDAY
+def next_month(d):
+ if d.month == 12:
+ return datetime.date(d.year+1, 1, 1)
+ else:
+ return datetime.date(d.year, d.month + 1, 1)
+def next_year(d):
+ return datetime.date(d.year + 1, 1, 1)
+
+def datetime_iter(start, end, next_f):
+ d = start
+ while end == None or d < end:
+ yield d
+ d = next_f(d)
+
+def datetime_iter_range(start, end, next_f):
+ for d in datetime_iter(start, end, next_f):
+ yield d, next_f(d) - ONEDAY
+
+def day_iter(start, end=None):
+ return (d for d in datetime_iter(start, end, next_day))
+def month_iter(start, end=None):
+ return (d for d in datetime_iter(first_day_of_month(start), end, next_month))
+def year_iter(start, end=None):
+ return (d for d in datetime_iter(first_day_of_year(start), end, next_year))
+def day_range_iter(start, end=None):
+ return ((d,d) for d in datetime_iter(start, end, next_day))
+def month_range_iter(start, end=None):
+ return (d for d in datetime_iter_range(first_day_of_month(start), end, next_month))
+def year_range_iter(start, end=None):
+ return (d for d in datetime_iter_range(first_day_of_year(start), end, next_year))
+
+
+def main(filename, title, currency, daterange):
+ try:
+ session = gnucash.Session(filename, is_new=False)
+ root = session.book.get_root_account()
+ curr = lookup_currency(session.book, currency)
+ for s, e in daterange:
+ make_summary(root, curr, s, e, title)
+ session.save()
+ session.end()
+ except:
+ logging.exception("Exception occured")
+ finally:
+ if 'session' in locals():
+ session.destroy()
+
+def usage(msg):
+ import sys
+ print msg
+ print "Usage:", sys.argv[0], \
+ "filename.gnc", "transaction_title", "currency", \
+ "yearly|monthly|daily", \
+ "start_year start_month start_day", \
+ "end_year end_month end_day"
+ exit(-1)
+
+if __name__ == '__main__':
+ # logging.basicConfig(level=logging.DEBUG)
+ import sys
+ if len(sys.argv) != 11:
+ usage("Invalid number of arguments")
+ exit(-1)
+ cmd, filename, title, currency, period, syear, smonth, sday, eyear, emonth, eday = sys.argv
+ start = datetime.date(int(syear), int(smonth), int(sday))
+ end = datetime.date(int(eyear), int(emonth), int(eday))
+ if period == "daily":
+ daterange = day_range_iter(start, end)
+ elif period == "monthly":
+ daterange = month_range_iter(start, end)
+ elif period == "yearly":
+ daterange = year_range_iter(start, end)
+ else:
+ usage("Invalid period '%s'" % (period,))
+ main(filename, title, currency, daterange)