gncmerge.py
author Fabien Ninoles <fabien@tzone.org>
Sat, 23 May 2015 22:34:03 -0400
changeset 1 db0e341384e1
permissions -rw-r--r--
Add gncmerge.

#!/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)