2
* Copyright (c) 2003-2014, John Wiegley. All rights reserved.
4
* Redistribution and use in source and binary forms, with or without
5
* modification, are permitted provided that the following conditions are
8
* - Redistributions of source code must retain the above copyright
9
* notice, this list of conditions and the following disclaimer.
11
* - Redistributions in binary form must reproduce the above copyright
12
* notice, this list of conditions and the following disclaimer in the
13
* documentation and/or other materials provided with the distribution.
15
* - Neither the name of New Artisans LLC nor the names of its
16
* contributors may be used to endorse or promote products derived from
17
* this software without specific prior written permission.
19
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
#include "iterators.h"
43
void post_splitter::print_title(const value_t& val)
45
if (! report.HANDLED(no_titles)) {
46
std::ostringstream buf;
48
post_chain->title(buf.str());
52
void post_splitter::flush()
54
foreach (value_to_posts_map::value_type& pair, posts_map) {
55
preflush_func(pair.first);
57
foreach (post_t * post, pair.second)
64
(*postflush_func)(pair.first);
68
void post_splitter::operator()(post_t& post)
70
bind_scope_t bound_scope(report, post);
71
value_t result(group_by_expr.calc(bound_scope));
73
if (! result.is_null()) {
74
value_to_posts_map::iterator i = posts_map.find(result);
75
if (i != posts_map.end()) {
76
(*i).second.push_back(&post);
78
std::pair<value_to_posts_map::iterator, bool> inserted
79
= posts_map.insert(value_to_posts_map::value_type(result, posts_list()));
80
assert(inserted.second);
81
(*inserted.first).second.push_back(&post);
86
void truncate_xacts::flush()
91
xact_t * xact = (*posts.begin())->xact;
94
foreach (post_t * post, posts)
95
if (xact != post->xact) {
101
xact = (*posts.begin())->xact;
104
foreach (post_t * post, posts) {
105
if (xact != post->xact) {
112
if (head_count > 0 && i < head_count)
114
else if (head_count < 0 && i >= - head_count)
118
if (! print && tail_count) {
119
if (tail_count > 0 && l - i <= tail_count)
121
else if (tail_count < 0 && l - i > - tail_count)
126
item_handler<post_t>::operator()(*post);
130
item_handler<post_t>::flush();
133
void truncate_xacts::operator()(post_t& post)
138
if (last_xact != post.xact) {
141
last_xact = post.xact;
144
if (tail_count == 0 && head_count > 0 &&
145
static_cast<int>(xacts_seen) >= head_count) {
151
posts.push_back(&post);
154
void sort_posts::post_accumulated_posts()
156
std::stable_sort(posts.begin(), posts.end(),
157
compare_items<post_t>(sort_order));
159
foreach (post_t * post, posts) {
160
post->xdata().drop_flags(POST_EXT_SORT_CALC);
161
item_handler<post_t>::operator()(*post);
168
void split_string(const string& str, const char ch,
169
std::list<string>& strings)
171
const char * b = str.c_str();
172
for (const char * p = b; *p; p++) {
174
strings.push_back(string(b, static_cast<std::string::size_type>(p - b)));
178
strings.push_back(string(b));
181
account_t * create_temp_account_from_path(std::list<string>& account_names,
182
temporaries_t& temps,
185
account_t * new_account = NULL;
186
foreach (const string& name, account_names) {
188
new_account = new_account->find_account(name);
190
new_account = master->find_account(name, false);
192
new_account = &temps.create_account(name, master);
196
assert(new_account != NULL);
201
void anonymize_posts::render_commodity(amount_t& amt)
203
commodity_t& comm(amt.commodity());
206
bool newly_added = false;
208
commodity_index_map::iterator i = comms.find(&comm);
209
if (i == comms.end()) {
212
comms.insert(commodity_index_map::value_type(&comm, id));
217
std::ostringstream buf;
219
buf << static_cast<char>('A' + (id % 26));
224
if (amt.has_annotation())
226
(*commodity_pool_t::current_pool->find_or_create(buf.str(),
230
(*commodity_pool_t::current_pool->find_or_create(buf.str()));
233
amt.commodity().set_flags(comm.flags());
234
amt.commodity().set_precision(comm.precision());
238
void anonymize_posts::operator()(post_t& post)
241
unsigned int message_digest[5];
242
bool copy_xact_details = false;
244
if (last_xact != post.xact) {
245
temps.copy_xact(*post.xact);
246
last_xact = post.xact;
247
copy_xact_details = true;
249
xact_t& xact = temps.last_xact();
252
if (copy_xact_details) {
253
xact.copy_details(*post.xact);
255
std::ostringstream buf;
256
buf << reinterpret_cast<uintmax_t>(post.xact->payee.c_str())
257
<< integer_gen() << post.xact->payee.c_str();
260
sha << buf.str().c_str();
261
sha.Result(message_digest);
263
xact.payee = to_hex(message_digest);
266
xact.journal = post.xact->journal;
269
std::list<string> account_names;
271
for (account_t * acct = post.account;
273
acct = acct->parent) {
274
std::ostringstream buf;
275
buf << integer_gen() << acct << acct->fullname();
278
sha << buf.str().c_str();
279
sha.Result(message_digest);
281
account_names.push_front(to_hex(message_digest));
284
account_t * new_account =
285
create_temp_account_from_path(account_names, temps, xact.journal->master);
286
post_t& temp = temps.copy_post(post, xact, new_account);
288
temp.add_flags(POST_ANONYMIZED);
290
render_commodity(temp.amount);
291
if (temp.amount.has_annotation()) {
292
temp.amount.annotation().tag = none;
293
if (temp.amount.annotation().price)
294
render_commodity(*temp.amount.annotation().price);
298
render_commodity(*temp.cost);
299
if (temp.assigned_amount)
300
render_commodity(*temp.assigned_amount);
305
void calc_posts::operator()(post_t& post)
307
post_t::xdata_t& xdata(post.xdata());
310
assert(last_post->has_xdata());
311
if (calc_running_total)
312
xdata.total = last_post->xdata().total;
313
xdata.count = last_post->xdata().count + 1;
318
post.add_to_value(xdata.visited_value, amount_expr);
319
xdata.add_flags(POST_EXT_VISITED);
321
account_t * acct = post.reported_account();
322
acct->xdata().add_flags(ACCOUNT_EXT_VISITED);
324
if (calc_running_total)
325
add_or_set_value(xdata.total, xdata.visited_value);
327
item_handler<post_t>::operator()(post);
333
void handle_value(const value_t& value,
336
temporaries_t& temps,
337
post_handler_ptr handler,
338
const date_t& date = date_t(),
339
const bool act_date_p = true,
340
const value_t& total = value_t(),
341
const bool direct_amount = false,
342
const bool mark_visited = false,
343
const bool bidir_link = true)
345
post_t& post = temps.create_post(*xact, account, bidir_link);
346
post.add_flags(ITEM_GENERATED);
348
// If the account for this post is all virtual, then report the post as
349
// such. This allows subtotal reports to show "(Account)" for accounts
350
// that contain only virtual posts.
351
if (account && account->has_xdata() &&
352
account->xdata().has_flags(ACCOUNT_EXT_AUTO_VIRTUALIZE)) {
353
if (! account->xdata().has_flags(ACCOUNT_EXT_HAS_NON_VIRTUALS)) {
354
post.add_flags(POST_VIRTUAL);
355
if (! account->xdata().has_flags(ACCOUNT_EXT_HAS_UNB_VIRTUALS))
356
post.add_flags(POST_MUST_BALANCE);
360
post_t::xdata_t& xdata(post.xdata());
362
if (is_valid(date)) {
366
xdata.value_date = date;
371
switch (value.type()) {
372
case value_t::BOOLEAN:
373
case value_t::INTEGER:
374
temp.in_place_cast(value_t::AMOUNT);
377
case value_t::AMOUNT:
378
post.amount = temp.as_amount();
381
case value_t::BALANCE:
382
case value_t::SEQUENCE:
383
xdata.compound_value = temp;
384
xdata.add_flags(POST_EXT_COMPOUND);
387
case value_t::DATETIME:
394
if (! total.is_null())
398
xdata.add_flags(POST_EXT_DIRECT_AMT);
400
DEBUG("filters.changed_value.rounding", "post.amount = " << post.amount);
405
post.xdata().add_flags(POST_EXT_VISITED);
406
post.account->xdata().add_flags(ACCOUNT_EXT_VISITED);
411
void collapse_posts::report_subtotal()
416
std::size_t displayed_count = 0;
417
foreach (post_t * post, component_posts) {
418
bind_scope_t bound_scope(report, *post);
419
if (only_predicate(bound_scope) && display_predicate(bound_scope))
423
if (displayed_count == 1) {
424
item_handler<post_t>::operator()(*last_post);
426
else if (only_collapse_if_zero && ! subtotal.is_zero()) {
427
foreach (post_t * post, component_posts)
428
item_handler<post_t>::operator()(*post);
431
date_t earliest_date;
434
foreach (post_t * post, component_posts) {
435
date_t date = post->date();
436
date_t value_date = post->value_date();
437
if (! is_valid(earliest_date) || date < earliest_date)
438
earliest_date = date;
439
if (! is_valid(latest_date) || value_date > latest_date)
440
latest_date = value_date;
443
xact_t& xact = temps.create_xact();
444
xact.payee = last_xact->payee;
445
xact._date = (is_valid(earliest_date) ?
446
earliest_date : last_xact->_date);
448
DEBUG("filters.collapse", "Pseudo-xact date = " << *xact._date);
449
DEBUG("filters.collapse", "earliest date = " << earliest_date);
450
DEBUG("filters.collapse", "latest date = " << latest_date);
452
handle_value(/* value= */ subtotal,
453
/* account= */ totals_account,
456
/* handler= */ handler,
457
/* date= */ latest_date,
458
/* act_date_p= */ false);
461
component_posts.clear();
469
void collapse_posts::operator()(post_t& post)
471
// If we've reached a new xact, report on the subtotal
472
// accumulated thus far.
474
if (last_xact != post.xact && count > 0)
477
post.add_to_value(subtotal, amount_expr);
479
component_posts.push_back(&post);
481
last_xact = post.xact;
486
void related_posts::flush()
488
if (posts.size() > 0) {
489
foreach (post_t * post, posts) {
491
foreach (post_t * r_post, post->xact->posts) {
492
post_t::xdata_t& xdata(r_post->xdata());
493
if (! xdata.has_flags(POST_EXT_HANDLED) &&
494
(! xdata.has_flags(POST_EXT_RECEIVED) ?
495
! r_post->has_flags(ITEM_GENERATED | POST_VIRTUAL) :
497
xdata.add_flags(POST_EXT_HANDLED);
498
item_handler<post_t>::operator()(*r_post);
504
item_handler<post_t>::flush();
507
display_filter_posts::display_filter_posts(post_handler_ptr handler,
510
: item_handler<post_t>(handler), report(_report),
511
display_amount_expr(report.HANDLER(display_amount_).expr),
512
display_total_expr(report.HANDLER(display_total_).expr),
513
show_rounding(_show_rounding)
516
TRACE_CTOR(display_filter_posts, "post_handler_ptr, report_t&, bool");
519
bool display_filter_posts::output_rounding(post_t& post)
521
bind_scope_t bound_scope(report, post);
522
value_t new_display_total;
525
new_display_total = (display_total_expr.calc(bound_scope)
526
.strip_annotations(report.what_to_keep()));
528
DEBUG("filters.changed_value.rounding",
529
"rounding.new_display_total = " << new_display_total);
532
// Allow the posting to be displayed if:
533
// 1. Its display_amount would display as non-zero, or
534
// 2. The --empty option was specified, or
535
// 3. a) The account of the posting is <Revalued>, and
536
// b) the revalued option is specified, and
537
// c) the --no-rounding option is not specified.
539
if (post.account == revalued_account) {
541
last_display_total = new_display_total;
545
if (value_t repriced_amount = (display_amount_expr.calc(bound_scope)
546
.strip_annotations(report.what_to_keep()))) {
547
if (! last_display_total.is_null()) {
548
DEBUG("filters.changed_value.rounding",
549
"rounding.repriced_amount = " << repriced_amount);
551
value_t precise_display_total(new_display_total.truncated() -
552
repriced_amount.truncated());
554
DEBUG("filters.changed_value.rounding",
555
"rounding.precise_display_total = " << precise_display_total);
556
DEBUG("filters.changed_value.rounding",
557
"rounding.last_display_total = " << last_display_total);
559
if (value_t diff = precise_display_total - last_display_total) {
560
DEBUG("filters.changed_value.rounding",
561
"rounding.diff = " << diff);
563
handle_value(/* value= */ diff,
564
/* account= */ rounding_account,
565
/* xact= */ post.xact,
567
/* handler= */ handler,
568
/* date= */ date_t(),
569
/* act_date_p= */ true,
570
/* total= */ precise_display_total,
571
/* direct_amount= */ true,
572
/* mark_visited= */ false,
573
/* bidir_link= */ false);
577
last_display_total = new_display_total;
580
return report.HANDLED(empty);
584
void display_filter_posts::operator()(post_t& post)
586
if (output_rounding(post))
587
item_handler<post_t>::operator()(post);
590
changed_value_posts::changed_value_posts
591
(post_handler_ptr handler,
593
bool _for_accounts_report,
594
bool _show_unrealized,
595
display_filter_posts * _display_filter)
596
: item_handler<post_t>(handler), report(_report),
597
total_expr(report.HANDLED(revalued_total_) ?
598
report.HANDLER(revalued_total_).expr :
599
report.HANDLER(display_total_).expr),
600
display_total_expr(report.HANDLER(display_total_).expr),
601
changed_values_only(report.HANDLED(revalued_only)),
602
historical_prices_only(report.HANDLED(historical)),
603
for_accounts_report(_for_accounts_report),
604
show_unrealized(_show_unrealized), last_post(NULL),
605
display_filter(_display_filter)
607
string gains_equity_account_name;
608
if (report.HANDLED(unrealized_gains_))
609
gains_equity_account_name = report.HANDLER(unrealized_gains_).str();
611
gains_equity_account_name = _("Equity:Unrealized Gains");
612
gains_equity_account =
613
report.session.journal->master->find_account(gains_equity_account_name);
614
gains_equity_account->add_flags(ACCOUNT_GENERATED);
616
string losses_equity_account_name;
617
if (report.HANDLED(unrealized_losses_))
618
losses_equity_account_name = report.HANDLER(unrealized_losses_).str();
620
losses_equity_account_name = _("Equity:Unrealized Losses");
621
losses_equity_account =
622
report.session.journal->master->find_account(losses_equity_account_name);
623
losses_equity_account->add_flags(ACCOUNT_GENERATED);
627
TRACE_CTOR(changed_value_posts,
628
"post_handler_ptr, report_t&, bool, bool, display_filter_posts *");
631
void changed_value_posts::flush()
633
if (last_post && last_post->date() <= report.terminus.date()) {
634
if (! historical_prices_only) {
635
if (! for_accounts_report)
636
output_intermediate_prices(*last_post, report.terminus.date());
637
output_revaluation(*last_post, report.terminus.date());
641
item_handler<post_t>::flush();
644
void changed_value_posts::output_revaluation(post_t& post, const date_t& date)
647
post.xdata().date = date;
650
bind_scope_t bound_scope(report, post);
651
repriced_total = total_expr.calc(bound_scope);
654
post.xdata().date = date_t();
657
post.xdata().date = date_t();
659
DEBUG("filters.changed_value",
660
"output_revaluation(last_total) = " << last_total);
661
DEBUG("filters.changed_value",
662
"output_revaluation(repriced_total) = " << repriced_total);
664
if (! last_total.is_null()) {
665
if (value_t diff = repriced_total - last_total) {
666
DEBUG("filters.changed_value", "output_revaluation(strip(diff)) = "
667
<< diff.strip_annotations(report.what_to_keep()));
669
xact_t& xact = temps.create_xact();
670
xact.payee = _("Commodities revalued");
671
xact._date = is_valid(date) ? date : post.value_date();
673
if (! for_accounts_report) {
676
/* account= */ revalued_account,
679
/* handler= */ handler,
680
/* date= */ *xact._date,
681
/* act_date_p= */ true,
682
/* total= */ repriced_total);
684
else if (show_unrealized) {
686
(/* value= */ - diff,
687
/* account= */ (diff < 0L ?
688
losses_equity_account :
689
gains_equity_account),
692
/* handler= */ handler,
693
/* date= */ *xact._date,
694
/* act_date_p= */ true,
695
/* total= */ value_t(),
696
/* direct_amount= */ false,
697
/* mark_visited= */ true);
704
struct insert_prices_in_map {
705
price_map_t& all_prices;
707
insert_prices_in_map(price_map_t& _all_prices)
708
: all_prices(_all_prices) {}
710
void operator()(datetime_t& date, const amount_t& price) {
711
all_prices.insert(price_map_t::value_type(date, price));
716
void changed_value_posts::output_intermediate_prices(post_t& post,
717
const date_t& current)
719
// To fix BZ#199, examine the balance of last_post and determine whether the
720
// price of that amount changed after its date and before the new post's
721
// date. If so, generate an output_revaluation for that price change.
722
// Mostly this is only going to occur if the user has a series of pricing
723
// entries, since a posting-based revaluation would be seen here as a post.
725
value_t display_total(last_total);
727
if (display_total.type() == value_t::SEQUENCE) {
728
xact_t& xact(temps.create_xact());
730
xact.payee = _("Commodities revalued");
731
xact._date = is_valid(current) ? current : post.value_date();
733
post_t& temp(temps.copy_post(post, xact));
734
temp.add_flags(ITEM_GENERATED);
736
post_t::xdata_t& xdata(temp.xdata());
737
if (is_valid(current))
738
xdata.date = current;
740
DEBUG("filters.revalued", "intermediate last_total = " << last_total);
742
switch (last_total.type()) {
743
case value_t::BOOLEAN:
744
case value_t::INTEGER:
745
last_total.in_place_cast(value_t::AMOUNT);
748
case value_t::AMOUNT:
749
temp.amount = last_total.as_amount();
752
case value_t::BALANCE:
753
case value_t::SEQUENCE:
754
xdata.compound_value = last_total;
755
xdata.add_flags(POST_EXT_COMPOUND);
758
case value_t::DATETIME:
765
bind_scope_t inner_scope(report, temp);
766
display_total = display_total_expr.calc(inner_scope);
768
DEBUG("filters.revalued", "intermediate display_total = " << display_total);
771
switch (display_total.type()) {
773
case value_t::INTEGER:
774
case value_t::SEQUENCE:
777
case value_t::AMOUNT:
778
display_total.in_place_cast(value_t::BALANCE);
781
case value_t::BALANCE: {
782
price_map_t all_prices;
784
foreach (const balance_t::amounts_map::value_type& amt_comm,
785
display_total.as_balance().amounts)
786
amt_comm.first->map_prices(insert_prices_in_map(all_prices),
788
datetime_t(post.value_date()), true);
790
// Choose the last price from each day as the price to use
791
typedef std::map<const date_t, bool> date_map;
792
date_map pricing_dates;
794
BOOST_REVERSE_FOREACH(const price_map_t::value_type& price, all_prices) {
795
// This insert will fail if a later price has already been inserted
797
DEBUG("filters.revalued",
798
"re-inserting " << price.second << " at " << price.first.date());
799
pricing_dates.insert(date_map::value_type(price.first.date(), true));
802
// Go through the time-sorted prices list, outputting a revaluation for
803
// each price difference.
804
foreach (const date_map::value_type& price, pricing_dates) {
805
output_revaluation(post, price.first);
806
last_total = repriced_total;
816
void changed_value_posts::operator()(post_t& post)
819
if (! for_accounts_report && ! historical_prices_only)
820
output_intermediate_prices(*last_post, post.value_date());
821
output_revaluation(*last_post, post.value_date());
824
if (changed_values_only)
825
post.xdata().add_flags(POST_EXT_DISPLAYED);
827
item_handler<post_t>::operator()(post);
829
bind_scope_t bound_scope(report, post);
830
last_total = total_expr.calc(bound_scope);
834
void subtotal_posts::report_subtotal(const char * spec_fmt,
835
const optional<date_interval_t>& interval)
837
if (component_posts.empty())
840
optional<date_t> range_start = interval ? interval->start : none;
841
optional<date_t> range_finish = interval ? interval->inclusive_end() : none;
843
if (! range_start || ! range_finish) {
844
foreach (post_t * post, component_posts) {
845
date_t date = post->date();
846
date_t value_date = post->value_date();
847
#if defined(__GNUC__) && __GNUC__ >= 4 && __GNUC_MINOR__ >= 7
848
#pragma GCC diagnostic push
849
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
851
if (! range_start || date < *range_start)
853
if (! range_finish || value_date > *range_finish)
854
range_finish = value_date;
855
#if defined(__GNUC__) && __GNUC__ >= 4 && __GNUC_MINOR__ >= 7
856
#pragma GCC diagnostic pop
860
component_posts.clear();
862
std::ostringstream out_date;
864
out_date << format_date(*range_finish, FMT_CUSTOM, spec_fmt);
866
else if (date_format) {
867
out_date << "- " << format_date(*range_finish, FMT_CUSTOM,
868
date_format->c_str());
871
out_date << "- " << format_date(*range_finish);
874
xact_t& xact = temps.create_xact();
875
xact.payee = out_date.str();
876
xact._date = *range_start;
878
foreach (values_map::value_type& pair, values)
879
handle_value(/* value= */ pair.second.value,
880
/* account= */ pair.second.account,
883
/* handler= */ handler,
884
/* date= */ *range_finish,
885
/* act_date_p= */ false);
890
void subtotal_posts::operator()(post_t& post)
892
component_posts.push_back(&post);
894
account_t * acct = post.reported_account();
898
// jww (2012-04-06): The problem with doing this early is that
899
// fn_display_amount will recalculate this again. For example, if you
900
// use --invert, it will invert both here and in the display amount,
901
// effectively negating it.
902
bind_scope_t bound_scope(*amount_expr.get_context(), post);
903
value_t amount(amount_expr.calc(bound_scope));
905
value_t amount(post.amount);
908
post.xdata().compound_value = amount;
909
post.xdata().add_flags(POST_EXT_COMPOUND);
911
values_map::iterator i = values.find(acct->fullname());
912
if (i == values.end()) {
914
std::pair<values_map::iterator, bool> result =
916
values.insert(values_pair
918
acct_value_t(acct, amount, post.has_flags(POST_VIRTUAL),
919
post.has_flags(POST_MUST_BALANCE))));
921
assert(result.second);
924
if (post.has_flags(POST_VIRTUAL) != (*i).second.is_virtual)
925
throw_(std::logic_error,
926
_("'equity' cannot accept virtual and "
927
"non-virtual postings to the same account"));
929
add_or_set_value((*i).second.value, amount);
932
// If the account for this post is all virtual, mark it as
933
// such, so that `handle_value' can show "(Account)" for accounts
934
// that contain only virtual posts.
936
post.reported_account()->xdata().add_flags(ACCOUNT_EXT_AUTO_VIRTUALIZE);
938
if (! post.has_flags(POST_VIRTUAL))
939
post.reported_account()->xdata().add_flags(ACCOUNT_EXT_HAS_NON_VIRTUALS);
940
else if (! post.has_flags(POST_MUST_BALANCE))
941
post.reported_account()->xdata().add_flags(ACCOUNT_EXT_HAS_UNB_VIRTUALS);
944
void interval_posts::report_subtotal(const date_interval_t& ival)
947
subtotal_posts::report_subtotal();
949
subtotal_posts::report_subtotal(NULL, ival);
953
struct sort_posts_by_date {
954
bool operator()(post_t * left, post_t * right) const {
955
return left->date() < right->date();
960
void interval_posts::operator()(post_t& post)
962
// If there is a duration (such as weekly), we must generate the
963
// report in two passes. Otherwise, we only have to check whether the
964
// post falls within the reporting period.
966
if (interval.duration) {
967
all_posts.push_back(&post);
969
else if (interval.find_period(post.date())) {
970
item_handler<post_t>::operator()(post);
974
void interval_posts::flush()
976
if (! interval.duration) {
977
item_handler<post_t>::flush();
981
// Sort all the postings we saw by date ascending
982
std::stable_sort(all_posts.begin(), all_posts.end(),
983
sort_posts_by_date());
985
// Determine the beginning interval by using the earliest post
986
if (all_posts.front() &&
987
! interval.find_period(all_posts.front()->date()))
988
throw_(std::logic_error, _("Failed to find period for interval report"));
990
// Walk the interval forward reporting all posts within each one
991
// before moving on, until we reach the end of all_posts
992
bool saw_posts = false;
993
for (std::deque<post_t *>::iterator i = all_posts.begin();
994
i != all_posts.end(); ) {
997
DEBUG("filters.interval",
998
"Considering post " << post->date() << " = " << post->amount);
1000
DEBUG("filters.interval", "interval is:");
1001
debug_interval(interval);
1003
assert(! interval.finish || post->date() < *interval.finish);
1005
if (interval.within_period(post->date())) {
1006
DEBUG("filters.interval", "Calling subtotal_posts::operator()");
1007
subtotal_posts::operator()(*post);
1012
DEBUG("filters.interval",
1013
"Calling subtotal_posts::report_subtotal()");
1014
report_subtotal(interval);
1017
else if (generate_empty_posts) {
1018
// Generate a null posting, so the intervening periods can be
1019
// seen when -E is used, or if the calculated amount ends up
1021
xact_t& null_xact = temps.create_xact();
1022
null_xact._date = interval.inclusive_end();
1024
post_t& null_post = temps.create_post(null_xact, empty_account);
1025
null_post.add_flags(POST_CALCULATED);
1026
null_post.amount = 0L;
1028
subtotal_posts::operator()(null_post);
1029
report_subtotal(interval);
1032
DEBUG("filters.interval", "Advancing interval");
1037
// If the last postings weren't reported, do so now.
1039
DEBUG("filters.interval",
1040
"Calling subtotal_posts::report_subtotal() at end");
1041
report_subtotal(interval);
1044
// Tell our parent class to flush
1045
subtotal_posts::flush();
1049
struct create_post_from_amount
1051
post_handler_ptr handler;
1053
account_t& balance_account;
1054
temporaries_t& temps;
1056
explicit create_post_from_amount(post_handler_ptr _handler,
1058
account_t& _balance_account,
1059
temporaries_t& _temps)
1060
: handler(_handler), xact(_xact),
1061
balance_account(_balance_account), temps(_temps) {
1062
TRACE_CTOR(create_post_from_amount,
1063
"post_handler_ptr, xact_t&, account_t&, temporaries_t&");
1065
create_post_from_amount(const create_post_from_amount& other)
1066
: handler(other.handler), xact(other.xact),
1067
balance_account(other.balance_account), temps(other.temps) {
1068
TRACE_CTOR(create_post_from_amount, "copy");
1070
~create_post_from_amount() throw() {
1071
TRACE_DTOR(create_post_from_amount);
1074
void operator()(const amount_t& amount) {
1075
post_t& balance_post = temps.create_post(xact, &balance_account);
1076
balance_post.amount = - amount;
1077
(*handler)(balance_post);
1082
void posts_as_equity::report_subtotal()
1085
foreach (post_t * post, component_posts) {
1086
date_t date = post->date();
1087
if (! is_valid(finish) || date > finish)
1090
component_posts.clear();
1092
xact_t& xact = temps.create_xact();
1093
xact.payee = _("Opening Balances");
1094
xact._date = finish;
1097
foreach (values_map::value_type& pair, values) {
1098
value_t value(pair.second.value.strip_annotations(report.what_to_keep()));
1099
if (! value.is_zero()) {
1100
if (value.is_balance()) {
1101
foreach (const balance_t::amounts_map::value_type& amount_pair,
1102
value.as_balance_lval().amounts) {
1103
if (! amount_pair.second.is_zero())
1104
handle_value(/* value= */ amount_pair.second,
1105
/* account= */ pair.second.account,
1108
/* handler= */ handler,
1110
/* act_date_p= */ false);
1113
handle_value(/* value= */ value.to_amount(),
1114
/* account= */ pair.second.account,
1117
/* handler= */ handler,
1119
/* act_date_p= */ false);
1123
if (! pair.second.is_virtual || pair.second.must_balance)
1128
// This last part isn't really needed, since an Equity:Opening
1129
// Balances posting with a null amount will automatically balance with
1130
// all the other postings generated. But it does make the full
1131
// balancing amount clearer to the user.
1132
if (! total.is_zero()) {
1133
create_post_from_amount post_creator(handler, xact,
1134
*balance_account, temps);
1135
if (total.is_balance())
1136
total.as_balance_lval().map_sorted_amounts(post_creator);
1138
post_creator(total.to_amount());
1142
void by_payee_posts::flush()
1144
foreach (payee_subtotals_map::value_type& pair, payee_subtotals)
1145
pair.second->report_subtotal(pair.first.c_str());
1147
item_handler<post_t>::flush();
1149
payee_subtotals.clear();
1152
void by_payee_posts::operator()(post_t& post)
1154
payee_subtotals_map::iterator i = payee_subtotals.find(post.payee());
1155
if (i == payee_subtotals.end()) {
1156
payee_subtotals_pair
1158
shared_ptr<subtotal_posts>(new subtotal_posts(handler, amount_expr)));
1159
std::pair<payee_subtotals_map::iterator, bool> result
1160
= payee_subtotals.insert(temp);
1162
assert(result.second);
1163
if (! result.second)
1168
(*(*i).second)(post);
1171
void transfer_details::operator()(post_t& post)
1173
xact_t& xact = temps.copy_xact(*post.xact);
1174
xact._date = post.date();
1176
post_t& temp = temps.copy_post(post, xact);
1177
temp.set_state(post.state());
1179
bind_scope_t bound_scope(scope, temp);
1180
value_t substitute(expr.calc(bound_scope));
1182
if (! substitute.is_null()) {
1183
switch (which_element) {
1185
temp._date = substitute.to_date();
1189
string account_name = substitute.to_string();
1190
if (! account_name.empty() &&
1191
account_name[account_name.length() - 1] != ':') {
1192
account_t * prev_account = temp.account;
1193
temp.account->remove_post(&temp);
1195
account_name += ':';
1196
account_name += prev_account->fullname();
1198
std::list<string> account_names;
1199
split_string(account_name, ':', account_names);
1200
temp.account = create_temp_account_from_path(account_names, temps,
1201
xact.journal->master);
1202
temp.account->add_post(&temp);
1204
temp.account->add_flags(prev_account->flags());
1205
if (prev_account->has_xdata())
1206
temp.account->xdata().add_flags(prev_account->xdata().flags());
1212
xact.payee = substitute.to_string();
1217
item_handler<post_t>::operator()(temp);
1220
void day_of_week_posts::flush()
1222
for (int i = 0; i < 7; i++) {
1223
foreach (post_t * post, days_of_the_week[i])
1224
subtotal_posts::operator()(*post);
1225
subtotal_posts::report_subtotal("%As");
1226
days_of_the_week[i].clear();
1229
subtotal_posts::flush();
1232
void generate_posts::add_period_xacts(period_xacts_list& period_xacts)
1234
foreach (period_xact_t * xact, period_xacts)
1235
foreach (post_t * post, xact->posts)
1236
add_post(xact->period, *post);
1239
void generate_posts::add_post(const date_interval_t& period, post_t& post)
1241
pending_posts.push_back(pending_posts_pair(period, &post));
1244
void budget_posts::report_budget_items(const date_t& date)
1246
if (pending_posts.size() == 0)
1251
std::list<pending_posts_list::iterator> posts_to_erase;
1254
for (pending_posts_list::iterator i = pending_posts.begin();
1255
i != pending_posts.end();
1257
pending_posts_list::value_type& pair(*i);
1259
optional<date_t> begin = pair.first.start;
1261
optional<date_t> range_begin;
1262
if (pair.first.range)
1263
range_begin = pair.first.range->begin();
1265
DEBUG("budget.generate", "Finding period for pending post");
1266
if (! pair.first.find_period(range_begin ? *range_begin : date))
1268
if (! pair.first.start)
1269
throw_(std::logic_error,
1270
_("Failed to find period for periodic transaction"));
1271
begin = pair.first.start;
1275
DEBUG("budget.generate", "begin = " << *begin);
1276
DEBUG("budget.generate", "date = " << date);
1277
if (pair.first.finish)
1278
DEBUG("budget.generate", "pair.first.finish = " << *pair.first.finish);
1281
if (*begin <= date &&
1282
(! pair.first.finish || *begin < *pair.first.finish)) {
1283
post_t& post = *pair.second;
1286
if (! pair.first.start)
1287
posts_to_erase.push_back(i);
1289
DEBUG("budget.generate", "Reporting budget for "
1290
<< post.reported_account()->fullname());
1292
xact_t& xact = temps.create_xact();
1293
xact.payee = _("Budget transaction");
1296
post_t& temp = temps.copy_post(post, xact);
1297
temp.amount.in_place_negate();
1299
if (flags & BUDGET_WRAP_VALUES) {
1302
seq.push_back(temp.amount);
1304
temp.xdata().compound_value = seq;
1305
temp.xdata().add_flags(POST_EXT_COMPOUND);
1308
item_handler<post_t>::operator()(temp);
1314
foreach (pending_posts_list::iterator& i, posts_to_erase)
1315
pending_posts.erase(i);
1319
void budget_posts::operator()(post_t& post)
1321
bool post_in_budget = false;
1323
foreach (pending_posts_list::value_type& pair, pending_posts) {
1324
for (account_t * acct = post.reported_account();
1326
acct = acct->parent) {
1327
if (acct == (*pair.second).reported_account()) {
1328
post_in_budget = true;
1329
// Report the post as if it had occurred in the parent account.
1330
if (post.reported_account() != acct)
1331
post.set_reported_account(acct);
1338
if (post_in_budget && flags & BUDGET_BUDGETED) {
1339
report_budget_items(post.date());
1340
item_handler<post_t>::operator()(post);
1342
else if (! post_in_budget && flags & BUDGET_UNBUDGETED) {
1343
item_handler<post_t>::operator()(post);
1347
void budget_posts::flush()
1349
if (flags & BUDGET_BUDGETED)
1350
report_budget_items(terminus);
1352
item_handler<post_t>::flush();
1355
void forecast_posts::add_post(const date_interval_t& period, post_t& post)
1357
date_interval_t i(period);
1358
if (! i.start && ! i.find_period(CURRENT_DATE()))
1361
generate_posts::add_post(i, post);
1363
// Advance the period's interval until it is at or beyond the current
1365
while (*i.start < CURRENT_DATE())
1369
void forecast_posts::flush()
1372
date_t last = CURRENT_DATE();
1374
// If there are period transactions to apply in a continuing series until
1375
// the forecast condition is met, generate those transactions now. Note
1376
// that no matter what, we abandon forecasting beyond the next 5 years.
1378
// It works like this:
1380
// Earlier, in forecast_posts::add_period_xacts, we cut up all the periodic
1381
// transactions into their components postings, so that we have N "periodic
1382
// postings". For example, if the user had this:
1385
// Expenses:Food $10
1386
// Expenses:Auto:Gas $20
1388
// Expenses:Food $100
1389
// Expenses:Auto:Gas $200
1391
// We now have 4 periodic postings in `pending_posts'.
1393
// Each periodic postings gets its own copy of its parent transaction's
1394
// period, which is modified as we go. This is found in the second member
1395
// of the pending_posts_list for each posting.
1397
// The algorithm below works by iterating through the N periodic postings
1398
// over and over, until each of them mets the termination critera for the
1399
// forecast and is removed from the set.
1401
while (pending_posts.size() > 0) {
1402
// At each step through the loop, we find the first periodic posting whose
1403
// period contains the earliest starting date.
1404
pending_posts_list::iterator least = pending_posts.begin();
1405
for (pending_posts_list::iterator i = ++pending_posts.begin();
1406
i != pending_posts.end();
1408
assert((*i).first.start);
1409
assert((*least).first.start);
1410
if (*(*i).first.start < *(*least).first.start)
1415
if ((*least).first.finish)
1416
assert(*(*least).first.start < *(*least).first.finish);
1419
// If the next date in the series for this periodic posting is more than 5
1420
// years beyond the last valid post we generated, drop it from further
1422
date_t& next(*(*least).first.next);
1423
assert(next > *(*least).first.start);
1425
if (static_cast<std::size_t>((next - last).days()) >
1426
static_cast<std::size_t>(365U) * forecast_years) {
1427
DEBUG("filters.forecast",
1428
"Forecast transaction exceeds " << forecast_years
1429
<< " years beyond today");
1430
pending_posts.erase(least);
1434
// `post' refers to the posting defined in the period transaction. We
1435
// make a copy of it within a temporary transaction with the payee
1436
// "Forecast transaction".
1437
post_t& post = *(*least).second;
1438
xact_t& xact = temps.create_xact();
1439
xact.payee = _("Forecast transaction");
1441
post_t& temp = temps.copy_post(post, xact);
1443
// Submit the generated posting
1444
DEBUG("filters.forecast",
1445
"Forecast transaction: " << temp.date()
1446
<< " " << temp.account->fullname()
1447
<< " " << temp.amount);
1448
item_handler<post_t>::operator()(temp);
1450
// If the generated posting matches the user's report query, check whether
1451
// it also fails to match the continuation condition for the forecast. If
1452
// it does, drop this periodic posting from consideration.
1453
if (temp.has_xdata() && temp.xdata().has_flags(POST_EXT_MATCHES)) {
1454
DEBUG("filters.forecast", " matches report query");
1455
bind_scope_t bound_scope(context, temp);
1456
if (! pred(bound_scope)) {
1457
DEBUG("filters.forecast", " fails to match continuation criteria");
1458
pending_posts.erase(least);
1463
// Increment the 'least', but remove it from pending_posts if it
1464
// exceeds its own boundaries.
1466
if (! (*least).first.start) {
1467
pending_posts.erase(least);
1472
item_handler<post_t>::flush();
1475
inject_posts::inject_posts(post_handler_ptr handler,
1476
const string& tag_list,
1478
: item_handler<post_t>(handler)
1480
scoped_array<char> buf(new char[tag_list.length() + 1]);
1481
std::strcpy(buf.get(), tag_list.c_str());
1483
for (char * q = std::strtok(buf.get(), ",");
1485
q = std::strtok(NULL, ",")) {
1486
std::list<string> account_names;
1487
split_string(q, ':', account_names);
1489
account_t * account =
1490
create_temp_account_from_path(account_names, temps, master);
1491
account->add_flags(ACCOUNT_GENERATED);
1494
(tags_list_pair(q, tag_mapping_pair(account, tag_injected_set())));
1497
TRACE_CTOR(inject_posts, "post_handler_ptr, string, account_t *");
1500
void inject_posts::operator()(post_t& post)
1502
foreach (tags_list_pair& pair, tags_list) {
1503
optional<value_t> tag_value = post.get_tag(pair.first, false);
1504
// When checking if the transaction has the tag, only inject once
1507
pair.second.second.find(post.xact) == pair.second.second.end() &&
1508
(tag_value = post.xact->get_tag(pair.first)))
1509
pair.second.second.insert(post.xact);
1512
xact_t& xact = temps.copy_xact(*post.xact);
1513
xact._date = post.date();
1514
xact.add_flags(ITEM_GENERATED);
1515
post_t& temp = temps.copy_post(post, xact);
1517
temp.account = pair.second.first;
1518
temp.amount = tag_value->to_amount();
1519
temp.add_flags(ITEM_GENERATED);
1521
item_handler<post_t>::operator()(temp);
1525
item_handler<post_t>::operator()(post);
1528
} // namespace ledger