#!/usr/bin/env python # -*- coding: iso-8859-1 -*- # # $Source: /home/blais/repos/cvsroot/tracktools/bin/expenses,v $ # $Id: expenses,v 1.11 2005/08/25 23:21:03 blais Exp $ # # Copyright (C) 2003, Martin Blais # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # """expenses [] Parse expenses sheet and compute some information. """ doc_file_format = """\ File Format ----------- The format of each entry in the expenses file should be following: Category ** ----------- - category ; YYYY-MM-DD ; amount ; currency ; description If the category is in parentheses, then the amounts don't count towards the totals. This can be used to enter related information that is not to be counted for tax purposes (e.g. bank withdrawals). """ # FIXME todo # ---------- # # - add possibility to specify currency rates dynamically __version__ = "$Revision: 1.11 $" __author__ = "Martin Blais " import sys, os import string, re import optparse from datetime import * xchange = { 'CAD': 1.000, 'AUD': 0.93524, 'USD': 1.31465 } def isCatValid(cat): return not (cat.startswith('(') and cat.endswith(')')) class Entry: """Represents a single contiguous unit of work.""" def __init__(self): self.date = None self.amount = 0 self.currency = None self.local_amount = 0 self.description = '' def __str__(self): return '%s ; %7.2f$ ; %s' % \ (self.date, self.local_amount, self.description) def str_orig_header(self): return '%s ; %s ; %s ; %s ; %s ; %s ; %s' % \ ('Date', 'Amount', 'Currency', 'Amount Here', 'Subtotals','Method', 'Description') str_orig_header = classmethod(str_orig_header) def str_orig(self): return '%s ; %7.2f ; %s ; %s ; %s ; %s ; %s' % \ (self.date, self.amount, self.currency, '', '', self.description) def __cmp__(self, other): return cmp(self.start, other.start) cre = re.compile('^(\(?[\w\s/\-]+\)?) \*\*\s*$') dayf = '(\d\d\d\d)-(\d\d)-(\d\d)' sep = '\s*;\s*' ere = re.compile('- ' + '(.*)' + sep + dayf + sep + '([-\d\.]+)' + sep + '([A-Z][A-Z][A-Z])' + sep + '(.*)\s*$') def parseFile(f): """Parse timesheet file and return a list of Entry objects.""" entries_list = [] entries = {} e = None lc = 0 curcat = None while 1: l = f.readline() lc += 1 if not l: break #print '====', l.strip() mo = cre.match(l) if mo: c = mo.group(1).strip() curcat = entries.setdefault(c, []) mo = ere.match(l) if mo: if curcat == None: raise SystemExit( 'Error: you need to specify category before line %d' % lc) e = Entry() # Note: the category was added after, we're not using it yet. cat, y, m, d, a, e.currency, e.description = \ map(string.strip, mo.groups()) try: e.date = date(int(y), int(m), int(d)) except ValueError, e: raise ValueError("Error in converting date: (%s) %s" % \ ((y, m, d), e)) e.amount = float(a) if e.currency not in xchange: raise SystemExit('Error: unknown currency: %s' % e.currency) e.local_amount = e.amount * xchange[e.currency] curcat.append(e) entries_list.append(e) return entries, entries_list def dump(entries): for c, v in entries.iteritems(): print >> sys.stderr, c print >> sys.stderr, '-' * len(c) for e in v: print >> sys.stderr, e print >> sys.stderr print >> sys.stderr def report_summary(entries): print '========================================' print 'Summary' print '========================================' print maxlen = max(map(len, entries.keys())) fmt = ' %%%ds : %%10.2f' % (maxlen + 2) def pout(fun): totpos = 0. totneg = 0. tot = 0. for c, ee in entries.iteritems(): if fun(c): continue totc = 0. for e in ee: totc += e.local_amount if totc >= 0: totpos += totc else: totneg += totc tot += totc print fmt % (c, totc) return totpos, totneg, tot totpos, totneg, tot = pout(lambda x: not isCatValid(x)) print print '-' * (maxlen + 20) print fmt % ('Total', totpos) print fmt % ('Payments', totneg) print '-' * (maxlen + 20) print fmt % ('Outstanding Balance', tot) print print print 'Non-Accountable Categories:' print '-' * (maxlen + 20) pout(lambda x: isCatValid(x)) print def report_cvs(entries): l = 1 print 'Expenses Report' print ';', Entry.str_orig_header() print l += 3 print 'Currency Table' bl = l skeys = xchange.keys() skeys.sort() # keys must be sorted for lookup for k in skeys: v = xchange[k] print '; ', '%s ; %7.5f' % (k, v) print l += 2 + len(xchange) tabdesc = 'B%d:B%d,C%d:C%d' % ((bl, bl+len(xchange)) * 2) for c, v in entries.iteritems(): if not isCatValid(c): continue print print c l += 2 ol = 0 for e in v: print ';', e.str_orig() l += 1 el = l print l += 1 ## Will need to fiddle by hand a bit. ## =C10*lookup(D10,$B$4:$B$6,$C$4:$C$6) def main(): import optparse parser = optparse.OptionParser(__doc__.strip(), version=__version__[1:-1]) parser.add_option('--help-format', action='store_true', help='print help on file format') parser.add_option('-d', '--debug', action='store_true', help='print debugging output') parser.add_option('-c', '--cvs', action='store_true', help="Output expenses report importable CVS format") global opts opts, args = parser.parse_args() if opts.help_format: print doc_file_format sys.exit(0) if len(args) != 1: raise SystemExit('Error: please specify a single expenses file.') fn = args[0] # read entries entries = [] try: if fn == '-': f = sys.stdin else: f = open(fn, 'r') entries, entries_list = parseFile(f) f.close() except IOError, e: raise SystemExit("Error: reading file '%s'" % fn + str(e)) if opts.debug: dump(entries) if not opts.cvs: report_summary(entries) else: report_cvs(entries) if __name__ == '__main__': main()