summaryrefslogtreecommitdiff
path: root/ugbudget/ugbudget.py
blob: f75a44495bf998ed81f358c8a3d92f6b7eedc9b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import argparse
import collections
import csv
import gnucashxml
import os
import sys
from decimal import Decimal

def main():
    parser = argparse.ArgumentParser(description='The Usable GnuCash Budget Tool')

    cmd_group = parser.add_mutually_exclusive_group(required=True)
    cmd_group.add_argument('--create-tags', action='store_true',
                           help='Update tags-file with any unmapped accounts')
    cmd_group.add_argument('--report', action='store_true',
                           help='Produce a budget vs. actuals report')
    parser.add_argument('tags_file', metavar='tags-file',
                        help='The mapping of GnuCash accounts to budget tags')
    parser.add_argument('data_file', metavar='gnucash-file',
                        help='The GnuCash data file to process')

    args = parser.parse_args()

    book = gnucashxml.from_filename(args.data_file)

    if args.create_tags:
        create_tags(book, args.tags_file)
    elif args.report:
        report(book, args.tags_file)

def read_tags(filename):
    '''
    Read a TSV file where each row maps a GnuCash account to a tag. A tag is
    one or more tab-separated values which are treated as budget categories,
    subcategories, etc.
    '''
    tag_header = ('category',)
    tags = {}
    if os.path.isfile(filename):
        with open(filename, 'rb') as f:
            reader = csv.reader(f, csv.excel_tab)
            for row in reader:
                if row[0] == "account":
                    tag_header = row[1:]
                else:
                    tags[row[0]] = tuple(row[1:])
    return (tag_header, tags)

def write_tags(filename, tag_header, tags):
    '''
    Write a tags file as described by read_tags().
    '''
    with open(filename, 'wb') as f:
        writer = csv.writer(f, csv.excel_tab)
        writer.writerow(['account'] + list(tag_header))
        for account in sorted(tags):
            writer.writerow([account] + list(tags[account]))

def create_tags(book, tags_file):
    '''
    Read a GnuCash data file and add any new, unmapped accounts to tags_file.
    '''
    (tag_header, tags) = read_tags(tags_file)
    for (acc, children, splits) in book.walk():
        if not children:
            if acc.actype == "INCOME" or acc.actype == "EXPENSE":
                acc_name = gnucash_account_fullname(acc)
                if acc_name not in tags:
                    tags[acc_name] = tag_header
    write_tags(tags_file, tag_header, tags)

def report(book, tags_file):
    (tag_header, tags) = read_tags(tags_file)
    report = collections.defaultdict(lambda: collections.defaultdict(Decimal))
    for (acc, children, splits) in book.walk():
        acc_name = gnucash_account_fullname(acc)
        if acc_name in tags:
            mapping = tuple([acc.actype] + list(tags[acc_name]))
            for split in splits:
                date = split.transaction.date.strftime("%Y-%m-01")
                report[date][mapping] += split.value.copy_negate()

    writer = csv.writer(sys.stdout, csv.excel_tab)
    writer.writerow(['month', 'account_type'] + list(tag_header) + ['value'])
    for month in sorted(report):
        for (mapping, value) in report[month].iteritems():
            writer.writerow([month] + list(mapping) + [value])

def gnucash_account_fullname(acc, partial=''):
    if acc.parent:
        if partial:
            partial = "%s:%s" % (acc.name, partial)
        else:
            partial = acc.name
        return gnucash_account_fullname(acc.parent, partial)
    else:
        return partial

main()