summaryrefslogtreecommitdiff
path: root/timeclock
diff options
context:
space:
mode:
Diffstat (limited to 'timeclock')
-rwxr-xr-xtimeclock461
1 files changed, 461 insertions, 0 deletions
diff --git a/timeclock b/timeclock
new file mode 100755
index 00000000..496c3471
--- /dev/null
+++ b/timeclock
@@ -0,0 +1,461 @@
+#!/usr/bin/env python
+
+# timeclock, a time-keeping program based on the Ledger library
+#
+# Copyright (c) 2003-2004, New Artisans LLC. All rights reserved.
+#
+# This program is made available under the terms of the BSD Public
+# License. See the LICENSE file included with the distribution for
+# details and disclaimer.
+#
+# This program implements a simple timeclock, using the identical
+# format as my timeclock.el module for Emacs (which is part of the
+# Emacs 21 distribution). This allows you to use either this script
+# or that module for creating time events.
+#
+# Usage is very simple: Set the environment variable TIMELOG to the
+# path to your timelog file (if this variable is unset, any events
+# created will simply be printed to stdout). Once this variable is
+# set:
+#
+# timeclock in "project" what aspect of the project will I do today
+# timeclock out what did I accomplish
+#
+# The description text is optional, but the project is required when
+# clocking in. This project should be a account name, which means it
+# can use ":" to separate the project from the task, for example:
+#
+# timeclock in Client:Meetings at the code review meeting
+#
+# To generate a balance report of time spent, use "timeclock" with no
+# arguments, or "timeclock balance". The options available are the
+# same as those used for ledger.
+
+import os
+import sys
+import string
+import time
+
+true, false = 1, 0
+
+from ledger import *
+
+home = os.getenv ("HOME")
+config.init_file = home + "/.timeclockrc";
+config.cache_file = home + "/.timeclock-cache";
+
+# Define some functions for reporting time quantities
+
+workday = 8 * 60 * 60 # length of a nominal workday
+
+def secstr (secs):
+ return "%d:%02d" % (abs (secs) / 60 / 60, (abs (secs) / 60) % 60)
+
+def daystr (amt):
+ dy = int (amt) / int (workday)
+ amt = amt - (dy * workday)
+ if dy >= 5:
+ wk = dy / 5
+ dy = dy % 5
+ if dy: amt = "%sw %sd %s" % (wk, dy, secstr (amt))
+ else: amt = "%sw %s" % (wk, secstr (amt))
+ else:
+ if dy: amt = "%sd %s" % (dy, secstr (amt))
+ else: amt = secstr (amt)
+ return amt
+
+def adaystr (details):
+ result = ""
+ for amt in account_xdata(details.account).total:
+ if amt.commodity ().symbol == "s":
+ result = daystr (float (amt))
+ break
+ return result
+
+config.amount_expr = "{1.00h}*(a/{3600.00h})"
+config.total_expr = "{1.00h}*(O/{3600.00h})"
+config.balance_format = "%20@adaystr() %8T %2_%-a\n";
+
+# Help text specific to timeclock
+
+def show_version (arg):
+ print """Timeclock, a command-line timekeeping tool
+
+Copyright (c) 2003-2004, New Artisans LLC. All rights reserved.
+
+This program is made available under the terms of the BSD Public
+License. See the LICENSE file included with the distribution for
+details and disclaimer."""
+ sys.exit (0)
+
+def option_help (arg):
+ print """usage: timeclock [options] COMMAND [ACCT REGEX]...
+
+Basic options:
+ -h, --help display this help text
+ -v, --version show version information
+ -i, --init FILE initialize ledger by loading FILE (def: ~/.ledgerrc)
+ --cache FILE use FILE as a binary cache when --file is not used
+ -f, --file FILE read ledger data from FILE
+ -o, --output FILE write output to FILE
+
+Report filtering:
+ -b, --begin DATE set report begin date
+ -e, --end DATE set report end date
+ -c, --current show only current and past entries (not future)
+ -C, --cleared consider only cleared transactions
+ -U, --uncleared consider only uncleared transactions
+ -R, --real consider only real (non-virtual) transactions
+ -Z, --actual consider only actual (non-automated) transactions
+ -r, --related calculate report using related transactions
+
+Output customization:
+ -F, --format STR use STR as the format; for each report type, use:
+ --balance-format --register-format
+ --plot-amount-format --plot-total-format
+ -y, --date-format STR use STR as the date format (def: %Y/%m/%d)
+ --wide for the default register report, use 132 columns
+ -E, --empty balance: show accounts with zero balance
+ -n, --collapse register: collapse entries with multiple transactions
+ -s, --subtotal balance: show sub-accounts; register: show subtotals
+ -S, --sort EXPR sort report according to the value expression EXPR
+ -p, --period STR report using the given period
+ --period-sort EXPR sort each report period's entries by EXPR
+ --dow show a days-of-the-week report
+ -W, --weekly show weekly sub-totals
+ -M, --monthly show monthly sub-totals
+ -Y, --yearly show yearly sub-totals
+ -l, --limit EXPR calculate only transactions matching EXPR
+ -d, --display EXPR display only transactions matching EXPR
+ -t, --amount EXPR use EXPR to calculate the displayed amount
+ -T, --total EXPR use EXPR to calculate the displayed total
+ -j, --amount-data print only raw amount data (useful for scripting)
+ -J, --total-data print only raw total data
+
+Commodity reporting:
+ -A, --average report average transaction amount
+ -D, --deviation report deviation from the average
+
+Commands:
+ balance [REGEXP]... show balance totals for matching accounts
+ register [REGEXP]... show register of matching events"""
+ sys.exit (0)
+
+# This call registers all of the default command-line options that
+# Ledger supports into the option handling mechanism. Skip this call
+# if you wish to do all of your own processing -- in which case simply
+# modify the 'config' object however you like.
+
+add_config_option_handlers ()
+
+add_option_handler ("help", "h", option_help)
+add_option_handler ("version", "v", show_version)
+
+# Process the command-line arguments, test whether caching should be
+# enabled, and then process any option settings from the execution
+# environment. Some historical environment variable names are also
+# supported.
+
+args = process_arguments (sys.argv[1:])
+config.use_cache = not config.data_file
+process_environment (os.environ, "TIMECLOCK_")
+
+if os.environ.has_key ("TIMELOG"):
+ process_option ("file", os.getenv ("TIMELOG"))
+
+# The command word is in the first argument. Canonicalize it to a
+# unique, simple form that the remaining code can use to find out
+# which command was specified.
+
+if len (args) == 0:
+ args = ["balance"]
+
+command = args.pop (0);
+
+if command == "balance" or command == "bal" or command == "b":
+ command = "b"
+elif command == "register" or command == "reg" or command == "r":
+ command = "r"
+elif command == "entry":
+ command = "e"
+elif command == "in" or command == "out":
+ if config.data_file:
+ log = open (config.data_file, "a")
+ else:
+ log = sys.stdout
+
+ if command == "in":
+ if len (args) == 0:
+ print "A project name is required when clocking in."
+ sys.exit (1)
+ log.write ("i %s %s" % (time.strftime ("%Y/%m/%d %H:%M:%S"),
+ args.pop (0)))
+ if len (args) > 0:
+ log.write (" %s\n" % string.join (args, " "))
+ else:
+ log.write ("o %s" % time.strftime ("%Y/%m/%d %H:%M:%S"))
+ if len (args) > 0:
+ log.write (" %s" % string.join (args, " "))
+
+ log.write ("\n")
+ log.close ()
+ sys.exit (0)
+else:
+ print "Unrecognized command:", command
+ sys.exit (1)
+
+# Create the main journal object, into which all entries will be
+# recorded. Once done, the 'journal' may be iterated to yield those
+# entries, in the same order as which they appeared in the journal
+# file.
+
+journal = Journal ()
+
+# This parser is intended only for timelog files.
+
+class Event:
+ def __init__(self, kind, when, desc):
+ self.kind = kind
+ self.when = when
+ self.desc = desc
+
+class Interval:
+ def __init__(self, begin, end):
+ self.begin = begin
+ self.end = end
+
+ def length(self):
+ "Return the length of the interval in seconds."
+ return self.end.when - self.begin.when
+
+def parse_timelog(path, journal):
+ import re
+ if not os.path.exists (path):
+ print "Cannot read timelog file '%s'" % path
+ sys.exit (1)
+ file = open(path)
+ history = []
+ begin = None
+ linenum = 0
+ for line in file:
+ linenum += 1
+ match = re.match("([iIoO])\s+([0-9/]+\s+[0-9:]+)\s*(.+)", line)
+ if match:
+ (kind, when, desc) = match.groups()
+ when = time.strptime(when, "%Y/%m/%d %H:%M:%S")
+ when = time.mktime(when)
+ event = Event(kind, when, desc)
+ if kind == "i" or kind == "I":
+ begin = event
+ else:
+ if begin.desc:
+ match = re.match ("(.+?) (.+)", begin.desc)
+ if match:
+ acct = match.group (1)
+ desc = match.group (2)
+ else:
+ acct = begin.desc
+ desc = ""
+ else:
+ acct = "Misc"
+ desc = event.desc
+
+ l = Interval(begin, event).length ()
+ e = Entry ()
+ e.date = int (begin.when)
+ e.payee = desc
+
+ x = Transaction (journal.find_account (acct),
+ Amount ("%ss" % l), TRANSACTION_VIRTUAL)
+ e.add_transaction (x)
+
+ if not journal.add_entry (e):
+ print "%s, %d: Failed to entry" % (path, linenum)
+ sys.exit (1)
+
+parse_timelog (config.data_file, journal)
+
+# Now that everything has been correctly parsed (parse_ledger_data
+# would have thrown an exception if not), we can take time to further
+# process the configuration options. This changes the configuration a
+# bit based on previous option settings, the command word, and the
+# remaining arguments.
+
+if command == "b" and \
+ config.amount_expr == "{1.00h}*(a/{3600.00h})":
+ config.amount_expr = "a"
+
+config.process_options (command, args);
+
+# Determine the format string to used, based on the command.
+
+if config.format_string:
+ format = config.format_string
+elif command == "b":
+ format = config.balance_format
+elif command == "r":
+ format = config.register_format
+else:
+ format = config.print_format
+
+# The following two classes are responsible for outputing transactions
+# and accounts to the user. There are corresponding C++ versions to
+# these, but they rely on I/O streams, which Boost.Python does not
+# provide a conversion layer for.
+
+class FormatTransactions (TransactionHandler):
+ last_entry = None
+ output = None
+
+ def __init__ (self, fmt):
+ try:
+ i = string.index (fmt, '%/')
+ self.formatter = Format (fmt[: i])
+ self.nformatter = Format (fmt[i + 2 :])
+ except ValueError:
+ self.formatter = Format (fmt)
+ self.nformatter = None
+
+ self.last_entry = None
+
+ if config.output_file:
+ self.output = open (config.output_file, "w")
+ else:
+ self.output = sys.stdout
+
+ TransactionHandler.__init__ (self)
+
+ def __del__ (self):
+ if config.output_file:
+ self.output.close ()
+
+ def flush (self):
+ self.output.flush ()
+
+ def __call__ (self, xact):
+ if not transaction_has_xdata (xact) or \
+ not transaction_xdata (xact).dflags & TRANSACTION_DISPLAYED:
+ if self.nformatter is not None and \
+ self.last_entry is not None and \
+ xact.entry == self.last_entry:
+ self.output.write (self.nformatter.format (xact))
+ else:
+ self.output.write (self.formatter.format (xact))
+ self.last_entry = xact.entry
+ transaction_xdata (xact).dflags |= TRANSACTION_DISPLAYED
+
+class FormatAccounts (AccountHandler):
+ output = None
+
+ def __init__ (self, fmt, pred):
+ self.formatter = Format (fmt)
+ self.predicate = AccountPredicate (pred)
+
+ if config.output_file:
+ self.output = open (config.output_file, "w")
+ else:
+ self.output = sys.stdout
+
+ AccountHandler.__init__ (self)
+
+ def __del__ (self):
+ if config.output_file:
+ self.output.close ()
+
+ def final (self, account):
+ if account_has_xdata (account):
+ xdata = account_xdata (account)
+ if xdata.dflags & ACCOUNT_TO_DISPLAY:
+ print "-------------------- ---------"
+ xdata.value = xdata.total
+ self.output.write (self.formatter.format (account))
+
+ def flush (self):
+ self.output.flush ()
+
+ def __call__ (self, account):
+ if display_account (account, self.predicate):
+ if not account.parent:
+ account_xdata (account).dflags |= ACCOUNT_TO_DISPLAY
+ else:
+ self.output.write (self.formatter.format (account))
+ account_xdata (account).dflags |= ACCOUNT_DISPLAYED
+
+# Set the final transaction handler: for balances and equity reports,
+# it will simply add the value of the transaction to the account's
+# xdata, which is used a bit later to report those totals. For all
+# other reports, the transaction data is sent to the configured output
+# location (default is sys.stdout).
+
+if command == "b":
+ handler = SetAccountValue ()
+else:
+ handler = FormatTransactions (format)
+
+# Chain transaction filters on top of the base handler. Most of these
+# filters customize the output for reporting. None of this is done
+# for balance or equity reports, which don't need it.
+
+if command != "b":
+ if config.display_predicate:
+ handler = FilterTransactions (handler, config.display_predicate)
+
+ handler = CalcTransactions (handler)
+
+ if config.sort_string:
+ handler = SortTransactions (handler, config.sort_string)
+
+ if config.show_revalued:
+ handler = ChangedValueTransactions (handler, config.show_revalued_only)
+
+ if config.show_collapsed:
+ handler = CollapseTransactions (handler);
+
+if config.show_subtotal and not (command == "b" or command == "E"):
+ handler = SubtotalTransactions (handler)
+
+if config.days_of_the_week:
+ handler = DowTransactions (handler)
+elif config.by_payee:
+ handler = ByPayeeTransactions (handler)
+
+if config.report_period:
+ handler = IntervalTransactions (handler, config.report_period,
+ config.report_period_sort)
+ handler = SortTransactions (handler, "d")
+
+# The next two transaction filters are used by all reports.
+
+if config.show_inverted:
+ handler = InvertTransactions (handler)
+
+if config.show_related:
+ handler = RelatedTransactions (handler, config.show_all_related)
+
+if config.predicate:
+ handler = FilterTransactions (handler, config.predicate)
+
+if config.comm_as_payee:
+ handler = SetCommAsPayee (handler)
+
+# Walk the journal's entries, and pass each entry's transaction to the
+# handler chain established above.
+
+walk_entries (journal, handler)
+
+# Flush the handlers, causing them to output whatever data is still
+# pending.
+
+handler.flush ()
+
+# For the balance and equity reports, the account totals now need to
+# be displayed. This is different from outputting transactions, in
+# that we are now outputting account totals to display a summary of
+# the transactions that were just walked.
+
+if command == "b":
+ acct_formatter = FormatAccounts (format, config.display_predicate)
+ sum_accounts (journal.master)
+ walk_accounts (journal.master, acct_formatter, config.sort_string)
+ acct_formatter.final (journal.master)
+ acct_formatter.flush ()