1 /*
2  * Copyright 2013-2014  Allan Anderson <agander93@gmail.com>
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License as
6  * published by the Free Software Foundation; either version 2 of
7  * the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "csvwriter.h"
19 
20 // ----------------------------------------------------------------------------
21 // QT Headers
22 
23 #include <QFile>
24 #include <QList>
25 #include <QDebug>
26 #include <QStringBuilder>
27 
28 // ----------------------------------------------------------------------------
29 // KDE Headers
30 
31 #include <KMessageBox>
32 #include <KLocalizedString>
33 
34 // ----------------------------------------------------------------------------
35 // Project Headers
36 
37 #include "mymoneymoney.h"
38 #include "mymoneyfile.h"
39 #include "mymoneyaccount.h"
40 #include "mymoneytransaction.h"
41 #include "mymoneytransactionfilter.h"
42 #include "mymoneysplit.h"
43 #include "mymoneypayee.h"
44 #include "mymoneyexception.h"
45 #include "csvexportdlg.h"
46 #include "csvexporter.h"
47 #include "mymoneyenums.h"
48 
CsvWriter()49 CsvWriter::CsvWriter() :
50     m_plugin(0),
51     m_firstSplit(false),
52     m_highestSplitCount(0),
53     m_noError(true)
54 {
55 }
56 
~CsvWriter()57 CsvWriter::~CsvWriter()
58 {
59 }
60 
write(const QString & filename,const QString & accountId,const bool accountData,const bool categoryData,const QDate & startDate,const QDate & endDate,const QString & separator)61 void CsvWriter::write(const QString& filename,
62                       const QString& accountId, const bool accountData,
63                       const bool categoryData,
64                       const QDate& startDate, const QDate& endDate,
65                       const QString& separator)
66 {
67   m_separator = separator;
68   QFile csvFile(filename);
69   if (csvFile.open(QIODevice::WriteOnly)) {
70     QTextStream s(&csvFile);
71     s.setCodec("UTF-8");
72 
73     m_plugin->exporterDialog()->show();
74     try {
75       if (categoryData) {
76         writeCategoryEntries(s);
77       }
78 
79       if (accountData) {
80         writeAccountEntry(s, accountId, startDate, endDate);
81       }
82       emit signalProgress(-1, -1);
83 
84     } catch (const MyMoneyException &e) {
85       KMessageBox::error(nullptr, i18n("Unexpected exception '%1'", QString::fromLatin1(e.what())));
86     }
87 
88     csvFile.close();
89     qDebug() << i18n("Export completed.\n");
90     delete m_plugin->exporterDialog();  //  Can now delete as export finished
91   } else {
92     KMessageBox::error(0, i18n("Unable to open file '%1' for writing", filename));
93   }
94 }
95 
writeAccountEntry(QTextStream & stream,const QString & accountId,const QDate & startDate,const QDate & endDate)96 void CsvWriter::writeAccountEntry(QTextStream& stream, const QString& accountId, const QDate& startDate, const QDate& endDate)
97 {
98   MyMoneyFile* file = MyMoneyFile::instance();
99   MyMoneyAccount account;
100   QString data;
101 
102   account = file->account(accountId);
103   MyMoneyTransactionFilter filter(accountId);
104 
105   QString type = account.accountTypeToString(account.accountType());
106   data = QString(i18n("Account Type:"));
107 
108   if (account.accountType() == eMyMoney::Account::Type::Investment) {
109     data += QString("%1\n\n").arg(type);
110     m_headerLine << QString(i18n("Date")) << QString(i18n("Security")) << QString(i18n("Action/Type")) << QString(i18n("Amount")) << QString(i18n("Quantity")) << QString(i18n("Price")) << QString(i18n("Interest")) << QString(i18n("Fees")) << QString(i18n("Account")) << QString(i18n("Memo")) << QString(i18n("Status"));
111     data += m_headerLine.join(m_separator);
112     extractInvestmentEntries(accountId, startDate, endDate);
113   } else {
114     data += QString("%1\n\n").arg(type);
115     m_headerLine << QString(i18n("Date")) << QString(i18n("Payee")) << QString(i18n("Amount")) << QString(i18n("Account/Cat")) << QString(i18n("Memo")) << QString(i18n("Status")) << QString(i18n("Number"));
116     filter.setDateFilter(startDate, endDate);
117 
118     QList<MyMoneyTransaction> trList = file->transactionList(filter);
119     QList<MyMoneyTransaction>::ConstIterator it;
120     signalProgress(0, trList.count());
121     int count = 0;
122     m_highestSplitCount = 0;
123     for (it = trList.constBegin(); it != trList.constEnd(); ++it) {
124       writeTransactionEntry(*it, accountId, ++count);
125       if (m_noError)
126         signalProgress(count, 0);
127     }
128     data += m_headerLine.join(m_separator);
129   }
130 
131   QString result;
132   QMap<QString, QString>::const_iterator it_map = m_map.constBegin();
133   while (it_map != m_map.constEnd()) {
134     result += it_map.value();
135     ++it_map;
136   }
137 
138   stream << data << result << QLatin1Char('\n');
139 }
140 
writeCategoryEntries(QTextStream & s)141 void CsvWriter::writeCategoryEntries(QTextStream &s)
142 {
143   MyMoneyFile* file = MyMoneyFile::instance();
144   MyMoneyAccount income;
145   MyMoneyAccount expense;
146 
147   income = file->income();
148   expense = file->expense();
149 
150   QStringList list = income.accountList() + expense.accountList();
151   emit signalProgress(0, list.count());
152   QStringList::Iterator it_catList;
153   int count = 0;
154   for (it_catList = list.begin(); it_catList != list.end(); ++it_catList) {
155     writeCategoryEntry(s, *it_catList, "");
156     emit signalProgress(++count, 0);
157   }
158 }
159 
writeCategoryEntry(QTextStream & s,const QString & accountId,const QString & leadIn)160 void CsvWriter::writeCategoryEntry(QTextStream &s, const QString& accountId, const QString& leadIn)
161 {
162   MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId);
163   QString name = format(acc.name());
164 
165   s << leadIn << name;
166   s << (acc.accountGroup() == eMyMoney::Account::Type::Expense ? QLatin1Char('E') : QLatin1Char('I'));
167   s << endl;
168 
169   foreach (const auto sAccount, acc.accountList())
170     writeCategoryEntry(s, sAccount, name);
171 }
172 
173 
writeTransactionEntry(const MyMoneyTransaction & t,const QString & accountId,const int count)174 void CsvWriter::writeTransactionEntry(const MyMoneyTransaction& t, const QString& accountId, const int count)
175 {
176   m_firstSplit = true;
177   m_noError = true;
178   MyMoneyFile* file = MyMoneyFile::instance();
179   MyMoneySplit split = t.splitByAccount(accountId);
180   QList<MyMoneySplit> splits = t.splits();
181   if (splits.count() < 2) {
182     KMessageBox::sorry(0, i18n("Transaction number '%1' is missing an account assignment.\n"
183                                "Date '%2', Payee '%3'.\nTransaction dropped.\n", count, t.postDate().toString(Qt::ISODate), file->payee(split.payeeId()).name()),
184                        i18n("Invalid transaction"));
185     m_noError = false;
186     return;
187   }
188 
189   QString str;
190   str += QLatin1Char('\n');
191 
192   str += QString("%1" + m_separator).arg(t.postDate().toString(Qt::ISODate));
193   MyMoneyPayee payee = file->payee(split.payeeId());
194   str += format(payee.name());
195 
196   str += format(split.value());
197 
198   if (splits.count() > 1) {
199     MyMoneySplit sp = t.splitByAccount(accountId, false);
200     str += format(file->accountToCategory(sp.accountId()));
201   }
202 
203   str += format(split.memo());
204 
205   switch (split.reconcileFlag()) {
206     case eMyMoney::Split::State::Cleared:
207       str += QLatin1String("C") + m_separator;
208       break;
209 
210     case eMyMoney::Split::State::Reconciled:
211     case eMyMoney::Split::State::Frozen:
212       str += QLatin1String("R") + m_separator;
213       break;
214 
215     default:
216       str += m_separator;
217       break;
218   }
219 
220   str += format(split.number(), false);
221 
222   if (splits.count() > 2) {
223     QList<MyMoneySplit>::ConstIterator it;
224     for (it = splits.constBegin(); it != splits.constEnd(); ++it) {
225       if (!((*it) == split)) {
226         writeSplitEntry(str, *it, splits.count() - 1, it+1 == splits.constEnd());
227       }
228     }
229   }
230   QString date = t.postDate().toString(Qt::ISODate);
231   m_map.insertMulti(date, str);
232 }
233 
writeSplitEntry(QString & str,const MyMoneySplit & split,const int splitCount,const int lastEntry)234 void CsvWriter::writeSplitEntry(QString &str, const MyMoneySplit& split, const int splitCount, const int lastEntry)
235 {
236   if (m_firstSplit) {
237     m_firstSplit = false;
238     str += m_separator;
239   }
240   MyMoneyFile* file = MyMoneyFile::instance();
241   str += format(file->accountToCategory(split.accountId()));
242 
243   if (splitCount > m_highestSplitCount) {
244     m_highestSplitCount++;
245     m_headerLine << QString(i18n("splitCategory")) << QString(i18n("splitMemo")) << QString(i18n("splitAmount"));
246     m_headerLine.join(m_separator);
247   }
248   str += format(split.memo());
249 
250   str += format(split.value(), 2, !lastEntry);
251 }
252 
extractInvestmentEntries(const QString & accountId,const QDate & startDate,const QDate & endDate)253 void CsvWriter::extractInvestmentEntries(const QString& accountId, const QDate& startDate, const QDate& endDate)
254 {
255   MyMoneyFile* file = MyMoneyFile::instance();
256 
257   foreach (const auto sAccount, file->account(accountId).accountList()) {
258     MyMoneyTransactionFilter filter(sAccount);
259     filter.setDateFilter(startDate, endDate);
260     QList<MyMoneyTransaction> list = file->transactionList(filter);
261     QList<MyMoneyTransaction>::ConstIterator itList;
262     signalProgress(0, list.count());
263     int count = 0;
264     for (itList = list.constBegin(); itList != list.constEnd(); ++itList) {
265       writeInvestmentEntry(*itList, ++count);
266       signalProgress(count, 0);
267     }
268   }
269 }
270 
writeInvestmentEntry(const MyMoneyTransaction & t,const int count)271 void CsvWriter::writeInvestmentEntry(const MyMoneyTransaction& t, const int count)
272 {
273   QString strQuantity;
274   QString strAmount;
275   QString strPrice;
276   QString strAccName;
277   QString strCheckingAccountName;
278   QString strMemo;
279   QString strAction;
280   QString strStatus;
281   QString strInterest;
282   QString strFees;
283   MyMoneyFile* file = MyMoneyFile::instance();
284   QString chkAccnt;
285   QList<MyMoneySplit> lst = t.splits();
286   QList<MyMoneySplit>::Iterator itSplit;
287   eMyMoney::Account::Type typ;
288   QString chkAccntId;
289   MyMoneyMoney qty;
290   MyMoneyMoney value;
291   QMap<eMyMoney::Account::Type, QString> map;
292 
293   for (int i = 0; i < lst.count(); i++) {
294     MyMoneyAccount acc = file->account(lst[i].accountId());
295     typ = acc.accountType();
296     map.insert(typ, lst[i].accountId());
297 
298     if (typ == eMyMoney::Account::Type::Stock) {
299       switch (lst[i].reconcileFlag()) {
300         case eMyMoney::Split::State::Cleared:
301           strStatus =  QLatin1Char('C');
302           break;
303 
304         case eMyMoney::Split::State::Reconciled:
305         case eMyMoney::Split::State::Frozen:
306           strStatus =  QLatin1Char('R');
307           break;
308 
309         default:
310           strStatus.clear();
311           break;
312       }
313     }
314   }
315   //
316   //  Add date.
317   //
318   QString str = QString("\n%1" + m_separator).arg(t.postDate().toString(Qt::ISODate));
319   for (itSplit = lst.begin(); itSplit != lst.end(); ++itSplit) {
320     MyMoneyAccount acc = file->account((*itSplit).accountId());
321     //
322     //  eMyMoney::Account::Type::Checkings.
323     //
324     if ((acc.accountType() == eMyMoney::Account::Type::Checkings) || (acc.accountType() == eMyMoney::Account::Type::Cash) || (acc.accountType() == eMyMoney::Account::Type::Savings)) {
325       chkAccntId = (*itSplit).accountId();
326       chkAccnt = file->account(chkAccntId).name();
327       strCheckingAccountName = format(file->accountToCategory(chkAccntId));
328       strAmount = format((*itSplit).value());
329     } else if (acc.accountType() == eMyMoney::Account::Type::Income) {
330       //
331       //  eMyMoney::Account::Type::Income.
332       //
333       qty = (*itSplit).shares();
334       value = (*itSplit).value();
335       strInterest = format(value);
336     } else if (acc.accountType() == eMyMoney::Account::Type::Expense) {
337       //
338       //  eMyMoney::Account::Type::Expense.
339       //
340       qty = (*itSplit).shares();
341       value = (*itSplit).value();
342       strFees = format(value);
343     }  else if (acc.accountType() == eMyMoney::Account::Type::Stock) {
344       //
345       //  eMyMoney::Account::Type::Stock.
346       //
347       strMemo = format((*itSplit).memo());
348       //
349       //  Actions.
350       //
351       if ((*itSplit).action() == QLatin1String("Dividend")) {
352         strAction = QLatin1String("DivX");
353       } else if ((*itSplit).action() == QLatin1String("IntIncome")) {
354         strAction = QLatin1String("IntIncX");
355       }
356       if ((strAction == QLatin1String("DivX")) || (strAction == QLatin1String("IntIncX"))) {
357         if ((map.value(eMyMoney::Account::Type::Checkings).isEmpty()) && (map.value(eMyMoney::Account::Type::Cash).isEmpty())) {
358           KMessageBox::sorry(0, i18n("Transaction number '%1' is missing an account assignment.\n"
359                                      "Date '%2', Amount '%3'.\nTransaction dropped.\n", count, t.postDate().toString(Qt::ISODate), strAmount),
360                              i18n("Invalid transaction"));
361           return;
362         }
363       } else if ((*itSplit).action() == QLatin1String("Buy")) {
364         qty = (*itSplit).shares();
365         if (qty.isNegative()) {
366           strAction = QLatin1String("Sell");
367         } else {
368           strAction = QLatin1String("Buy");
369         }
370       } else if ((*itSplit).action() == QLatin1String("Add")) {
371         qty = (*itSplit).shares();
372         if (qty.isNegative()) {
373           strAction = QLatin1String("Shrsout");
374         } else {
375           strAction = QLatin1String("Shrsin");
376         }
377       } else if ((*itSplit).action() == QLatin1String("Reinvest")) {
378         qty = (*itSplit).shares();
379         strAmount = format((*itSplit).value());
380         strAction = QLatin1String("ReinvDiv");
381       } else {
382         strAction = (*itSplit).action();
383       }
384       //
385       //  Add action.
386       //
387       if ((strAction == QLatin1String("Buy")) || (strAction == QLatin1String("Sell")) || (strAction == QLatin1String("ReinvDiv"))) {
388         //
389         //  Add total.
390         // TODO: value is not used below
391         if (strAction == QLatin1String("Sell")) {
392           value = -value;
393         }
394         //
395         //  Add price.
396         //
397         strPrice = format((*itSplit).price(), 6);
398         if (!qty.isZero()) {
399           //
400           //  Add quantity.
401           //
402           if (strAction == QLatin1String("Sell")) {
403             qty = -qty;
404           }
405           strQuantity = format(qty);
406         }
407       } else if ((strAction == QLatin1String("Shrsin")) || (strAction == QLatin1String("Shrsout"))) {
408         //
409         //  Add quantity for "Shrsin" || "Shrsout".
410         //
411         if (strAction == QLatin1String("Shrsout")) {
412           qty = -qty;
413         }
414         strQuantity = format(qty);
415       }
416       strAccName = format(acc.name());
417       strAction += m_separator;
418     }
419 
420    if (strAmount.isEmpty())
421       strAmount = m_separator;
422 
423    if (strQuantity.isEmpty())
424       strQuantity = m_separator;
425 
426     if (strPrice.isEmpty())
427       strPrice = m_separator;
428 
429     if (strCheckingAccountName.isEmpty()) {
430       strCheckingAccountName = m_separator;
431     }
432     if (strInterest.isEmpty()) {
433       strInterest = m_separator;
434     }
435     if (strFees.isEmpty()) {
436       strFees = m_separator;
437     }
438   }  //  end of itSplit loop
439   str += strAccName + strAction + strAmount + strQuantity + strPrice + strInterest + strFees + strCheckingAccountName + strMemo + strStatus;
440   QString date = t.postDate().toString(Qt::ISODate);
441   m_map.insertMulti(date, str);
442 }
443 
444 /**
445  * Format string field according to csv rules
446  * @param s string to format
447  * @param withSeparator append field separator to string (default = true)
448  * @return csv formatted string
449  */
format(const QString & s,bool withSeparator)450 QString CsvWriter::format(const QString &s, bool withSeparator)
451 {
452   if (s.isEmpty())
453     return withSeparator ? m_separator : QString();
454   QString m = s;
455   m.remove('\'');
456 #if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
457   m.replace(QLatin1Char('"'), QStringLiteral("\"\""));
458 #else
459   m.replace(QLatin1Char('"'), QLatin1String("\"\""));
460 #endif
461   return QString("\"%1\"%2").arg(m, withSeparator ? m_separator : QString());
462 }
463 
464 /**
465  * format money value according to csv rules
466  * @param value value to format
467  * @param prec precision used for formatting (default = 2)
468  * @param withSeparator append field separator to string (default = true)
469  * @return formatted value as string
470  */
format(const MyMoneyMoney & value,int prec,bool withSeparator)471 QString CsvWriter::format(const MyMoneyMoney &value, int prec, bool withSeparator)
472 {
473   return QString("\"%1\"%2").arg(value.formatMoney("", prec, false), withSeparator ? m_separator : QString());
474 }
475