gncmerge.py
changeset 1 db0e341384e1
--- /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)