diff -r 811cd790a493 -r db0e341384e1 gncmerge.py --- /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 = "" + 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)