diff options
-rwxr-xr-x | acprep | 1 | ||||
-rw-r--r-- | doc/ledger.1 | 298 | ||||
-rw-r--r-- | src/account.cc | 13 | ||||
-rw-r--r-- | src/account.h | 1 | ||||
-rw-r--r-- | src/archive.cc | 20 | ||||
-rw-r--r-- | src/archive.h | 6 | ||||
-rw-r--r-- | src/chain.cc | 5 | ||||
-rw-r--r-- | src/draft.h | 10 | ||||
-rw-r--r-- | src/filters.cc | 11 | ||||
-rw-r--r-- | src/filters.h | 34 | ||||
-rw-r--r-- | src/global.cc | 174 | ||||
-rw-r--r-- | src/global.h | 1 | ||||
-rw-r--r-- | src/hooks.h | 81 | ||||
-rw-r--r-- | src/item.h | 6 | ||||
-rw-r--r-- | src/journal.cc | 29 | ||||
-rw-r--r-- | src/journal.h | 13 | ||||
-rw-r--r-- | src/post.h | 2 | ||||
-rw-r--r-- | src/py_journal.cc | 143 | ||||
-rw-r--r-- | src/py_post.cc | 6 | ||||
-rw-r--r-- | src/py_xact.cc | 26 | ||||
-rw-r--r-- | src/pyinterp.cc | 12 | ||||
-rw-r--r-- | src/report.cc | 336 | ||||
-rw-r--r-- | src/report.h | 49 | ||||
-rw-r--r-- | src/session.cc | 6 | ||||
-rw-r--r-- | src/session.h | 6 | ||||
-rw-r--r-- | src/textual.cc | 13 | ||||
-rw-r--r-- | src/utils.cc | 4 | ||||
-rw-r--r-- | src/utils.h | 8 | ||||
-rw-r--r-- | src/value.cc | 6 | ||||
-rw-r--r-- | src/xact.cc | 66 | ||||
-rw-r--r-- | src/xact.h | 74 | ||||
-rw-r--r-- | test/baseline/opt-pricedb-format.test (renamed from test/baseline/opt-pricesdb-format.test) | 2 | ||||
-rw-r--r-- | tools/Makefile.am | 4 |
33 files changed, 914 insertions, 552 deletions
@@ -764,6 +764,7 @@ class PrepareBuild(CommandLineApp): self.CPPFLAGS.append('-D_GLIBCXX_FULLY_DYNAMIC_STRING=1') self.configure_args.append('--disable-shared') + self.configure_args.append('--enable-doxygen') self.options.use_glibcxx_debug = True self.locate_my_libraries() diff --git a/doc/ledger.1 b/doc/ledger.1 index 10fda0c5..6007aec3 100644 --- a/doc/ledger.1 +++ b/doc/ledger.1 @@ -1,4 +1,4 @@ -.Dd March 15, 2009 +.Dd November 12, 2009 .Dt ledger 1 .Sh NAME .Nm ledger @@ -9,45 +9,52 @@ ledger .Op Ar options .Op Ar arguments .Sh DESCRIPTION -Ledger is a command-line accounting tool providing the user access to the -power of double-entry accounting. It is only a reporting tool, which means it -never modifies your data files, nor can it be used to create or remove data. +Ledger is a command-line accounting tool based on the power and completeness +of double-entry accounting. It is only a reporting tool, which means it never +modifies your data files, but it does offers a large selection of reports, and +different ways to customize them to your liking. .Pp .Sh COMMANDS -Ledger accepts several top-level commands, each of which is used to generate a -different report. Most of them accept a +Ledger accepts several top-level commands, each of which generates a different +kind of basic report. Most of them accept a .Ar report-query -argument, in order to determine what to report. To understand what is -accepted by +argument, in order to determine what should be reported. To understand the +syntax of a .Ar report-query , see the section on .Sx QUERIES . -In its most basic form, simply specifying one or more strings will produce a +In its most basic form, simply specifying one or more strings produces a report for all accounts containing those strings. .Pp -If no command at all is given, Ledger enters a +If no command is given, Ledger enters a .Tn REPL , or command loop, allowing several commands to be executed against the same dataset without reparsing. .Pp -The following is a summary of all the reporting commands accepted by Ledger: +The following is a complete list of reporting commands accepted by Ledger: .Pp -.Bl -tag -width foo -.It Nm balance Oo Ar query Oc -Produce a balance report showing subtotals for matching leaf accounts, and -aggregate totals for all the parents of those accounts. The most common -options with this command are: +.Bl -tag -width balance +.It Nm balance Oo Ar report-query Oc +Produces a balance report showing totals for all matching accounts, and +aggregate totals for parents of those accounts. Options most commonly used +with this command are: .Pp .Bl -tag -compact -width "--collapse (-n)" +.It Fl \-basis Pq Fl B +Report in terms of cost basis, not amount or value. This is the only form of +report which is guaranteed to always balance to zero, when no +.Ar report-query +is specified. .It Fl \-collapse Pq Fl n -Only show totals in the top-most accounts. +Only show totals for the top-most accounts. .It Fl \-empty Pq Fl E -Show matching accounts whose total happens to be zero. +Also show accounts whose total is zero. .It Fl \-flat -Rather than displaying a hierarchical tree, flatten it to show only subtotals -for accounts directly matching the query. +Rather than display a hierarchical tree, flatten the report to show subtotals +for only accounts matching +.Ar report-query . .It Fl \-no\-total -Suppress the final total usually shown at the bottom of the report. +Suppress the summary total shown at the bottom of the report (when not zero). .El .Pp The synonyms @@ -55,31 +62,198 @@ The synonyms and .Nm b are also accepted. -.It Nm csv Oo Ar query Oc +.It Nm budget Oo Ar report-query Oc +A special balance report which includes three extra columns: the amount +budgeted during the reporting period, how spending differed from the budget, +and the percentage of budget spent (exceeds 100% if you go over budget). +Note that budgeting requires one or more +.Do +periodic transactions +.Dc +to be defined in your data file(s). See the manual for more information. +.It Nm cleared Oo Ar report-query Oc +A special balance report which adds two extra columns: the cleared balance for +each account, and the date of the most recent cleared posting in that account. +For this accounting to be meaningful, the cleared flag must be set on at least +one posting. See the manual for more information. +.It Nm csv Oo Ar report-query Oc +Report of postings matching the +.Ar report-query +in CSV format (comma-separated values). Useful for exporting data to a +spreadsheet for further analysis or charting. +.It Nm draft Oo Ar draft-template Oc +Generate and display a new, properly formatted Ledger transaction by comparing +the +.Ar draft-template +to the transactions in your data file(s). For more information on draft +templates and using this command to quickly create new transactions, see the +section +.Sx DRAFTS . +.Pp +The synonyms +.Nm entry +and +.Nm xact +are also accepted. .It Nm emacs Oo Ar query Oc +Outputs posting and transaction data in a format readily consumed by the Emacs +editor, in a series of Lisp forms. This is used by the +.Li ledger.el +Emacs mode to process reporting data from Ledger. +.Pp The synonym .Nm lisp is also accepted. -.It Nm equity Oo Ar query Oc -.It Nm generate -.It Nm prices Oo Ar query Oc -.It Nm pricesdb Oo Ar query Oc -.It Nm print Oo Ar query Oc -.It Nm register Oo Ar query Oc +.It Nm equity Oo Ar report-query Oc +Prints a series of transactions that balance current totals for +accounts matching the +.Ar report-query +in a special account called +.Li Equity:Opening Balances . +The purpose of this report is to close the books for a prior year, while using +these equity transactions to carry forward those balances. +.It Nm prices Oo Ar report-query Oc +Reports prices for all commodities in postings matching the +.Ar report-query . +The prices are reported with the granularity of a single day. +.It Nm pricesdb Oo Ar report-query Oc +Reports prices for all commodities in postings matching the +.Ar report-query . +Prices are reported down to the second, using the same format as the +.Li ~/.pricedb +file. +.It Nm print Oo Ar report-query Oc +Prints out the full transactions of any matching postings using the same +format as they would appear in a data file. This can be used to extract +subsets from a Ledger file to transfer to other files. +.It Nm push Oo Ar options Oc +In the +.Tn REPL , +this command pushes a set of command-line options, so that they will apply to +all subsequent reports. +.It Nm pop +In the +.Tn REPL , +pops any option settings that have been pushed. +.It Nm register Oo Ar report-query Oc +List all postings matching the +.Ar report-query . +This is one of the most common commands, and can be used to provide a variety +of useful reports. Options most commonly used +with this command are: +.Pp +.Bl -tag -compact -width "--collapse (-n)" +.It Fl \-average Pq Fl A +Show the running average, rather than a running total. +.It Fl \-current Pq Fl c +Don't show postings beyond the present day. +.It Fl \-exchange Ar commodity Pq Fl X +Render all values in the given +.Ar commodity , +if a price conversion rate can be determined. Rates are always displayed +relative to the date of the posting they are calculated for. This means a +.Nm register +report is a historical value report. For current values, it may be preferable +to use the +.Nm balance +report. +.It Fl \-gain Pq Fl G +Show any gains (or losses) in commodity values over time. +.It Fl \-head Ar number +Only show the top +.Ar number +postings. +.It Fl \-invert +Invert the value of amounts shown. +.It Fl \-market Pq Fl V +Show current market values for all amounts. This is determined in a somewhat +magical fashion. It is probably more straightforward to use +.Fl \-exchange Pq Fl X . +.It Fl \-period Ar time-period Pq Fl p +Show postings only for the given +.Ar time-period . +.It Fl \-related Pq Fl r +Show postings that are related to those that would have been shown. It has +the effect of displaying the +.Do +other side +.Dc +of the values. +.It Fl \-sort Ar value-expression Pq Fl S +Sort postings by evaluating the given +.Ar value-expression . +Note that a comma-separated list of expressions is allowed, in which case each +sorting term is used in order to determine the final ordering. For example, +to search by date and then amount, one would use: +.Li -S 'date, amount' . +.It Fl \-tail Ar number +Only show the last +.Ar number +postings. +.It Fl \-uncleared Pq Fl U +Only show uncleared (i.e., recent) postings. +.El +.Pp +There are also several grouping options that can be useful: +.Pp +.Bl -tag -compact -width "--collapse (-n)" +.It Fl \-by-payee Pq Fl P +Group postings by common payee names. +.It Fl \-daily Pq Fl D +Group postings by day. +.It Fl \-weekly Pq Fl W +Group postings by week (starting on Sundays). +.It Fl \-start-of-week Ar day-name +Set the start of each grouped way to the given +.Ar day-name . +.It Fl \-monthly Pq Fl M +Group postings by month. +.It Fl \-quarterly +Group postings by fiscal quarter. +.It Fl \-yearly Pq Fl Y +Group postings by year. +.It Fl \-dow +Group postings by the day of the week on which they took place. +.It Fl \-subtotal Pq Fl s +Group all postings together. This is very similar to the totals shown by the +.Nm balance +report. +.El +.Pp The synonyms .Nm reg and .Nm r are also accepted. -.It Nm reload -Used solely by the -.It Nm xact Oo Ar date Oc -The synonym -.Nm entry -is also accepted. -.Tn REPL , -and causes an immediate reloading of all journal files in the session. -.It Nm stats Oo Ar query Oc +.It Nm server +This command requires that Python support be active. If so, it starts up an +HTTP server listening for requests on port 9000. This provides an alternate +interface to creating and viewing reports. Note that this is very much a +work-in-progress, and will not be fully functional until a later version. +.It Nm stats Oo Ar report-query Oc +Provides summary information about all the postings matching +.Ar report-query . +It provides information such as: +.Bl -bullet -offset indent -compact +.It +Time range of all matching postings +.It +Unique payees +.It +Unique accounts +.It +Postings total +.It +Uncleared postings +.It +Days since last posting +.It +More... +.El +.It Nm xml Oo Ar report-query Oc +Outputs data relating to the current report in XML format. It includes all +accounts and commodities involved in the report, plus the postings and the +transactions they are contained in. See the manual for more information. .El .Pp .Sh OPTIONS @@ -349,6 +523,56 @@ for example: .It Nm xact .El .Pp +.Sh DRAFTS +.Pp +.Sh FORMATS +.Pp +.Sh DEBUG COMMANDS +In addition to the regular reporting commands, Ledger also accepts several +debug commands: +.Bl -tag -width balance +.It Nm args Oo Ar report-query Oc +Accepts a +.Ar report-query +as its argument and displays it back to the user along with a complete +analysis of how Ledger interpreted it. Useful if you want to understand how +report queries are translated into value expressions. +.It Nm eval Oo Ar value-expression Oc +Evaluates the given +.Ar value-expression +and prints the result. For more on value expressions, see the section +.Sx EXPRESSIONS . +.It Nm format Oo Ar format-string Oc +Accepts a +.Ar format-string +and displays an analysis of how it was parsed, and what it would look like +applied to a sample transaction. For more on format strings, see the section +.Sx FORMATS . +.It Nm generate +Generates 50 randomly composed yet valid Ledger transactions. +.It Nm parse Oo Ar value-expression Oc +Parses the given +.Ar value-expression +and display an analysis of the expression tree and its evaluated value. For +more on value expressions, see the section +.Sx EXPRESSIONS . +.It Nm python Oo Ar file Oc +Invokes a Python interpreter to read the given +.Ar file . +What is special about this is that the ledger module is builtin, not read from +disk, so it doesn't require Ledger to be installed anywhere, or the shared +library variants to be built. +.It Nm reload +Used only in the +.Tn REPL , +it causes an immediate reloading of all data files for the current session. +.It Nm template Oo Ar draft-template Oc +Accepts a +.Ar draft-template +and displays information about how it was parsed. See the section on +.Sx DRAFTS . +.El +.Pp .Sh SEE ALSO .Xr beancount 1, .Xr hledger 1 diff --git a/src/account.cc b/src/account.cc index 5cc7e070..da43745a 100644 --- a/src/account.cc +++ b/src/account.cc @@ -327,16 +327,25 @@ bool account_t::valid() const return true; } +bool account_t::children_with_xdata() const +{ + foreach (const accounts_map::value_type& pair, accounts) + if (pair.second->has_xdata() || + pair.second->children_with_xdata()) + return true; + + return false; +} + std::size_t account_t::children_with_flags(xdata_t::flags_t flags) const { std::size_t count = 0; bool grandchildren_visited = false; - foreach (const accounts_map::value_type& pair, accounts) { + foreach (const accounts_map::value_type& pair, accounts) if (pair.second->has_xflags(flags) || pair.second->children_with_flags(flags)) count++; - } // Although no immediately children were visited, if any progeny at all were // visited, it counts as one. diff --git a/src/account.h b/src/account.h index 0ac1aeb6..1e56fe23 100644 --- a/src/account.h +++ b/src/account.h @@ -241,6 +241,7 @@ public: bool has_xflags(xdata_t::flags_t flags) const { return xdata_ && xdata_->has_flags(flags); } + bool children_with_xdata() const; std::size_t children_with_flags(xdata_t::flags_t flags) const; #if defined(HAVE_BOOST_SERIALIZATION) diff --git a/src/archive.cc b/src/archive.cc index 5ea6cd8e..d36712ca 100644 --- a/src/archive.cc +++ b/src/archive.cc @@ -201,23 +201,23 @@ bool archive_t::should_load(const std::list<path>& data_files) return true; } -bool archive_t::should_save(shared_ptr<journal_t> journal) +bool archive_t::should_save(journal_t& journal) { std::list<path> data_files; DEBUG("archive.journal", "Should the archive be saved?"); - if (journal->was_loaded) { + if (journal.was_loaded) { DEBUG("archive.journal", "No, it's one we loaded before"); return false; } - if (journal->sources.empty()) { + if (journal.sources.empty()) { DEBUG("archive.journal", "No, there were no sources!"); return false; } - foreach (const journal_t::fileinfo_t& i, journal->sources) { + foreach (const journal_t::fileinfo_t& i, journal.sources) { if (i.from_stream) { DEBUG("archive.journal", "No, one source was from a stream"); return false; @@ -241,14 +241,14 @@ bool archive_t::should_save(shared_ptr<journal_t> journal) return true; } -void archive_t::save(shared_ptr<journal_t> journal) +void archive_t::save(journal_t& journal) { INFO_START(archive, "Saved journal file cache"); ofstream stream(file, std::ios::binary); write_header_bits(stream); - sources = journal->sources; + sources = journal.sources; #if defined(DEBUG_ON) foreach (const journal_t::fileinfo_t& i, sources) @@ -263,12 +263,12 @@ void archive_t::save(shared_ptr<journal_t> journal) DEBUG("archive.journal", "Archiving journal with " << sources.size() << " sources"); - oa << *journal; + oa << journal; INFO_FINISH(archive); } -bool archive_t::load(shared_ptr<journal_t> journal) +bool archive_t::load(journal_t& journal) { INFO_START(archive, "Read cached journal file"); @@ -282,8 +282,8 @@ bool archive_t::load(shared_ptr<journal_t> journal) archive_t temp; iarchive >> temp; - iarchive >> *journal.get(); - journal->was_loaded = true; + iarchive >> journal; + journal.was_loaded = true; INFO_FINISH(archive); diff --git a/src/archive.h b/src/archive.h index 60ead5a9..03fc970a 100644 --- a/src/archive.h +++ b/src/archive.h @@ -69,10 +69,10 @@ public: bool read_header(); bool should_load(const std::list<path>& data_files); - bool should_save(shared_ptr<journal_t> journal); + bool should_save(journal_t& journal); - void save(shared_ptr<journal_t> journal); - bool load(shared_ptr<journal_t> journal); + void save(journal_t& journal); + bool load(journal_t& journal); #if defined(HAVE_BOOST_SERIALIZATION) private: diff --git a/src/chain.cc b/src/chain.cc index 5839bd9e..55ef467b 100644 --- a/src/chain.cc +++ b/src/chain.cc @@ -159,6 +159,11 @@ post_handler_ptr chain_post_handlers(report_t& report, handler.reset(new sort_posts(handler, "date")); } + if (report.HANDLED(date_)) + handler.reset(new transfer_details(handler, transfer_details::SET_DATE, + report.session.journal->master, + report.HANDLER(date_).str(), + report)); if (report.HANDLED(account_)) handler.reset(new transfer_details(handler, transfer_details::SET_ACCOUNT, report.session.journal->master, diff --git a/src/draft.h b/src/draft.h index faefa67b..277b4ff8 100644 --- a/src/draft.h +++ b/src/draft.h @@ -30,17 +30,17 @@ */ /** - * @addtogroup derive + * @addtogroup expr */ /** - * @file derive.h + * @file draft.h * @author John Wiegley * * @ingroup report */ -#ifndef _DERIVE_H -#define _DERIVE_H +#ifndef _DRAFT_H +#define _DRAFT_H #include "exprbase.h" #include "value.h" @@ -110,4 +110,4 @@ value_t template_command(call_scope_t& args); } // namespace ledger -#endif // _DERIVE_H +#endif // _DRAFT_H diff --git a/src/filters.cc b/src/filters.cc index 4b54a0cd..8c5d099e 100644 --- a/src/filters.cc +++ b/src/filters.cc @@ -706,22 +706,27 @@ void transfer_details::operator()(post_t& post) temp.set_state(post.state()); bind_scope_t bound_scope(scope, temp); + value_t substitute(expr.calc(bound_scope)); switch (which_element) { - case SET_PAYEE: - xact.payee = expr.calc(bound_scope).to_string(); + case SET_DATE: + xact.set_date(substitute.to_date()); break; case SET_ACCOUNT: { std::list<string> account_names; temp.account->remove_post(&temp); - split_string(expr.calc(bound_scope).to_string(), ':', account_names); + split_string(substitute.to_string(), ':', account_names); temp.account = create_temp_account_from_path(account_names, temps, xact.journal->master); temp.account->add_post(&temp); break; } + case SET_PAYEE: + xact.payee = substitute.to_string(); + break; + default: assert(false); break; diff --git a/src/filters.h b/src/filters.h index ba3692ac..80bbe5b4 100644 --- a/src/filters.h +++ b/src/filters.h @@ -61,6 +61,35 @@ public: virtual void operator()(post_t&) {} }; +class collect_posts : public item_handler<post_t> +{ +public: + std::vector<post_t *> posts; + + collect_posts() : item_handler<post_t>() { + TRACE_CTOR(collect_posts, ""); + } + virtual ~collect_posts() { + TRACE_DTOR(collect_posts); + } + + std::size_t length() const { + return posts.size(); + } + + std::vector<post_t *>::iterator begin() { + return posts.begin(); + } + std::vector<post_t *>::iterator end() { + return posts.end(); + } + + virtual void flush() {} + virtual void operator()(post_t& post) { + posts.push_back(&post); + } +}; + class posts_iterator; class pass_down_posts : public item_handler<post_t> @@ -550,8 +579,9 @@ class transfer_details : public item_handler<post_t> public: enum element_t { - SET_PAYEE, - SET_ACCOUNT + SET_DATE, + SET_ACCOUNT, + SET_PAYEE } which_element; transfer_details(post_handler_ptr handler, diff --git a/src/global.cc b/src/global.cc index 0c8cedd8..e120e5d5 100644 --- a/src/global.cc +++ b/src/global.cc @@ -194,7 +194,7 @@ void global_scope_t::execute_command(strings_list args, bool at_repl) if (! at_repl) session().read_journal_files(); - normalize_report_options(verb); + report().normalize_options(verb); if (! bool(command = look_for_command(bound_scope, verb))) throw_(std::logic_error, _("Unrecognized command '%1'") << verb); @@ -416,178 +416,6 @@ expr_t::func_t global_scope_t::look_for_command(scope_t& scope, return expr_t::func_t(); } -void global_scope_t::normalize_report_options(const string& verb) -{ - // Patch up some of the reporting options based on what kind of - // command it was. - - report_t& rep(report()); - -#ifdef HAVE_ISATTY - if (! rep.HANDLED(force_color)) { - if (! rep.HANDLED(no_color) && isatty(STDOUT_FILENO)) - rep.HANDLER(color).on_only(string("?normalize")); - if (rep.HANDLED(color) && ! isatty(STDOUT_FILENO)) - rep.HANDLER(color).off(); - } - if (! rep.HANDLED(force_pager)) { - if (rep.HANDLED(pager_) && ! isatty(STDOUT_FILENO)) - rep.HANDLER(pager_).off(); - } -#endif - - item_t::use_effective_date = (rep.HANDLED(effective) && - ! rep.HANDLED(actual_dates)); - - rep.session.journal->commodity_pool->keep_base = rep.HANDLED(base); - rep.session.journal->commodity_pool->get_quotes = rep.session.HANDLED(download); - - if (rep.session.HANDLED(price_exp_)) - rep.session.journal->commodity_pool->quote_leeway = - rep.session.HANDLER(price_exp_).value.as_long(); - - if (rep.session.HANDLED(price_db_)) - rep.session.journal->commodity_pool->price_db = - rep.session.HANDLER(price_db_).str(); - else - rep.session.journal->commodity_pool->price_db = none; - - if (rep.HANDLED(date_format_)) - set_date_format(rep.HANDLER(date_format_).str().c_str()); - if (rep.HANDLED(datetime_format_)) - set_datetime_format(rep.HANDLER(datetime_format_).str().c_str()); - if (rep.HANDLED(start_of_week_)) { - if (optional<date_time::weekdays> weekday = - string_to_day_of_week(rep.HANDLER(start_of_week_).str())) - start_of_week = *weekday; - } - - if (verb == "print" || verb == "xact" || verb == "dump") { - rep.HANDLER(related).on_only(string("?normalize")); - rep.HANDLER(related_all).on_only(string("?normalize")); - } - else if (verb == "equity") { - rep.HANDLER(equity).on_only(string("?normalize")); - } - - if (verb == "print") - rep.HANDLER(limit_).on(string("?normalize"), "actual"); - - if (! rep.HANDLED(empty)) - rep.HANDLER(display_).on(string("?normalize"), "amount|(!post&total)"); - - if (verb[0] != 'b' && verb[0] != 'r') - rep.HANDLER(base).on_only(string("?normalize")); - - // If a time period was specified with -p, check whether it also gave a - // begin and/or end to the report period (though these can be overridden - // using -b or -e). Then, if no _duration_ was specified (such as monthly), - // then ignore the period since the begin/end are the only interesting - // details. - if (rep.HANDLED(period_)) { - if (! rep.HANDLED(sort_all_)) - rep.HANDLER(sort_xacts_).on_only(string("?normalize")); - - date_interval_t interval(rep.HANDLER(period_).str()); - - if (! rep.HANDLED(begin_) && interval.start) { - string predicate = - "date>=[" + to_iso_extended_string(*interval.start) + "]"; - rep.HANDLER(limit_).on(string("?normalize"), predicate); - } - if (! rep.HANDLED(end_) && interval.end) { - string predicate = - "date<[" + to_iso_extended_string(*interval.end) + "]"; - rep.HANDLER(limit_).on(string("?normalize"), predicate); - } - - if (! interval.duration) - rep.HANDLER(period_).off(); - } - - // If -j or -J were specified, set the appropriate format string now so as - // to avoid option ordering issues were we to have done it during the - // initial parsing of the options. - if (rep.HANDLED(amount_data)) { - rep.HANDLER(format_) - .on_with(string("?normalize"), rep.HANDLER(plot_amount_format_).value); - } - else if (rep.HANDLED(total_data)) { - rep.HANDLER(format_) - .on_with(string("?normalize"), rep.HANDLER(plot_total_format_).value); - } - - // If the --exchange (-X) option was used, parse out any final price - // settings that may be there. - if (rep.HANDLED(exchange_) && - rep.HANDLER(exchange_).str().find('=') != string::npos) { - value_t(0L).exchange_commodities(rep.HANDLER(exchange_).str(), true, - rep.terminus); - } - - long cols = 0; - if (rep.HANDLED(columns_)) - cols = rep.HANDLER(columns_).value.to_long(); - else if (const char * columns = std::getenv("COLUMNS")) - cols = lexical_cast<long>(columns); - else - cols = 80L; - - if (cols > 0) { - DEBUG("auto.columns", "cols = " << cols); - - if (! rep.HANDLER(date_width_).specified) - rep.HANDLER(date_width_) - .on_with(none, static_cast<long>(format_date(CURRENT_DATE(), - FMT_PRINTED).length())); - - long date_width = rep.HANDLER(date_width_).value.to_long(); - long payee_width = (rep.HANDLER(payee_width_).specified ? - rep.HANDLER(payee_width_).value.to_long() : - int(double(cols) * 0.263157)); - long account_width = (rep.HANDLER(account_width_).specified ? - rep.HANDLER(account_width_).value.to_long() : - int(double(cols) * 0.302631)); - long amount_width = (rep.HANDLER(amount_width_).specified ? - rep.HANDLER(amount_width_).value.to_long() : - int(double(cols) * 0.157894)); - long total_width = (rep.HANDLER(total_width_).specified ? - rep.HANDLER(total_width_).value.to_long() : - amount_width); - - DEBUG("auto.columns", "date_width = " << date_width); - DEBUG("auto.columns", "payee_width = " << payee_width); - DEBUG("auto.columns", "account_width = " << account_width); - DEBUG("auto.columns", "amount_width = " << amount_width); - DEBUG("auto.columns", "total_width = " << total_width); - - if (! rep.HANDLER(date_width_).specified && - ! rep.HANDLER(payee_width_).specified && - ! rep.HANDLER(account_width_).specified && - ! rep.HANDLER(amount_width_).specified && - ! rep.HANDLER(total_width_).specified) { - long total = (4 /* the spaces between */ + date_width + payee_width + - account_width + amount_width + total_width); - if (total > cols) { - DEBUG("auto.columns", "adjusting account down"); - account_width -= total - cols; - DEBUG("auto.columns", "account_width now = " << account_width); - } - } - - if (! rep.HANDLER(date_width_).specified) - rep.HANDLER(date_width_).on_with(string("?normalize"), date_width); - if (! rep.HANDLER(payee_width_).specified) - rep.HANDLER(payee_width_).on_with(string("?normalize"), payee_width); - if (! rep.HANDLER(account_width_).specified) - rep.HANDLER(account_width_).on_with(string("?normalize"), account_width); - if (! rep.HANDLER(amount_width_).specified) - rep.HANDLER(amount_width_).on_with(string("?normalize"), amount_width); - if (! rep.HANDLER(total_width_).specified) - rep.HANDLER(total_width_).on_with(string("?normalize"), total_width); - } -} - void global_scope_t::visit_man_page() const { int pid = fork(); diff --git a/src/global.h b/src/global.h index f7b87973..ab3afed4 100644 --- a/src/global.h +++ b/src/global.h @@ -62,7 +62,6 @@ public: void normalize_session_options(); expr_t::func_t look_for_precommand(scope_t& scope, const string& verb); expr_t::func_t look_for_command(scope_t& scope, const string& verb); - void normalize_report_options(const string& verb); char * prompt_string(); diff --git a/src/hooks.h b/src/hooks.h deleted file mode 100644 index 20c7750c..00000000 --- a/src/hooks.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2003-2009, John Wiegley. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * - Neither the name of New Artisans LLC nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/** - * @addtogroup util - */ - -/** - * @file hooks.h - * @author John Wiegley - * - * @ingroup util - */ -#ifndef _HOOKS_H -#define _HOOKS_H - -template <typename T, typename Data> -class hooks_t : public boost::noncopyable -{ -public: - typedef boost::function<bool (Data&, bool)> function_t; - -protected: - std::list<T *> list; - -public: - hooks_t() { - TRACE_CTOR(hooks_t, ""); - } - ~hooks_t() throw() { - TRACE_DTOR(hooks_t); - } - - void add_hook(T * func, const bool prepend = false) { - if (prepend) - list.push_front(func); - else - list.push_back(func); - } - - void remove_hook(T * func) { - list.remove(func); - } - - bool run_hooks(Data& item) { - foreach (T * func, list) - if (! (*func)(item)) - return false; - return true; - } -}; - -#endif // _HOOKS_H @@ -172,6 +172,12 @@ public: virtual optional<date_t> effective_date() const { return _date_eff; } + virtual void set_date(const date_t& date) { + if (use_effective_date) + _date_eff = date; + else + _date = date; + } void set_state(state_t new_state) { _state = new_state; diff --git a/src/journal.cc b/src/journal.cc index b7ad9a23..2366ce30 100644 --- a/src/journal.cc +++ b/src/journal.cc @@ -126,16 +126,23 @@ bool journal_t::add_xact(xact_t * xact) { xact->journal = this; - if (! xact->finalize() || ! xact_finalize_hooks.run_hooks(*xact)) { + if (! xact->finalize()) { xact->journal = NULL; return false; } + extend_xact(xact); xacts.push_back(xact); return true; } +void journal_t::extend_xact(xact_base_t * xact) +{ + foreach (auto_xact_t * auto_xact, auto_xacts) + auto_xact->extend_xact(*xact); +} + bool journal_t::remove_xact(xact_t * xact) { bool found = false; @@ -204,6 +211,26 @@ std::size_t journal_t::read(const path& pathname, return count; } +bool journal_t::has_xdata() +{ + foreach (xact_t * xact, xacts) + if (xact->has_xdata()) + return true; + + foreach (auto_xact_t * xact, auto_xacts) + if (xact->has_xdata()) + return true; + + foreach (period_xact_t * xact, period_xacts) + if (xact->has_xdata()) + return true; + + if (master->has_xdata() || master->children_with_xdata()) + return true; + + return false; +} + void journal_t::clear_xdata() { foreach (xact_t * xact, xacts) diff --git a/src/journal.h b/src/journal.h index 78269348..f7124736 100644 --- a/src/journal.h +++ b/src/journal.h @@ -43,15 +43,14 @@ #define _JOURNAL_H #include "utils.h" -#include "hooks.h" #include "times.h" namespace ledger { class commodity_pool_t; +class xact_base_t; class xact_t; class auto_xact_t; -class xact_finalizer_t; class period_xact_t; class account_t; class scope_t; @@ -114,7 +113,6 @@ public: bool was_loaded; shared_ptr<commodity_pool_t> commodity_pool; - hooks_t<xact_finalizer_t, xact_t> xact_finalize_hooks; journal_t(); journal_t(const path& pathname); @@ -138,6 +136,7 @@ public: account_t * find_account_re(const string& regexp); bool add_xact(xact_t * xact); + void extend_xact(xact_base_t * xact); bool remove_xact(xact_t * xact); xacts_list::iterator xacts_begin() { @@ -159,13 +158,6 @@ public: return period_xacts.end(); } - void add_xact_finalizer(xact_finalizer_t * finalizer) { - xact_finalize_hooks.add_hook(finalizer); - } - void remove_xact_finalizer(xact_finalizer_t * finalizer) { - xact_finalize_hooks.remove_hook(finalizer); - } - std::size_t read(std::istream& in, const path& pathname, account_t * master = NULL, @@ -180,6 +172,7 @@ public: const path * original_file = NULL, bool strict = false); + bool has_xdata(); void clear_xdata(); bool valid() const; @@ -92,7 +92,7 @@ public: { TRACE_CTOR(post_t, "copy"); } - ~post_t() { + virtual ~post_t() { TRACE_DTOR(post_t); } diff --git a/src/py_journal.cc b/src/py_journal.cc index 88447b92..bea14d66 100644 --- a/src/py_journal.cc +++ b/src/py_journal.cc @@ -33,9 +33,14 @@ #include "pyinterp.h" #include "pyutils.h" -#include "hooks.h" #include "journal.h" #include "xact.h" +#include "post.h" +#include "chain.h" +#include "filters.h" +#include "iterators.h" +#include "scope.h" +#include "report.h" namespace ledger { @@ -130,51 +135,113 @@ namespace { return journal.find_account(name, auto_create); } - struct py_xact_finalizer_t : public xact_finalizer_t { - object pyobj; - py_xact_finalizer_t() {} - py_xact_finalizer_t(object obj) : pyobj(obj) {} - py_xact_finalizer_t(const py_xact_finalizer_t& other) - : pyobj(other.pyobj) {} - virtual bool operator()(xact_t& xact) { - return call<bool>(pyobj.ptr(), xact); - } - }; - - std::list<py_xact_finalizer_t> py_finalizers; - - void py_add_xact_finalizer(journal_t& journal, object x) + std::size_t py_read(journal_t& journal, const string& pathname) { - py_finalizers.push_back(py_xact_finalizer_t(x)); - journal.add_xact_finalizer(&py_finalizers.back()); + return journal.read(pathname); } - void py_remove_xact_finalizer(journal_t& journal, object x) + struct collector_wrapper { - for (std::list<py_xact_finalizer_t>::iterator i = py_finalizers.begin(); - i != py_finalizers.end(); - i++) - if ((*i).pyobj == x) { - journal.remove_xact_finalizer(&(*i)); - py_finalizers.erase(i); - return; - } - } + journal_t& journal; + report_t report; + collect_posts * posts_collector; + post_handler_ptr chain; + + collector_wrapper(journal_t& _journal, report_t& base) + : journal(_journal), report(base), + posts_collector(new collect_posts) {} + ~collector_wrapper() { + journal.clear_xdata(); + } - void py_run_xact_finalizers(journal_t& journal, xact_t& xact) + std::size_t length() const { + return posts_collector->length(); + } + + std::vector<post_t *>::iterator begin() { + return posts_collector->begin(); + } + std::vector<post_t *>::iterator end() { + return posts_collector->end(); + } + }; + + shared_ptr<collector_wrapper> + py_collect(journal_t& journal, const string& query) { - journal.xact_finalize_hooks.run_hooks(xact); + if (journal.has_xdata()) { + PyErr_SetString(PyExc_RuntimeError, + _("Cannot have multiple journal collections open at once")); + throw_error_already_set(); + } + + report_t& current_report(downcast<report_t>(*scope_t::default_scope)); + shared_ptr<collector_wrapper> coll(new collector_wrapper(journal, + current_report)); + std::auto_ptr<journal_t> save_journal + (current_report.session.journal.release()); + current_report.session.journal.reset(&journal); + + try { + strings_list remaining = + process_arguments(split_arguments(query.c_str()), coll->report); + coll->report.normalize_options("register"); + + value_t args; + foreach (const string& arg, remaining) + args.push_back(string_value(arg)); + coll->report.parse_query_args(args, "@Journal.collect"); + + journal_posts_iterator walker(coll->journal); + coll->chain = + chain_post_handlers(coll->report, + post_handler_ptr(coll->posts_collector)); + pass_down_posts(coll->chain, walker); + } + catch (...) { + current_report.session.journal.release(); + current_report.session.journal.reset(save_journal.release()); + throw; + } + current_report.session.journal.release(); + current_report.session.journal.reset(save_journal.release()); + + return coll; } - std::size_t py_read(journal_t& journal, const string& pathname) + post_t * posts_getitem(collector_wrapper& collector, long i) { - return journal.read(pathname); + post_t * post = collector.posts_collector->posts[i]; + std::cerr << typeid(post).name() << std::endl; + std::cerr << typeid(*post).name() << std::endl; + std::cerr << typeid(post->account).name() << std::endl; + std::cerr << typeid(*post->account).name() << std::endl; + return post; } } // unnamed namespace void export_journal() { + class_< item_handler<post_t>, shared_ptr<item_handler<post_t> >, + boost::noncopyable >("PostHandler") + ; + + class_< collect_posts, bases<item_handler<post_t> >, + shared_ptr<collect_posts>, boost::noncopyable >("PostCollector") + .def("__len__", &collect_posts::length) + .def("__iter__", range<return_internal_reference<> > + (&collect_posts::begin, &collect_posts::end)) + ; + + class_< collector_wrapper, shared_ptr<collector_wrapper>, + boost::noncopyable >("PostCollectorWrapper", no_init) + .def("__len__", &collector_wrapper::length) + .def("__getitem__", posts_getitem, return_internal_reference<>()) + .def("__iter__", range<return_internal_reference<> > + (&collector_wrapper::begin, &collector_wrapper::end)) + ; + class_< journal_t::fileinfo_t > ("FileInfo") .def(init<path>()) @@ -206,11 +273,6 @@ void export_journal() .add_property("commodity_pool", make_getter(&journal_t::commodity_pool, return_internal_reference<>())) -#if 0 - .add_property("xact_finalize_hooks", - make_getter(&journal_t::xact_finalize_hooks), - make_setter(&journal_t::xact_finalize_hooks)) -#endif .def("add_account", &journal_t::add_account) .def("remove_account", &journal_t::remove_account) @@ -223,12 +285,8 @@ void export_journal() .def("add_xact", &journal_t::add_xact) .def("remove_xact", &journal_t::remove_xact) - .def("add_xact_finalizer", py_add_xact_finalizer) - .def("remove_xact_finalizer", py_remove_xact_finalizer) - .def("run_xact_finalizers", py_run_xact_finalizers) - .def("__len__", xacts_len) - .def("__getitem__", xacts_getitem, return_internal_reference<1>()) + .def("__getitem__", xacts_getitem, return_internal_reference<>()) .def("__iter__", range<return_internal_reference<> > (&journal_t::xacts_begin, &journal_t::xacts_end)) @@ -243,8 +301,11 @@ void export_journal() .def("read", py_read) + .def("has_xdata", &journal_t::has_xdata) .def("clear_xdata", &journal_t::clear_xdata) + .def("collect", py_collect) + .def("valid", &journal_t::valid) ; } diff --git a/src/py_post.cc b/src/py_post.cc index 80e7ee52..8aabea28 100644 --- a/src/py_post.cc +++ b/src/py_post.cc @@ -115,8 +115,10 @@ void export_post() make_getter(&post_t::xdata_t::datetime), make_setter(&post_t::xdata_t::datetime)) .add_property("account", - make_getter(&post_t::xdata_t::account), - make_setter(&post_t::xdata_t::account)) + make_getter(&post_t::xdata_t::account, + return_internal_reference<>()), + make_setter(&post_t::xdata_t::account, + with_custodian_and_ward<1, 2>())) .add_property("sort_values", make_getter(&post_t::xdata_t::sort_values), make_setter(&post_t::xdata_t::sort_values)) diff --git a/src/py_xact.cc b/src/py_xact.cc index 6feb6080..59c599d9 100644 --- a/src/py_xact.cc +++ b/src/py_xact.cc @@ -122,16 +122,12 @@ void export_xact() .def("lookup", &xact_t::lookup) + .def("has_xdata", &xact_t::has_xdata) .def("clear_xdata", &xact_t::clear_xdata) .def("valid", &xact_t::valid) ; - class_< xact_finalizer_t, boost::noncopyable > - ("TransactionFinalizer", no_init) - .def("__call__", &xact_finalizer_t::operator()) - ; - class_< auto_xact_t, bases<xact_base_t> > ("AutomatedTransaction") .def(init<predicate_t>()) @@ -142,16 +138,6 @@ void export_xact() .def("extend_xact", &auto_xact_t::extend_xact) ; - class_< auto_xact_finalizer_t, bases<xact_finalizer_t> > - ("AutomatedTransactionFinalizer") - .add_property("journal", - make_getter(&auto_xact_finalizer_t::journal, - return_internal_reference<>()), - make_setter(&auto_xact_finalizer_t::journal, - with_custodian_and_ward<1, 2>())) - .def("__call__", &auto_xact_finalizer_t::operator()) - ; - class_< period_xact_t, bases<xact_base_t> > ("PeriodicTransaction") .def(init<string>()) @@ -162,16 +148,6 @@ void export_xact() make_getter(&period_xact_t::period_string), make_setter(&period_xact_t::period_string)) ; - - class_< func_finalizer_t, bases<xact_finalizer_t> > - ("FunctionalFinalizer", init<func_finalizer_t::func_t>()) - .add_property("func", - make_getter(&func_finalizer_t::func), - make_setter(&func_finalizer_t::func)) - .def("__call__", &func_finalizer_t::operator()) - ; - - scope().attr("extend_xact_base") = &extend_xact_base; } } // namespace ledger diff --git a/src/pyinterp.cc b/src/pyinterp.cc index 394739c4..0a56049c 100644 --- a/src/pyinterp.cc +++ b/src/pyinterp.cc @@ -60,19 +60,19 @@ void export_xact(); void initialize_for_python() { - export_account(); + export_times(); + export_utils(); + export_commodity(); export_amount(); + export_value(); + export_account(); export_balance(); - export_commodity(); export_expr(); export_format(); export_item(); - export_journal(); export_post(); - export_times(); - export_utils(); - export_value(); export_xact(); + export_journal(); } struct python_run diff --git a/src/report.cc b/src/report.cc index b866970f..49633350 100644 --- a/src/report.cc +++ b/src/report.cc @@ -33,6 +33,7 @@ #include "report.h" #include "session.h" +#include "pool.h" #include "format.h" #include "query.h" #include "output.h" @@ -47,6 +48,201 @@ namespace ledger { +void report_t::normalize_options(const string& verb) +{ + // Patch up some of the reporting options based on what kind of + // command it was. + +#ifdef HAVE_ISATTY + if (! HANDLED(force_color)) { + if (! HANDLED(no_color) && isatty(STDOUT_FILENO)) + HANDLER(color).on_only(string("?normalize")); + if (HANDLED(color) && ! isatty(STDOUT_FILENO)) + HANDLER(color).off(); + } + if (! HANDLED(force_pager)) { + if (HANDLED(pager_) && ! isatty(STDOUT_FILENO)) + HANDLER(pager_).off(); + } +#endif + + item_t::use_effective_date = (HANDLED(effective) && + ! HANDLED(actual_dates)); + + session.journal->commodity_pool->keep_base = HANDLED(base); + session.journal->commodity_pool->get_quotes = session.HANDLED(download); + + if (session.HANDLED(price_exp_)) + session.journal->commodity_pool->quote_leeway = + session.HANDLER(price_exp_).value.as_long(); + + if (session.HANDLED(price_db_)) + session.journal->commodity_pool->price_db = + session.HANDLER(price_db_).str(); + else + session.journal->commodity_pool->price_db = none; + + if (HANDLED(date_format_)) + set_date_format(HANDLER(date_format_).str().c_str()); + if (HANDLED(datetime_format_)) + set_datetime_format(HANDLER(datetime_format_).str().c_str()); + if (HANDLED(start_of_week_)) { + if (optional<date_time::weekdays> weekday = + string_to_day_of_week(HANDLER(start_of_week_).str())) + start_of_week = *weekday; + } + + if (verb == "print" || verb == "xact" || verb == "dump") { + HANDLER(related).on_only(string("?normalize")); + HANDLER(related_all).on_only(string("?normalize")); + } + else if (verb == "equity") { + HANDLER(equity).on_only(string("?normalize")); + } + + if (verb == "print") + HANDLER(limit_).on(string("?normalize"), "actual"); + + if (! HANDLED(empty)) + HANDLER(display_).on(string("?normalize"), "amount|(!post&total)"); + + if (verb[0] != 'b' && verb[0] != 'r') + HANDLER(base).on_only(string("?normalize")); + + // If a time period was specified with -p, check whether it also gave a + // begin and/or end to the report period (though these can be overridden + // using -b or -e). Then, if no _duration_ was specified (such as monthly), + // then ignore the period since the begin/end are the only interesting + // details. + if (HANDLED(period_)) { + if (! HANDLED(sort_all_)) + HANDLER(sort_xacts_).on_only(string("?normalize")); + + date_interval_t interval(HANDLER(period_).str()); + + if (! HANDLED(begin_) && interval.start) { + string predicate = + "date>=[" + to_iso_extended_string(*interval.start) + "]"; + HANDLER(limit_).on(string("?normalize"), predicate); + } + if (! HANDLED(end_) && interval.end) { + string predicate = + "date<[" + to_iso_extended_string(*interval.end) + "]"; + HANDLER(limit_).on(string("?normalize"), predicate); + } + + if (! interval.duration) + HANDLER(period_).off(); + } + + // If -j or -J were specified, set the appropriate format string now so as + // to avoid option ordering issues were we to have done it during the + // initial parsing of the options. + if (HANDLED(amount_data)) { + HANDLER(format_) + .on_with(string("?normalize"), HANDLER(plot_amount_format_).value); + } + else if (HANDLED(total_data)) { + HANDLER(format_) + .on_with(string("?normalize"), HANDLER(plot_total_format_).value); + } + + // If the --exchange (-X) option was used, parse out any final price + // settings that may be there. + if (HANDLED(exchange_) && + HANDLER(exchange_).str().find('=') != string::npos) { + value_t(0L).exchange_commodities(HANDLER(exchange_).str(), true, + terminus); + } + + long cols = 0; + if (HANDLED(columns_)) + cols = HANDLER(columns_).value.to_long(); + else if (const char * columns = std::getenv("COLUMNS")) + cols = lexical_cast<long>(columns); + else + cols = 80L; + + if (cols > 0) { + DEBUG("auto.columns", "cols = " << cols); + + if (! HANDLER(date_width_).specified) + HANDLER(date_width_) + .on_with(none, static_cast<long>(format_date(CURRENT_DATE(), + FMT_PRINTED).length())); + + long date_width = HANDLER(date_width_).value.to_long(); + long payee_width = (HANDLER(payee_width_).specified ? + HANDLER(payee_width_).value.to_long() : + int(double(cols) * 0.263157)); + long account_width = (HANDLER(account_width_).specified ? + HANDLER(account_width_).value.to_long() : + int(double(cols) * 0.302631)); + long amount_width = (HANDLER(amount_width_).specified ? + HANDLER(amount_width_).value.to_long() : + int(double(cols) * 0.157894)); + long total_width = (HANDLER(total_width_).specified ? + HANDLER(total_width_).value.to_long() : + amount_width); + + DEBUG("auto.columns", "date_width = " << date_width); + DEBUG("auto.columns", "payee_width = " << payee_width); + DEBUG("auto.columns", "account_width = " << account_width); + DEBUG("auto.columns", "amount_width = " << amount_width); + DEBUG("auto.columns", "total_width = " << total_width); + + if (! HANDLER(date_width_).specified && + ! HANDLER(payee_width_).specified && + ! HANDLER(account_width_).specified && + ! HANDLER(amount_width_).specified && + ! HANDLER(total_width_).specified) { + long total = (4 /* the spaces between */ + date_width + payee_width + + account_width + amount_width + total_width); + if (total > cols) { + DEBUG("auto.columns", "adjusting account down"); + account_width -= total - cols; + DEBUG("auto.columns", "account_width now = " << account_width); + } + } + + if (! HANDLER(date_width_).specified) + HANDLER(date_width_).on_with(string("?normalize"), date_width); + if (! HANDLER(payee_width_).specified) + HANDLER(payee_width_).on_with(string("?normalize"), payee_width); + if (! HANDLER(account_width_).specified) + HANDLER(account_width_).on_with(string("?normalize"), account_width); + if (! HANDLER(amount_width_).specified) + HANDLER(amount_width_).on_with(string("?normalize"), amount_width); + if (! HANDLER(total_width_).specified) + HANDLER(total_width_).on_with(string("?normalize"), total_width); + } +} + +void report_t::parse_query_args(const value_t& args, const string& whence) +{ + query_t query(args, what_to_keep()); + if (! query) + throw_(std::runtime_error, + _("Invalid query predicate: %1") << query.text()); + + HANDLER(limit_).on(whence, query.text()); + + DEBUG("report.predicate", + "Predicate = " << HANDLER(limit_).str()); + + if (query.tokens_remaining()) { + query.parse_again(); + if (! query) + throw_(std::runtime_error, + _("Invalid display predicate: %1") << query.text()); + + HANDLER(display_).on(whence, query.text()); + + DEBUG("report.predicate", + "Display predicate = " << HANDLER(display_).str()); + } +} + void report_t::posts_report(post_handler_ptr handler) { journal_posts_iterator walker(*session.journal.get()); @@ -353,6 +549,69 @@ value_t report_t::fn_lot_tag(call_scope_t& scope) return NULL_VALUE; } +value_t report_t::fn_to_boolean(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::BOOLEAN); + return args.value_at(0); +} + +value_t report_t::fn_to_int(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::INTEGER); + return args.value_at(0); +} + +value_t report_t::fn_to_datetime(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::DATETIME); + return args.value_at(0); +} + +value_t report_t::fn_to_date(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::DATE); + return args.value_at(0); +} + +value_t report_t::fn_to_amount(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::AMOUNT); + return args.value_at(0); +} + +value_t report_t::fn_to_balance(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::BALANCE); + return args.value_at(0); +} + +value_t report_t::fn_to_string(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::STRING); + return args.value_at(0); +} + +value_t report_t::fn_to_mask(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::MASK); + return args.value_at(0); +} + +value_t report_t::fn_to_sequence(call_scope_t& scope) +{ + interactive_t args(scope, "v"); + args.value_at(0).in_place_cast(value_t::SEQUENCE); + return args.value_at(0); +} + namespace { value_t fn_black(call_scope_t&) { return string_value("black"); @@ -393,54 +652,6 @@ namespace { value_t fn_null(call_scope_t&) { return NULL_VALUE; } - - template <class Type = post_t, - class handler_ptr = post_handler_ptr, - void (report_t::*report_method)(handler_ptr) = - &report_t::posts_report> - class reporter - { - shared_ptr<item_handler<Type> > handler; - - report_t& report; - string whence; - - public: - reporter(item_handler<Type> * _handler, report_t& _report, - const string& _whence) - : handler(_handler), report(_report), whence(_whence) {} - - value_t operator()(call_scope_t& args) - { - if (args.size() > 0) { - query_t query(args.value(), report.what_to_keep()); - if (! query) - throw_(std::runtime_error, - _("Invalid query predicate: %1") << query.text()); - - report.HANDLER(limit_).on(whence, query.text()); - - DEBUG("report.predicate", - "Predicate = " << report.HANDLER(limit_).str()); - - if (query.tokens_remaining()) { - query.parse_again(); - if (! query) - throw_(std::runtime_error, - _("Invalid display predicate: %1") << query.text()); - - report.HANDLER(display_).on(whence, query.text()); - - DEBUG("report.predicate", - "Display predicate = " << report.HANDLER(display_).str()); - } - } - - (report.*report_method)(handler_ptr(handler)); - - return true; - } - }; } value_t report_t::reload_command(call_scope_t&) @@ -561,6 +772,7 @@ option_t<report_t> * report_t::lookup_option(const char * p) break; case 'd': OPT(daily); + else OPT(date_); else OPT(date_format_); else OPT(datetime_format_); else OPT(depth_); @@ -634,7 +846,7 @@ option_t<report_t> * report_t::lookup_option(const char * p) else OPT(plot_total_format_); else OPT(price); else OPT(prices_format_); - else OPT(pricesdb_format_); + else OPT(pricedb_format_); else OPT(print_format_); else OPT(payee_width_); else OPT(prepend_format_); @@ -822,6 +1034,24 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, return MAKE_FUNCTOR(report_t::fn_today); else if (is_eq(p, "t")) return MAKE_FUNCTOR(report_t::fn_display_amount); + else if (is_eq(p, "to_boolean")) + return MAKE_FUNCTOR(report_t::fn_to_boolean); + else if (is_eq(p, "to_int")) + return MAKE_FUNCTOR(report_t::fn_to_int); + else if (is_eq(p, "to_datetime")) + return MAKE_FUNCTOR(report_t::fn_to_datetime); + else if (is_eq(p, "to_date")) + return MAKE_FUNCTOR(report_t::fn_to_date); + else if (is_eq(p, "to_amount")) + return MAKE_FUNCTOR(report_t::fn_to_amount); + else if (is_eq(p, "to_balance")) + return MAKE_FUNCTOR(report_t::fn_to_balance); + else if (is_eq(p, "to_string")) + return MAKE_FUNCTOR(report_t::fn_to_string); + else if (is_eq(p, "to_mask")) + return MAKE_FUNCTOR(report_t::fn_to_mask); + else if (is_eq(p, "to_sequence")) + return MAKE_FUNCTOR(report_t::fn_to_sequence); break; case 'T': @@ -929,12 +1159,12 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, (new format_posts(*this, report_format(HANDLER(prices_format_)), maybe_format(HANDLER(prepend_format_))), *this, "#prices")); - else if (is_eq(p, "pricesdb")) + else if (is_eq(p, "pricedb")) return expr_t::op_t::wrap_functor (reporter<post_t, post_handler_ptr, &report_t::commodities_report> - (new format_posts(*this, report_format(HANDLER(pricesdb_format_)), + (new format_posts(*this, report_format(HANDLER(pricedb_format_)), maybe_format(HANDLER(prepend_format_))), - *this, "#pricesdb")); + *this, "#pricedb")); break; case 'r': @@ -951,8 +1181,6 @@ expr_t::ptr_op_t report_t::lookup(const symbol_t::kind_t kind, case 's': if (is_eq(p, "stats") || is_eq(p, "stat")) return WRAP_FUNCTOR(report_statistics); - else if (is_eq(p, "server")) - return session.lookup(symbol_t::COMMAND, "server"); break; case 'x': diff --git a/src/report.h b/src/report.h index e27abbf9..c656829b 100644 --- a/src/report.h +++ b/src/report.h @@ -44,6 +44,7 @@ #include "interactive.h" #include "expr.h" +#include "query.h" #include "chain.h" #include "stream.h" #include "option.h" @@ -124,6 +125,9 @@ public: output_stream.close(); } + void normalize_options(const string& verb); + void parse_query_args(const value_t& args, const string& whence); + void posts_report(post_handler_ptr handler); void generate_report(post_handler_ptr handler); void xact_report(post_handler_ptr handler, xact_t& xact); @@ -155,6 +159,15 @@ public: value_t fn_lot_date(call_scope_t& scope); value_t fn_lot_price(call_scope_t& scope); value_t fn_lot_tag(call_scope_t& scope); + value_t fn_to_boolean(call_scope_t& scope); + value_t fn_to_int(call_scope_t& scope); + value_t fn_to_datetime(call_scope_t& scope); + value_t fn_to_date(call_scope_t& scope); + value_t fn_to_amount(call_scope_t& scope); + value_t fn_to_balance(call_scope_t& scope); + value_t fn_to_string(call_scope_t& scope); + value_t fn_to_mask(call_scope_t& scope); + value_t fn_to_sequence(call_scope_t& scope); value_t fn_now(call_scope_t&) { return terminus; @@ -217,6 +230,7 @@ public: HANDLER(csv_format_).report(out); HANDLER(current).report(out); HANDLER(daily).report(out); + HANDLER(date_).report(out); HANDLER(date_format_).report(out); HANDLER(datetime_format_).report(out); HANDLER(depth_).report(out); @@ -262,7 +276,7 @@ public: HANDLER(prepend_format_).report(out); HANDLER(price).report(out); HANDLER(prices_format_).report(out); - HANDLER(pricesdb_format_).report(out); + HANDLER(pricedb_format_).report(out); HANDLER(print_format_).report(out); HANDLER(quantity).report(out); HANDLER(quarterly).report(out); @@ -450,6 +464,7 @@ public: parent->HANDLER(period_).on(string("--daily"), "daily"); }); + OPTION(report_t, date_); OPTION(report_t, date_format_); OPTION(report_t, datetime_format_); @@ -712,11 +727,11 @@ public: OPTION__(report_t, prices_format_, CTOR(report_t, prices_format_) { on(none, - "%-.9(date) %-8(account) %(justify(scrub(display_amount), 12, " + "%(date) %-8(account) %(justify(scrub(display_amount), 12, " " 2 + 9 + 8 + 12, true, color))\n"); }); - OPTION__(report_t, pricesdb_format_, CTOR(report_t, pricesdb_format_) { + OPTION__(report_t, pricedb_format_, CTOR(report_t, pricedb_format_) { on(none, "P %(datetime) %(account) %(scrub(display_amount))\n"); }); @@ -896,6 +911,34 @@ public: DO_(args) { value = args[1].to_long(); specified = true; }); }; + +template <class Type = post_t, + class handler_ptr = post_handler_ptr, + void (report_t::*report_method)(handler_ptr) = + &report_t::posts_report> +class reporter +{ + shared_ptr<item_handler<Type> > handler; + + report_t& report; + string whence; + +public: + reporter(item_handler<Type> * _handler, + report_t& _report, const string& _whence) + : handler(_handler), report(_report), whence(_whence) {} + + value_t operator()(call_scope_t& args) + { + if (args.size() > 0) + report.parse_query_args(args.value(), whence); + + (report.*report_method)(handler_ptr(handler)); + + return true; + } +}; + } // namespace ledger #endif // _REPORT_H diff --git a/src/session.cc b/src/session.cc index 9fb8df46..0d6a6829 100644 --- a/src/session.cc +++ b/src/session.cc @@ -106,7 +106,7 @@ std::size_t session_t::read_data(const string& master_account) if (! (cache && cache->should_load(HANDLER(file_).data_files) && - cache->load(journal))) { + cache->load(*journal.get()))) { #endif // HAVE_BOOST_SERIALIZATION if (price_db_path) { if (exists(*price_db_path)) { @@ -142,8 +142,8 @@ std::size_t session_t::read_data(const string& master_account) assert(xact_count == journal->xacts.size()); #if defined(HAVE_BOOST_SERIALIZATION) - if (cache && cache->should_save(journal)) - cache->save(journal); + if (cache && cache->should_save(*journal.get())) + cache->save(*journal.get()); } #endif // HAVE_BOOST_SERIALIZATION diff --git a/src/session.h b/src/session.h index bde37f46..5c4612a0 100644 --- a/src/session.h +++ b/src/session.h @@ -58,9 +58,9 @@ class session_t : public symbol_scope_t friend void set_session_context(session_t * session); public: - bool flush_on_next_data_file; - date_t::year_type current_year; - shared_ptr<journal_t> journal; + bool flush_on_next_data_file; + date_t::year_type current_year; + std::auto_ptr<journal_t> journal; explicit session_t(); virtual ~session_t() { diff --git a/src/textual.cc b/src/textual.cc index 8f0dd89c..27ea13b8 100644 --- a/src/textual.cc +++ b/src/textual.cc @@ -79,8 +79,6 @@ namespace { optional<date_t::year_type> current_year; - scoped_ptr<auto_xact_finalizer_t> auto_xact_finalizer; - instance_t(std::list<account_t *>& _account_stack, std::list<string>& _tag_stack, #if defined(TIMELOG_SUPPORT) @@ -227,9 +225,6 @@ instance_t::~instance_t() TRACE_DTOR(instance_t); account_stack.pop_front(); - - if (auto_xact_finalizer.get()) - journal.remove_xact_finalizer(auto_xact_finalizer.get()); } void instance_t::parse() @@ -546,11 +541,6 @@ void instance_t::automated_xact_directive(char * line) try { - if (! auto_xact_finalizer.get()) { - auto_xact_finalizer.reset(new auto_xact_finalizer_t(&journal)); - journal.add_xact_finalizer(auto_xact_finalizer.get()); - } - std::auto_ptr<auto_xact_t> ae (new auto_xact_t(query_t(string(skip_ws(line + 1)), keep_details_t(true, true, true)))); @@ -601,8 +591,7 @@ void instance_t::period_xact_directive(char * line) pe->journal = &journal; if (pe->finalize()) { - extend_xact_base(&journal, *pe.get()); - + journal.extend_xact(pe.get()); journal.period_xacts.push_back(pe.get()); pe->pos = position_t(); diff --git a/src/utils.cc b/src/utils.cc index 6cef1a8c..d661f410 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -417,6 +417,8 @@ void report_memory(std::ostream& out, bool report_all) namespace ledger { +#if defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + string::string() : std::string() { TRACE_CTOR(string, ""); } @@ -453,6 +455,8 @@ string::~string() throw() { TRACE_DTOR(string); } +#endif // defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + string empty_string(""); strings_list split_arguments(const char * line) diff --git a/src/utils.h b/src/utils.h index 8ddc3c44..f0a165c2 100644 --- a/src/utils.h +++ b/src/utils.h @@ -181,6 +181,8 @@ void report_memory(std::ostream& out, bool report_all = false); namespace ledger { +#if defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + class string : public std::string { public: @@ -253,6 +255,12 @@ inline bool operator!=(const char* __lhs, const string& __rhs) inline bool operator!=(const string& __lhs, const char* __rhs) { return __lhs.compare(__rhs) != 0; } +#else // defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + +typedef std::string string; + +#endif // defined(VERIFY_ON) || defined(HAVE_BOOST_PYTHON) + extern string empty_string; strings_list split_arguments(const char * line); diff --git a/src/value.cc b/src/value.cc index ae86eb7c..910a9140 100644 --- a/src/value.cc +++ b/src/value.cc @@ -1146,6 +1146,12 @@ void value_t::in_place_cast(type_t cast_type) case AMOUNT: set_amount(amount_t(as_string())); return; + case DATE: + set_date(parse_date(as_string())); + return; + case DATETIME: + set_datetime(parse_datetime(as_string())); + return; case MASK: set_mask(as_string()); return; diff --git a/src/xact.cc b/src/xact.cc index 8ac5280a..1cece187 100644 --- a/src/xact.cc +++ b/src/xact.cc @@ -76,6 +76,15 @@ bool xact_base_t::remove_post(post_t * post) return true; } +bool xact_base_t::has_xdata() +{ + foreach (post_t * post, posts) + if (post->has_xdata()) + return true; + + return false; +} + void xact_base_t::clear_xdata() { foreach (post_t * post, posts) @@ -372,6 +381,55 @@ bool xact_base_t::finalize() return true; } +bool xact_base_t::verify() +{ + // Scan through and compute the total balance for the xact. + + value_t balance; + + foreach (post_t * post, posts) { + if (! post->must_balance()) + continue; + + amount_t& p(post->cost ? *post->cost : post->amount); + assert(! p.is_null()); + + // If the amount was a cost, it very likely has the "keep_precision" flag + // set, meaning commodity display precision is ignored when displaying the + // amount. We never want this set for the balance, so we must clear the + // flag in a temporary to avoid it propagating into the balance. + add_or_set_value(balance, p.keep_precision() ? + p.rounded().reduced() : p.reduced()); + } + VERIFY(balance.valid()); + + // Now that the post list has its final form, calculate the balance once + // more in terms of total cost, accounting for any possible gain/loss + // amounts. + + foreach (post_t * post, posts) { + if (! post->cost) + continue; + + if (post->amount.commodity() == post->cost->commodity()) + throw_(balance_error, + _("A posting's cost must be of a different commodity than its amount")); + } + + if (! balance.is_null() && ! balance.is_zero()) { + add_error_context(item_context(*this, _("While balancing transaction"))); + add_error_context(_("Unbalanced remainder is:")); + add_error_context(value_context(balance)); + add_error_context(_("Amount to balance against:")); + add_error_context(value_context(magnitude())); + throw_(balance_error, _("Transaction does not balance")); + } + + VERIFY(valid()); + + return true; +} + xact_t::xact_t(const xact_t& e) : xact_base_t(e), code(e.code), payee(e.payee) { @@ -486,6 +544,8 @@ void auto_xact_t::extend_xact(xact_base_t& xact) try { + bool needs_further_verification = false; + foreach (post_t * initial_post, initial_posts) { if (! initial_post->has_flags(ITEM_GENERATED) && predicate(*initial_post)) { @@ -555,10 +615,16 @@ void auto_xact_t::extend_xact(xact_base_t& xact) xact.add_post(new_post); new_post->account->add_post(new_post); + + if (new_post->must_balance()) + needs_further_verification = true; } } } + if (needs_further_verification) + xact.verify(); + } catch (const std::exception& err) { add_error_context(item_context(*this, _("While applying automated transaction"))); @@ -77,8 +77,10 @@ public: value_t magnitude() const; - virtual bool finalize(); + bool finalize(); + bool verify(); + bool has_xdata(); void clear_xdata(); virtual bool valid() const { @@ -140,11 +142,6 @@ private: #endif // HAVE_BOOST_SERIALIZATION }; -struct xact_finalizer_t { - virtual ~xact_finalizer_t() {} - virtual bool operator()(xact_t& xact) = 0; -}; - class auto_xact_t : public xact_base_t { public: @@ -183,39 +180,6 @@ private: #endif // HAVE_BOOST_SERIALIZATION }; -struct auto_xact_finalizer_t : public xact_finalizer_t -{ - journal_t * journal; - - auto_xact_finalizer_t() : journal(NULL) { - TRACE_CTOR(auto_xact_finalizer_t, ""); - } - auto_xact_finalizer_t(const auto_xact_finalizer_t& other) - : xact_finalizer_t(), journal(other.journal) { - TRACE_CTOR(auto_xact_finalizer_t, "copy"); - } - auto_xact_finalizer_t(journal_t * _journal) : journal(_journal) { - TRACE_CTOR(auto_xact_finalizer_t, "journal_t *"); - } - ~auto_xact_finalizer_t() throw() { - TRACE_DTOR(auto_xact_finalizer_t); - } - - virtual bool operator()(xact_t& xact); - -#if defined(HAVE_BOOST_SERIALIZATION) -private: - /** Serialization. */ - - friend class boost::serialization::access; - - template<class Archive> - void serialize(Archive& ar, const unsigned int /* version */) { - ar & journal; - } -#endif // HAVE_BOOST_SERIALIZATION -}; - class period_xact_t : public xact_base_t { public: @@ -253,38 +217,6 @@ private: #endif // HAVE_BOOST_SERIALIZATION }; -class func_finalizer_t : public xact_finalizer_t -{ - func_finalizer_t(); - -public: - typedef function<bool (xact_t& xact)> func_t; - - func_t func; - - func_finalizer_t(func_t _func) : func(_func) { - TRACE_CTOR(func_finalizer_t, "func_t"); - } - func_finalizer_t(const func_finalizer_t& other) : - xact_finalizer_t(), func(other.func) { - TRACE_CTOR(func_finalizer_t, "copy"); - } - ~func_finalizer_t() throw() { - TRACE_DTOR(func_finalizer_t); - } - - virtual bool operator()(xact_t& xact) { - return func(xact); - } -}; - -void extend_xact_base(journal_t * journal, xact_base_t& xact); - -inline bool auto_xact_finalizer_t::operator()(xact_t& xact) { - extend_xact_base(journal, xact); - return true; -} - typedef std::list<xact_t *> xacts_list; typedef std::list<auto_xact_t *> auto_xacts_list; typedef std::list<period_xact_t *> period_xacts_list; diff --git a/test/baseline/opt-pricesdb-format.test b/test/baseline/opt-pricedb-format.test index b90371cd..5dbff609 100644 --- a/test/baseline/opt-pricesdb-format.test +++ b/test/baseline/opt-pricedb-format.test @@ -1,4 +1,4 @@ -pricesdb --pricesdb-format='P %(date) %(scrub(display_amount))\n' +pricedb --pricedb-format='P %(date) %(scrub(display_amount))\n' <<< P 2009/01/01 13:30:00 AAPL $10.00 P 2009/01/01 14:30:00 AAPL $20.00 diff --git a/tools/Makefile.am b/tools/Makefile.am index 8c28277b..9821bf4c 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -89,7 +89,6 @@ libledger_report_la_LDFLAGS = -release $(VERSION) pkginclude_HEADERS = \ src/utils.h \ src/flags.h \ - src/hooks.h \ src/error.h \ src/times.h \ src/mask.h \ @@ -440,7 +439,8 @@ fullcheck: cppunittests ###################################################################### -EXTRA_DIST += doc/README doc/LICENSE doc/NEWS doc/ledger.pdf +EXTRA_DIST += doc/README doc/NEWS doc/ledger.pdf +EXTRA_DIST += doc/LICENSE doc/LICENSE-sha1 doc/LICENSE-utfcpp if USE_DOXYGEN EXTRA_DIST += doc/Doxyfile doc/refman.pdf endif |