/// <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(); }