summaryrefslogtreecommitdiff
path: root/main.py
blob: 57ba84c5bcc575b59076a1a6ed26ce69000438dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#!/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 == "xml":
    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 expat 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

# Configure the output file

if config.output_file:
    out = open (config.output_file, "w")
else:
    out = sys.stdout

# 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 (out, format)
elif command == "x":
    handler = FormatEmacsTransactions (out)
elif command == "X":
    handler = FormatXmlEntries (out, config.show_totals)
else:
    handler = FormatTransactions (out, format)

if command == "w":
    write_textual_journal(journal, args, handler, out);
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.reconcile_balance:
	  reconcilable = False
	  if config.reconcile_balance == "<all>":
	    reconcilable = True
	  else:
	    target_balance = Value (config.reconcile_balance)

	  cutoff = time.time ()
	  if config.reconcile_date:
	    cutoff = parse_date (config.reconcile_date)

	  handler = ReconcileTransactions (handler, target_balance,
					   cutoff, reconcilable)

	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 = FormatAccount (out, 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 ()

    if account_has_xdata (journal.master):
      xdata = account_xdata (journal.master)
      if not config.show_collapsed and xdata.total:
	out.write("--------------------\n")
	xdata.value = xdata.total
	# jww (2005-02-15): yet to convert
	#acct_formatter.format.format (out, details_t (journal.master))

elif command == "E":
    acct_formatter = FormatEquity (out, 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!