diff options
author | John Wiegley <johnw@newartisans.com> | 2004-11-08 06:43:11 +0000 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2008-04-13 02:40:47 -0400 |
commit | c9fb11bd60a2170fb896d77ff8d7706f563ad597 (patch) | |
tree | 42bdf09e7d8727ba31d1d8dae9b4eb4b2a605441 /main.py | |
parent | fa2ceaed13c031add578ee8eb33da0c9980b9fb1 (diff) | |
download | fork-ledger-c9fb11bd60a2170fb896d77ff8d7706f563ad597.tar.gz fork-ledger-c9fb11bd60a2170fb896d77ff8d7706f563ad597.tar.bz2 fork-ledger-c9fb11bd60a2170fb896d77ff8d7706f563ad597.zip |
updated to version 2.0
Diffstat (limited to 'main.py')
-rw-r--r-- | main.py | 479 |
1 files changed, 479 insertions, 0 deletions
diff --git a/main.py b/main.py new file mode 100644 index 00000000..7b150821 --- /dev/null +++ b/main.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python + +# Ledger, the command-line accounting 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. +# +# This script provides a Python front-end to the ledger library, and +# replicates the functionality of the C++ front-end, main.cc. It is +# provided as an example, and as a starting point for creating custom +# front-ends based on the Ledger module. See the documentation for an +# API reference, and how to use this module. + +import os +import sys +import string +import time + +true, false = 1, 0 + +from ledger import * + +# 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 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 () + +output_for_emacs = false +def handle_emacs_option (arg): + global output_for_emacs + output_for_emacs = true +add_option_handler ("emacs", "", handle_emacs_option) + +averages = {} +compute_monthly_avg = false + +def get_index (xact): + return time.strftime ("%Y/%m", time.localtime (xact.entry.date)) + +class ComputeMonthlyAvg (TransactionHandler): + def __call__ (self, xact): + global averages + index = get_index (xact) + if not averages.has_key(index): + averages[index] = [Value (), 0] + add_transaction_to (xact, averages[index][0]) + averages[index][1] += 1 + TransactionHandler.__call__ (self, xact) + +def monthly_avg (details): + index = get_index (xact) + return averages[index][0] / averages[index][1] + +def show_monthly_averages (arg): + global compute_monthly_avg + compute_monthly_avg = true + config.report_period = "monthly"; + config.total_expr = "@monthly_avg()" + +add_option_handler ("monthly-avg", "", show_monthly_averages) + +# 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, "LEDGER_") + +if os.environ.has_key ("LEDGER"): + process_option ("file", os.getenv ("LEDGER")) +if os.environ.has_key ("PRICE_HIST"): + process_option ("price-db", os.getenv ("PRICE_HIST")) +if os.environ.has_key ("PRICE_EXP"): + process_option ("price-exp", os.getenv ("PRICE_EXP")) + +# If no argument remain, then no command word was given. Report the +# default help text and exit. + +if len (args) == 0: + option_help () + sys.exit (0) + +# 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. + +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 == "print" or command == "p": + command = "p" +elif command == "entry": + command = "e" +elif command == "equity": + command = "E" +elif command == "prices": + command = "P" +else: + print "Unrecognized command:", command + sys.exit (1) + +# Create all the parser objects to be used. They are all registered, +# so that Ledger will try each one in turn whenever it is presented +# with a data file. They are attempted in reverse order to their +# registry. Note that Gnucash parsing is only available if the Ledger +# module was built with such support (which requires the xmlparse C +# library). + +text_parser = TextualParser () +bin_parser = BinaryParser () +qif_parser = QifParser () +gnucash_parser = None +try: + gnucash_parser = GnucashParser () +except: + pass + +register_parser (text_parser) +register_parser (bin_parser) +if gnucash_parser: + register_parser (gnucash_parser) +register_parser (qif_parser) + +# Parse all entries from the user specified locations (found in +# 'config') into the journal object we created. The two parsers given +# as explicit arguments indicate: the parser to be used for standard +# input, and the parser to be used for cache files. + +parse_ledger_data (journal, bin_parser) + +# 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. + +config.process_options (command, args); + +# If the command is "e", use the method journal.derive_entry to create +# a brand new entry based on the arguments given. + +new_entry = None +if command == "e": + new_entry = derive_new_entry (journal, args) + if new_entry is None: + sys.exit (1) + +# 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 +elif command == "E": + format = config.equity_format +elif command == "P": + min_val = 0 + def vmin(d, val): + global min_val + if not min_val or val < min_val: + min_val = val + return val + return min_val + + max_val = 0 + def vmax(d, val): + global max_val + if not max_val or val > max_val: + max_val = val + return val + return max_val + + format = config.prices_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 FormatEntries (FormatTransactions): + def __init__ (self, fmt): + self.last_entry = None + FormatTransactions.__init__(self, fmt) + + def flush (self): + self.format_last_entry () + self.last_entry = None + FormatTransactions.flush (self) + + def format_last_entry (self): + first = true + for x in self.last_entry: + if transaction_has_xdata (x) and \ + transaction_xdata (x).dflags & TRANSACTION_TO_DISPLAY: + if first or self.nformatter is None: + self.output.write (self.formatter.format (x)) + first = false + else: + self.output.write (self.nformatter.format (x)) + transaction_xdata (x).dflags |= TRANSACTION_TO_DISPLAY + + def __call__ (self, xact): + if self.last_entry and self.last_entry != xact.entry: + self.format_last_entry () + + transaction_xdata (xact).dflags |= TRANSACTION_TO_DISPLAY + + self.last_entry = xact.entry; + +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 + +class FormatEquity (AccountHandler): + output = None + + def __init__ (self, fmt, pred): + 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.predicate = AccountPredicate (pred) + self.total = Value () + + if config.output_file: + self.output = open (config.output_file, "w") + else: + self.output = sys.stdout + + AccountHandler.__init__ (self) + + header_entry = Entry () + header_entry.payee = "Opening Balances" + header_entry.date = int (time.time ()) + self.output.write (self.formatter.format (header_entry)) + + def __del__ (self): + if config.output_file: + self.output.close () + + def flush (self): + summary = Account (Account (), "Equity:Opening Balances") + account_xdata (summary).value = - self.total + self.output.write (self.nformatter.format (summary)) + self.output.flush () + + def __call__ (self, account): + if display_account (account, self.predicate): + self.output.write (self.nformatter.format (account)) + if account_has_xdata (account): + self.total += account_xdata (account).value + 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" or command == "E": + handler = SetAccountValue () +elif command == "p" or command == "e": + handler = FormatEntries (format) +elif output_for_emacs: + import emacs + handler = emacs.EmacsFormatTransactions () +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 not (command == "b" or command == "E"): + 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") + +if compute_monthly_avg: + handler = ComputeMonthlyAvg (handler) + +# The next set of 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.budget_flags: + handler = BudgetTransactions (handler, config.budget_flags) + handler.add_period_entries (journal) +elif config.forecast_limit: + handler = ForecastTransactions (handler, config.forecast_limit) + handler.add_period_entries (journal) + +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. And although a journal's entries +# can be walked using Python, it is significantly faster to do this +# simple walk in C++, using `walk_entries'. +# +# if command == "e": +# for xact in new_entry: +# handler (xact) +# else: +# for entry in journal: +# for xact in entry: +# handler (xact) + +if command == "e": + walk_transactions (new_entry, handler) +elif command == "P": + walk_commodities (handler) +else: + 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 () + +elif command == "E": + acct_formatter = FormatEquity (format, config.display_predicate) + sum_accounts (journal.master) + walk_accounts (journal.master, acct_formatter, config.sort_string) + acct_formatter.flush () + +# If it were important to clean things up, we would have to clear out +# the accumulated xdata at this point: + +#clear_transactions_xdata () +#clear_accounts_xdata () + +# If the cache is being used, and is dirty, update it now. + +if config.use_cache and config.cache_dirty and config.cache_file: + write_binary_journal (config.cache_file, journal); + +# We're done! |