diff options
author | John Wiegley <johnw@newartisans.com> | 2004-05-13 18:37:12 -0400 |
---|---|---|
committer | johnw <johnw@newartisans.com> | 2004-05-13 18:37:12 -0400 |
commit | 9ca1d24d30a485300b9486b18c068660da630cc1 (patch) | |
tree | 50d29d69278934be7e318a565d6223a6a0ab2c36 /reports.cc | |
parent | 31dafc3c163e4ceae42b1477c4eaeb4425b2b883 (diff) | |
download | fork-ledger-9ca1d24d30a485300b9486b18c068660da630cc1.tar.gz fork-ledger-9ca1d24d30a485300b9486b18c068660da630cc1.tar.bz2 fork-ledger-9ca1d24d30a485300b9486b18c068660da630cc1.zip |
initial import into Darcs
Diffstat (limited to 'reports.cc')
-rw-r--r-- | reports.cc | 597 |
1 files changed, 433 insertions, 164 deletions
@@ -1,13 +1,15 @@ #include "ledger.h" -#define LEDGER_VERSION "1.3" +#define LEDGER_VERSION "1.6" -#include <fstream> +#include <cstring> #include <unistd.h> namespace ledger { static bool cleared_only = false; +static bool uncleared_only = false; +static bool cost_basis = false; static bool show_virtual = true; static bool get_quotes = false; static bool show_children = false; @@ -15,6 +17,12 @@ static bool show_sorted = false; static bool show_empty = false; static bool show_subtotals = true; static bool full_names = false; +static bool print_monthly = false; +static bool gnuplot_safe = false; + +static amount * lower_limit = NULL; + +static mask * negonly_regexp = NULL; static std::time_t begin_date; static bool have_beginning = false; @@ -65,28 +73,145 @@ static bool matches_date_range(entry * ent) // Balance reporting code // -static void display_total(std::ostream& out, totals& balance, - account * acct, bool top_level, - int * headlines) +static bool satisfies_limit(totals& balance) { - bool displayed = false; + bool satisfies = true; + bool invert = false; - if (acct->checked == 1 && - (show_empty || ! acct->balance.is_zero())) { - displayed = true; + assert(lower_limit); - acct->balance.print(out, 20); - if (show_subtotals && top_level) - balance.credit(acct->balance); + if (balance.is_negative()) + invert = true; + else + lower_limit->negate(); - if (acct->parent && ! full_names && ! top_level) { - for (const account * a = acct; a; a = a->parent) - out << " "; - out << acct->name << std::endl; + balance.credit(lower_limit); + if (balance.is_negative()) + satisfies = invert; + else + satisfies = ! invert; + + lower_limit->negate(); + balance.credit(lower_limit); + + if (invert) + lower_limit->negate(); + + return satisfies; +} + +static bool satisfies_limit(amount * balance) +{ + bool satisfies = true; + bool invert = false; + + assert(lower_limit); + + if (balance->is_negative()) + invert = true; + else + lower_limit->negate(); + + balance->credit(lower_limit); + if (balance->is_negative()) + satisfies = invert; + else + satisfies = ! invert; + + lower_limit->negate(); + balance->credit(lower_limit); + + if (invert) + lower_limit->negate(); + + return satisfies; +} + +static void adjust_total(account * acct) +{ + for (accounts_map_iterator i = acct->children.begin(); + i != acct->children.end(); + i++) + adjust_total((*i).second); + + if (acct->checked == 1) { + if (! show_empty && acct->balance.is_zero()) + acct->checked = 2; + else if (lower_limit && ! satisfies_limit(acct->balance)) + acct->checked = 2; + else if (negonly_regexp && negonly_regexp->match(acct->as_str()) && + ! acct->balance.is_negative()) + acct->checked = 2; + + if (acct->checked == 2) { + acct->balance.negate(); + for (account * a = acct->parent; a; a = a->parent) + a->balance.credit(acct->balance); + } + } +} + +static int acct_visible_children(account * acct) +{ + int count = 0; + for (accounts_map_iterator i = acct->children.begin(); + i != acct->children.end(); + i++) { + if ((*i).second->checked == 1) { + if ((*i).second->children.size() == 0) + count++; + else + count += acct_visible_children((*i).second); + } + } + return count; +} + +static void display_total(std::ostream& out, totals& balance, + account * acct, int level, int * headlines) +{ + // If the number of visible children is exactly one, do not print + // the parent account, but just the one child (whose name will + // output with sufficiently qualification). + + if (acct->checked == 1 && acct_visible_children(acct) != 1) { + if (acct->balance.is_zero()) { + out.width(20); + out << " "; + } else { + acct->balance.print(out, 20); + } + + if (level == 0 || full_names || ! show_subtotals) { + if (show_subtotals) { + balance.credit(acct->balance); + (*headlines)++; + } + + out << " " << acct->as_str() << std::endl; } else { - out << " " << acct->as_str() << std::endl; - (*headlines)++; + for (int i = 0; i < level + 1; i++) + out << " "; + + assert(acct->parent); + if (acct_visible_children(acct->parent) == 1) { + /* If the account has no other siblings, instead of printing: + Parent + Child + print: + Parent:Child */ + const account * parent; + for (parent = acct->parent; + parent->parent && acct_visible_children(parent->parent) == 1; + parent = parent->parent) {} + + out << acct->as_str(parent) << std::endl; + } else { + out << acct->name << std::endl; + } } + + level++; } // Display balances for all child accounts @@ -94,10 +219,10 @@ static void display_total(std::ostream& out, totals& balance, for (accounts_map_iterator i = acct->children.begin(); i != acct->children.end(); i++) - display_total(out, balance, (*i).second, ! displayed, headlines); + display_total(out, balance, (*i).second, level, headlines); } -void report_balances(std::ostream& out, regexps_map& regexps) +void report_balances(std::ostream& out, regexps_list& regexps) { // Walk through all of the ledger entries, computing the account // totals @@ -105,7 +230,8 @@ void report_balances(std::ostream& out, regexps_map& regexps) for (entries_list_iterator i = main_ledger->entries.begin(); i != main_ledger->entries.end(); i++) { - if ((cleared_only && ! (*i)->cleared) || ! matches_date_range(*i)) + if ((cleared_only && ! (*i)->cleared) || + (uncleared_only && (*i)->cleared) || ! matches_date_range(*i)) continue; for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); @@ -117,16 +243,17 @@ void report_balances(std::ostream& out, regexps_map& regexps) for (account * acct = (*x)->acct; acct; acct = show_subtotals ? acct->parent : NULL) { + bool by_exclusion = false; + bool match = false; + if (acct->checked == 0) { if (regexps.empty()) { if (! (show_children || ! acct->parent)) acct->checked = 2; else acct->checked = 1; - } - else { - bool by_exclusion = false; - bool match = matches(regexps, acct->as_str(), &by_exclusion); + } else { + match = matches(regexps, acct->as_str(), &by_exclusion); if (! match) { acct->checked = 2; } @@ -144,9 +271,26 @@ void report_balances(std::ostream& out, regexps_map& regexps) if (acct->checked == 1) { amount * street = (*x)->cost->street(get_quotes); + if (cost_basis && + street->commdty() == (*x)->cost->commdty() && + (*x)->cost->has_price()) { + street = (*x)->cost->value(); + } acct->balance.credit(street); delete street; } + else if (show_subtotals) { + if (! regexps.empty() && ! match) { + for (account * a = acct->parent; a; a = a->parent) { + if (matches(regexps, a->as_str(), &by_exclusion) && + ! by_exclusion) { + match = true; + break; + } + } + if (! match) break; + } + } } } } @@ -159,12 +303,14 @@ void report_balances(std::ostream& out, regexps_map& regexps) for (accounts_map_iterator i = main_ledger->accounts.begin(); i != main_ledger->accounts.end(); - i++) - display_total(out, balance, (*i).second, true, &headlines); + i++) { + adjust_total((*i).second); + display_total(out, balance, (*i).second, 0, &headlines); + } // Print the total of all the balances shown - if (show_subtotals && headlines > 1) { + if (show_subtotals && headlines > 1 && ! balance.is_zero()) { out << "--------------------" << std::endl; balance.print(out, 20); out << std::endl; @@ -188,98 +334,183 @@ static std::string truncated(const std::string& str, int width) return buf; } -void print_register(const std::string& acct_name, std::ostream& out, - regexps_map& regexps) +enum periodicity_t { + PERIOD_NONE, + PERIOD_MONTHLY, + PERIOD_WEEKLY_SUN, + PERIOD_WEEKLY_MON +}; + +void print_register_transaction(std::ostream& out, entry *ent, + transaction *xact, totals& balance) { - mask acct_regex(acct_name); + char buf[32]; + std::strftime(buf, 31, "%m/%d ", std::localtime(&ent->date)); + out << buf; - // Walk through all of the ledger entries, printing their register - // formatted equivalent + out.width(25); + if (ent->desc.empty()) + out << " "; + else + out << std::left << truncated(ent->desc, 25); + out << " "; + + // Always display the street value, if prices have been + // specified + + amount * street = xact->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, print + // "<Splits...>", unless the -s option is being used (show + // children), in which case print all of the splits, like + // gnucash does. + + transaction * xp; + if (ent->xacts.size() == 2) { + if (xact == ent->xacts.front()) + xp = ent->xacts.back(); + else + xp = ent->xacts.front(); + } else { + xp = xact; + } + std::string xact_str = xp->acct_as_str(); - totals balance; + if (xp == xact && ! show_subtotals) + xact_str = "<Splits...>"; - for (entries_list_iterator i = main_ledger->entries.begin(); - i != main_ledger->entries.end(); - i++) { - if ((cleared_only && ! (*i)->cleared) || - ! matches_date_range(*i) || ! (*i)->matches(regexps)) + out.width(22); + out << std::left << truncated(xact_str, 22) << " "; + + out.width(12); + out << std::right << street->as_str(true); + delete street; + + balance.print(out, 12); + + out << std::endl; + + if (! show_children || xp != xact) + return; + + for (std::list<transaction *>::iterator y = ent->xacts.begin(); + y != ent->xacts.end(); + y++) { + if (xact == *y) continue; - for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); - x != (*i)->xacts.end(); - x++) { - if (! acct_regex.match((*x)->acct->as_str())) - continue; + out << " "; - char buf[32]; - std::strftime(buf, 31, "%m/%d ", std::localtime(&(*i)->date)); - out << buf; + out.width(22); + out << std::left << truncated((*y)->acct_as_str(), 22) << " "; - out.width(25); - if ((*i)->desc.empty()) - out << " "; - else - out << std::left << truncated((*i)->desc, 25); - out << " "; + out.width(12); + street = (*y)->cost->street(get_quotes); + out << std::right << street->as_str(true) << std::endl; + delete street; + } +} - // Always display the street value, if prices have been - // specified +void print_register_period(std::ostream& out, std::time_t date, + account *acct, amount& sum, totals& balance) +{ + char buf[32]; + std::strftime(buf, 31, "%Y/%m/%d ", std::localtime(&date)); + out << buf; - amount * street = (*x)->cost->street(get_quotes); - balance.credit(street); + if (! gnuplot_safe) { + out.width(20); + std::strftime(buf, 31, "%B", std::localtime(&date)); + out << std::left << truncated(buf, 20); + out << " "; - // If there are two transactions, use the one which does not - // refer to this account. If there are more than two, print - // "<Splits...>", unless the -s option is being used (show - // children), in which case print all of the splits, like - // gnucash does. + out.width(22); + out << std::left << truncated(acct->as_str(), 22) << " "; + } else { + commodity * cmdty = sum.commdty(); + cmdty->symbol = ""; + cmdty->separate = false; + cmdty->thousands = false; + cmdty->european = false; + } - transaction * xact; - if (! full_names && (*i)->xacts.size() == 2) { - if (*x == (*i)->xacts.front()) - xact = (*i)->xacts.back(); - else - xact = (*i)->xacts.front(); - } else { - xact = *x; - } - std::string xact_str = xact->acct_as_str(); + out.width(12); + out << std::right << sum.as_str(); - if (xact == *x && ! show_subtotals) - xact_str = "<Splits...>"; + if (! gnuplot_safe) + balance.print(out, 12); - out.width(22); - out << std::left << truncated(xact_str, 22) << " "; + out << std::endl; +} - out.width(12); - out << std::right << street->as_str(true); - delete street; +void print_register(std::ostream& out, const std::string& acct_name, + regexps_list& regexps, periodicity_t period = PERIOD_NONE) +{ + mask acct_regex(acct_name); - balance.print(out, 12); + // Walk through all of the ledger entries, printing their register + // formatted equivalent - out << std::endl; + totals balance; + amount * period_sum = NULL; // jww (2004-04-27): should be 'totals' type + std::time_t last_date; + account * last_acct; + int last_mon = -1; - if (! show_children || xact != *x) - continue; + for (entries_list_iterator i = main_ledger->entries.begin(); + i != main_ledger->entries.end(); + i++) { + if ((cleared_only && ! (*i)->cleared) || + (uncleared_only && (*i)->cleared) || + ! matches_date_range(*i) || ! (*i)->matches(regexps)) + continue; - for (std::list<transaction *>::iterator y = (*i)->xacts.begin(); - y != (*i)->xacts.end(); - y++) { - if (*x == *y) - continue; + int entry_mon = std::localtime(&(*i)->date)->tm_mon; - out << " "; + if (period_sum && period == PERIOD_MONTHLY && + last_mon != -1 && entry_mon != last_mon) { + assert(last_acct); + print_register_period(out, last_date, last_acct, + *period_sum, balance); + delete period_sum; + period_sum = NULL; + } - out.width(22); - out << std::left << truncated((*y)->acct_as_str(), 22) << " "; + for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); + x != (*i)->xacts.end(); + x++) { + if (! acct_regex.match((*x)->acct->as_str()) || + (lower_limit && ! satisfies_limit((*x)->cost))) + continue; + + if (period == PERIOD_NONE) { + print_register_transaction(out, *i, *x, balance); + } else { + amount * street = (*x)->cost->street(get_quotes); + balance.credit(street); + + if (period_sum) { + period_sum->credit(street); + delete street; + } else { + period_sum = street; + } - out.width(12); - street = (*y)->cost->street(get_quotes); - out << std::right << street->as_str(true) << std::endl; - delete street; + last_acct = (*x)->acct; + last_date = (*i)->date; + last_mon = entry_mon; } } } + + if (period_sum) { + if (last_acct) + print_register_period(out, last_date, last_acct, + *period_sum, balance); + delete period_sum; + } } ////////////////////////////////////////////////////////////////////// @@ -289,14 +520,14 @@ void print_register(const std::string& acct_name, std::ostream& out, // balances. // -static void equity_entry(account * acct, regexps_map& regexps, +static void equity_entry(account * acct, regexps_list& regexps, std::ostream& out) { if (! acct->balance.is_zero() && (regexps.empty() || matches(regexps, acct->as_str()))) { entry opening(main_ledger); - opening.date = std::time(NULL); + opening.date = have_ending ? end_date : std::time(NULL); opening.cleared = true; opening.desc = "Opening Balance"; @@ -330,7 +561,7 @@ static void equity_entry(account * acct, regexps_map& regexps, equity_entry((*i).second, regexps, out); } -void equity_ledger(std::ostream& out, regexps_map& regexps) +void equity_ledger(std::ostream& out, regexps_list& regexps) { // The account have their current totals already generated as a // result of parsing. We just have to output those values. @@ -347,9 +578,9 @@ void equity_ledger(std::ostream& out, regexps_map& regexps) void add_new_entry(int index, int argc, char **argv) { - regexps_map regexps; - entry added(main_ledger); - entry * matching = NULL; + regexps_list regexps; + entry added(main_ledger); + entry * matching = NULL; assert(index < argc); @@ -423,14 +654,17 @@ void add_new_entry(int index, int argc, char **argv) account * acct = NULL; commodity * cmdty = NULL; - for (std::list<transaction *>::iterator x = matching->xacts.begin(); - x != matching->xacts.end(); - x++) { - if (acct_regex.match((*x)->acct->as_str())) { - acct = (*x)->acct; - cmdty = (*x)->cost->commdty(); - break; - } + + if (matching) { + for (std::list<transaction *>::iterator x = matching->xacts.begin(); + x != matching->xacts.end(); + x++) { + if (acct_regex.match((*x)->acct->as_str())) { + acct = (*x)->acct; + cmdty = (*x)->cost->commdty(); + break; + } + } } if (acct) @@ -466,7 +700,12 @@ void add_new_entry(int index, int argc, char **argv) } } else { transaction * xact = new transaction(); - xact->acct = matching->xacts.back()->acct; + if (! matching) { + std::cerr << "Error: Could not figure out the account to draw from." + << std::endl; + std::exit(1); + } + xact->acct = matching->xacts.back()->acct; xact->cost = NULL; added.xacts.push_back(xact); } @@ -480,8 +719,8 @@ void add_new_entry(int index, int argc, char **argv) // "wash" ugly ledger files. It's written here, instead of ledger.cc, // in order to access the static globals above. -void book::print(std::ostream& out, regexps_map& regexps, - bool shortcut) const +void book::print(std::ostream& out, regexps_list& regexps, + bool shortcut) const { for (entries_list_const_iterator i = entries.begin(); i != entries.end(); @@ -506,25 +745,33 @@ static void show_help(std::ostream& out) << " -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 - << " -C also show cleared transactions" << std::endl + << " -C show only cleared transactions and balances" << std::endl << " -d DATE specify a date mask ('-d mon', for all mondays)" << std::endl + << " -E also show accounts with zero totals" << std::endl << " -f FILE specify pathname of ledger data file" << std::endl << " -F print each account's full name" << std::endl << " -h display this help text" << std::endl << " -i FILE read the list of inclusion regexps from FILE" << std::endl - << " -n do not generate totals for parent accounts" << std::endl + << " -l AMT don't print balance totals whose abs value is <AMT" << std::endl + << " -M print register using monthly sub-totals" << std::endl + << " -G use with -M to produce gnuplot-friendly output" << std::endl + << " -n do not calculate parent account totals" << std::endl + << " -N REGEX accounts matching REGEXP only display if negative" << std::endl << " -p ARG set a price, or read prices from a file" << std::endl << " -P download price quotes from the Internet" << std::endl << " (works by running the command \"getquote SYMBOL\")" << std::endl << " -R do not factor in virtual transactions" << std::endl << " -s show sub-accounts in balance totals" << std::endl - << " -S show empty accounts in balance totals" << std::endl + << " -S sort the output of \"print\" by date" << std::endl + << " -U show only uncleared transactions and balances" << std::endl << " -v display version information" << 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; + << " equity generate equity ledger for all entries" << std::endl + << " entry output a newly formed entry, based on arguments" + << std::endl; } ////////////////////////////////////////////////////////////////////// @@ -534,15 +781,18 @@ static void show_help(std::ostream& out) int main(int argc, char * argv[]) { - std::istream * file = NULL; - std::string prices; - regexps_map regexps; - int index; + int index; + std::string prices; + std::string limit; + regexps_list regexps; + + std::vector<std::string> files; // Parse the command-line options int c; - while (-1 != (c = getopt(argc, argv, "+b:e:d:cChRV:f:i:p:PvsSEnF"))) { + while (-1 != (c = getopt(argc, argv, + "+b:e:d:cCUhBRV:f:i:p:PvsSEnFMGl:N:"))) { switch (char(c)) { case 'b': have_beginning = true; @@ -574,19 +824,27 @@ int main(int argc, char * argv[]) break; case 'h': show_help(std::cout); break; - case 'f': file = new std::ifstream(optarg); break; - + case 'f': files.push_back(optarg); break; case 'C': cleared_only = true; break; + case 'U': uncleared_only = true; break; + case 'B': cost_basis = true; break; case 'R': show_virtual = false; break; case 's': show_children = true; break; case 'S': show_sorted = true; break; case 'E': show_empty = true; break; case 'n': show_subtotals = false; break; case 'F': full_names = true; break; + case 'M': print_monthly = true; break; + case 'G': gnuplot_safe = true; break; + + case 'N': + negonly_regexp = new mask(optarg); + break; // -i path-to-file-of-regexps case 'i': - read_regexps(optarg, regexps); + if (access(optarg, R_OK) != -1) + read_regexps(optarg, regexps); break; // -p "COMMODITY=PRICE" @@ -599,6 +857,10 @@ int main(int argc, char * argv[]) get_quotes = true; break; + case 'l': + limit = optarg; + break; + case 'v': std::cout << "Ledger Accouting Tool " LEDGER_VERSION << std::endl @@ -620,20 +882,6 @@ int main(int argc, char * argv[]) index = optind; - // 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++]; @@ -655,49 +903,64 @@ int main(int argc, char * argv[]) for (; index < argc; index++) regexps.push_back(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"); + // A ledger data file must be specified - delete file; + int entry_count = 0; + + main_ledger = new book; + + if (files.empty()) { + if (char * p = std::getenv("LEDGER")) { + for (p = std::strtok(p, ":"); p; p = std::strtok(NULL, ":")) { + char * sep = std::strrchr(p, '='); + if (sep) *sep++ = '\0'; + entry_count += parse_ledger_file(main_ledger, std::string(p), + regexps, command == "equity", sep); + } + } + } else { + for (std::vector<std::string>::iterator i = files.begin(); + i != files.end(); i++) { + char buf[4096]; + char * p = buf; + std::strcpy(p, (*i).c_str()); + char * sep = std::strrchr(p, '='); + if (sep) *sep++ = '\0'; + entry_count += parse_ledger_file(main_ledger, std::string(p), + regexps, command == "equity", sep); + } + } - if (! main_ledger) + if (entry_count == 0) { + std::cerr << ("Please specify ledger file(s) using -f option " + "or LEDGER environment variable.") << std::endl; return 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 { + if (access(prices.c_str(), R_OK) != -1) + read_prices(prices); + else parse_price_setting(prices); - } } + // Parse the lower limit, if specified + + if (! limit.empty()) + lower_limit = create_amount(limit); + // Process the command if (command == "balance" || command == "bal") { report_balances(std::cout, regexps); } else if (command == "register" || command == "reg") { - if (show_sorted) + if (show_sorted || print_monthly) main_ledger->sort(cmp_entry_date()); - print_register(argv[name_index], std::cout, regexps); + print_register(std::cout, argv[name_index], regexps, + print_monthly ? PERIOD_MONTHLY : PERIOD_NONE); } else if (command == "print") { if (show_sorted) @@ -721,6 +984,12 @@ int main(int argc, char * argv[]) // process is about to give back its heap to the OS. delete main_ledger; + + if (lower_limit) + delete lower_limit; + + if (negonly_regexp) + delete negonly_regexp; #endif return 0; |