1 /*
2  * Copyright 2004-2005  Ace Jones <acejones@users.sourceforge.net>
3  * Copyright 2008-2010  Alvaro Soliverez <asoliverez@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License as
7  * published by the Free Software Foundation; either version 2 of
8  * the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "objectinfotable.h"
20 
21 // ----------------------------------------------------------------------------
22 // QT Includes
23 
24 #include <QList>
25 #include <QDate>
26 #include <QDebug>
27 
28 // ----------------------------------------------------------------------------
29 // KDE Includes
30 
31 #include <KLocalizedString>
32 
33 // ----------------------------------------------------------------------------
34 // Project Includes
35 
36 #include "mymoneyfile.h"
37 #include "mymoneyinstitution.h"
38 #include "mymoneyaccount.h"
39 #include "mymoneyaccountloan.h"
40 #include "mymoneysecurity.h"
41 #include "mymoneyprice.h"
42 #include "mymoneypayee.h"
43 #include "mymoneymoney.h"
44 #include "mymoneysplit.h"
45 #include "mymoneytransaction.h"
46 #include "mymoneyreport.h"
47 #include "mymoneyschedule.h"
48 #include "mymoneyexception.h"
49 #include "kmymoneyutils.h"
50 #include "reportaccount.h"
51 #include "mymoneyenums.h"
52 
53 namespace reports
54 {
55 
56 // ****************************************************************************
57 //
58 // ObjectInfoTable implementation
59 //
60 // ****************************************************************************
61 
62 /**
63   * TODO
64   *
65   * - Collapse 2- & 3- groups when they are identical
66   * - Way more test cases (especially splits & transfers)
67   * - Option to collapse splits
68   * - Option to exclude transfers
69   *
70   */
71 
ObjectInfoTable(const MyMoneyReport & _report)72 ObjectInfoTable::ObjectInfoTable(const MyMoneyReport& _report): ListTable(_report)
73 {
74   // separated into its own method to allow debugging (setting breakpoints
75   // directly in ctors somehow does not work for me (ipwizard))
76   // TODO: remove the init() method and move the code back to the ctor
77   init();
78 }
79 
init()80 void ObjectInfoTable::init()
81 {
82   m_columns.clear();
83   m_group.clear();
84   m_subtotal.clear();
85   switch (m_config.rowType()) {
86     case eMyMoney::Report::RowType::Schedule:
87       constructScheduleTable();
88       m_columns << ctNextDueDate << ctName;
89       break;
90 
91     default:
92       qDebug() << "ObjectInfoTable::ObjectInfoTable(): unhandled row type" << static_cast<uint>(m_config.rowType());
93       qDebug() << "turned into eMyMoney::Report::RowType::AccountInfo";
94       m_config.setRowType(eMyMoney::Report::RowType::AccountInfo);
95       // intentional fall through
96     case eMyMoney::Report::RowType::AccountInfo:
97       constructAccountTable();
98       m_columns << ctType << ctName;
99       break;
100 
101     case eMyMoney::Report::RowType::AccountLoanInfo:
102       constructAccountLoanTable();
103       m_columns << ctType << ctName;
104       break;
105   }
106 
107   // Sort the data to match the report definition
108   m_subtotal << ctValue;
109 
110   switch (m_config.rowType()) {
111     case eMyMoney::Report::RowType::Schedule:
112       m_group << ctType;
113       m_subtotal << ctValue;
114       break;
115     case eMyMoney::Report::RowType::AccountInfo:
116     case eMyMoney::Report::RowType::AccountLoanInfo:
117       m_group << ctTopCategory << ctInstitution;
118       m_subtotal << ctCurrentBalance;
119       break;
120     default:
121       throw MYMONEYEXCEPTION_CSTRING("ObjectInfoTable::ObjectInfoTable(): unhandled row type");
122   }
123 
124   QVector<cellTypeE> sort = QVector<cellTypeE>::fromList(m_group) << QVector<cellTypeE>::fromList(m_columns) << ctID << ctRank;
125 
126   switch (m_config.rowType()) {
127     case eMyMoney::Report::RowType::Schedule:
128       if (m_config.detailLevel() == eMyMoney::Report::DetailLevel::All) {
129         m_columns << ctPayee << ctPaymentType << ctOccurrence
130                   << ctNextDueDate << ctCategory << ctValue;
131       } else {
132         m_columns << ctPayee << ctPaymentType << ctOccurrence
133                   << ctNextDueDate << ctValue;
134       }
135       break;
136     case eMyMoney::Report::RowType::AccountInfo:
137       m_columns << ctNumber << ctDescription
138                 << ctOpeningDate << ctCurrencyName << ctBalanceWarning
139                 << ctCreditWarning << ctMaxCreditLimit
140                 << ctTax << ctFavorite;
141       break;
142     case eMyMoney::Report::RowType::AccountLoanInfo:
143       m_columns << ctNumber << ctDescription
144                 << ctOpeningDate << ctCurrencyName << ctPayee
145                 << ctLoanAmount << ctInterestRate << ctNextInterestChange
146                 << ctPeriodicPayment << ctFinalPayment << ctFavorite;
147       break;
148     default:
149       m_columns.clear();
150   }
151 
152   TableRow::setSortCriteria(sort);
153   qSort(m_rows);
154 }
155 
constructScheduleTable()156 void ObjectInfoTable::constructScheduleTable()
157 {
158   MyMoneyFile* file = MyMoneyFile::instance();
159 
160   QList<MyMoneySchedule> schedules;
161 
162   schedules = file->scheduleList(QString(), eMyMoney::Schedule::Type::Any, eMyMoney::Schedule::Occurrence::Any, eMyMoney::Schedule::PaymentType::Any, m_config.fromDate(), m_config.toDate(), false);
163 
164   QList<MyMoneySchedule>::const_iterator it_schedule = schedules.constBegin();
165   while (it_schedule != schedules.constEnd()) {
166     MyMoneySchedule schedule = *it_schedule;
167 
168     ReportAccount account(schedule.account());
169 
170     if (m_config.includes(account))  {
171       //get fraction for account
172       int fraction = account.fraction();
173 
174       //use base currency fraction if not initialized
175       if (fraction == -1)
176         fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction();
177 
178       TableRow scheduleRow;
179 
180       //convert to base currency if needed
181       MyMoneyMoney xr = MyMoneyMoney::ONE;
182       if (m_config.isConvertCurrency() && account.isForeignCurrency()) {
183         xr = account.baseCurrencyPrice(QDate::currentDate()).reduce();
184       }
185 
186       // help for sort and render functions
187       scheduleRow[ctRank] = QLatin1Char('1');
188 
189       //schedule data
190       scheduleRow[ctID] = schedule.id();
191       scheduleRow[ctName] = schedule.name();
192       scheduleRow[ctNextDueDate] = schedule.nextDueDate().toString(Qt::ISODate);
193       scheduleRow[ctType] = KMyMoneyUtils::scheduleTypeToString(schedule.type());
194       scheduleRow[ctOccurrence] = i18nc("Frequency of schedule", schedule.occurrenceToString().toLatin1());
195       scheduleRow[ctPaymentType] = KMyMoneyUtils::paymentMethodToString(schedule.paymentType());
196 
197       //scheduleRow["category"] = account.name();
198 
199       //to get the payee we must look into the splits of the transaction
200       MyMoneyTransaction transaction = schedule.transaction();
201       MyMoneySplit split = transaction.splitByAccount(account.id(), true);
202       scheduleRow[ctValue] = (split.value() * xr).toString();
203       MyMoneyPayee payee = file->payee(split.payeeId());
204       scheduleRow[ctPayee] = payee.name();
205       m_rows += scheduleRow;
206 
207       //the text matches the main split
208       bool transaction_text = m_config.match(split);
209 
210       if (m_config.detailLevel() == eMyMoney::Report::DetailLevel::All) {
211         //get the information for all splits
212         QList<MyMoneySplit> splits = transaction.splits();
213         QList<MyMoneySplit>::const_iterator split_it = splits.constBegin();
214         for (; split_it != splits.constEnd(); ++split_it) {
215           if ((*split_it).id() == split.id()) {
216             continue;
217           }
218           TableRow splitRow;
219           ReportAccount splitAcc((*split_it).accountId());
220 
221           splitRow[ctRank] = QLatin1Char('2');
222           splitRow[ctID] = schedule.id();
223           splitRow[ctName] = schedule.name();
224           splitRow[ctPayee] = payee.name();
225           splitRow[ctType] = KMyMoneyUtils::scheduleTypeToString(schedule.type());
226           splitRow[ctNextDueDate] = schedule.nextDueDate().toString(Qt::ISODate);
227 
228           if ((*split_it).value() == MyMoneyMoney::autoCalc) {
229             splitRow[ctSplit] = MyMoneyMoney::autoCalc.toString();
230           } else if (! splitAcc.isIncomeExpense()) {
231             splitRow[ctSplit] = (*split_it).value().toString();
232           } else {
233             splitRow[ctSplit] = (- (*split_it).value()).toString();
234           }
235 
236           //if it is an assett account, mark it as a transfer
237           if (! splitAcc.isIncomeExpense()) {
238             splitRow[ctCategory] = ((* split_it).value().isNegative())
239                                    ? i18n("Transfer from %1" , splitAcc.fullName())
240                                    : i18n("Transfer to %1" , splitAcc.fullName());
241           } else {
242             splitRow [ctCategory] = splitAcc.fullName();
243           }
244 
245           //add the split only if it matches the text or it matches the main split
246           if (m_config.match((*split_it)) || transaction_text) {
247             // only add separate rows when we have a split transaction
248             // otherwise, we simply copy the category to the
249             // already added row and go on
250             if (splits.count() > 2) {
251               m_rows += splitRow;
252             } else {
253               m_rows.last()[ctCategory] = splitRow [ctCategory];
254             }
255           }
256         }
257       }
258     }
259     ++it_schedule;
260   }
261 }
262 
constructAccountTable()263 void ObjectInfoTable::constructAccountTable()
264 {
265   MyMoneyFile* file = MyMoneyFile::instance();
266 
267   //make sure we have all subaccounts of investment accounts
268   includeInvestmentSubAccounts();
269 
270   QList<MyMoneyAccount> accounts;
271   file->accountList(accounts);
272   QList<MyMoneyAccount>::const_iterator it_account = accounts.constBegin();
273   while (it_account != accounts.constEnd()) {
274     TableRow accountRow;
275     ReportAccount account(*it_account);
276 
277     if (m_config.includes(account)
278         && account.accountType() != eMyMoney::Account::Type::Stock
279         && !account.isClosed()) {
280       MyMoneyMoney value;
281       accountRow[ctRank] = QLatin1Char('1');
282       accountRow[ctTopCategory] = MyMoneyAccount::accountTypeToString(account.accountGroup());
283       if (!account.institutionId().isEmpty()) {
284         accountRow[ctInstitution] = (file->institution(account.institutionId())).name();
285       } else {
286         accountRow[ctInstitution] = QStringLiteral("Accounts with no institution assigned");
287       }
288       accountRow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType());
289       accountRow[ctName] = account.name();
290       accountRow[ctNumber] = account.number();
291       accountRow[ctDescription] = account.description();
292       accountRow[ctOpeningDate] = account.openingDate().toString(Qt::ISODate);
293       //accountRow["currency"] = (file->currency(account.currencyId())).tradingSymbol();
294       accountRow[ctCurrencyName] = (file->currency(account.currencyId())).name();
295       accountRow[ctBalanceWarning] = account.value("minBalanceEarly");
296       accountRow[ctMaxBalanceLimit] = account.value("minBalanceAbsolute");
297       accountRow[ctCreditWarning] = account.value("maxCreditEarly");
298       accountRow[ctMaxCreditLimit] = account.value("maxCreditAbsolute");
299       accountRow[ctTax] = account.value("Tax") == QLatin1String("Yes") ? i18nc("Is this a tax account?", "Yes") : QString();
300       accountRow[ctOpeningBalance] = account.value("OpeningBalanceAccount") == QLatin1String("Yes") ? i18nc("Is this an opening balance account?", "Yes") : QString();
301       accountRow[ctFavorite] = account.value("PreferredAccount") == QLatin1String("Yes") ? i18nc("Is this a favorite account?", "Yes") : QString();
302 
303       //investment accounts show the balances of all its subaccounts
304       if (account.accountType() == eMyMoney::Account::Type::Investment) {
305         value = investmentBalance(account);
306       } else {
307         value = file->balance(account.id());
308       }
309 
310       //convert to base currency if needed
311       if (m_config.isConvertCurrency() && account.isForeignCurrency()) {
312         MyMoneyMoney xr = account.baseCurrencyPrice(QDate::currentDate()).reduce();
313         value = value * xr;
314       }
315       accountRow[ctCurrentBalance] = value.toString();
316 
317       m_rows += accountRow;
318     }
319     ++it_account;
320   }
321 }
322 
constructAccountLoanTable()323 void ObjectInfoTable::constructAccountLoanTable()
324 {
325   MyMoneyFile* file = MyMoneyFile::instance();
326 
327   QList<MyMoneyAccount> accounts;
328   file->accountList(accounts);
329   QList<MyMoneyAccount>::const_iterator it_account = accounts.constBegin();
330   while (it_account != accounts.constEnd()) {
331     TableRow accountRow;
332     ReportAccount account(*it_account);
333     MyMoneyAccountLoan loan = *it_account;
334 
335     if (m_config.includes(account) && account.isLoan() && !account.isClosed()) {
336       //convert to base currency if needed
337       MyMoneyMoney xr = MyMoneyMoney::ONE;
338       if (m_config.isConvertCurrency() && account.isForeignCurrency()) {
339         xr = account.baseCurrencyPrice(QDate::currentDate()).reduce();
340       }
341 
342       accountRow[ctRank] = QLatin1Char('1');
343       accountRow[ctTopCategory] = MyMoneyAccount::accountTypeToString(account.accountGroup());
344       if (!account.institutionId().isEmpty()) {
345         accountRow[ctInstitution] = (file->institution(account.institutionId())).name();
346       } else {
347         accountRow[ctInstitution] = QStringLiteral("Accounts with no institution assigned");
348       }
349       accountRow[ctType] = MyMoneyAccount::accountTypeToString(account.accountType());
350       accountRow[ctName] = account.name();
351       accountRow[ctNumber] = account.number();
352       accountRow[ctDescription] = account.description();
353       accountRow[ctOpeningDate] = account.openingDate().toString(Qt::ISODate);
354       //accountRow["currency"] = (file->currency(account.currencyId())).tradingSymbol();
355       accountRow[ctCurrencyName] = (file->currency(account.currencyId())).name();
356       accountRow[ctPayee] = file->payee(loan.payee()).name();
357       accountRow[ctLoanAmount] = (loan.loanAmount() * xr).toString();
358       accountRow[ctInterestRate] = (loan.interestRate(QDate::currentDate()) / MyMoneyMoney(100, 1) * xr).toString();
359       accountRow[ctNextInterestChange] = loan.nextInterestChange().toString(Qt::ISODate);
360       accountRow[ctPeriodicPayment] = (loan.periodicPayment() * xr).toString();
361       accountRow[ctFinalPayment] = (loan.finalPayment() * xr).toString();
362       accountRow[ctFavorite] = account.value("PreferredAccount") == QLatin1String("Yes") ? i18nc("Is this a favorite account?", "Yes") : QString();
363 
364       MyMoneyMoney value = file->balance(account.id());
365       value = value * xr;
366       accountRow[ctCurrentBalance] = value.toString();
367       m_rows += accountRow;
368     }
369     ++it_account;
370   }
371 }
372 
investmentBalance(const MyMoneyAccount & acc)373 MyMoneyMoney ObjectInfoTable::investmentBalance(const MyMoneyAccount& acc)
374 {
375   MyMoneyFile* file = MyMoneyFile::instance();
376   MyMoneyMoney value = file->balance(acc.id());
377 
378   foreach (const auto sAccount, acc.accountList()) {
379     auto stock = file->account(sAccount);
380     try {
381       MyMoneyMoney val;
382       MyMoneyMoney balance = file->balance(stock.id());
383       MyMoneySecurity security = file->security(stock.currencyId());
384       const MyMoneyPrice &price = file->price(stock.currencyId(), security.tradingCurrency());
385       val = balance * price.rate(security.tradingCurrency());
386       // adjust value of security to the currency of the account
387       MyMoneySecurity accountCurrency = file->currency(acc.currencyId());
388       val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id());
389       val = val.convert(acc.fraction());
390       value += val;
391     } catch (const MyMoneyException &e) {
392       qWarning("%s", qPrintable(QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e.what())));
393     }
394   }
395   return value;
396 }
397 
398 }
399