#!/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 () 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 == "output": command = "w" elif command == "emacs": command = "x" elif command == "entry": command = "e" elif command == "equity": command = "E" elif command == "prices": command = "P" elif command == "pricesdb": command = "D"; 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). bin_parser = BinaryParser () gnucash_parser = None xml_parser = None try: xml_parser = GnucashParser () except: pass try: gnucash_parser = GnucashParser () except: pass try: ofx_parser = OfxParser () except: pass qif_parser = QifParser () text_parser = TextualParser () register_parser (bin_parser) if xml_parser: register_parser (xml_parser) if gnucash_parser: register_parser (gnucash_parser) if ofx_parser: register_parser (ofx_parser) register_parser (qif_parser) register_parser (text_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 elif command == "D": format = config.pricesdb_format elif command == "w": format = config.write_xact_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 command == "x": import emacs handler = emacs.EmacsFormatTransactions () else: handler = FormatTransactions (format) if command == "w": if config.output_file: out = open (config.output_file, "w") else: out = sys.stdout write_textual_journal(journal, args, handler, out); if config.output_file: out.close () else: # 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.head_entries or config.tail_entries: handler = TruncateEntries (handler, config.head_entries, config.tail_entries) 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" or command == "D": walk_commodities (handler) else: walk_entries (journal, handler) # Flush the handlers, causing them to output whatever data is still # pending. if command != "P" and command != "D": 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_all_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!