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