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()
|