never update the binary cache (and don't create one if none was there). This is because certain intermediary accounts get created during generation of these reports, which should never be recorded in the cache as actual accounts. Also, run the account filter both before and after the budgeting and forecasting filters, to ensure that only the accounts the user is interested in get included in the report.
576 lines
17 KiB
C++
576 lines
17 KiB
C++
#include <ledger.h>
|
|
#include "acconf.h"
|
|
#include "debug.h"
|
|
#ifdef USE_BOOST_PYTHON
|
|
#include "py_eval.h"
|
|
#endif
|
|
|
|
using namespace ledger;
|
|
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <memory>
|
|
#include <algorithm>
|
|
#include <exception>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
|
|
#ifdef HAVE_UNIX_PIPES
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#include "fdstream.hpp"
|
|
#endif
|
|
|
|
#if !defined(DEBUG_LEVEL) || DEBUG_LEVEL <= RELEASE
|
|
|
|
#define auto_ptr bogus_auto_ptr
|
|
|
|
// This version of auto_ptr does not delete on deconstruction.
|
|
namespace std {
|
|
template <typename T>
|
|
struct bogus_auto_ptr {
|
|
T * ptr;
|
|
bogus_auto_ptr() : ptr(NULL) {}
|
|
explicit bogus_auto_ptr(T * _ptr) : ptr(_ptr) {}
|
|
T& operator*() const throw() {
|
|
return *ptr;
|
|
}
|
|
T * operator->() const throw() {
|
|
return ptr;
|
|
}
|
|
T * get() const throw() { return ptr; }
|
|
T * release() throw() {
|
|
T * tmp = ptr;
|
|
ptr = 0;
|
|
return tmp;
|
|
}
|
|
void reset(T * p = 0) throw() {
|
|
if (p != ptr) {
|
|
delete ptr;
|
|
ptr = p;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
#endif
|
|
|
|
item_handler<transaction_t> *
|
|
chain_xact_handlers(const std::string& command,
|
|
item_handler<transaction_t> * base_formatter,
|
|
journal_t * journal,
|
|
account_t * master,
|
|
std::list<item_handler<transaction_t> *>& ptrs)
|
|
{
|
|
item_handler<transaction_t> * formatter = NULL;
|
|
|
|
ptrs.push_back(formatter = base_formatter);
|
|
|
|
// format_transactions write each transaction received to the
|
|
// output stream.
|
|
if (! (command == "b" || command == "E")) {
|
|
// truncate_entries cuts off a certain number of _entries_ from
|
|
// being displayed. It does not affect calculation.
|
|
if (config.head_entries || config.tail_entries)
|
|
ptrs.push_back(formatter =
|
|
new truncate_entries(formatter,
|
|
config.head_entries,
|
|
config.tail_entries));
|
|
|
|
// filter_transactions will only pass through transactions
|
|
// matching the `display_predicate'.
|
|
if (! config.display_predicate.empty())
|
|
ptrs.push_back(formatter =
|
|
new filter_transactions(formatter,
|
|
config.display_predicate));
|
|
|
|
// calc_transactions computes the running total. When this
|
|
// appears will determine, for example, whether filtered
|
|
// transactions are included or excluded from the running total.
|
|
ptrs.push_back(formatter = new calc_transactions(formatter));
|
|
|
|
// reconcile_transactions will pass through only those
|
|
// transactions which can be reconciled to a given balance
|
|
// (calculated against the transactions which it receives).
|
|
if (! config.reconcile_balance.empty()) {
|
|
value_t target_balance(config.reconcile_balance);
|
|
time_t cutoff = now;
|
|
if (! config.reconcile_date.empty())
|
|
parse_date(config.reconcile_date.c_str(), &cutoff);
|
|
ptrs.push_back(formatter =
|
|
new reconcile_transactions(formatter, target_balance,
|
|
cutoff));
|
|
}
|
|
|
|
// sort_transactions will sort all the transactions it sees, based
|
|
// on the `sort_order' value expression.
|
|
if (! config.sort_string.empty())
|
|
ptrs.push_back(formatter =
|
|
new sort_transactions(formatter, config.sort_string));
|
|
|
|
// changed_value_transactions adds virtual transactions to the
|
|
// list to account for changes in market value of commodities,
|
|
// which otherwise would affect the running total unpredictably.
|
|
if (config.show_revalued)
|
|
ptrs.push_back(formatter =
|
|
new changed_value_transactions(formatter,
|
|
config.show_revalued_only));
|
|
|
|
// collapse_transactions causes entries with multiple transactions
|
|
// to appear as entries with a subtotaled transaction for each
|
|
// commodity used.
|
|
if (config.show_collapsed)
|
|
ptrs.push_back(formatter = new collapse_transactions(formatter));
|
|
}
|
|
|
|
// subtotal_transactions combines all the transactions it receives
|
|
// into one subtotal entry, which has one transaction for each
|
|
// commodity in each account.
|
|
//
|
|
// period_transactions is like subtotal_transactions, but it
|
|
// subtotals according to time periods rather than totalling
|
|
// everything.
|
|
//
|
|
// dow_transactions is like period_transactions, except that it
|
|
// reports all the transactions that fall on each subsequent day
|
|
// of the week.
|
|
if (config.show_subtotal && ! (command == "b" || command == "E"))
|
|
ptrs.push_back(formatter = new subtotal_transactions(formatter));
|
|
|
|
if (config.days_of_the_week)
|
|
ptrs.push_back(formatter = new dow_transactions(formatter));
|
|
else if (config.by_payee)
|
|
ptrs.push_back(formatter = new by_payee_transactions(formatter));
|
|
|
|
if (! config.report_period.empty()) {
|
|
ptrs.push_back(formatter =
|
|
new interval_transactions(formatter,
|
|
config.report_period,
|
|
config.report_period_sort));
|
|
ptrs.push_back(formatter = new sort_transactions(formatter, "d"));
|
|
}
|
|
|
|
// invert_transactions inverts the value of the transactions it
|
|
// receives.
|
|
if (config.show_inverted)
|
|
ptrs.push_back(formatter = new invert_transactions(formatter));
|
|
|
|
// related_transactions will pass along all transactions related
|
|
// to the transaction received. If `show_all_related' is true,
|
|
// then all the entry's transactions are passed; meaning that if
|
|
// one transaction of an entry is to be printed, all the
|
|
// transaction for that entry will be printed.
|
|
if (config.show_related)
|
|
ptrs.push_back(formatter =
|
|
new related_transactions(formatter,
|
|
config.show_all_related));
|
|
|
|
// This filter_transactions will only pass through transactions
|
|
// matching the `predicate'.
|
|
if (! config.predicate.empty())
|
|
ptrs.push_back(formatter = new filter_transactions(formatter,
|
|
config.predicate));
|
|
|
|
// budget_transactions takes a set of transactions from a data
|
|
// file and uses them to generate "budget transactions" which
|
|
// balance against the reported transactions.
|
|
//
|
|
// forecast_transactions is a lot like budget_transactions, except
|
|
// that it adds entries only for the future, and does not balance
|
|
// them against anything but the future balance.
|
|
|
|
if (config.budget_flags) {
|
|
// Don't generate a cache file after calculating a budget report,
|
|
// since certain intermediary accounts may get created which
|
|
// aren't intended to be saved. For example, the user might have
|
|
// an "Expenses" budget, to catch all other expenses. This will
|
|
// result in an "Expenses" account being created in the journal --
|
|
// to reflect the calculated totals -- even though no such account
|
|
// was ever actually used. Because budgeting and forecasting
|
|
// might create such "ghost" accounts for internal purposes, we
|
|
// don't want to change the cache.
|
|
config.use_cache = false;
|
|
|
|
budget_transactions * handler
|
|
= new budget_transactions(formatter, config.budget_flags);
|
|
handler->add_period_entries(journal->period_entries);
|
|
ptrs.push_back(formatter = handler);
|
|
|
|
// Apply this before the budget handler, so that only matching
|
|
// transactions are calculated toward the budget. The use of
|
|
// filter_transactions above will further clean the results so
|
|
// that no automated transactions that don't match the filter get
|
|
// reported.
|
|
if (! config.predicate.empty())
|
|
ptrs.push_back(formatter = new filter_transactions(formatter,
|
|
config.predicate));
|
|
}
|
|
else if (! config.forecast_limit.empty()) {
|
|
config.use_cache = false; // see note above
|
|
|
|
forecast_transactions * handler
|
|
= new forecast_transactions(formatter, config.forecast_limit);
|
|
handler->add_period_entries(journal->period_entries);
|
|
ptrs.push_back(formatter = handler);
|
|
|
|
// See above, under budget_transactions.
|
|
if (! config.predicate.empty())
|
|
ptrs.push_back(formatter = new filter_transactions(formatter,
|
|
config.predicate));
|
|
}
|
|
|
|
if (config.comm_as_payee)
|
|
ptrs.push_back(formatter = new set_comm_as_payee(formatter));
|
|
|
|
return formatter;
|
|
}
|
|
|
|
int parse_and_report(int argc, char * argv[], char * envp[])
|
|
{
|
|
std::auto_ptr<journal_t> journal(new journal_t);
|
|
|
|
// Parse command-line arguments, and those set in the environment
|
|
|
|
std::list<std::string> args;
|
|
process_arguments(config_options, argc - 1, argv + 1, false, args);
|
|
|
|
if (args.empty()) {
|
|
option_help(std::cerr);
|
|
return 1;
|
|
}
|
|
strings_list::iterator arg = args.begin();
|
|
|
|
if (config.cache_file == "<none>")
|
|
config.use_cache = false;
|
|
else
|
|
config.use_cache = config.data_file.empty() && config.price_db.empty();
|
|
DEBUG_PRINT("ledger.config.cache", "1. use_cache = " << config.use_cache);
|
|
|
|
process_environment(config_options, envp, "LEDGER_");
|
|
|
|
#if 1
|
|
// These are here for backwards compatability, but are deprecated.
|
|
|
|
if (const char * p = std::getenv("LEDGER"))
|
|
process_option(config_options, "file", p);
|
|
if (const char * p = std::getenv("LEDGER_INIT"))
|
|
process_option(config_options, "init-file", p);
|
|
if (const char * p = std::getenv("PRICE_HIST"))
|
|
process_option(config_options, "price-db", p);
|
|
if (const char * p = std::getenv("PRICE_EXP"))
|
|
process_option(config_options, "price-exp", p);
|
|
#endif
|
|
|
|
const char * p = std::getenv("HOME");
|
|
std::string home = p ? p : "";
|
|
|
|
if (config.init_file.empty())
|
|
config.init_file = home + "/.ledgerrc";
|
|
if (config.price_db.empty())
|
|
config.price_db = home + "/.pricedb";
|
|
|
|
if (config.cache_file.empty())
|
|
config.cache_file = home + "/.ledger-cache";
|
|
|
|
if (config.data_file == config.cache_file)
|
|
config.use_cache = false;
|
|
DEBUG_PRINT("ledger.config.cache", "2. use_cache = " << config.use_cache);
|
|
|
|
// Read the command word, canonicalize it to its one letter form,
|
|
// then configure the system based on the kind of report to be
|
|
// generated
|
|
|
|
std::string command = *arg++;
|
|
|
|
if (command == "balance" || command == "bal" || command == "b")
|
|
command = "b";
|
|
else if (command == "register" || command == "reg" || command == "r")
|
|
command = "r";
|
|
else if (command == "print" || command == "p")
|
|
command = "p";
|
|
else if (command == "output")
|
|
command = "w";
|
|
else if (command == "emacs")
|
|
command = "x";
|
|
else if (command == "xml")
|
|
command = "X";
|
|
else if (command == "entry")
|
|
command = "e";
|
|
else if (command == "equity")
|
|
command = "E";
|
|
else if (command == "prices")
|
|
command = "P";
|
|
else if (command == "pricesdb")
|
|
command = "D";
|
|
else
|
|
throw error(std::string("Unrecognized command '") + command + "'");
|
|
|
|
// Parse initialization files, ledger data, price database, etc.
|
|
|
|
std::auto_ptr<binary_parser_t> bin_parser(new binary_parser_t);
|
|
#ifdef HAVE_XMLPARSE
|
|
std::auto_ptr<xml_parser_t> xml_parser(new xml_parser_t);
|
|
std::auto_ptr<gnucash_parser_t> gnucash_parser(new gnucash_parser_t);
|
|
#endif
|
|
#ifdef HAVE_LIBOFX
|
|
std::auto_ptr<ofx_parser_t> ofx_parser(new ofx_parser_t);
|
|
#endif
|
|
std::auto_ptr<qif_parser_t> qif_parser(new qif_parser_t);
|
|
std::auto_ptr<textual_parser_t> text_parser(new textual_parser_t);
|
|
|
|
register_parser(bin_parser.get());
|
|
#ifdef HAVE_XMLPARSE
|
|
register_parser(xml_parser.get());
|
|
register_parser(gnucash_parser.get());
|
|
#endif
|
|
#ifdef HAVE_LIBOFX
|
|
register_parser(ofx_parser.get());
|
|
#endif
|
|
register_parser(qif_parser.get());
|
|
register_parser(text_parser.get());
|
|
|
|
parse_ledger_data(journal.get(), bin_parser.get(), text_parser.get()
|
|
#ifdef HAVE_XMLPARSE
|
|
, xml_parser.get()
|
|
#endif
|
|
);
|
|
|
|
// process the command word and its following arguments
|
|
|
|
std::string first_arg;
|
|
if (command == "w") {
|
|
if (arg == args.end())
|
|
throw error("The 'output' command requires a file argument");
|
|
first_arg = *arg++;
|
|
}
|
|
|
|
config.process_options(command, arg, args.end());
|
|
|
|
std::auto_ptr<entry_t> new_entry;
|
|
if (command == "e") {
|
|
new_entry.reset(derive_new_entry(*journal, arg, args.end()));
|
|
if (! new_entry.get())
|
|
return 1;
|
|
}
|
|
|
|
// Configure the output stream
|
|
|
|
#ifdef HAVE_UNIX_PIPES
|
|
int status, pfd[2]; // Pipe file descriptors
|
|
#endif
|
|
std::ostream * out = &std::cout;
|
|
|
|
if (! config.output_file.empty()) {
|
|
out = new std::ofstream(config.output_file.c_str());
|
|
}
|
|
#ifdef HAVE_UNIX_PIPES
|
|
else if (! config.pager.empty()) {
|
|
status = pipe(pfd);
|
|
if (status == -1)
|
|
throw error("Failed to create pipe");
|
|
|
|
status = fork();
|
|
if (status < 0) {
|
|
throw error("Failed to fork child process");
|
|
}
|
|
else if (status == 0) { // child
|
|
const char *arg0;
|
|
|
|
// Duplicate pipe's reading end into stdin
|
|
status = dup2(pfd[0], STDIN_FILENO);
|
|
if (status == -1)
|
|
perror("dup2");
|
|
|
|
// Close unuseful file descriptors: the pipe's writing and
|
|
// reading ends (the latter is not needed anymore, after the
|
|
// duplication).
|
|
close(pfd[1]);
|
|
close(pfd[0]);
|
|
|
|
// Find command name: its the substring starting right of the
|
|
// rightmost '/' character in the pager pathname. See manpage
|
|
// for strrchr.
|
|
arg0 = std::strrchr(config.pager.c_str(), '/');
|
|
if (arg0 != NULL)
|
|
arg0++;
|
|
else
|
|
arg0 = config.pager.c_str(); // No slashes in pager.
|
|
|
|
execlp(config.pager.c_str(), arg0, (char *)0);
|
|
perror("execl");
|
|
exit(1);
|
|
}
|
|
else { // parent
|
|
close(pfd[0]);
|
|
out = new boost::fdostream(pfd[1]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Compile the format strings
|
|
|
|
const std::string * format;
|
|
|
|
if (! config.format_string.empty())
|
|
format = &config.format_string;
|
|
else if (command == "b")
|
|
format = &config.balance_format;
|
|
else if (command == "r")
|
|
format = &config.register_format;
|
|
else if (command == "E")
|
|
format = &config.equity_format;
|
|
else if (command == "P")
|
|
format = &config.prices_format;
|
|
else if (command == "D")
|
|
format = &config.pricesdb_format;
|
|
else if (command == "w")
|
|
format = &config.write_xact_format;
|
|
else
|
|
format = &config.print_format;
|
|
|
|
#ifdef USE_BOOST_PYTHON
|
|
|
|
// If Python support is compiled, we can easily report minimum and
|
|
// maximum values for each commodity. There is a line in config.cc
|
|
// which configures the prices report to call these two functions,
|
|
// if Python is available.
|
|
|
|
if (command == "P")
|
|
python_eval("\
|
|
min_val = 0\n\
|
|
def vmin(d, val):\n\
|
|
global min_val\n\
|
|
if not min_val or val < min_val:\n\
|
|
min_val = val\n\
|
|
return val\n\
|
|
return min_val\n\
|
|
\n\
|
|
max_val = 0\n\
|
|
def vmax(d, val):\n\
|
|
global max_val\n\
|
|
if not max_val or val > max_val:\n\
|
|
max_val = val\n\
|
|
return val\n\
|
|
return max_val\n", PY_EVAL_MULTI);
|
|
|
|
#endif // USE_BOOST_PYTHON
|
|
|
|
// Walk the entries based on the report type and the options
|
|
|
|
item_handler<transaction_t> * formatter;
|
|
std::list<item_handler<transaction_t> *> formatter_ptrs;
|
|
|
|
if (command == "b" || command == "E")
|
|
formatter = new set_account_value;
|
|
else if (command == "p" || command == "e")
|
|
formatter = new format_entries(*out, *format);
|
|
else if (command == "x")
|
|
formatter = new format_emacs_transactions(*out);
|
|
else if (command == "X") {
|
|
#ifdef HAVE_XMLPARSE
|
|
formatter = new format_xml_entries(*out, config.show_totals);
|
|
#else
|
|
throw error("XML support was not compiled into this copy of Ledger");
|
|
#endif
|
|
} else
|
|
formatter = new format_transactions(*out, *format);
|
|
|
|
if (command == "w") {
|
|
write_textual_journal(*journal, first_arg, *formatter, *out);
|
|
} else {
|
|
formatter = chain_xact_handlers(command, formatter, journal.get(),
|
|
journal->master, formatter_ptrs);
|
|
if (command == "e")
|
|
walk_transactions(new_entry->transactions, *formatter);
|
|
else if (command == "P" || command == "D")
|
|
walk_commodities(commodity_t::commodities, *formatter);
|
|
else
|
|
walk_entries(journal->entries, *formatter);
|
|
|
|
if (command != "P" && command != "D")
|
|
formatter->flush();
|
|
}
|
|
|
|
// For the balance and equity reports, output the sum totals.
|
|
|
|
if (command == "b") {
|
|
format_account acct_formatter(*out, *format, config.display_predicate);
|
|
sum_accounts(*journal->master);
|
|
walk_accounts(*journal->master, acct_formatter, config.sort_string);
|
|
acct_formatter.flush();
|
|
|
|
if (account_has_xdata(*journal->master)) {
|
|
account_xdata_t& xdata = account_xdata(*journal->master);
|
|
if (! config.show_collapsed && xdata.total) {
|
|
*out << "--------------------\n";
|
|
xdata.value = xdata.total;
|
|
acct_formatter.format.format(*out, details_t(*journal->master));
|
|
}
|
|
}
|
|
}
|
|
else if (command == "E") {
|
|
format_equity acct_formatter(*out, *format, config.display_predicate);
|
|
sum_accounts(*journal->master);
|
|
walk_accounts(*journal->master, acct_formatter, config.sort_string);
|
|
acct_formatter.flush();
|
|
}
|
|
|
|
#if DEBUG_LEVEL >= BETA
|
|
clear_all_xdata();
|
|
|
|
if (! config.output_file.empty())
|
|
delete out;
|
|
|
|
for (std::list<item_handler<transaction_t> *>::iterator i
|
|
= formatter_ptrs.begin();
|
|
i != formatter_ptrs.end();
|
|
i++)
|
|
delete *i;
|
|
formatter_ptrs.clear();
|
|
#endif
|
|
|
|
// Write out the binary cache, if need be
|
|
|
|
if (config.use_cache && config.cache_dirty && ! config.cache_file.empty()) {
|
|
std::ofstream stream(config.cache_file.c_str());
|
|
write_binary_journal(stream, journal.get());
|
|
}
|
|
|
|
#ifdef HAVE_UNIX_PIPES
|
|
if (! config.pager.empty()) {
|
|
delete out;
|
|
close(pfd[1]);
|
|
|
|
// Wait for child to finish
|
|
wait(&status);
|
|
if (status & 0xffff != 0)
|
|
throw error("Something went wrong in the pager");
|
|
}
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char * argv[], char * envp[])
|
|
{
|
|
std::ios::sync_with_stdio(false);
|
|
|
|
try {
|
|
return parse_and_report(argc, argv, envp);
|
|
}
|
|
catch (const std::exception& err) {
|
|
std::cerr << "Error: " << err.what() << std::endl;
|
|
return 1;
|
|
}
|
|
catch (int& val) {
|
|
return val; // this acts like a std::setjmp
|
|
}
|
|
}
|
|
|
|
// main.cc ends here.
|