From 43ee6c4887a0c51f935656e829b8a57c6079ebb7 Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Mon, 16 Apr 2018 20:17:51 -0700 Subject: First pass at tagging and report functions. --- .gitignore | 3 ++ setup.py | 22 ++++++++++++ ugbudget/__init__.py | 0 ugbudget/ugbudget.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 .gitignore create mode 100755 setup.py create mode 100644 ugbudget/__init__.py create mode 100644 ugbudget/ugbudget.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48a6e54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +build/ +*.sw[op] diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..6a393bc --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages +setup( + name="ugbudget", + version="0.1", + packages=find_packages(), + + install_requires=['gnucashxml >= 1.0'], + + author="Jesse Morgan", + author_email="jesse@jesterpm.net", + description="Usable Gnucash Budget Tools is a collection of tools to make budgeting with GnuCash simpler.", + license="MIT", + keywords="gnucash budget", + + entry_points={ + 'console_scripts': [ + 'ugbudget = ugbudget.ugbudget:main' + ], + } +) diff --git a/ugbudget/__init__.py b/ugbudget/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ugbudget/ugbudget.py b/ugbudget/ugbudget.py new file mode 100644 index 0000000..f75a444 --- /dev/null +++ b/ugbudget/ugbudget.py @@ -0,0 +1,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() -- cgit v1.2.3