diff options
author | John Wiegley <johnw@newartisans.com> | 2003-10-08 23:05:15 +0000 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2003-10-08 23:05:15 +0000 |
commit | 072d5131b49ff64de40d1c9e44a0abcf3b0f1d73 (patch) | |
tree | aa3425496ac09539bacb15991dca36657bdcbcfb /reports.cc | |
parent | 331f389cc68b9ca54c50dddb58cb2e7bb3523cf6 (diff) | |
download | fork-ledger-072d5131b49ff64de40d1c9e44a0abcf3b0f1d73.tar.gz fork-ledger-072d5131b49ff64de40d1c9e44a0abcf3b0f1d73.tar.bz2 fork-ledger-072d5131b49ff64de40d1c9e44a0abcf3b0f1d73.zip |
*** empty log message ***
Diffstat (limited to 'reports.cc')
-rw-r--r-- | reports.cc | 636 |
1 files changed, 636 insertions, 0 deletions
diff --git a/reports.cc b/reports.cc new file mode 100644 index 00000000..6b4d697d --- /dev/null +++ b/reports.cc @@ -0,0 +1,636 @@ +#include "ledger.h" + +#define LEDGER_VERSION "1.1" + +#include <fstream> +#include <unistd.h> + +namespace ledger { + +extern book * parse_ledger(std::istream& in, regexps_map& regexps, + bool compute_balances); +#ifdef READ_GNUCASH +extern book * parse_gnucash(std::istream& in, bool compute_balances); +#endif + +extern bool parse_date(const char * date_str, std::time_t * result, + const int year = -1); +extern void parse_price_setting(const std::string& setting); + + +static bool show_cleared; +static bool show_virtual; +static bool get_quotes; +static bool show_children; +static bool show_empty; +static bool show_subtotals; +static bool full_names; + +static std::time_t begin_date; +static bool have_beginning; +static std::time_t end_date; +static bool have_ending; + + +////////////////////////////////////////////////////////////////////// +// +// Balance reporting code +// + +static void display_total(std::ostream& out, totals& balance, + account * acct, bool top_level, + int * headlines) +{ + bool displayed = false; + + if (acct->checked == 1 && + (show_empty || ! acct->balance.is_zero())) { + displayed = true; + + acct->balance.print(out, 20); + if (show_subtotals && top_level) + balance.credit(acct->balance); + + if (acct->parent && ! full_names && ! top_level) { + for (const account * a = acct; a; a = a->parent) + out << " "; + out << acct->name << std::endl; + } else { + out << " " << acct->as_str() << std::endl; + (*headlines)++; + } + } + + // Display balances for all child accounts + + for (accounts_map_iterator i = acct->children.begin(); + i != acct->children.end(); + i++) + display_total(out, balance, (*i).second, ! displayed, headlines); +} + +void report_balances(std::ostream& out, regexps_map& regexps) +{ + // Walk through all of the ledger entries, computing the account + // totals + + for (entries_list_iterator i = main_ledger->entries.begin(); + i != main_ledger->entries.end(); + i++) { + if ((have_beginning && difftime((*i)->date, begin_date) < 0) || + (have_ending && difftime((*i)->date, end_date) >= 0) || + (show_cleared && ! (*i)->cleared)) + continue; + + for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); + x != (*i)->xacts.end(); + x++) { + if (! show_virtual && (*x)->is_virtual) + continue; + + for (account * acct = (*x)->acct; + acct; + acct = show_subtotals ? acct->parent : NULL) { + if (acct->checked == 0) { + if (regexps.empty()) { + if (! (show_children || ! acct->parent)) + acct->checked = 2; + else + acct->checked = 1; + } + else { + bool by_exclusion; + bool match = matches(regexps, acct->as_str(), + &by_exclusion); + if (! match) { + acct->checked = 2; + } + else if (by_exclusion) { + if (! (show_children || ! acct->parent)) + acct->checked = 2; + else + acct->checked = 1; + } + else { + acct->checked = 1; + } + } + } + + if (acct->checked == 1) + acct->balance.credit((*x)->cost->street(get_quotes)); + } + } + } + + // Walk through all the top-level accounts, giving the balance + // report for each, and then for each of their children. + + totals balance; + int headlines = 0; + + for (accounts_map_iterator i = main_ledger->accounts.begin(); + i != main_ledger->accounts.end(); + i++) + display_total(out, balance, (*i).second, true, &headlines); + + // Print the total of all the balances shown + + if (show_subtotals && headlines > 1) { + out << "--------------------" << std::endl; + balance.print(out, 20); + out << std::endl; + } +} + +////////////////////////////////////////////////////////////////////// +// +// Register printing code +// + +static std::string truncated(const std::string& str, int width) +{ + char buf[256]; + memset(buf, '\0', 255); + std::strncpy(buf, str.c_str(), width); + if (buf[width - 1]) + std::strcpy(&buf[width - 3], "..."); + else + buf[width] = '\0'; + return buf; +} + +void print_register(const std::string& acct_name, std::ostream& out, + regexps_map& regexps) +{ + account * acct = main_ledger->find_account(acct_name, false); + if (! acct) { + std::cerr << "Error: Unknown account name: " << acct_name + << std::endl; + return; + } + + // Walk through all of the ledger entries, printing their register + // formatted equivalent + + totals balance; + + for (entries_list_iterator i = main_ledger->entries.begin(); + i != main_ledger->entries.end(); + i++) { + if (! (*i)->matches(regexps)) + continue; + + for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); + x != (*i)->xacts.end(); + x++) { + if ((*x)->acct != acct || ! show_cleared && (*i)->cleared) + continue; + + char buf[32]; + std::strftime(buf, 31, "%m.%d ", std::localtime(&(*i)->date)); + out << buf; + +#if 0 + if ((*i)->cleared) + out << "* "; + else + out << " "; + + out.width(4); + if ((*i)->code.empty()) + out << " "; + else + out << std::left << (*i)->code; +#endif + out << " "; + + out.width(30); + if ((*i)->desc.empty()) + out << " "; + else + out << std::left << truncated((*i)->desc, 30); + out << " "; + + // Always display the street value, if prices have been + // specified + + amount * street = (*x)->cost->street(get_quotes); + balance.credit(street); + + // If there are two transactions, use the one which does not + // refer to this account. If there are more than two, we will + // just have to print all of the splits, like gnucash does. + + transaction * xact; + if ((*i)->xacts.size() == 2) { + if (*x == (*i)->xacts.front()) + xact = (*i)->xacts.back(); + else + xact = (*i)->xacts.front(); + } else { + xact = *x; + } + + out.width(22); + out << std::left << truncated(xact->acct_as_str(), 22) << " "; + + out.width(12); + out << std::right << street->as_str(true); + delete street; + + balance.print(out, 12); + + out << std::endl; + + if (! show_subtotals || xact != *x) + continue; + + for (std::list<transaction *>::iterator y = (*i)->xacts.begin(); + y != (*i)->xacts.end(); + y++) { + if (*x == *y) + continue; + + out << " "; + + out.width(22); + out << std::left << truncated((*y)->acct_as_str(), 22) << " "; + + out.width(12); + street = (*y)->cost->street(get_quotes); + out << std::right << street->as_str(true) << std::endl; + delete street; + } + } + } +} + +////////////////////////////////////////////////////////////////////// +// +// Create an Equity file based on a ledger. This is used for +// archiving past years, and starting out a new year with compiled +// balances. +// + +static void equity_entry(account * acct, regexps_map& regexps, + std::ostream& out) +{ + if (! acct->balance.is_zero() && + (regexps.empty() || matches(regexps, acct->as_str()))) { + entry opening; + + opening.date = std::time(NULL); + opening.cleared = true; + opening.desc = "Opening Balance"; + + transaction * xact; + for (totals::const_iterator i = acct->balance.amounts.begin(); + i != acct->balance.amounts.end(); + i++) { + // Skip it, if there is a zero balance for the commodity + if ((*i).second->is_zero()) + continue; + + xact = new transaction(); + xact->acct = const_cast<account *>(acct); + xact->cost = (*i).second->street(get_quotes); + opening.xacts.push_back(xact); + + xact = new transaction(); + xact->acct = main_ledger->find_account("Equity:Opening Balances"); + xact->cost = (*i).second->street(get_quotes); + xact->cost->negate(); + opening.xacts.push_back(xact); + } + + opening.print(out); + } + + // Display balances for all child accounts + + for (accounts_map_iterator i = acct->children.begin(); + i != acct->children.end(); + i++) + equity_entry((*i).second, regexps, out); +} + +void equity_ledger(std::ostream& out, regexps_map& regexps) +{ + // The account have their current totals already generated as a + // result of parsing. We just have to output those values. + // totals + + for (accounts_map_iterator i = main_ledger->accounts.begin(); + i != main_ledger->accounts.end(); + i++) + equity_entry((*i).second, regexps, out); +} + +} // namespace ledger + +using namespace ledger; + +static void show_help(std::ostream& out) +{ + std::cerr + << "usage: ledger [options] COMMAND [options] [REGEXPS]" << std::endl + << std::endl + << "ledger options:" << std::endl + << " -C also show cleared transactions" << std::endl + << " -d DATE specify an implicit date range (e.g., -d april)" + << std::endl + << " -b DATE specify a beginning date" << std::endl + << " -e DATE specify an ending date" << std::endl + << " -c do not show future entries (same as -e TODAY)" << std::endl + << " -f FILE specify pathname of ledger data file" << std::endl + << " -h display this help text" << std::endl + << " -R do not factor any virtual transactions" << std::endl + << " -V FILE use virtual mappings listed in FILE" << std::endl + << " -i FILE read the list of inclusion regexps from FILE" << std::endl + << " -p FILE read the list of prices from FILE" << std::endl + << " -P download price quotes from the Internet" << std::endl + << " (works by running the command \"getquote SYMBOL\")" + << std::endl + << " -v display version information" << std::endl + << " -w print out warnings where applicable" << std::endl + << std::endl + << "commands:" << std::endl + << " balance show balance totals" << std::endl + << " register display a register for ACCOUNT" << std::endl + << " print print all ledger entries" << std::endl + << " equity generate equity ledger for all entries" << std::endl + << std::endl + << "`balance' options:" << std::endl + << " -F print each account's full name" << std::endl + << " -n do not generate totals for parent accounts" << std::endl + << " -s show sub-accounts in balance totals" << std::endl + << " -S show empty accounts in balance totals" << std::endl; +} + +////////////////////////////////////////////////////////////////////// +// +// Command-line parser and top-level logic. +// + +int main(int argc, char * argv[]) +{ + std::istream * file = NULL; + std::string prices; + regexps_map regexps; + int index; + + have_beginning = false; + have_ending = false; + show_cleared = false; + show_virtual = true; + show_children = false; + show_empty = false; + show_subtotals = true; + full_names = false; + + // Parse the command-line options + + int c; + while (-1 != (c = getopt(argc, argv, "+b:e:d:cChRV:wf:i:p:PvsSnF"))) { + switch (char(c)) { + case 'b': + case 'e': { + std::time_t when; + if (! parse_date(optarg, &when)) { + std::cerr << "Error: Bad date string: " << optarg << std::endl; + return 1; + } + + if (c == 'b') { + begin_date = when; + have_beginning = true; + } else { + end_date = when; + have_ending = true; + } + break; + } + +#if 0 + case 'd': { + if (! parse_date(optarg, &begin_date)) { + std::cerr << "Error: Bad date string: " << optarg << std::endl; + return 1; + } + have_beginning = true; + + struct std::tm when, then; + std::memset(&then, 0, sizeof(struct std::tm)); + + std::time_t now = std::time(NULL); + struct std::tm * now_tm = std::localtime(&now); + + for (const char ** f = formats; *f; f++) { + memset(&when, INT_MAX, sizeof(struct std::tm)); + if (strptime(optarg, *f, &when)) { + then.tm_hour = 0; + then.tm_min = 0; + then.tm_sec = 0; + + if (when.tm_year != -1) + then.tm_year = when.tm_year + 1; + else + then.tm_year = now_tm->tm_year; + + if (std::strcmp(*f, "%Y") == 0) { + then.tm_mon = 0; + then.tm_mday = 1; + } else { + if (when.tm_mon != -1) + then.tm_mon = when.tm_mon + 1; + else + then.tm_mon = now_tm->tm_mon; + + if (when.tm_mday != -1) + then.tm_mday = when.tm_mday + 1; + else + then.tm_mday = now_tm->tm_mday; + } + + end_date = std::mktime(&then); + have_ending = true; + break; + } + } + break; + } +#endif + + case 'c': + end_date = std::time(NULL); + have_ending = true; + break; + + case 'h': show_help(std::cout); break; + case 'f': file = new std::ifstream(optarg); break; + + case 'C': show_cleared = true; break; + case 'R': show_virtual = false; break; + case 'w': use_warnings = true; break; + case 's': show_children = true; break; + case 'S': show_empty = true; break; + case 'n': show_subtotals = false; break; + case 'F': full_names = true; break; + + // -i path-to-file-of-regexps + case 'i': + read_regexps(optarg, regexps); + break; + + // -p "COMMODITY=PRICE" + // -p path-to-price-database + case 'p': + prices = optarg; + break; + + case 'P': + get_quotes = true; + break; + + case 'v': + std::cout + << "Ledger Accouting Tool " LEDGER_VERSION << std::endl + << " Copyright (c) 2003 John Wiegley <johnw@newartisans.com>" + << std::endl << std::endl + << "This program is made available under the terms of the BSD" + << std::endl + << "Public License. See the LICENSE file included with the" + << std::endl + << "distribution for details and disclaimer." << std::endl; + return 0; + } + } + + if (optind == argc) { + show_help(std::cout); + return 1; + } + + index = optind; + + if (use_warnings && (have_beginning || have_ending)) { + std::cout << "Reporting"; + + if (have_beginning) { + char buf[32]; + std::strftime(buf, 31, "%Y.%m.%d", std::localtime(&begin_date)); + std::cout << " from " << buf; + } + + if (have_ending) { + char buf[32]; + std::strftime(buf, 31, "%Y.%m.%d", std::localtime(&end_date)); + std::cout << " until " << buf; + } + + std::cout << std::endl; + } + + // A ledger data file must be specified + + if (! file) { + const char * p = std::getenv("LEDGER"); + if (p) + file = new std::ifstream(p); + + if (! file || ! *file) { + std::cerr << ("Please specify ledger file using -f option " + "or LEDGER environment variable.") + << std::endl; + return 1; + } + } + + // Read the command word + + const std::string command = argv[index++]; + + int name_index = index; + if (command == "register") { + if (optind == argc) { + std::cerr << ("Error: Must specify an account name " + "after the 'register' command.") << std::endl; + return 1; + } + index++; + } + + // Compile the list of specified regular expressions, which can be + // specified after the command, or using the '-i FILE' option + + for (; index < argc; index++) + regexps.push_back(new mask(argv[index])); + + // Parse the ledger + +#ifdef READ_GNUCASH + char buf[32]; + file->get(buf, 31); + file->seekg(0); + + if (std::strncmp(buf, "<?xml version=\"1.0\"?>", 21) == 0) + main_ledger = parse_gnucash(*file, command == "equity"); + else +#endif + main_ledger = parse_ledger(*file, regexps, command == "equity"); + + delete file; + + if (! main_ledger) + std::exit(1); + + // Record any prices specified by the user + + if (! prices.empty()) { + if (access(prices.c_str(), R_OK) != -1) { + std::ifstream pricedb(prices.c_str()); + while (! pricedb.eof()) { + char buf[80]; + pricedb.getline(buf, 79); + if (*buf && ! std::isspace(*buf)) + parse_price_setting(buf); + } + } else { + parse_price_setting(prices); + } + } + + // Process the command + + if (command == "balance") { + report_balances(std::cout, regexps); + } + else if (command == "register") { + print_register(argv[name_index], std::cout, regexps); + } + else if (command == "print") { + main_ledger->sort(cmp_entry_date()); + main_ledger->print(std::cout, regexps, true); + } + else if (command == "equity") { + equity_ledger(std::cout, regexps); + } + +#ifdef DEBUG + // Ordinarily, deleting the main ledger just isn't necessary at + // this point. + + delete main_ledger; + + // Delete the known regexp maps. + + for (regexps_map_iterator r = regexps.begin(); + r != regexps.end(); + r++) { + delete *r; + } +#endif +} + +// reports.cc ends here. |