/// <summary> /// Ported from changed_value_posts::output_revaluation /// </summary> public void OutputRevaluation(Post post, Date date) { if (date.IsValid()) { post.XData.Date = date; } try { BindScope boundScope = new BindScope(Report, post); RepricedTotal = TotalExpr.Calc(boundScope); } finally { post.XData.Date = default(Date); } if (!Value.IsNullOrEmpty(LastTotal)) { Value diff = RepricedTotal - LastTotal; if (!Value.IsNullOrEmptyOrFalse(diff)) { Xact xact = Temps.CreateXact(); xact.Payee = "Commodities revalued"; xact.Date = date.IsValid() ? date : post.ValueDate; if (!ForAccountsReports) { FiltersCommon.HandleValue( /* value= */ diff, /* account= */ RevaluedAccount, /* xact= */ xact, /* temps= */ Temps, /* handler= */ (PostHandler)Handler, /* date= */ xact.Date.Value, /* act_date_p= */ true, /* total= */ RepricedTotal); } else if (ShowUnrealized) { FiltersCommon.HandleValue( /* value= */ diff.Negated(), /* account= */ (diff.IsLessThan(Value.Zero) ? LossesEquityAccount : GainsEquityAccount), /* xact= */ xact, /* temps= */ Temps, /* handler= */ (PostHandler)Handler, /* date= */ xact.Date.Value, /* act_date_p= */ true, /* total= */ new Value(), /* direct_amount= */ false, /* mark_visited= */ true); } } } }
/// <summary> /// Ported from changed_value_posts::output_intermediate_prices /// </summary> public void OutputIntermediatePrices(Post post, Date current) { // To fix BZ#199, examine the balance of last_post and determine whether the // price of that amount changed after its date and before the new post's // date. If so, generate an output_revaluation for that price change. // Mostly this is only going to occur if the user has a series of pricing // entries, since a posting-based revaluation would be seen here as a post. Value displayTotal = Value.Clone(LastTotal); if (displayTotal.Type == ValueTypeEnum.Sequence) { Xact xact = Temps.CreateXact(); xact.Payee = "Commodities revalued"; xact.Date = current.IsValid() ? current : post.ValueDate; Post temp = Temps.CopyPost(post, xact); temp.Flags |= SupportsFlagsEnum.ITEM_GENERATED; PostXData xdata = temp.XData; if (current.IsValid()) { xdata.Date = current; } Logger.Current.Debug("filters.revalued", () => String.Format("intermediate last_total = {0}", LastTotal)); switch (LastTotal.Type) { case ValueTypeEnum.Boolean: case ValueTypeEnum.Integer: LastTotal.InPlaceCast(ValueTypeEnum.Amount); temp.Amount = LastTotal.AsAmount; break; case ValueTypeEnum.Amount: temp.Amount = LastTotal.AsAmount; break; case ValueTypeEnum.Balance: case ValueTypeEnum.Sequence: xdata.CompoundValue = LastTotal; xdata.Compound = true; break; default: throw new InvalidOperationException(); } BindScope innerScope = new BindScope(Report, temp); displayTotal = DisplayTotalExpr.Calc(innerScope); Logger.Current.Debug("filters.revalued", () => String.Format("intermediate display_total = {0}", displayTotal)); } switch (displayTotal.Type) { case ValueTypeEnum.Void: case ValueTypeEnum.Integer: case ValueTypeEnum.Sequence: break; case ValueTypeEnum.Amount: case ValueTypeEnum.Balance: { if (displayTotal.Type == ValueTypeEnum.Amount) { displayTotal.InPlaceCast(ValueTypeEnum.Balance); } IDictionary <DateTime, Amount> allPrices = new SortedDictionary <DateTime, Amount>(); foreach (KeyValuePair <Commodity, Amount> amtComm in displayTotal.AsBalance.Amounts) { amtComm.Key.MapPrices((d, a) => allPrices[d] = a, current, post.ValueDate, true); } // Choose the last price from each day as the price to use IDictionary <Date, bool> pricingDates = new SortedDictionary <Date, bool>(); foreach (KeyValuePair <DateTime, Amount> price in allPrices.Reverse()) { // This insert will fail if a later price has already been inserted // for that date. var priceDate = (Date)price.Key.Date; Logger.Current.Debug("filters.revalued", () => String.Format("re-inserting {0} at {1}", price.Value, priceDate)); pricingDates[priceDate] = true; } // Go through the time-sorted prices list, outputting a revaluation for // each price difference. foreach (KeyValuePair <Date, bool> price in pricingDates) { OutputRevaluation(post, price.Key); LastTotal = RepricedTotal; } break; } default: throw new InvalidOperationException(); } }
/// <summary> /// Ported from changed_value_posts::output_revaluation /// </summary> public void OutputRevaluation(Post post, Date date) { if (date.IsValid()) { post.XData.Date = date; } try { BindScope boundScope = new BindScope(Report, post); RepricedTotal = TotalExpr.Calc(boundScope); } finally { post.XData.Date = default(Date); } Logger.Current.Debug("filters.changed_value", () => String.Format("output_revaluation(last_total) = {0}", LastTotal)); Logger.Current.Debug("filters.changed_value", () => String.Format("output_revaluation(repriced_total) = {0}", RepricedTotal)); if (!Value.IsNullOrEmpty(LastTotal)) { Value diff = RepricedTotal - LastTotal; if (!Value.IsNullOrEmptyOrFalse(diff)) { Logger.Current.Debug("filters.changed_value", () => String.Format("output_revaluation(strip(diff)) = {0}", diff.StripAnnotations(Report.WhatToKeep()))); Xact xact = Temps.CreateXact(); xact.Payee = "Commodities revalued"; xact.Date = date.IsValid() ? date : post.ValueDate; if (!ForAccountsReports) { FiltersCommon.HandleValue( /* value= */ diff, /* account= */ RevaluedAccount, /* xact= */ xact, /* temps= */ Temps, /* handler= */ (PostHandler)Handler, /* date= */ xact.Date.Value, /* act_date_p= */ true, /* total= */ RepricedTotal); } else if (ShowUnrealized) { FiltersCommon.HandleValue( /* value= */ diff.Negated(), /* account= */ (diff.IsLessThan(Value.Zero) ? LossesEquityAccount : GainsEquityAccount), /* xact= */ xact, /* temps= */ Temps, /* handler= */ (PostHandler)Handler, /* date= */ xact.Date.Value, /* act_date_p= */ true, /* total= */ new Value(), /* direct_amount= */ false, /* mark_visited= */ true); } } } }
/// <summary> /// Ported from report_budget_items /// </summary> public void ReportBudgetItems(Date date) { if (!PendingPosts.Any()) { return; } bool reported; do { IList <PendingPostsPair> postsToErase = new List <PendingPostsPair>(); reported = false; foreach (PendingPostsPair pair in PendingPosts) { Date?begin = pair.DateInterval.Start; if (!begin.HasValue) { Date?rangeBegin = null; if (pair.DateInterval.Range != null) { rangeBegin = pair.DateInterval.Range.Begin; } Logger.Debug(DebugBudgetGenerate, () => "Finding period for pending post"); if (!pair.DateInterval.FindPeriod(rangeBegin ?? date)) { continue; } if (!pair.DateInterval.Start.HasValue) { throw new LogicError(LogicError.ErrorMessageFailedToFindPeriodForPeriodicTransaction); } begin = pair.DateInterval.Start; } Logger.Debug(DebugBudgetGenerate, () => String.Format("begin = {0}", begin)); Logger.Debug(DebugBudgetGenerate, () => String.Format("date = {0}", date)); if (pair.DateInterval.Finish.HasValue) { Logger.Debug(DebugBudgetGenerate, () => String.Format("pair.first.finish = {0}", pair.DateInterval.Finish)); } if (begin <= date && (!pair.DateInterval.Finish.HasValue || begin < pair.DateInterval.Finish)) { Post post = pair.Post; pair.DateInterval++; if (!pair.DateInterval.Start.HasValue) { postsToErase.Add(pair); } Logger.Debug(DebugBudgetGenerate, () => "Reporting budget for " + post.ReportedAccount.FullName); Xact xact = Temps.CreateXact(); xact.Payee = "Budget transaction"; xact.Date = begin.Value; Post temp = Temps.CopyPost(post, xact); temp.Amount.InPlaceNegate(); if (Flags.HasFlag(ReportBudgetFlags.BUDGET_WRAP_VALUES)) { Value seq = new Value(); seq.PushBack(Value.Get(0)); seq.PushBack(Value.Get(temp.Amount)); temp.XData.CompoundValue = seq; temp.XData.Compound = true; } base.Handle(temp); reported = true; } } foreach (PendingPostsPair pair in postsToErase) { PendingPosts.Remove(pair); } }while (reported); }
/// <summary> /// Ported from forecast_posts::flush /// </summary> public override void Flush() { IList <Post> passed = new List <Post>(); Date last = TimesCommon.Current.CurrentDate; // If there are period transactions to apply in a continuing series until // the forecast condition is met, generate those transactions now. Note // that no matter what, we abandon forecasting beyond the next 5 years. // // It works like this: // // Earlier, in forecast_posts::add_period_xacts, we cut up all the periodic // transactions into their components postings, so that we have N "periodic // postings". For example, if the user had this: // // ~ daily // Expenses:Food $10 // Expenses:Auto:Gas $20 // ~ monthly // Expenses:Food $100 // Expenses:Auto:Gas $200 // // We now have 4 periodic postings in `pending_posts'. // // Each periodic postings gets its own copy of its parent transaction's // period, which is modified as we go. This is found in the second member // of the pending_posts_list for each posting. // // The algorithm below works by iterating through the N periodic postings // over and over, until each of them mets the termination critera for the // forecast and is removed from the set. while (PendingPosts.Any()) { // At each step through the loop, we find the first periodic posting whose // period contains the earliest starting date. PendingPostsPair least = PendingPosts.First(); foreach (PendingPostsPair i in PendingPosts) { if (!i.DateInterval.Start.HasValue) { throw new InvalidOperationException("Start is empty"); } if (!least.DateInterval.Start.HasValue) { throw new InvalidOperationException("least.Start is empty"); } if (i.DateInterval.Start < least.DateInterval.Start) { least = i; } } // If the next date in the series for this periodic posting is more than 5 // years beyond the last valid post we generated, drop it from further // consideration. Date next = least.DateInterval.Next.Value; if (next <= least.DateInterval.Start) { throw new InvalidOperationException("next <= least.DateInterval.Start"); } if (((next - last).Days) > (365 * ForecastYears)) { Logger.Current.Debug("filters.forecast", () => String.Format("Forecast transaction exceeds {0} years beyond today", ForecastYears)); PendingPosts.Remove(least); continue; } // `post' refers to the posting defined in the period transaction. We // make a copy of it within a temporary transaction with the payee // "Forecast transaction". Post post = least.Post; Xact xact = Temps.CreateXact(); xact.Payee = "Forecast transaction"; xact.Date = next; Post temp = Temps.CopyPost(post, xact); // Submit the generated posting Logger.Current.Debug("filters.forecast", () => String.Format("Forecast transaction: {0} {1} {2}", temp.GetDate(), temp.Account.FullName, temp.Amount)); base.Handle(temp); // If the generated posting matches the user's report query, check whether // it also fails to match the continuation condition for the forecast. If // it does, drop this periodic posting from consideration. if (temp.HasXData && temp.XData.Matches) { Logger.Current.Debug("filters.forecast", () => " matches report query"); BindScope boundScope = new BindScope(Context, temp); if (!Pred.Calc(boundScope).Bool) { Logger.Current.Debug("filters.forecast", () => " fails to match continuation criteria"); PendingPosts.Remove(least); continue; } } // Increment the 'least', but remove it from pending_posts if it // exceeds its own boundaries. ++least.DateInterval; if (!least.DateInterval.Start.HasValue) { PendingPosts.Remove(least); continue; } } base.Flush(); }
public override void Flush() { if (Interval.Duration == null) { base.Flush(); return; } // Sort all the postings we saw by date ascending // [DM] Enumerable.OrderBy is a stable sort that preserve original positions for equal items AllPosts = AllPosts.OrderBy(p => p, new IntervalPostCompare()).ToList(); // only if the interval has no start use the earliest post if (!(Interval.Begin.HasValue && Interval.FindPeriod(Interval.Begin.Value))) { // Determine the beginning interval by using the earliest post if (AllPosts.Any() && !Interval.FindPeriod(AllPosts.First().GetDate())) { throw new LogicError(LogicError.ErrorMessageFailedToFindPeriodForIntervalReport); } } // Walk the interval forward reporting all posts within each one // before moving on, until we reach the end of all_posts bool sawPosts = false; for (int i = 0; i < AllPosts.Count;) { Post post = AllPosts[i]; Logger.Current.Debug("filters.interval", () => String.Format("Considering post {0} = {1}", post.GetDate(), post.Amount)); Logger.Current.Debug("filters.interval", () => String.Format("interval is:{0}", DebugInterval(Interval))); if (Interval.Finish.HasValue && post.GetDate() >= Interval.Finish.Value) { throw new InvalidOperationException("assert(! interval.finish || post->date() < *interval.finish)"); } if (Interval.WithinPeriod(post.GetDate())) { Logger.Current.Debug("filters.interval", () => "Calling subtotal_posts::operator()"); base.Handle(post); ++i; sawPosts = true; } else { if (sawPosts) { Logger.Current.Debug("filters.interval", () => "Calling subtotal_posts::report_subtotal()"); ReportSubtotal(Interval); sawPosts = false; } else if (GenerateEmptyPosts) { // Generate a null posting, so the intervening periods can be // seen when -E is used, or if the calculated amount ends up // being non-zero Xact nullXact = Temps.CreateXact(); nullXact.Date = Interval.InclusiveEnd.Value; Post nullPost = Temps.CreatePost(nullXact, EmptytAccount); nullPost.Flags |= SupportsFlagsEnum.POST_CALCULATED; nullPost.Amount = new Amount(0); base.Handle(nullPost); ReportSubtotal(Interval); } Logger.Current.Debug("filters.interval", () => "Advancing interval"); ++Interval; } } // If the last postings weren't reported, do so now. if (sawPosts) { Logger.Current.Debug("filters.interval", () => "Calling subtotal_posts::report_subtotal() at end"); ReportSubtotal(Interval); } // Tell our parent class to flush base.Flush(); }