Add gncmerge.
authorFabien Ninoles <fabien@tzone.org>
Sat, 23 May 2015 22:34:03 -0400
changeset 1 db0e341384e1
parent 0 811cd790a493
child 2 64f48a8c758c
Add gncmerge.
gncmerge.py
specs.org
--- /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)
--- a/specs.org	Sun Jul 14 22:47:29 2013 -0400
+++ b/specs.org	Sat May 23 22:34:03 2015 -0400
@@ -2,31 +2,26 @@
 
 ** Definition
 
-This tool should allow to:
-
-- Budget your future expenses and incomes.
-  - Report your budget from months to months.
-  - Create recurring budget transaction.
-  - Create morgage/investment prediction.
-- Import your current transactions journal and show how it get over/under your budget.
-- Allow you to adjust your budget accordingly.
-  - That should also be reflected on recurring transactions.
-- Allow to create alternate scenario based on present budget.
-- For every expenses, there must be a budget liabilities.
-- For every income, there must be a budget asset.
-- Since Equity = Asset - Liabilities, all budget transaction must also balance.
+- A budget is about realizing your dreams, addressing your
+  obligations, inside your potentiel.  It tracks how much
+  are you effective into putting your potentiel inside the 
+  - You sum up your ressources availables for the next period.
+  - You removed your obligations from it.
+  - You share the rest among your dreams.
+  - You track where you spend your resources.
+  - You compare between them.
 
-** Workflow
+* Resources
+It doesn't matter *where* it is, it matters *when* it is available.
+So, there is two types of ressources:
+- Actual, which you can spend now.
+- Future, which isn't available.
+The future ressources isn't important really.  You should concentrate
+on what you have now.
 
-*** New incomes are split into budget envelops
-**** Manually created transaction
-**** Importing from QIF/QFX/CSV
-**** Recurring transaction
-*** New expenses are split into budget envelops
-**** Manual
-**** Importing
-**** Recurring
-***** Simple
-***** Recurring with a budget
-*** Reporting
-*** Exporting
+* Obligations
+Obligations are spendings that you cant avoid.  They take your
+ressources and freeze them.  You should prioritize on them.
+There is also two kind of obligations:
+- Predictable: You know when they happen
+- Unpredictable: You don't know when they happen