diff --git a/kmymoney/mymoney/mymoneyforecast.h b/kmymoney/mymoney/mymoneyforecast.h --- a/kmymoney/mymoney/mymoneyforecast.h +++ b/kmymoney/mymoney/mymoneyforecast.h @@ -4,6 +4,7 @@ begin : Wed May 30 2007 copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com + (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** @@ -21,29 +22,36 @@ // ---------------------------------------------------------------------------- // QT Includes -#include -#include -#include -#include +#include // ---------------------------------------------------------------------------- // Project Includes -#include "mymoneymoney.h" #include "kmm_mymoney_export.h" #include "mymoneyunittestable.h" +class QDate; + +class MyMoneyMoney; +class MyMoneyAccount; +class MyMoneyBudget; +class MyMoneySchedule; +class MyMoneyTransaction; + +template class QList; +template class QMap; + /** * * * @author Alvaro Soliverez */ -class MyMoneyAccount; -class MyMoneyBudget; -class MyMoneySchedule; -class MyMoneyTransaction; +class MyMoneyForecastPrivate; class KMM_MYMONEY_EXPORT MyMoneyForecast { + Q_DECLARE_PRIVATE(MyMoneyForecast) + MyMoneyForecastPrivate * d_ptr; + KMM_MYMONEY_UNIT_TESTABLE public: @@ -60,7 +68,11 @@ * - includeScheduledTransactions = true */ MyMoneyForecast(); - ~MyMoneyForecast(); + MyMoneyForecast(const MyMoneyForecast & other); + MyMoneyForecast(MyMoneyForecast && other); + MyMoneyForecast & operator=(MyMoneyForecast other); + friend void swap(MyMoneyForecast& first, MyMoneyForecast& second); + virtual ~MyMoneyForecast(); /** * calculate forecast based on historic transactions @@ -142,113 +154,41 @@ /** * number of days to go back in history to calculate forecast */ - int historyDays() const { - return (m_historyStartDate.daysTo(m_historyEndDate) + 1); - } - - void setAccountsCycle(int accountsCycle) { - m_accountsCycle = accountsCycle; - } - void setForecastCycles(int forecastCycles) { - m_forecastCycles = forecastCycles; - } - void setForecastDays(int forecastDays) { - m_forecastDays = forecastDays; - } - void setBeginForecastDate(const QDate &beginForecastDate) { - m_beginForecastDate = beginForecastDate; - } - void setBeginForecastDay(int beginDay) { - m_beginForecastDay = beginDay; - } - void setForecastMethod(int forecastMethod) { - m_forecastMethod = forecastMethod; - } - void setHistoryStartDate(const QDate &historyStartDate) { - m_historyStartDate = historyStartDate; - } - void setHistoryEndDate(const QDate &historyEndDate) { - m_historyEndDate = historyEndDate; - } - void setHistoryStartDate(int daysToStartDate) { - setHistoryStartDate(QDate::currentDate().addDays(-daysToStartDate)); - } - void setHistoryEndDate(int daysToEndDate) { - setHistoryEndDate(QDate::currentDate().addDays(-daysToEndDate)); - } - void setForecastStartDate(const QDate &_startDate) { - m_forecastStartDate = _startDate; - } - void setForecastEndDate(const QDate &_endDate) { - m_forecastEndDate = _endDate; - } - void setSkipOpeningDate(bool _skip) { - m_skipOpeningDate = _skip; - } - void setHistoryMethod(int historyMethod) { - m_historyMethod = historyMethod; - } - void setIncludeUnusedAccounts(bool _bool) { - m_includeUnusedAccounts = _bool; - } - void setForecastDone(bool _bool) { - m_forecastDone = _bool; - } - void setIncludeFutureTransactions(bool _bool) { - m_includeFutureTransactions = _bool; - } - void setIncludeScheduledTransactions(bool _bool) { - m_includeScheduledTransactions = _bool; - } - - int accountsCycle() const { - return m_accountsCycle; - } - int forecastCycles() const { - return m_forecastCycles; - } - int forecastDays() const { - return m_forecastDays; - } - const QDate& beginForecastDate() const { - return m_beginForecastDate; - } - int beginForecastDay() const { - return m_beginForecastDay; - } - int forecastMethod() const { - return m_forecastMethod; - } - const QDate& historyStartDate() const { - return m_historyStartDate; - } - const QDate& historyEndDate() const { - return m_historyEndDate; - } - const QDate& forecastStartDate() const { - return m_forecastStartDate; - } - const QDate& forecastEndDate() const { - return m_forecastEndDate; - } - bool skipOpeningDate() const { - return m_skipOpeningDate; - } - int historyMethod() const { - return m_historyMethod; - } - bool isIncludingUnusedAccounts() const { - return m_includeUnusedAccounts; - } - bool isForecastDone() const { - return m_forecastDone; - } - bool isIncludingFutureTransactions() const { - return m_includeFutureTransactions; - } - bool isIncludingScheduledTransactions() const { - return m_includeScheduledTransactions; - } + int historyDays() const; + + void setAccountsCycle(int accountsCycle); + void setForecastCycles(int forecastCycles); + void setForecastDays(int forecastDays); + void setBeginForecastDate(const QDate &beginForecastDate); + void setBeginForecastDay(int beginDay); + void setForecastMethod(int forecastMethod); + void setHistoryStartDate(const QDate &historyStartDate); + void setHistoryEndDate(const QDate &historyEndDate); + void setHistoryStartDate(int daysToStartDate); + void setHistoryEndDate(int daysToEndDate); + void setForecastStartDate(const QDate &_startDate); + void setForecastEndDate(const QDate &_endDate); + void setSkipOpeningDate(bool _skip); + void setHistoryMethod(int historyMethod); + void setIncludeUnusedAccounts(bool _bool); + void setForecastDone(bool _bool); + void setIncludeFutureTransactions(bool _bool); + void setIncludeScheduledTransactions(bool _bool); + int accountsCycle() const; + int forecastCycles() const; + int forecastDays() const; + QDate beginForecastDate() const; + int beginForecastDay() const; + QDate historyStartDate() const; + QDate historyEndDate() const; + QDate forecastStartDate() const; + QDate forecastEndDate() const; + bool skipOpeningDate() const; + int historyMethod() const; + bool isIncludingUnusedAccounts() const; + bool isForecastDone() const; + bool isIncludingFutureTransactions() const; + bool isIncludingScheduledTransactions() const; /** * This method modifies a scheduled loan transaction such that all @@ -262,228 +202,28 @@ */ static void calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances); -private: - - enum EForecastMethod {eScheduled = 0, eHistoric = 1 }; - - /** - * daily balances of an account - */ - typedef QMap dailyBalances; - - /** - * map of trends of an account - */ - typedef QMap trendBalances; - /** * Returns the list of accounts to be forecast. Only Asset and Liability are returned. */ static QList forecastAccountList(); +}; - /** - * Returns the list of accounts to create a budget. Only Income and Expenses are returned. - */ - QList budgetAccountList(); - - /** - * calculate daily forecast balance based on historic transactions - */ - void calculateHistoricDailyBalances(); - - /** - * calculate monthly budget balance based on historic transactions - */ - void calculateHistoricMonthlyBalances(); - - /** - * calculate monthly budget balance based on historic transactions - */ - void calculateScheduledMonthlyBalances(); - - /** - * calculate forecast based on future and scheduled transactions - */ - void doFutureScheduledForecast(); - - /** - * add future transactions to forecast - */ - void addFutureTransactions(); - - /** - * add scheduled transactions to forecast - */ - void addScheduledTransactions(); - - /** - * calculate daily forecast balance based on future and scheduled transactions - */ - void calculateScheduledDailyBalances(); - - /** - * set the starting balance for an accounts - */ - void setStartingBalance(const MyMoneyAccount& acc); - - /** - * Returns the day moving average for the account @a acc based on the daily balances of a given number of @p forecastTerms - * It returns the moving average for a given @p trendDay of the forecastTerm - * With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred - * at that day and the day before,for the last 3 months - */ - MyMoneyMoney accountMovingAverage(const MyMoneyAccount& acc, const int trendDay, const int forecastTerms); - - /** - * Returns the weighted moving average for a given @p trendDay - */ - MyMoneyMoney accountWeightedMovingAverage(const MyMoneyAccount& acc, const int trendDay, const int totalWeight); - - /** - * Returns the linear regression for a given @p trendDay - */ - MyMoneyMoney accountLinearRegression(const MyMoneyAccount &acc, const int trendDay, const int totalWeight, const MyMoneyMoney& meanTerms); - - /** - * calculate daily forecast trend based on historic transactions - */ - void calculateAccountTrendList(); - - /** - * set the internal list of accounts to be forecast - */ - void setForecastAccountList(); - - /** - * set the internal list of accounts to create a budget - */ - void setBudgetAccountList(); - - /** - * get past transactions for the accounts to be forecast - */ - void pastTransactions(); - - /** - * calculate the day to start forecast and sets the begin date - * The quantity of forecast days will be counted from this date - * Depends on the values of begin day and accounts cycle - * The rules to calculate begin day are as follows: - * - if beginDay is 0, begin date is current date - * - if the day of the month set by beginDay has not passed, that will be used - * - if adding an account cycle to beginDay, will not go past the beginDay of next month, - * that date will be used, otherwise it will add account cycle to beginDay until it is past current date - * It returns the total amount of Forecast Days from current date. - */ - int calculateBeginForecastDay(); - - /** - * remove accounts from the list if the accounts has no transactions in the forecast timeframe. - * Used for scheduled-forecast method. - */ - void purgeForecastAccountsList(QMap& accountList); - - /** - * daily forecast balance of accounts - */ - QMap m_accountList; - - /** - * daily past balance of accounts - */ - QMap m_accountListPast; - - /** - * daily forecast trends of accounts - */ - QMap m_accountTrendList; - - /** - * list of forecast account ids. - */ - QSet m_forecastAccounts; - - /** - * cycle of accounts in days - */ - int m_accountsCycle; - - /** - * number of cycles to use in forecast - */ - int m_forecastCycles; - - /** - * number of days to forecast - */ - int m_forecastDays; - - /** - * date to start forecast - */ - QDate m_beginForecastDate; - - /** - * day to start forecast - */ - int m_beginForecastDay; - - /** - * forecast method - */ - int m_forecastMethod; - - /** - * history method - */ - int m_historyMethod; - - /** - * start date of history - */ - QDate m_historyStartDate; - - /** - * end date of history - */ - QDate m_historyEndDate; - - /** - * start date of forecast - */ - QDate m_forecastStartDate; - - /** - * end date of forecast - */ - QDate m_forecastEndDate; - - /** - * skip opening date when fetching transactions of an account - */ - bool m_skipOpeningDate; - - /** - * include accounts with no transactions in the forecast timeframe. default is false. - */ - bool m_includeUnusedAccounts; - - /** - * forecast already done - */ - bool m_forecastDone; - - /** - * include future transactions when doing a scheduled-based forecast - */ - bool m_includeFutureTransactions; +inline void swap(MyMoneyForecast& first, MyMoneyForecast& second) // krazy:exclude=inline +{ + using std::swap; + swap(first.d_ptr, second.d_ptr); +} - /** - * include scheduled transactions when doing a scheduled-based forecast - */ - bool m_includeScheduledTransactions; +inline MyMoneyForecast::MyMoneyForecast(MyMoneyForecast && other) : MyMoneyForecast() // krazy:exclude=inline +{ + swap(*this, other); +} -}; +inline MyMoneyForecast & MyMoneyForecast::operator=(MyMoneyForecast other) // krazy:exclude=inline +{ + swap(*this, other); + return *this; +} /** * Make it possible to hold @ref MyMoneyForecast objects inside @ref QVariant objects. diff --git a/kmymoney/mymoney/mymoneyforecast.cpp b/kmymoney/mymoney/mymoneyforecast.cpp --- a/kmymoney/mymoney/mymoneyforecast.cpp +++ b/kmymoney/mymoney/mymoneyforecast.cpp @@ -4,6 +4,7 @@ begin : Wed May 30 2007 copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com + (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** @@ -23,6 +24,7 @@ #include #include #include +#include // ---------------------------------------------------------------------------- // KDE Includes @@ -37,737 +39,1143 @@ #include "mymoneybudget.h" #include "mymoneyschedule.h" #include "mymoneyprice.h" +#include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneyfinancialcalculator.h" -MyMoneyForecast::MyMoneyForecast() : +enum class eForecastMethod {Scheduled = 0, Historic = 1 }; + +/** + * daily balances of an account + */ +typedef QMap dailyBalances; + +/** + * map of trends of an account + */ +typedef QMap trendBalances; + +class MyMoneyForecastPrivate +{ + Q_DECLARE_PUBLIC(MyMoneyForecast) + +public: + MyMoneyForecastPrivate(MyMoneyForecast *qq) : + q_ptr(qq), m_accountsCycle(30), m_forecastCycles(3), m_forecastDays(90), m_beginForecastDay(0), - m_forecastMethod(eScheduled), + m_forecastMethod(eForecastMethod::Scheduled), m_historyMethod(1), m_skipOpeningDate(true), m_includeUnusedAccounts(false), m_forecastDone(false), m_includeFutureTransactions(true), m_includeScheduledTransactions(true) -{ - setHistoryStartDate(QDate::currentDate().addDays(-forecastCycles()*accountsCycle())); - setHistoryEndDate(QDate::currentDate().addDays(-1)); -} + { + } + eForecastMethod forecastMethod() const + { + return m_forecastMethod; + } -void MyMoneyForecast::doForecast() -{ - int fDays = calculateBeginForecastDay(); - int fMethod = forecastMethod(); - int fAccCycle = accountsCycle(); - int fCycles = forecastCycles(); + /** + * Returns the list of accounts to create a budget. Only Income and Expenses are returned. + */ + QList budgetAccountList() + { + auto file = MyMoneyFile::instance(); + + QList accList; + QStringList emptyStringList; + //Get all accounts from the file and check if they are of the right type to calculate forecast + file->accountList(accList, emptyStringList, false); + QList::iterator accList_t = accList.begin(); + for (; accList_t != accList.end();) { + auto acc = *accList_t; + if (acc.isClosed() //check the account is not closed + || (!acc.isIncomeExpense())) { + //remove the account if it is not of the correct type + accList_t = accList.erase(accList_t); + } else { + ++accList_t; + } + } + return accList; + } - //validate settings - if (fAccCycle < 1 - || fCycles < 1 - || fDays < 1) { - throw MYMONEYEXCEPTION("Illegal settings when calling doForecast. Settings must be higher than 0"); + /** + * calculate daily forecast balance based on historic transactions + */ + void calculateHistoricDailyBalances() + { + Q_Q(MyMoneyForecast); + auto file = MyMoneyFile::instance(); + + calculateAccountTrendList(); + + //Calculate account daily balances + QSet::ConstIterator it_n; + for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { + auto acc = file->account(*it_n); + + //set the starting balance of the account + setStartingBalance(acc); + + switch (q->historyMethod()) { + case 0: + case 1: { + for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { + for (int t_day = 1; t_day <= q->accountsCycle(); ++t_day) { + MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before + MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][t_day]; //trend for that day + //balance of the day is the balance of the day before multiplied by the trend for the day + m_accountList[acc.id()][f_day] = balanceDayBefore; + m_accountList[acc.id()][f_day] += accountDailyTrend; //movement trend for that particular day + m_accountList[acc.id()][f_day] = m_accountList[acc.id()][f_day].convert(acc.fraction()); + //m_accountList[acc.id()][f_day] += m_accountListPast[acc.id()][f_day.addDays(-q->historyDays())]; + f_day = f_day.addDays(1); + } + } + } + break; + case 2: { + QDate baseDate = QDate::currentDate().addDays(-q->accountsCycle()); + for (int t_day = 1; t_day <= q->accountsCycle(); ++t_day) { + int f_day = 1; + QDate fDate = baseDate.addDays(q->accountsCycle() + 1); + while (fDate <= q->forecastEndDate()) { + + //the calculation is based on the balance for the last month, that is then multiplied by the trend + m_accountList[acc.id()][fDate] = m_accountListPast[acc.id()][baseDate] + (m_accountTrendList[acc.id()][t_day] * MyMoneyMoney(f_day, 1)); + m_accountList[acc.id()][fDate] = m_accountList[acc.id()][fDate].convert(acc.fraction()); + ++f_day; + fDate = baseDate.addDays(q->accountsCycle() * f_day); + } + baseDate = baseDate.addDays(1); + } + } + } + } } - //initialize global variables - setForecastDays(fDays); - setForecastStartDate(QDate::currentDate().addDays(1)); - setForecastEndDate(QDate::currentDate().addDays(fDays)); - setAccountsCycle(fAccCycle); - setForecastCycles(fCycles); - setHistoryStartDate(forecastCycles() * accountsCycle()); - setHistoryEndDate(QDate::currentDate().addDays(-1)); //yesterday + /** + * calculate monthly budget balance based on historic transactions + */ + void calculateHistoricMonthlyBalances() + { + Q_Q(MyMoneyForecast); + auto file = MyMoneyFile::instance(); - //clear all data before calculating - m_accountListPast.clear(); - m_accountList.clear(); - m_accountTrendList.clear(); + //Calculate account monthly balances + QSet::ConstIterator it_n; + for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { + auto acc = file->account(*it_n); + + for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate();) { + for (int f_day = 1; f_day <= q->accountsCycle() && f_date <= q->forecastEndDate(); ++f_day) { + MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][f_day]; //trend for that day + //check for leap year + if (f_date.month() == 2 && f_date.day() == 29) + f_date = f_date.addDays(1); //skip 1 day + m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyTrend; //movement trend for that particular day + f_date = f_date.addDays(1); + } + } + } + } - //set forecast accounts - setForecastAccountList(); + /** + * calculate monthly budget balance based on historic transactions + */ + void calculateScheduledMonthlyBalances() + { + Q_Q(MyMoneyForecast); + auto file = MyMoneyFile::instance(); - switch (fMethod) { - case eScheduled: - doFutureScheduledForecast(); - calculateScheduledDailyBalances(); - break; - case eHistoric: - pastTransactions(); - calculateHistoricDailyBalances(); - break; - default: - break; + //Calculate account monthly balances + QSet::ConstIterator it_n; + for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { + auto acc = file->account(*it_n); + + for (QDate f_date = q->forecastStartDate(); f_date <= q->forecastEndDate(); f_date = f_date.addDays(1)) { + //get the trend for the day + MyMoneyMoney accountDailyBalance = m_accountList[acc.id()][f_date]; + + //do not add if it is the beginning of the month + //otherwise we end up with duplicated values as reported by Marko Käning + if (f_date != QDate(f_date.year(), f_date.month(), 1)) + m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyBalance; + } + } } - //flag the forecast as done - m_forecastDone = true; -} + /** + * calculate forecast based on future and scheduled transactions + */ + void doFutureScheduledForecast() + { + Q_Q(MyMoneyForecast); + auto file = MyMoneyFile::instance(); -MyMoneyForecast::~MyMoneyForecast() -{ -} + if (q->isIncludingFutureTransactions()) + addFutureTransactions(); -void MyMoneyForecast::pastTransactions() -{ - MyMoneyFile* file = MyMoneyFile::instance(); - MyMoneyTransactionFilter filter; + if (q->isIncludingScheduledTransactions()) + addScheduledTransactions(); - filter.setDateFilter(historyStartDate(), historyEndDate()); - filter.setReportAllSplits(false); + //do not show accounts with no transactions + if (!q->isIncludingUnusedAccounts()) + purgeForecastAccountsList(m_accountList); - QList transactions = file->transactionList(filter); - QList::const_iterator it_t = transactions.constBegin(); + //adjust value of investments to deep currency + QSet::ConstIterator it_n; + for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { + auto acc = file->account(*it_n); - //Check past transactions - for (; it_t != transactions.constEnd(); ++it_t) { - const QList& splits = (*it_t).splits(); - QList::const_iterator it_s = splits.begin(); - for (; it_s != splits.end(); ++it_s) { - if (!(*it_s).shares().isZero()) { - MyMoneyAccount acc = file->account((*it_s).accountId()); + if (acc.isInvest()) { + //get the id of the security for that account + MyMoneySecurity undersecurity = file->security(acc.currencyId()); - //workaround for stock accounts which have faulty opening dates - QDate openingDate; - if (acc.accountType() == eMyMoney::Account::Stock) { - MyMoneyAccount parentAccount = file->account(acc.parentAccountId()); - openingDate = parentAccount.openingDate(); - } else { - openingDate = acc.openingDate(); - } + //only do it if the security is not an actual currency + if (! undersecurity.isCurrency()) { + //set the default value + MyMoneyMoney rate = MyMoneyMoney::ONE; - if (isForecastAccount(acc) //If it is one of the accounts we are checking, add the amount of the transaction - && ((openingDate < (*it_t).postDate() && skipOpeningDate()) - || !skipOpeningDate())) { //don't take the opening day of the account to calculate balance - dailyBalances balance; - //FIXME deal with leap years - balance = m_accountListPast[acc.id()]; - if (acc.accountType() == eMyMoney::Account::Income) {//if it is income, the balance is stored as negative number - balance[(*it_t).postDate()] += ((*it_s).shares() * MyMoneyMoney::MINUS_ONE); - } else { - balance[(*it_t).postDate()] += (*it_s).shares(); + for (QDate it_day = QDate::currentDate(); it_day <= q->forecastEndDate();) { + //get the price for the tradingCurrency that day + const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_day); + if (price.isValid()) { + rate = price.rate(undersecurity.tradingCurrency()); + } + //value is the amount of shares multiplied by the rate of the deep currency + m_accountList[acc.id()][it_day] = m_accountList[acc.id()][it_day] * rate; + it_day = it_day.addDays(1); } - // check if this is a new account for us - m_accountListPast[acc.id()] = balance; } } } } - //purge those accounts with no transactions on the period - if (isIncludingUnusedAccounts() == false) - purgeForecastAccountsList(m_accountListPast); - - //calculate running sum - QSet::ConstIterator it_n; - for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); - m_accountListPast[acc.id()][historyStartDate().addDays(-1)] = file->balance(acc.id(), historyStartDate().addDays(-1)); - for (QDate it_date = historyStartDate(); it_date <= historyEndDate();) { - m_accountListPast[acc.id()][it_date] += m_accountListPast[acc.id()][it_date.addDays(-1)]; //Running sum - it_date = it_date.addDays(1); + /** + * add future transactions to forecast + */ + void addFutureTransactions() + { + Q_Q(MyMoneyForecast); + MyMoneyTransactionFilter filter; + auto file = MyMoneyFile::instance(); + + // collect and process all transactions that have already been entered but + // are located in the future. + filter.setDateFilter(q->forecastStartDate(), q->forecastEndDate()); + filter.setReportAllSplits(false); + + auto transactions = file->transactionList(filter); + auto it_t = transactions.constBegin(); + + for (; it_t != transactions.constEnd(); ++it_t) { + const QList& splits = (*it_t).splits(); + QList::const_iterator it_s = splits.begin(); + for (; it_s != splits.end(); ++it_s) { + if (!(*it_s).shares().isZero()) { + auto acc = file->account((*it_s).accountId()); + if (q->isForecastAccount(acc)) { + dailyBalances balance; + balance = m_accountList[acc.id()]; + //if it is income, the balance is stored as negative number + if (acc.accountType() == eMyMoney::Account::Income) { + balance[(*it_t).postDate()] += ((*it_s).shares() * MyMoneyMoney::MINUS_ONE); + } else { + balance[(*it_t).postDate()] += (*it_s).shares(); + } + m_accountList[acc.id()] = balance; + } + } + } + } + +#if 0 + QFile trcFile("forecast.csv"); + trcFile.open(QIODevice::WriteOnly); + QTextStream s(&trcFile); + + { + s << "Already present transactions\n"; + QMap::Iterator it_a; + QSet::ConstIterator it_n; + for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { + auto acc = file->account(*it_n); + it_a = m_accountList.find(*it_n); + s << "\"" << acc.name() << "\","; + for (int i = 0; i < 90; ++i) { + s << "\"" << (*it_a)[i].formatMoney("") << "\","; + } + s << "\n"; + } } +#endif + } - //adjust value of investments to deep currency - for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); + /** + * add scheduled transactions to forecast + */ + void addScheduledTransactions() + { + Q_Q(MyMoneyForecast); + auto file = MyMoneyFile::instance(); + + // now process all the schedules that may have an impact + QList schedule; + + schedule = file->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, + QDate(), q->forecastEndDate(), false); + if (schedule.count() > 0) { + QList::Iterator it; + do { + qSort(schedule); + it = schedule.begin(); + if (it == schedule.end()) + break; - if (acc.isInvest()) { - //get the id of the security for that account - MyMoneySecurity undersecurity = file->security(acc.currencyId()); - if (! undersecurity.isCurrency()) { //only do it if the security is not an actual currency - MyMoneyMoney rate = MyMoneyMoney::ONE; //set the default value - - for (QDate it_date = historyStartDate().addDays(-1) ; it_date <= historyEndDate();) { - //get the price for the tradingCurrency that day - const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); - if (price.isValid()) { - rate = price.rate(undersecurity.tradingCurrency()); + if ((*it).isFinished()) { + schedule.erase(it); + continue; + } + + QDate date = (*it).nextPayment((*it).lastPayment()); + if (!date.isValid()) { + schedule.erase(it); + continue; + } + + QDate nextDate = + (*it).adjustedNextPayment((*it).adjustedDate((*it).lastPayment(), + (*it).weekendOption())); + if (nextDate > q->forecastEndDate()) { + // We're done with this schedule, let's move on to the next + schedule.erase(it); + continue; + } + + // found the next schedule. process it + + auto acc = (*it).account(); + + if (!acc.id().isEmpty()) { + try { + if (acc.accountType() != eMyMoney::Account::Investment) { + MyMoneyTransaction t = (*it).transaction(); + + // only process the entry, if it is still active + if (!(*it).isFinished() && nextDate != QDate()) { + // make sure we have all 'starting balances' so that the autocalc works + QList::const_iterator it_s; + QMap balanceMap; + + for (it_s = t.splits().constBegin(); it_s != t.splits().constEnd(); ++it_s) { + auto acc = file->account((*it_s).accountId()); + if (q->isForecastAccount(acc)) { + // collect all overdues on the first day + QDate forecastDate = nextDate; + if (QDate::currentDate() >= nextDate) + forecastDate = QDate::currentDate().addDays(1); + + dailyBalances balance; + balance = m_accountList[acc.id()]; + for (QDate f_day = QDate::currentDate(); f_day < forecastDate;) { + balanceMap[acc.id()] += m_accountList[acc.id()][f_day]; + f_day = f_day.addDays(1); + } + } + } + + // take care of the autoCalc stuff + q->calculateAutoLoan(*it, t, balanceMap); + + // now add the splits to the balances + for (it_s = t.splits().constBegin(); it_s != t.splits().constEnd(); ++it_s) { + auto acc = file->account((*it_s).accountId()); + if (q->isForecastAccount(acc)) { + dailyBalances balance; + balance = m_accountList[acc.id()]; + //int offset = QDate::currentDate().daysTo(nextDate); + //if(offset <= 0) { // collect all overdues on the first day + // offset = 1; + //} + // collect all overdues on the first day + QDate forecastDate = nextDate; + if (QDate::currentDate() >= nextDate) + forecastDate = QDate::currentDate().addDays(1); + + if (acc.accountType() == eMyMoney::Account::Income) { + balance[forecastDate] += ((*it_s).shares() * MyMoneyMoney::MINUS_ONE); + } else { + balance[forecastDate] += (*it_s).shares(); + } + m_accountList[acc.id()] = balance; + } + } + } + } + (*it).setLastPayment(date); + + } catch (const MyMoneyException &e) { + qDebug() << Q_FUNC_INFO << " Schedule " << (*it).id() << " (" << (*it).name() << "): " << e.what(); + + schedule.erase(it); } - //value is the amount of shares multiplied by the rate of the deep currency - m_accountListPast[acc.id()][it_date] = m_accountListPast[acc.id()][it_date] * rate; - it_date = it_date.addDays(1); + } else { + // remove schedule from list + schedule.erase(it); } + } while (1); + } + +#if 0 + { + s << "\n\nAdded scheduled transactions\n"; + QMap::Iterator it_a; + QSet::ConstIterator it_n; + for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { + auto acc = file->account(*it_n); + it_a = m_accountList.find(*it_n); + s << "\"" << acc.name() << "\","; + for (int i = 0; i < 90; ++i) { + s << "\"" << (*it_a)[i].formatMoney("") << "\","; + } + s << "\n"; } } +#endif } -} -bool MyMoneyForecast::isForecastAccount(const MyMoneyAccount& acc) -{ - if (m_forecastAccounts.isEmpty()) { - setForecastAccountList(); + /** + * calculate daily forecast balance based on future and scheduled transactions + */ + void calculateScheduledDailyBalances() + { + Q_Q(MyMoneyForecast); + auto file = MyMoneyFile::instance(); + + //Calculate account daily balances + QSet::ConstIterator it_n; + for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { + auto acc = file->account(*it_n); + + //set the starting balance of the account + setStartingBalance(acc); + + for (QDate f_day = q->forecastStartDate(); f_day <= q->forecastEndDate();) { + MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before + m_accountList[acc.id()][f_day] += balanceDayBefore; //running sum + f_day = f_day.addDays(1); + } + } } - return m_forecastAccounts.contains(acc.id()); -} -void MyMoneyForecast::calculateAccountTrendList() -{ - MyMoneyFile* file = MyMoneyFile::instance(); - int auxForecastTerms; - int totalWeight = 0; + /** + * set the starting balance for an accounts + */ + void setStartingBalance(const MyMoneyAccount& acc) + { + Q_Q(MyMoneyForecast); + auto file = MyMoneyFile::instance(); - //Calculate account trends - QSet::ConstIterator it_n; - for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); - m_accountTrendList[acc.id()][0] = MyMoneyMoney(); // for today, the trend is 0 + //Get current account balance + if (acc.isInvest()) { //investments require special treatment + //get the security id of that account + MyMoneySecurity undersecurity = file->security(acc.currencyId()); - auxForecastTerms = forecastCycles(); - if (skipOpeningDate()) { + //only do it if the security is not an actual currency + if (! undersecurity.isCurrency()) { + //set the default value + MyMoneyMoney rate = MyMoneyMoney::ONE; + //get te + const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), QDate::currentDate()); + if (price.isValid()) { + rate = price.rate(undersecurity.tradingCurrency()); + } + m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()) * rate; + } + } else { + m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()); + } + //if the method is linear regression, we have to add the opening balance to m_accountListPast + if (forecastMethod() == eForecastMethod::Historic && q->historyMethod() == 2) { + //FIXME workaround for stock opening dates QDate openingDate; if (acc.accountType() == eMyMoney::Account::Stock) { - MyMoneyAccount parentAccount = file->account(acc.parentAccountId()); + auto parentAccount = file->account(acc.parentAccountId()); openingDate = parentAccount.openingDate(); } else { openingDate = acc.openingDate(); } - if (openingDate > historyStartDate()) { //if acc opened after forecast period - auxForecastTerms = 1 + ((openingDate.daysTo(historyEndDate()) + 1) / accountsCycle()); // set forecastTerms to a lower value, to calculate only based on how long this account was opened - } - } + //add opening balance only if it opened after the history start + if (openingDate >= q->historyStartDate()) { - switch (historyMethod()) { - //moving average - case 0: { - for (int t_day = 1; t_day <= accountsCycle(); t_day++) - m_accountTrendList[acc.id()][t_day] = accountMovingAverage(acc, t_day, auxForecastTerms); //moving average - break; - } - //weighted moving average - case 1: { - //calculate total weight for moving average - if (auxForecastTerms == forecastCycles()) { - totalWeight = (auxForecastTerms * (auxForecastTerms + 1)) / 2; //totalWeight is the triangular number of auxForecastTerms + MyMoneyMoney openingBalance; + + openingBalance = file->balance(acc.id(), openingDate); + + //calculate running sum + for (QDate it_date = openingDate; it_date <= q->historyEndDate(); it_date = it_date.addDays(1)) { + //investments require special treatment + if (acc.isInvest()) { + //get the security id of that account + MyMoneySecurity undersecurity = file->security(acc.currencyId()); + + //only do it if the security is not an actual currency + if (! undersecurity.isCurrency()) { + //set the default value + MyMoneyMoney rate = MyMoneyMoney::ONE; + + //get the rate for that specific date + const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); + if (price.isValid()) { + rate = price.rate(undersecurity.tradingCurrency()); + } + m_accountListPast[acc.id()][it_date] += openingBalance * rate; + } } else { - //if only taking a few periods, totalWeight is the sum of the weight for most recent periods - for (int i = 1, w = forecastCycles(); i <= auxForecastTerms; ++i, --w) - totalWeight += w; + m_accountListPast[acc.id()][it_date] += openingBalance; } - for (int t_day = 1; t_day <= accountsCycle(); t_day++) - m_accountTrendList[acc.id()][t_day] = accountWeightedMovingAverage(acc, t_day, totalWeight); - break; } - case 2: { - //calculate mean term - MyMoneyMoney meanTerms = MyMoneyMoney((auxForecastTerms * (auxForecastTerms + 1)) / 2, 1) / MyMoneyMoney(auxForecastTerms, 1); + } + } + } - for (int t_day = 1; t_day <= accountsCycle(); t_day++) - m_accountTrendList[acc.id()][t_day] = accountLinearRegression(acc, t_day, auxForecastTerms, meanTerms); - break; - } - default: - break; + /** + * Returns the day moving average for the account @a acc based on the daily balances of a given number of @p forecastTerms + * It returns the moving average for a given @p trendDay of the forecastTerm + * With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred + * at that day and the day before,for the last 3 months + */ + MyMoneyMoney accountMovingAverage(const MyMoneyAccount& acc, const int trendDay, const int forecastTerms) + { + Q_Q(MyMoneyForecast); + //Calculate a daily trend for the account based on the accounts of a given number of terms + //With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred at that day and the day before, + //for the last 3 months + MyMoneyMoney balanceVariation; + + for (int it_terms = 0; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term + MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before + MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; + balanceVariation += (balanceAfter - balanceBefore); //add the balance variation between days } + //calculate average of the variations + return (balanceVariation / MyMoneyMoney(forecastTerms, 1)).convert(10000); } -} -QList MyMoneyForecast::forecastAccountList() -{ - MyMoneyFile* file = MyMoneyFile::instance(); + /** + * Returns the weighted moving average for a given @p trendDay + */ + MyMoneyMoney accountWeightedMovingAverage(const MyMoneyAccount& acc, const int trendDay, const int totalWeight) + { + Q_Q(MyMoneyForecast); + MyMoneyMoney balanceVariation; - QList accList; - //Get all accounts from the file and check if they are of the right type to calculate forecast - file->accountList(accList); - QList::iterator accList_t = accList.begin(); - for (; accList_t != accList.end();) { - MyMoneyAccount acc = *accList_t; - if (acc.isClosed() //check the account is not closed - || (!acc.isAssetLiability())) { - //|| (acc.accountType() == eMyMoney::Account::Investment) ) {//check that it is not an Investment account and only include Stock accounts - //remove the account if it is not of the correct type - accList_t = accList.erase(accList_t); - } else { - ++accList_t; + for (int it_terms = 0, weight = 1; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++weight) { //sum for each term multiplied by weight + MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-2)]; //get balance for the day before + MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; + balanceVariation += ((balanceAfter - balanceBefore) * MyMoneyMoney(weight, 1)); //add the balance variation between days multiplied by its weight } + //calculate average of the variations + return (balanceVariation / MyMoneyMoney(totalWeight, 1)).convert(10000); } - return accList; -} -QList MyMoneyForecast::accountList() -{ - MyMoneyFile* file = MyMoneyFile::instance(); + /** + * Returns the linear regression for a given @p trendDay + */ + MyMoneyMoney accountLinearRegression(const MyMoneyAccount &acc, const int trendDay, const int actualTerms, const MyMoneyMoney& meanTerms) + { + Q_Q(MyMoneyForecast); + MyMoneyMoney meanBalance, totalBalance, totalTerms; + totalTerms = MyMoneyMoney(actualTerms, 1); - QList accList; - QStringList emptyStringList; - //Get all accounts from the file and check if they are present - file->accountList(accList, emptyStringList, false); - QList::iterator accList_t = accList.begin(); - for (; accList_t != accList.end();) { - MyMoneyAccount acc = *accList_t; - if (!isForecastAccount(acc)) { - accList_t = accList.erase(accList_t); //remove the account - } else { - ++accList_t; + //calculate mean balance + for (int it_terms = q->forecastCycles() - actualTerms; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms) { //sum for each term + totalBalance += m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; } - } - return accList; -} + meanBalance = totalBalance / MyMoneyMoney(actualTerms, 1); + meanBalance = meanBalance.convert(10000); -MyMoneyMoney MyMoneyForecast::calculateAccountTrend(const MyMoneyAccount& acc, int trendDays) -{ - MyMoneyFile* file = MyMoneyFile::instance(); - MyMoneyTransactionFilter filter; - MyMoneyMoney netIncome; - QDate startDate; - QDate openingDate = acc.openingDate(); + //calculate b1 - //validate arguments - if (trendDays < 1) { - throw MYMONEYEXCEPTION("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0"); - } + //first calculate x - mean x multiplied by y - mean y + MyMoneyMoney totalXY, totalSqX; + for (int it_terms = q->forecastCycles() - actualTerms, term = 1; (trendDay + (q->accountsCycle()*it_terms)) <= q->historyDays(); ++it_terms, ++term) { //sum for each term + MyMoneyMoney balance = m_accountListPast[acc.id()][q->historyStartDate().addDays(trendDay+(q->accountsCycle()*it_terms)-1)]; - //If it is a new account, we don't take into account the first day - //because it is usually a weird one and it would mess up the trend - if (openingDate.daysTo(QDate::currentDate()) < trendDays) { - startDate = (acc.openingDate()).addDays(1); - } else { - startDate = QDate::currentDate().addDays(-trendDays); - } - //get all transactions for the period - filter.setDateFilter(startDate, QDate::currentDate()); - if (acc.accountGroup() == eMyMoney::Account::Income - || acc.accountGroup() == eMyMoney::Account::Expense) { - filter.addCategory(acc.id()); - } else { - filter.addAccount(acc.id()); - } + MyMoneyMoney balMeanBal = balance - meanBalance; + MyMoneyMoney termMeanTerm = (MyMoneyMoney(term, 1) - meanTerms); - filter.setReportAllSplits(false); - QList transactions = file->transactionList(filter); - QList::const_iterator it_t = transactions.constBegin(); + totalXY += (balMeanBal * termMeanTerm).convert(10000); - //add all transactions for that account - for (; it_t != transactions.constEnd(); ++it_t) { - const QList& splits = (*it_t).splits(); - QList::const_iterator it_s = splits.begin(); - for (; it_s != splits.end(); ++it_s) { - if (!(*it_s).shares().isZero()) { - if (acc.id() == (*it_s).accountId()) netIncome += (*it_s).value(); - } + totalSqX += (termMeanTerm * termMeanTerm).convert(10000); } - } + totalXY = (totalXY / MyMoneyMoney(actualTerms, 1)).convert(10000); + totalSqX = (totalSqX / MyMoneyMoney(actualTerms, 1)).convert(10000); - //calculate trend of the account in the past period - MyMoneyMoney accTrend; + //check zero + if (totalSqX.isZero()) + return MyMoneyMoney(); - //don't take into account the first day of the account - if (openingDate.daysTo(QDate::currentDate()) < trendDays) { - accTrend = netIncome / MyMoneyMoney(openingDate.daysTo(QDate::currentDate()) - 1, 1); - } else { - accTrend = netIncome / MyMoneyMoney(trendDays, 1); - } - return accTrend; -} + MyMoneyMoney linReg = (totalXY / totalSqX).convert(10000); -MyMoneyMoney MyMoneyForecast::accountMovingAverage(const MyMoneyAccount &acc, const int trendDay, const int forecastTerms) -{ - //Calculate a daily trend for the account based on the accounts of a given number of terms - //With a term of 1 month and 3 terms, it calculates the trend taking the transactions occurred at that day and the day before, - //for the last 3 months - MyMoneyMoney balanceVariation; - - for (int it_terms = 0; (trendDay + (accountsCycle()*it_terms)) <= historyDays(); ++it_terms) { //sum for each term - MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][historyStartDate().addDays(trendDay+(accountsCycle()*it_terms)-2)]; //get balance for the day before - MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][historyStartDate().addDays(trendDay+(accountsCycle()*it_terms)-1)]; - balanceVariation += (balanceAfter - balanceBefore); //add the balance variation between days + return linReg; } - //calculate average of the variations - return (balanceVariation / MyMoneyMoney(forecastTerms, 1)).convert(10000); -} -MyMoneyMoney MyMoneyForecast::accountWeightedMovingAverage(const MyMoneyAccount &acc, const int trendDay, const int totalWeight) -{ - MyMoneyMoney balanceVariation; + /** + * calculate daily forecast trend based on historic transactions + */ + void calculateAccountTrendList() + { + Q_Q(MyMoneyForecast); + auto file = MyMoneyFile::instance(); + int auxForecastTerms; + int totalWeight = 0; - for (int it_terms = 0, weight = 1; (trendDay + (accountsCycle()*it_terms)) <= historyDays(); ++it_terms, ++weight) { //sum for each term multiplied by weight - MyMoneyMoney balanceBefore = m_accountListPast[acc.id()][historyStartDate().addDays(trendDay+(accountsCycle()*it_terms)-2)]; //get balance for the day before - MyMoneyMoney balanceAfter = m_accountListPast[acc.id()][historyStartDate().addDays(trendDay+(accountsCycle()*it_terms)-1)]; - balanceVariation += ((balanceAfter - balanceBefore) * MyMoneyMoney(weight, 1)); //add the balance variation between days multiplied by its weight - } - //calculate average of the variations - return (balanceVariation / MyMoneyMoney(totalWeight, 1)).convert(10000); -} + //Calculate account trends + QSet::ConstIterator it_n; + for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { + auto acc = file->account(*it_n); + m_accountTrendList[acc.id()][0] = MyMoneyMoney(); // for today, the trend is 0 -MyMoneyMoney MyMoneyForecast::accountLinearRegression(const MyMoneyAccount &acc, const int trendDay, const int actualTerms, const MyMoneyMoney& meanTerms) -{ - MyMoneyMoney meanBalance, totalBalance, totalTerms; - totalTerms = MyMoneyMoney(actualTerms, 1); + auxForecastTerms = q->forecastCycles(); + if (q->skipOpeningDate()) { - //calculate mean balance - for (int it_terms = forecastCycles() - actualTerms; (trendDay + (accountsCycle()*it_terms)) <= historyDays(); ++it_terms) { //sum for each term - totalBalance += m_accountListPast[acc.id()][historyStartDate().addDays(trendDay+(accountsCycle()*it_terms)-1)]; - } - meanBalance = totalBalance / MyMoneyMoney(actualTerms, 1); - meanBalance = meanBalance.convert(10000); - - //calculate b1 + QDate openingDate; + if (acc.accountType() == eMyMoney::Account::Stock) { + auto parentAccount = file->account(acc.parentAccountId()); + openingDate = parentAccount.openingDate(); + } else { + openingDate = acc.openingDate(); + } - //first calculate x - mean x multiplied by y - mean y - MyMoneyMoney totalXY, totalSqX; - for (int it_terms = forecastCycles() - actualTerms, term = 1; (trendDay + (accountsCycle()*it_terms)) <= historyDays(); ++it_terms, ++term) { //sum for each term - MyMoneyMoney balance = m_accountListPast[acc.id()][historyStartDate().addDays(trendDay+(accountsCycle()*it_terms)-1)]; + if (openingDate > q->historyStartDate()) { //if acc opened after forecast period + auxForecastTerms = 1 + ((openingDate.daysTo(q->historyEndDate()) + 1) / q->accountsCycle()); // set forecastTerms to a lower value, to calculate only based on how long this account was opened + } + } - MyMoneyMoney balMeanBal = balance - meanBalance; - MyMoneyMoney termMeanTerm = (MyMoneyMoney(term, 1) - meanTerms); + switch (q->historyMethod()) { + //moving average + case 0: { + for (int t_day = 1; t_day <= q->accountsCycle(); t_day++) + m_accountTrendList[acc.id()][t_day] = accountMovingAverage(acc, t_day, auxForecastTerms); //moving average + break; + } + //weighted moving average + case 1: { + //calculate total weight for moving average + if (auxForecastTerms == q->forecastCycles()) { + totalWeight = (auxForecastTerms * (auxForecastTerms + 1)) / 2; //totalWeight is the triangular number of auxForecastTerms + } else { + //if only taking a few periods, totalWeight is the sum of the weight for most recent periods + for (int i = 1, w = q->forecastCycles(); i <= auxForecastTerms; ++i, --w) + totalWeight += w; + } + for (int t_day = 1; t_day <= q->accountsCycle(); t_day++) + m_accountTrendList[acc.id()][t_day] = accountWeightedMovingAverage(acc, t_day, totalWeight); + break; + } + case 2: { + //calculate mean term + MyMoneyMoney meanTerms = MyMoneyMoney((auxForecastTerms * (auxForecastTerms + 1)) / 2, 1) / MyMoneyMoney(auxForecastTerms, 1); - totalXY += (balMeanBal * termMeanTerm).convert(10000); + for (int t_day = 1; t_day <= q->accountsCycle(); t_day++) + m_accountTrendList[acc.id()][t_day] = accountLinearRegression(acc, t_day, auxForecastTerms, meanTerms); + break; + } + default: + break; + } + } + } - totalSqX += (termMeanTerm * termMeanTerm).convert(10000); + /** + * set the internal list of accounts to be forecast + */ + void setForecastAccountList() + { + Q_Q(MyMoneyForecast); + //get forecast accounts + QList accList; + accList = q->forecastAccountList(); + + QList::const_iterator accList_t = accList.constBegin(); + for (; accList_t != accList.constEnd(); ++accList_t) { + m_forecastAccounts.insert((*accList_t).id()); + } } - totalXY = (totalXY / MyMoneyMoney(actualTerms, 1)).convert(10000); - totalSqX = (totalSqX / MyMoneyMoney(actualTerms, 1)).convert(10000); - //check zero - if (totalSqX.isZero()) - return MyMoneyMoney(); + /** + * set the internal list of accounts to create a budget + */ + void setBudgetAccountList() + { + //get budget accounts + QList accList; + accList = budgetAccountList(); - MyMoneyMoney linReg = (totalXY / totalSqX).convert(10000); + QList::const_iterator accList_t = accList.constBegin(); + for (; accList_t != accList.constEnd(); ++accList_t) { + m_forecastAccounts.insert((*accList_t).id()); + } + } - return linReg; -} + /** + * get past transactions for the accounts to be forecast + */ + void pastTransactions() + { + Q_Q(MyMoneyForecast); + auto file = MyMoneyFile::instance(); + MyMoneyTransactionFilter filter; + + filter.setDateFilter(q->historyStartDate(), q->historyEndDate()); + filter.setReportAllSplits(false); + + auto transactions = file->transactionList(filter); + auto it_t = transactions.constBegin(); + + //Check past transactions + for (; it_t != transactions.constEnd(); ++it_t) { + const QList& splits = (*it_t).splits(); + QList::const_iterator it_s = splits.begin(); + for (; it_s != splits.end(); ++it_s) { + if (!(*it_s).shares().isZero()) { + auto acc = file->account((*it_s).accountId()); + + //workaround for stock accounts which have faulty opening dates + QDate openingDate; + if (acc.accountType() == eMyMoney::Account::Stock) { + auto parentAccount = file->account(acc.parentAccountId()); + openingDate = parentAccount.openingDate(); + } else { + openingDate = acc.openingDate(); + } -void MyMoneyForecast::calculateHistoricDailyBalances() -{ - MyMoneyFile* file = MyMoneyFile::instance(); + if (q->isForecastAccount(acc) //If it is one of the accounts we are checking, add the amount of the transaction + && ((openingDate < (*it_t).postDate() && q->skipOpeningDate()) + || !q->skipOpeningDate())) { //don't take the opening day of the account to calculate balance + dailyBalances balance; + //FIXME deal with leap years + balance = m_accountListPast[acc.id()]; + if (acc.accountType() == eMyMoney::Account::Income) {//if it is income, the balance is stored as negative number + balance[(*it_t).postDate()] += ((*it_s).shares() * MyMoneyMoney::MINUS_ONE); + } else { + balance[(*it_t).postDate()] += (*it_s).shares(); + } + // check if this is a new account for us + m_accountListPast[acc.id()] = balance; + } + } + } + } - calculateAccountTrendList(); + //purge those accounts with no transactions on the period + if (q->isIncludingUnusedAccounts() == false) + purgeForecastAccountsList(m_accountListPast); - //Calculate account daily balances - QSet::ConstIterator it_n; - for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); + //calculate running sum + QSet::ConstIterator it_n; + for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { + auto acc = file->account(*it_n); + m_accountListPast[acc.id()][q->historyStartDate().addDays(-1)] = file->balance(acc.id(), q->historyStartDate().addDays(-1)); + for (QDate it_date = q->historyStartDate(); it_date <= q->historyEndDate();) { + m_accountListPast[acc.id()][it_date] += m_accountListPast[acc.id()][it_date.addDays(-1)]; //Running sum + it_date = it_date.addDays(1); + } + } - //set the starting balance of the account - setStartingBalance(acc); + //adjust value of investments to deep currency + for (it_n = m_forecastAccounts.begin(); it_n != m_forecastAccounts.end(); ++it_n) { + auto acc = file->account(*it_n); - switch (historyMethod()) { - case 0: - case 1: { - for (QDate f_day = forecastStartDate(); f_day <= forecastEndDate();) { - for (int t_day = 1; t_day <= accountsCycle(); ++t_day) { - MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before - MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][t_day]; //trend for that day - //balance of the day is the balance of the day before multiplied by the trend for the day - m_accountList[acc.id()][f_day] = balanceDayBefore; - m_accountList[acc.id()][f_day] += accountDailyTrend; //movement trend for that particular day - m_accountList[acc.id()][f_day] = m_accountList[acc.id()][f_day].convert(acc.fraction()); - //m_accountList[acc.id()][f_day] += m_accountListPast[acc.id()][f_day.addDays(-historyDays())]; - f_day = f_day.addDays(1); - } - } - } - break; - case 2: { - QDate baseDate = QDate::currentDate().addDays(-accountsCycle()); - for (int t_day = 1; t_day <= accountsCycle(); ++t_day) { - int f_day = 1; - QDate fDate = baseDate.addDays(accountsCycle() + 1); - while (fDate <= forecastEndDate()) { - - //the calculation is based on the balance for the last month, that is then multiplied by the trend - m_accountList[acc.id()][fDate] = m_accountListPast[acc.id()][baseDate] + (m_accountTrendList[acc.id()][t_day] * MyMoneyMoney(f_day, 1)); - m_accountList[acc.id()][fDate] = m_accountList[acc.id()][fDate].convert(acc.fraction()); - ++f_day; - fDate = baseDate.addDays(accountsCycle() * f_day); + if (acc.isInvest()) { + //get the id of the security for that account + MyMoneySecurity undersecurity = file->security(acc.currencyId()); + if (! undersecurity.isCurrency()) { //only do it if the security is not an actual currency + MyMoneyMoney rate = MyMoneyMoney::ONE; //set the default value + + for (QDate it_date = q->historyStartDate().addDays(-1) ; it_date <= q->historyEndDate();) { + //get the price for the tradingCurrency that day + const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); + if (price.isValid()) { + rate = price.rate(undersecurity.tradingCurrency()); } - baseDate = baseDate.addDays(1); + //value is the amount of shares multiplied by the rate of the deep currency + m_accountListPast[acc.id()][it_date] = m_accountListPast[acc.id()][it_date] * rate; + it_date = it_date.addDays(1); } } + } } } -} -MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, const QDate &forecastDate) -{ - dailyBalances balance; - MyMoneyMoney MM_amount = MyMoneyMoney(); + /** + * calculate the day to start forecast and sets the begin date + * The quantity of forecast days will be counted from this date + * Depends on the values of begin day and accounts cycle + * The rules to calculate begin day are as follows: + * - if beginDay is 0, begin date is current date + * - if the day of the month set by beginDay has not passed, that will be used + * - if adding an account cycle to beginDay, will not go past the beginDay of next month, + * that date will be used, otherwise it will add account cycle to beginDay until it is past current date + * It returns the total amount of Forecast Days from current date. + */ + int calculateBeginForecastDay() + { + Q_Q(MyMoneyForecast); + int fDays = q->forecastDays(); + int beginDay = q->beginForecastDay(); + int accCycle = q->accountsCycle(); + QDate beginDate; + + //if 0, beginDate is current date and forecastDays remains unchanged + if (beginDay == 0) { + q->setBeginForecastDate(QDate::currentDate()); + return fDays; + } - //Check if acc is not a forecast account, return 0 - if (!isForecastAccount(acc)) { - return MM_amount; - } + //adjust if beginDay more than days of current month + if (QDate::currentDate().daysInMonth() < beginDay) + beginDay = QDate::currentDate().daysInMonth(); - if (m_accountList.contains(acc.id())) { - balance = m_accountList.value(acc.id()); + //if beginDay still to come, calculate and return + if (QDate::currentDate().day() <= beginDay) { + beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); + fDays += QDate::currentDate().daysTo(beginDate); + q->setBeginForecastDate(beginDate); + return fDays; + } + + //adjust beginDay for next month + if (QDate::currentDate().addMonths(1).daysInMonth() < beginDay) + beginDay = QDate::currentDate().addMonths(1).daysInMonth(); + + //if beginDay of next month comes before 1 interval, use beginDay next month + if (QDate::currentDate().addDays(accCycle) >= + (QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1))) { + beginDate = QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1); + fDays += QDate::currentDate().daysTo(beginDate); + } else { //add intervals to current beginDay and take the first after current date + beginDay = ((((QDate::currentDate().day() - beginDay) / accCycle) + 1) * accCycle) + beginDay; + beginDate = QDate::currentDate().addDays(beginDay - QDate::currentDate().day()); + fDays += QDate::currentDate().daysTo(beginDate); + } + + q->setBeginForecastDate(beginDate); + return fDays; } - if (balance.contains(forecastDate)) { //if the date is not in the forecast, it returns 0 - MM_amount = balance.value(forecastDate); + + /** + * remove accounts from the list if the accounts has no transactions in the forecast timeframe. + * Used for scheduled-forecast method. + */ + void purgeForecastAccountsList(QMap& accountList) + { + m_forecastAccounts.intersect(accountList.keys().toSet()); } - return MM_amount; + + MyMoneyForecast *q_ptr; + + /** + * daily forecast balance of accounts + */ + QMap m_accountList; + + /** + * daily past balance of accounts + */ + QMap m_accountListPast; + + /** + * daily forecast trends of accounts + */ + QMap m_accountTrendList; + + /** + * list of forecast account ids. + */ + QSet m_forecastAccounts; + + /** + * cycle of accounts in days + */ + int m_accountsCycle; + + /** + * number of cycles to use in forecast + */ + int m_forecastCycles; + + /** + * number of days to forecast + */ + int m_forecastDays; + + /** + * date to start forecast + */ + QDate m_beginForecastDate; + + /** + * day to start forecast + */ + int m_beginForecastDay; + + /** + * forecast method + */ + eForecastMethod m_forecastMethod; + + /** + * history method + */ + int m_historyMethod; + + /** + * start date of history + */ + QDate m_historyStartDate; + + /** + * end date of history + */ + QDate m_historyEndDate; + + /** + * start date of forecast + */ + QDate m_forecastStartDate; + + /** + * end date of forecast + */ + QDate m_forecastEndDate; + + /** + * skip opening date when fetching transactions of an account + */ + bool m_skipOpeningDate; + + /** + * include accounts with no transactions in the forecast timeframe. default is false. + */ + bool m_includeUnusedAccounts; + + /** + * forecast already done + */ + bool m_forecastDone; + + /** + * include future transactions when doing a scheduled-based forecast + */ + bool m_includeFutureTransactions; + + /** + * include scheduled transactions when doing a scheduled-based forecast + */ + bool m_includeScheduledTransactions; +}; + +MyMoneyForecast::MyMoneyForecast() : + d_ptr(new MyMoneyForecastPrivate(this)) +{ + setHistoryStartDate(QDate::currentDate().addDays(-forecastCycles()*accountsCycle())); + setHistoryEndDate(QDate::currentDate().addDays(-1)); } -/** - * Returns the forecast balance trend for account @a acc for offset @p int - * offset is days from current date, inside forecast days. - * Returns 0 if offset not in range of forecast days. - */ -MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, int offset) +MyMoneyForecast::MyMoneyForecast(const MyMoneyForecast& other) : + d_ptr(new MyMoneyForecastPrivate(*other.d_func())) { - QDate forecastDate = QDate::currentDate().addDays(offset); - return forecastBalance(acc, forecastDate); } -void MyMoneyForecast::doFutureScheduledForecast() +MyMoneyForecast::~MyMoneyForecast() { - MyMoneyFile* file = MyMoneyFile::instance(); + Q_D(MyMoneyForecast); + delete d; +} + +void MyMoneyForecast::doForecast() +{ + Q_D(MyMoneyForecast); + auto fDays = d->calculateBeginForecastDay(); + auto fMethod = d->forecastMethod(); + auto fAccCycle = accountsCycle(); + auto fCycles = forecastCycles(); + + //validate settings + if (fAccCycle < 1 + || fCycles < 1 + || fDays < 1) { + throw MYMONEYEXCEPTION("Illegal settings when calling doForecast. Settings must be higher than 0"); + } + + //initialize global variables + setForecastDays(fDays); + setForecastStartDate(QDate::currentDate().addDays(1)); + setForecastEndDate(QDate::currentDate().addDays(fDays)); + setAccountsCycle(fAccCycle); + setForecastCycles(fCycles); + setHistoryStartDate(forecastCycles() * accountsCycle()); + setHistoryEndDate(QDate::currentDate().addDays(-1)); //yesterday - if (isIncludingFutureTransactions()) - addFutureTransactions(); + //clear all data before calculating + d->m_accountListPast.clear(); + d->m_accountList.clear(); + d->m_accountTrendList.clear(); - if (isIncludingScheduledTransactions()) - addScheduledTransactions(); + //set forecast accounts + d->setForecastAccountList(); - //do not show accounts with no transactions - if (!isIncludingUnusedAccounts()) - purgeForecastAccountsList(m_accountList); + switch (fMethod) { + case eForecastMethod::Scheduled: + d->doFutureScheduledForecast(); + d->calculateScheduledDailyBalances(); + break; + case eForecastMethod::Historic: + d->pastTransactions(); + d->calculateHistoricDailyBalances(); + break; + default: + break; + } - //adjust value of investments to deep currency - QSet::ConstIterator it_n; - for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); + //flag the forecast as done + d->m_forecastDone = true; +} - if (acc.isInvest()) { - //get the id of the security for that account - MyMoneySecurity undersecurity = file->security(acc.currencyId()); +bool MyMoneyForecast::isForecastAccount(const MyMoneyAccount& acc) +{ + Q_D(MyMoneyForecast); + if (d->m_forecastAccounts.isEmpty()) { + d->setForecastAccountList(); + } + return d->m_forecastAccounts.contains(acc.id()); +} - //only do it if the security is not an actual currency - if (! undersecurity.isCurrency()) { - //set the default value - MyMoneyMoney rate = MyMoneyMoney::ONE; +QList MyMoneyForecast::accountList() +{ + auto file = MyMoneyFile::instance(); - for (QDate it_day = QDate::currentDate(); it_day <= forecastEndDate();) { - //get the price for the tradingCurrency that day - const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_day); - if (price.isValid()) { - rate = price.rate(undersecurity.tradingCurrency()); - } - //value is the amount of shares multiplied by the rate of the deep currency - m_accountList[acc.id()][it_day] = m_accountList[acc.id()][it_day] * rate; - it_day = it_day.addDays(1); - } - } + QList accList; + QStringList emptyStringList; + //Get all accounts from the file and check if they are present + file->accountList(accList, emptyStringList, false); + QList::iterator accList_t = accList.begin(); + for (; accList_t != accList.end();) { + auto acc = *accList_t; + if (!isForecastAccount(acc)) { + accList_t = accList.erase(accList_t); //remove the account + } else { + ++accList_t; } } -} - -void MyMoneyForecast::addFutureTransactions() -{ - MyMoneyTransactionFilter filter; - MyMoneyFile* file = MyMoneyFile::instance(); + return accList; +} + +MyMoneyMoney MyMoneyForecast::calculateAccountTrend(const MyMoneyAccount& acc, int trendDays) +{ + auto file = MyMoneyFile::instance(); + MyMoneyTransactionFilter filter; + MyMoneyMoney netIncome; + QDate startDate; + QDate openingDate = acc.openingDate(); + + //validate arguments + if (trendDays < 1) { + throw MYMONEYEXCEPTION("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0"); + } + + //If it is a new account, we don't take into account the first day + //because it is usually a weird one and it would mess up the trend + if (openingDate.daysTo(QDate::currentDate()) < trendDays) { + startDate = (acc.openingDate()).addDays(1); + } else { + startDate = QDate::currentDate().addDays(-trendDays); + } + //get all transactions for the period + filter.setDateFilter(startDate, QDate::currentDate()); + if (acc.accountGroup() == eMyMoney::Account::Income + || acc.accountGroup() == eMyMoney::Account::Expense) { + filter.addCategory(acc.id()); + } else { + filter.addAccount(acc.id()); + } - // collect and process all transactions that have already been entered but - // are located in the future. - filter.setDateFilter(forecastStartDate(), forecastEndDate()); filter.setReportAllSplits(false); + auto transactions = file->transactionList(filter); + auto it_t = transactions.constBegin(); - QList transactions = file->transactionList(filter); - QList::const_iterator it_t = transactions.constBegin(); - + //add all transactions for that account for (; it_t != transactions.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); QList::const_iterator it_s = splits.begin(); for (; it_s != splits.end(); ++it_s) { if (!(*it_s).shares().isZero()) { - MyMoneyAccount acc = file->account((*it_s).accountId()); - if (isForecastAccount(acc)) { - dailyBalances balance; - balance = m_accountList[acc.id()]; - //if it is income, the balance is stored as negative number - if (acc.accountType() == eMyMoney::Account::Income) { - balance[(*it_t).postDate()] += ((*it_s).shares() * MyMoneyMoney::MINUS_ONE); - } else { - balance[(*it_t).postDate()] += (*it_s).shares(); - } - m_accountList[acc.id()] = balance; - } + if (acc.id() == (*it_s).accountId()) netIncome += (*it_s).value(); } } } -#if 0 - QFile trcFile("forecast.csv"); - trcFile.open(QIODevice::WriteOnly); - QTextStream s(&trcFile); + //calculate trend of the account in the past period + MyMoneyMoney accTrend; - { - s << "Already present transactions\n"; - QMap::Iterator it_a; - QSet::ConstIterator it_n; - for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); - it_a = m_accountList.find(*it_n); - s << "\"" << acc.name() << "\","; - for (int i = 0; i < 90; ++i) { - s << "\"" << (*it_a)[i].formatMoney("") << "\","; - } - s << "\n"; - } + //don't take into account the first day of the account + if (openingDate.daysTo(QDate::currentDate()) < trendDays) { + accTrend = netIncome / MyMoneyMoney(openingDate.daysTo(QDate::currentDate()) - 1, 1); + } else { + accTrend = netIncome / MyMoneyMoney(trendDays, 1); } -#endif - + return accTrend; } -void MyMoneyForecast::addScheduledTransactions() +MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, const QDate &forecastDate) { - MyMoneyFile* file = MyMoneyFile::instance(); - - // now process all the schedules that may have an impact - QList schedule; - - schedule = file->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, - QDate(), forecastEndDate(), false); - if (schedule.count() > 0) { - QList::Iterator it; - do { - qSort(schedule); - it = schedule.begin(); - if (it == schedule.end()) - break; - - if ((*it).isFinished()) { - schedule.erase(it); - continue; - } - - QDate date = (*it).nextPayment((*it).lastPayment()); - if (!date.isValid()) { - schedule.erase(it); - continue; - } - - QDate nextDate = - (*it).adjustedNextPayment((*it).adjustedDate((*it).lastPayment(), - (*it).weekendOption())); - if (nextDate > forecastEndDate()) { - // We're done with this schedule, let's move on to the next - schedule.erase(it); - continue; - } - - // found the next schedule. process it - - MyMoneyAccount acc = (*it).account(); - - if (!acc.id().isEmpty()) { - try { - if (acc.accountType() != eMyMoney::Account::Investment) { - MyMoneyTransaction t = (*it).transaction(); - - // only process the entry, if it is still active - if (!(*it).isFinished() && nextDate != QDate()) { - // make sure we have all 'starting balances' so that the autocalc works - QList::const_iterator it_s; - QMap balanceMap; - - for (it_s = t.splits().constBegin(); it_s != t.splits().constEnd(); ++it_s) { - MyMoneyAccount acc = file->account((*it_s).accountId()); - if (isForecastAccount(acc)) { - // collect all overdues on the first day - QDate forecastDate = nextDate; - if (QDate::currentDate() >= nextDate) - forecastDate = QDate::currentDate().addDays(1); - - dailyBalances balance; - balance = m_accountList[acc.id()]; - for (QDate f_day = QDate::currentDate(); f_day < forecastDate;) { - balanceMap[acc.id()] += m_accountList[acc.id()][f_day]; - f_day = f_day.addDays(1); - } - } - } - - // take care of the autoCalc stuff - calculateAutoLoan(*it, t, balanceMap); - - // now add the splits to the balances - for (it_s = t.splits().constBegin(); it_s != t.splits().constEnd(); ++it_s) { - MyMoneyAccount acc = file->account((*it_s).accountId()); - if (isForecastAccount(acc)) { - dailyBalances balance; - balance = m_accountList[acc.id()]; - //int offset = QDate::currentDate().daysTo(nextDate); - //if(offset <= 0) { // collect all overdues on the first day - // offset = 1; - //} - // collect all overdues on the first day - QDate forecastDate = nextDate; - if (QDate::currentDate() >= nextDate) - forecastDate = QDate::currentDate().addDays(1); - - if (acc.accountType() == eMyMoney::Account::Income) { - balance[forecastDate] += ((*it_s).shares() * MyMoneyMoney::MINUS_ONE); - } else { - balance[forecastDate] += (*it_s).shares(); - } - m_accountList[acc.id()] = balance; - } - } - } - } - (*it).setLastPayment(date); - - } catch (const MyMoneyException &e) { - qDebug() << Q_FUNC_INFO << " Schedule " << (*it).id() << " (" << (*it).name() << "): " << e.what(); + Q_D(MyMoneyForecast); + dailyBalances balance; + MyMoneyMoney MM_amount = MyMoneyMoney(); - schedule.erase(it); - } - } else { - // remove schedule from list - schedule.erase(it); - } - } while (1); + //Check if acc is not a forecast account, return 0 + if (!isForecastAccount(acc)) { + return MM_amount; } -#if 0 - { - s << "\n\nAdded scheduled transactions\n"; - QMap::Iterator it_a; - QSet::ConstIterator it_n; - for (it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); - it_a = m_accountList.find(*it_n); - s << "\"" << acc.name() << "\","; - for (int i = 0; i < 90; ++i) { - s << "\"" << (*it_a)[i].formatMoney("") << "\","; - } - s << "\n"; - } + if (d->m_accountList.contains(acc.id())) { + balance = d->m_accountList.value(acc.id()); } -#endif + if (balance.contains(forecastDate)) { //if the date is not in the forecast, it returns 0 + MM_amount = balance.value(forecastDate); + } + return MM_amount; } -void MyMoneyForecast::calculateScheduledDailyBalances() +/** + * Returns the forecast balance trend for account @a acc for offset @p int + * offset is days from current date, inside forecast days. + * Returns 0 if offset not in range of forecast days. + */ +MyMoneyMoney MyMoneyForecast::forecastBalance(const MyMoneyAccount& acc, int offset) { - MyMoneyFile* file = MyMoneyFile::instance(); - - //Calculate account daily balances - QSet::ConstIterator it_n; - for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); - - //set the starting balance of the account - setStartingBalance(acc); - - for (QDate f_day = forecastStartDate(); f_day <= forecastEndDate();) { - MyMoneyMoney balanceDayBefore = m_accountList[acc.id()][(f_day.addDays(-1))];//balance of the day before - m_accountList[acc.id()][f_day] += balanceDayBefore; //running sum - f_day = f_day.addDays(1); - } - } - - + QDate forecastDate = QDate::currentDate().addDays(offset); + return forecastBalance(acc, forecastDate); } int MyMoneyForecast::daysToMinimumBalance(const MyMoneyAccount& acc) { + Q_D(MyMoneyForecast); QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); dailyBalances balance; @@ -777,7 +1185,7 @@ return -1; } - balance = m_accountList[acc.id()]; + balance = d->m_accountList[acc.id()]; for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { if (minBalance > balance[it_day]) { @@ -790,14 +1198,15 @@ int MyMoneyForecast::daysToZeroBalance(const MyMoneyAccount& acc) { + Q_D(MyMoneyForecast); dailyBalances balance; //Check if acc is not a forecast account, return -1 if (!isForecastAccount(acc)) { return -2; } - balance = m_accountList[acc.id()]; + balance = d->m_accountList[acc.id()]; if (acc.accountGroup() == eMyMoney::Account::Asset) { for (QDate it_day = QDate::currentDate() ; it_day <= forecastEndDate();) { @@ -817,36 +1226,25 @@ return -1; } -void MyMoneyForecast::setForecastAccountList() -{ - - //get forecast accounts - QList accList; - accList = forecastAccountList(); - - QList::const_iterator accList_t = accList.constBegin(); - for (; accList_t != accList.constEnd(); ++accList_t) { - m_forecastAccounts.insert((*accList_t).id()); - } -} MyMoneyMoney MyMoneyForecast::accountCycleVariation(const MyMoneyAccount& acc) { + Q_D(MyMoneyForecast); MyMoneyMoney cycleVariation; - if (forecastMethod() == eHistoric) { + if (d->forecastMethod() == eForecastMethod::Historic) { switch (historyMethod()) { - case 0: - case 1: { - for (int t_day = 1; t_day <= accountsCycle() ; ++t_day) { - cycleVariation += m_accountTrendList[acc.id()][t_day]; - } - } - break; - case 2: { - cycleVariation = m_accountList[acc.id()][QDate::currentDate().addDays(accountsCycle())] - m_accountList[acc.id()][QDate::currentDate()]; - break; - } + case 0: + case 1: { + for (int t_day = 1; t_day <= accountsCycle() ; ++t_day) { + cycleVariation += d->m_accountTrendList[acc.id()][t_day]; + } + } + break; + case 2: { + cycleVariation = d->m_accountList[acc.id()][QDate::currentDate().addDays(accountsCycle())] - d->m_accountList[acc.id()][QDate::currentDate()]; + break; + } } } return cycleVariation; @@ -912,57 +1310,9 @@ return totalBalance / MyMoneyMoney(forecastDays(), 1); } -int MyMoneyForecast::calculateBeginForecastDay() -{ - int fDays = forecastDays(); - int beginDay = beginForecastDay(); - int accCycle = accountsCycle(); - QDate beginDate; - - //if 0, beginDate is current date and forecastDays remains unchanged - if (beginDay == 0) { - setBeginForecastDate(QDate::currentDate()); - return fDays; - } - - //adjust if beginDay more than days of current month - if (QDate::currentDate().daysInMonth() < beginDay) - beginDay = QDate::currentDate().daysInMonth(); - - //if beginDay still to come, calculate and return - if (QDate::currentDate().day() <= beginDay) { - beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); - fDays += QDate::currentDate().daysTo(beginDate); - setBeginForecastDate(beginDate); - return fDays; - } - - //adjust beginDay for next month - if (QDate::currentDate().addMonths(1).daysInMonth() < beginDay) - beginDay = QDate::currentDate().addMonths(1).daysInMonth(); - - //if beginDay of next month comes before 1 interval, use beginDay next month - if (QDate::currentDate().addDays(accCycle) >= - (QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1))) { - beginDate = QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1).addDays(beginDay - 1); - fDays += QDate::currentDate().daysTo(beginDate); - } else { //add intervals to current beginDay and take the first after current date - beginDay = ((((QDate::currentDate().day() - beginDay) / accCycle) + 1) * accCycle) + beginDay; - beginDate = QDate::currentDate().addDays(beginDay - QDate::currentDate().day()); - fDays += QDate::currentDate().daysTo(beginDate); - } - - setBeginForecastDate(beginDate); - return fDays; -} - -void MyMoneyForecast::purgeForecastAccountsList(QMap& accountList) -{ - m_forecastAccounts.intersect(accountList.keys().toSet()); -} - void MyMoneyForecast::createBudget(MyMoneyBudget& budget, QDate historyStart, QDate historyEnd, QDate budgetStart, QDate budgetEnd, const bool returnBudget) { + Q_D(MyMoneyForecast); // clear all data except the id and name QString name = budget.name(); budget = MyMoneyBudget(budget.id(), MyMoneyBudget()); @@ -976,7 +1326,7 @@ } //get forecast method - int fMethod = forecastMethod(); + auto fMethod = d->forecastMethod(); //set start date to 1st of month and end dates to last day of month, since we deal with full months in budget historyStart = QDate(historyStart.year(), historyStart.month(), 1); @@ -1003,37 +1353,36 @@ setSkipOpeningDate(false); //clear and set accounts list we are going to use. Categories, in this case - m_forecastAccounts.clear(); - setBudgetAccountList(); + d->m_forecastAccounts.clear(); + d->setBudgetAccountList(); //calculate budget according to forecast method switch (fMethod) { - case eScheduled: - doFutureScheduledForecast(); - calculateScheduledMonthlyBalances(); - break; - case eHistoric: - pastTransactions(); //get all transactions for history period - calculateAccountTrendList(); - calculateHistoricMonthlyBalances(); //add all balances of each month and put at the 1st day of each month - break; - default: - break; + case eForecastMethod::Scheduled: + d->doFutureScheduledForecast(); + d->calculateScheduledMonthlyBalances(); + break; + case eForecastMethod::Historic: + d->pastTransactions(); //get all transactions for history period + d->calculateAccountTrendList(); + d->calculateHistoricMonthlyBalances(); //add all balances of each month and put at the 1st day of each month + break; + default: + break; } //flag the forecast as done - m_forecastDone = true; + d->m_forecastDone = true; //only fill the budget if it is going to be used if (returnBudget) { //setup the budget itself - MyMoneyFile* file = MyMoneyFile::instance(); + auto file = MyMoneyFile::instance(); budget.setBudgetStart(budgetStart); //go through all the accounts and add them to budget - QSet::ConstIterator it_nc; - for (it_nc = m_forecastAccounts.constBegin(); it_nc != m_forecastAccounts.constEnd(); ++it_nc) { - MyMoneyAccount acc = file->account(*it_nc); + for (auto it_nc = d->m_forecastAccounts.constBegin(); it_nc != d->m_forecastAccounts.constEnd(); ++it_nc) { + auto acc = file->account(*it_nc); MyMoneyBudget::AccountGroup budgetAcc; budgetAcc.setId(acc.id()); @@ -1055,151 +1404,208 @@ } } } +int MyMoneyForecast::historyDays() const +{ + Q_D(const MyMoneyForecast); + return (d->m_historyStartDate.daysTo(d->m_historyEndDate) + 1); +} -void MyMoneyForecast::setBudgetAccountList() +void MyMoneyForecast::setAccountsCycle(int accountsCycle) { - //get budget accounts - QList accList; - accList = budgetAccountList(); + Q_D(MyMoneyForecast); + d->m_accountsCycle = accountsCycle; +} - QList::const_iterator accList_t = accList.constBegin(); - for (; accList_t != accList.constEnd(); ++accList_t) { - m_forecastAccounts.insert((*accList_t).id()); - } +void MyMoneyForecast::setForecastCycles(int forecastCycles) +{ + Q_D(MyMoneyForecast); + d->m_forecastCycles = forecastCycles; } -QList MyMoneyForecast::budgetAccountList() +void MyMoneyForecast::setForecastDays(int forecastDays) { - MyMoneyFile* file = MyMoneyFile::instance(); + Q_D(MyMoneyForecast); + d->m_forecastDays = forecastDays; +} - QList accList; - QStringList emptyStringList; - //Get all accounts from the file and check if they are of the right type to calculate forecast - file->accountList(accList, emptyStringList, false); - QList::iterator accList_t = accList.begin(); - for (; accList_t != accList.end();) { - MyMoneyAccount acc = *accList_t; - if (acc.isClosed() //check the account is not closed - || (!acc.isIncomeExpense())) { - //remove the account if it is not of the correct type - accList_t = accList.erase(accList_t); - } else { - ++accList_t; - } - } - return accList; +void MyMoneyForecast::setBeginForecastDate(const QDate &beginForecastDate) +{ + Q_D(MyMoneyForecast); + d->m_beginForecastDate = beginForecastDate; } -void MyMoneyForecast::calculateHistoricMonthlyBalances() +void MyMoneyForecast::setBeginForecastDay(int beginDay) { - MyMoneyFile* file = MyMoneyFile::instance(); - - //Calculate account monthly balances - QSet::ConstIterator it_n; - for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); - - for (QDate f_date = forecastStartDate(); f_date <= forecastEndDate();) { - for (int f_day = 1; f_day <= accountsCycle() && f_date <= forecastEndDate(); ++f_day) { - MyMoneyMoney accountDailyTrend = m_accountTrendList[acc.id()][f_day]; //trend for that day - //check for leap year - if (f_date.month() == 2 && f_date.day() == 29) - f_date = f_date.addDays(1); //skip 1 day - m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyTrend; //movement trend for that particular day - f_date = f_date.addDays(1); - } - } - } + Q_D(MyMoneyForecast); + d->m_beginForecastDay = beginDay; } -void MyMoneyForecast::calculateScheduledMonthlyBalances() +void MyMoneyForecast::setForecastMethod(int forecastMethod) { - MyMoneyFile* file = MyMoneyFile::instance(); + Q_D(MyMoneyForecast); + d->m_forecastMethod = static_cast(forecastMethod); +} -//Calculate account monthly balances - QSet::ConstIterator it_n; - for (it_n = m_forecastAccounts.constBegin(); it_n != m_forecastAccounts.constEnd(); ++it_n) { - MyMoneyAccount acc = file->account(*it_n); +void MyMoneyForecast::setHistoryStartDate(const QDate &historyStartDate) +{ + Q_D(MyMoneyForecast); + d->m_historyStartDate = historyStartDate; +} - for (QDate f_date = forecastStartDate(); f_date <= forecastEndDate(); f_date = f_date.addDays(1)) { - //get the trend for the day - MyMoneyMoney accountDailyBalance = m_accountList[acc.id()][f_date]; +void MyMoneyForecast::setHistoryEndDate(const QDate &historyEndDate) +{ + Q_D(MyMoneyForecast); + d->m_historyEndDate = historyEndDate; +} - //do not add if it is the beginning of the month - //otherwise we end up with duplicated values as reported by Marko Käning - if (f_date != QDate(f_date.year(), f_date.month(), 1)) - m_accountList[acc.id()][QDate(f_date.year(), f_date.month(), 1)] += accountDailyBalance; - } - } +void MyMoneyForecast::setHistoryStartDate(int daysToStartDate) +{ + Q_D(MyMoneyForecast); + setHistoryStartDate(QDate::currentDate().addDays(-daysToStartDate)); } -void MyMoneyForecast::setStartingBalance(const MyMoneyAccount &acc) +void MyMoneyForecast::setHistoryEndDate(int daysToEndDate) { - MyMoneyFile* file = MyMoneyFile::instance(); - - //Get current account balance - if (acc.isInvest()) { //investments require special treatment - //get the security id of that account - MyMoneySecurity undersecurity = file->security(acc.currencyId()); - - //only do it if the security is not an actual currency - if (! undersecurity.isCurrency()) { - //set the default value - MyMoneyMoney rate = MyMoneyMoney::ONE; - //get te - const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), QDate::currentDate()); - if (price.isValid()) { - rate = price.rate(undersecurity.tradingCurrency()); - } - m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()) * rate; - } - } else { - m_accountList[acc.id()][QDate::currentDate()] = file->balance(acc.id(), QDate::currentDate()); - } + Q_D(MyMoneyForecast); + setHistoryEndDate(QDate::currentDate().addDays(-daysToEndDate)); +} - //if the method is linear regression, we have to add the opening balance to m_accountListPast - if (forecastMethod() == eHistoric && historyMethod() == 2) { - //FIXME workaround for stock opening dates - QDate openingDate; - if (acc.accountType() == eMyMoney::Account::Stock) { - MyMoneyAccount parentAccount = file->account(acc.parentAccountId()); - openingDate = parentAccount.openingDate(); - } else { - openingDate = acc.openingDate(); - } +void MyMoneyForecast::setForecastStartDate(const QDate &_startDate) +{ + Q_D(MyMoneyForecast); + d->m_forecastStartDate = _startDate; +} - //add opening balance only if it opened after the history start - if (openingDate >= historyStartDate()) { +void MyMoneyForecast::setForecastEndDate(const QDate &_endDate) +{ + Q_D(MyMoneyForecast); + d->m_forecastEndDate = _endDate; +} + +void MyMoneyForecast::setSkipOpeningDate(bool _skip) +{ + Q_D(MyMoneyForecast); + d->m_skipOpeningDate = _skip; +} - MyMoneyMoney openingBalance; +void MyMoneyForecast::setHistoryMethod(int historyMethod) +{ + Q_D(MyMoneyForecast); + d->m_historyMethod = historyMethod; +} - openingBalance = file->balance(acc.id(), openingDate); +void MyMoneyForecast::setIncludeUnusedAccounts(bool _bool) +{ + Q_D(MyMoneyForecast); + d->m_includeUnusedAccounts = _bool; +} - //calculate running sum - for (QDate it_date = openingDate; it_date <= historyEndDate(); it_date = it_date.addDays(1)) { - //investments require special treatment - if (acc.isInvest()) { - //get the security id of that account - MyMoneySecurity undersecurity = file->security(acc.currencyId()); +void MyMoneyForecast::setForecastDone(bool _bool) +{ + Q_D(MyMoneyForecast); + d->m_forecastDone = _bool; +} - //only do it if the security is not an actual currency - if (! undersecurity.isCurrency()) { - //set the default value - MyMoneyMoney rate = MyMoneyMoney::ONE; +void MyMoneyForecast::setIncludeFutureTransactions(bool _bool) +{ + Q_D(MyMoneyForecast); + d->m_includeFutureTransactions = _bool; +} - //get the rate for that specific date - const MyMoneyPrice &price = file->price(undersecurity.id(), undersecurity.tradingCurrency(), it_date); - if (price.isValid()) { - rate = price.rate(undersecurity.tradingCurrency()); - } - m_accountListPast[acc.id()][it_date] += openingBalance * rate; - } - } else { - m_accountListPast[acc.id()][it_date] += openingBalance; - } - } - } - } +void MyMoneyForecast::setIncludeScheduledTransactions(bool _bool) +{ + Q_D(MyMoneyForecast); + d->m_includeScheduledTransactions = _bool; +} + +int MyMoneyForecast::accountsCycle() const +{ + Q_D(const MyMoneyForecast); + return d->m_accountsCycle; +} + +int MyMoneyForecast::forecastCycles() const +{ + Q_D(const MyMoneyForecast); + return d->m_forecastCycles; +} + +int MyMoneyForecast::forecastDays() const +{ + Q_D(const MyMoneyForecast); + return d->m_forecastDays; +} + +QDate MyMoneyForecast::beginForecastDate() const +{ + Q_D(const MyMoneyForecast); + return d->m_beginForecastDate; +} + +int MyMoneyForecast::beginForecastDay() const +{ + Q_D(const MyMoneyForecast); + return d->m_beginForecastDay; +} + +QDate MyMoneyForecast::historyStartDate() const +{ + Q_D(const MyMoneyForecast); + return d->m_historyStartDate; +} + +QDate MyMoneyForecast::historyEndDate() const +{ + Q_D(const MyMoneyForecast); + return d->m_historyEndDate; +} + +QDate MyMoneyForecast::forecastStartDate() const +{ + Q_D(const MyMoneyForecast); + return d->m_forecastStartDate; +} + +QDate MyMoneyForecast::forecastEndDate() const +{ + Q_D(const MyMoneyForecast); + return d->m_forecastEndDate; +} + +bool MyMoneyForecast::skipOpeningDate() const +{ + Q_D(const MyMoneyForecast); + return d->m_skipOpeningDate; +} + +int MyMoneyForecast::historyMethod() const +{ + Q_D(const MyMoneyForecast); + return d->m_historyMethod; +} + +bool MyMoneyForecast::isIncludingUnusedAccounts() const +{ + Q_D(const MyMoneyForecast); + return d->m_includeUnusedAccounts; +} + +bool MyMoneyForecast::isForecastDone() const +{ + Q_D(const MyMoneyForecast); + return d->m_forecastDone; +} + +bool MyMoneyForecast::isIncludingFutureTransactions() const +{ + Q_D(const MyMoneyForecast); + return d->m_includeFutureTransactions; +} + +bool MyMoneyForecast::isIncludingScheduledTransactions() const +{ + Q_D(const MyMoneyForecast); + return d->m_includeScheduledTransactions; } void MyMoneyForecast::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances) @@ -1275,3 +1681,24 @@ } } +QList MyMoneyForecast::forecastAccountList() +{ + auto file = MyMoneyFile::instance(); + + QList accList; + //Get all accounts from the file and check if they are of the right type to calculate forecast + file->accountList(accList); + QList::iterator accList_t = accList.begin(); + for (; accList_t != accList.end();) { + auto acc = *accList_t; + if (acc.isClosed() //check the account is not closed + || (!acc.isAssetLiability())) { + //|| (acc.accountType() == eMyMoney::Account::Investment) ) {//check that it is not an Investment account and only include Stock accounts + //remove the account if it is not of the correct type + accList_t = accList.erase(accList_t); + } else { + ++accList_t; + } + } + return accList; +} diff --git a/kmymoney/mymoney/tests/mymoneyforecast-test.h b/kmymoney/mymoney/tests/mymoneyforecast-test.h --- a/kmymoney/mymoney/tests/mymoneyforecast-test.h +++ b/kmymoney/mymoney/tests/mymoneyforecast-test.h @@ -22,6 +22,7 @@ #define KMM_MYMONEY_UNIT_TESTABLE friend class MyMoneyForecastTest; #include "mymoneyfile.h" +#include "mymoneymoney.h" #include "mymoneyseqaccessmgr.h" #include "mymoneyforecast.h" diff --git a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp --- a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp @@ -30,6 +30,7 @@ #include "mymoneyinstitution.h" #include "mymoneysecurity.h" +#include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneyschedule.h" #include "mymoneyenums.h"