1 /***************************************************************************
2                           mymoneyqifreader.cpp
3                              -------------------
4     begin                : Mon Jan 27 2003
5     copyright            : (C) 2000-2003 by Michael Edwardes
6     email                : mte@users.sourceforge.net
7                            Javier Campos Morales <javi_c@users.sourceforge.net>
8                            Felix Rodriguez <frodriguez@users.sourceforge.net>
9                            John C <thetacoturtle@users.sourceforge.net>
10                            Thomas Baumgart <ipwizard@users.sourceforge.net>
11                            Kevin Tambascio <ktambascio@users.sourceforge.net>
12                            Ace Jones <acejones@users.sourceforge.net>
13  ***************************************************************************/
14 
15 /***************************************************************************
16  *                                                                         *
17  *   This program is free software; you can redistribute it and/or modify  *
18  *   it under the terms of the GNU General Public License as published by  *
19  *   the Free Software Foundation; either version 2 of the License, or     *
20  *   (at your option) any later version.                                   *
21  *                                                                         *
22  ***************************************************************************/
23 
24 #include "mymoneyqifreader.h"
25 
26 // ----------------------------------------------------------------------------
27 // QT Headers
28 
29 #include <QFile>
30 #include <QStringList>
31 #include <QTimer>
32 #include <QRegExp>
33 #include <QBuffer>
34 #include <QByteArray>
35 #include <QInputDialog>
36 #include <QDir>
37 
38 // ----------------------------------------------------------------------------
39 // KDE Headers
40 
41 #include <kmessagebox.h>
42 #include <kconfig.h>
43 #include <KConfigGroup>
44 #include <KSharedConfig>
45 #include <KLocalizedString>
46 #include "kjobwidgets.h"
47 #include "kio/job.h"
48 
49 // ----------------------------------------------------------------------------
50 // Project Headers
51 
52 #include "mymoneyfile.h"
53 #include "mymoneysecurity.h"
54 #include "mymoneysplit.h"
55 #include "mymoneyexception.h"
56 #include "kmymoneysettings.h"
57 
58 #include "mymoneystatement.h"
59 
60 // define this to debug the code. Using external filters
61 // while debugging did not work too good for me, so I added
62 // this code.
63 // #define DEBUG_IMPORT
64 
65 #ifdef DEBUG_IMPORT
66 #ifdef __GNUC__
67 #warning "DEBUG_IMPORT defined --> external filter not available!!!!!!!"
68 #endif
69 #endif
70 
71 class MyMoneyQifReader::Private
72 {
73 public:
Private()74   Private() :
75       accountType(eMyMoney::Account::Type::Checkings),
76       firstTransaction(true),
77       mapCategories(true),
78       transactionType(MyMoneyQifReader::QifEntryTypeE::EntryUnknown)
79   {}
80 
81   const QString accountTypeToQif(eMyMoney::Account::Type type) const;
82 
83   /**
84    * finalize the current statement and add it to the statement list
85    */
86   void finishStatement();
87 
88   bool isTransfer(QString& name, const QString& leftDelim, const QString& rightDelim);
89 
90   /**
91    * Converts the QIF specific N-record of investment transactions into
92    * a category name
93    */
94   const QString typeToAccountName(const QString& type) const;
95 
96   /**
97    * Converts the QIF reconcile state to the KMyMoney reconcile state
98    */
99   eMyMoney::Split::State reconcileState(const QString& state) const;
100 
101   /**
102     */
103   void fixMultiLineMemo(QString& memo) const;
104 
105 public:
106   /**
107    * the statement that is currently collected/processed
108    */
109   MyMoneyStatement st;
110   /**
111    * the list of all statements to be sent to MyMoneyStatementReader
112    */
113   QList<MyMoneyStatement> statements;
114 
115   /**
116    * a list of already used hashes in this file
117    */
118   QMap<QString, bool> m_hashMap;
119 
120   QString st_AccountName;
121   QString st_AccountId;
122   eMyMoney::Account::Type accountType;
123   bool     firstTransaction;
124   bool     mapCategories;
125   MyMoneyQifReader::QifEntryTypeE  transactionType;
126 };
127 
fixMultiLineMemo(QString & memo) const128 void MyMoneyQifReader::Private::fixMultiLineMemo(QString& memo) const
129 {
130   memo.replace("\\n", "\n");
131 }
132 
finishStatement()133 void MyMoneyQifReader::Private::finishStatement()
134 {
135   // in case we have collected any data in the statement, we keep it
136   if ((st.m_listTransactions.count() + st.m_listPrices.count() + st.m_listSecurities.count()) > 0) {
137     statements += st;
138     qDebug("Statement with %d transactions, %d prices and %d securities added to the statement list",
139            st.m_listTransactions.count(), st.m_listPrices.count(), st.m_listSecurities.count());
140   }
141   eMyMoney::Statement::Type type = st.m_eType; //stash type and...
142   // start with a fresh statement
143   st = MyMoneyStatement();
144   st.m_skipCategoryMatching = !mapCategories;
145   st.m_eType = type;
146 }
147 
accountTypeToQif(eMyMoney::Account::Type type) const148 const QString MyMoneyQifReader::Private::accountTypeToQif(eMyMoney::Account::Type type) const
149 {
150   QString rc = "Bank";
151 
152   switch (type) {
153     default:
154       break;
155     case eMyMoney::Account::Type::Cash:
156       rc = "Cash";
157       break;
158     case eMyMoney::Account::Type::CreditCard:
159       rc = "CCard";
160       break;
161     case eMyMoney::Account::Type::Asset:
162       rc = "Oth A";
163       break;
164     case eMyMoney::Account::Type::Liability:
165       rc = "Oth L";
166       break;
167     case eMyMoney::Account::Type::Investment:
168       rc = "Port";
169       break;
170   }
171   return rc;
172 }
173 
typeToAccountName(const QString & type) const174 const QString MyMoneyQifReader::Private::typeToAccountName(const QString& type) const
175 {
176   if (type == "reinvint")
177     return i18nc("Category name", "Reinvested interest");
178 
179   if (type == "reinvdiv")
180     return i18nc("Category name", "Reinvested dividend");
181 
182   if (type == "reinvlg")
183     return i18nc("Category name", "Reinvested dividend (long term)");
184 
185   if (type == "reinvsh")
186     return i18nc("Category name", "Reinvested dividend (short term)");
187 
188   if (type == "div")
189     return i18nc("Category name", "Dividend");
190 
191   if (type == "intinc")
192     return i18nc("Category name", "Interest");
193 
194   if (type == "cgshort")
195     return i18nc("Category name", "Capital Gain (short term)");
196 
197   if (type == "cgmid")
198     return i18nc("Category name", "Capital Gain (mid term)");
199 
200   if (type == "cglong")
201     return i18nc("Category name", "Capital Gain (long term)");
202 
203   if (type == "rtrncap")
204     return i18nc("Category name", "Returned capital");
205 
206   if (type == "miscinc")
207     return i18nc("Category name", "Miscellaneous income");
208 
209   if (type == "miscexp")
210     return i18nc("Category name", "Miscellaneous expense");
211 
212   if (type == "sell" || type == "buy")
213     return i18nc("Category name", "Investment fees");
214 
215   return i18n("Unknown QIF type %1", type);
216 }
217 
isTransfer(QString & tmp,const QString & leftDelim,const QString & rightDelim)218 bool MyMoneyQifReader::Private::isTransfer(QString& tmp, const QString& leftDelim, const QString& rightDelim)
219 {
220   // it's a transfer, extract the account name
221   // I've seen entries like this
222   //
223   // S[Mehrwertsteuer]/_VATCode_N_I          (The '/' is the Quicken class symbol)
224   //
225   // so extracting is a bit more complex and we use a regexp for it
226   QRegExp exp(QString("\\%1(.*)\\%2(.*)").arg(leftDelim, rightDelim));
227 
228   bool rc;
229   if ((rc = (exp.indexIn(tmp) != -1)) == true) {
230     tmp = exp.cap(1) + exp.cap(2);
231     tmp = tmp.trimmed();
232   }
233   return rc;
234 }
235 
reconcileState(const QString & state) const236 eMyMoney::Split::State MyMoneyQifReader::Private::reconcileState(const QString& state) const
237 {
238   if (state == "X" || state == "R")       // Reconciled
239     return eMyMoney::Split::State::Reconciled;
240 
241   if (state == "*")                     // Cleared
242     return eMyMoney::Split::State::Cleared;
243 
244   return eMyMoney::Split::State::NotReconciled;
245 }
246 
247 
MyMoneyQifReader()248 MyMoneyQifReader::MyMoneyQifReader() :
249     d(new Private),
250     m_file(nullptr),
251     m_extractedLine(0),
252     m_autoCreatePayee(true),
253     m_pos(0),
254     m_linenumber(0),
255     m_ft(nullptr)
256 {
257   m_skipAccount = false;
258   m_transactionsProcessed =
259     m_transactionsSkipped = 0;
260   m_progressCallback = 0;
261   m_file = 0;
262   m_entryType = EntryUnknown;
263   m_processingData = false;
264   m_userAbort = false;
265   m_warnedInvestment = false;
266   m_warnedSecurity = false;
267   m_warnedPrice = false;
268 
269   connect(&m_filter, SIGNAL(bytesWritten(qint64)), this, SLOT(slotSendDataToFilter()));
270   connect(&m_filter, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter()));
271   connect(&m_filter, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotImportFinished()));
272   connect(&m_filter, SIGNAL(readyReadStandardError()), this, SLOT(slotReceivedErrorFromFilter()));
273 }
274 
~MyMoneyQifReader()275 MyMoneyQifReader::~MyMoneyQifReader()
276 {
277   delete m_file;
278   delete d;
279 }
280 
setCategoryMapping(bool map)281 void MyMoneyQifReader::setCategoryMapping(bool map)
282 {
283   d->mapCategories = map;
284 }
285 
setURL(const QUrl & url)286 void MyMoneyQifReader::setURL(const QUrl &url)
287 {
288   m_url = url;
289 }
290 
setProfile(const QString & profile)291 void MyMoneyQifReader::setProfile(const QString& profile)
292 {
293   m_qifProfile.loadProfile("Profile-" + profile);
294 }
295 
slotSendDataToFilter()296 void MyMoneyQifReader::slotSendDataToFilter()
297 {
298   long len;
299 
300   if (m_file->atEnd()) {
301     m_filter.closeWriteChannel();
302   } else {
303     len = m_file->read(m_buffer, sizeof(m_buffer));
304     if (len == -1) {
305       qWarning("Failed to read block from QIF import file");
306       m_filter.closeWriteChannel();
307       m_filter.kill();
308     } else {
309       m_filter.write(m_buffer, len);
310     }
311   }
312 }
313 
slotReceivedErrorFromFilter()314 void MyMoneyQifReader::slotReceivedErrorFromFilter()
315 {
316   qWarning("%s", qPrintable(QString(m_filter.readAllStandardError())));
317 }
318 
slotReceivedDataFromFilter()319 void MyMoneyQifReader::slotReceivedDataFromFilter()
320 {
321   parseReceivedData(m_filter.readAllStandardOutput());
322 }
323 
parseReceivedData(const QByteArray & data)324 void MyMoneyQifReader::parseReceivedData(const QByteArray& data)
325 {
326   const char* buff = data.data();
327   int len = data.length();
328 
329   m_pos += len;
330   // signalProgress(m_pos, 0);
331 
332   while (len) {
333     // process char
334     if (*buff == '\n' || *buff == '\r') {
335       // found EOL
336       if (!m_lineBuffer.isEmpty()) {
337         m_qifLines << QString::fromUtf8(m_lineBuffer.trimmed());
338       }
339       m_lineBuffer = QByteArray();
340     } else {
341       // collect all others
342       m_lineBuffer += (*buff);
343     }
344     ++buff;
345     --len;
346   }
347 }
348 
slotImportFinished()349 void MyMoneyQifReader::slotImportFinished()
350 {
351   // check if the last EOL char was missing and add the trailing line
352   if (!m_lineBuffer.isEmpty()) {
353     m_qifLines << QString::fromUtf8(m_lineBuffer.trimmed());
354   }
355   qDebug("Read %ld bytes", m_pos);
356   QTimer::singleShot(0, this, SLOT(slotProcessData()));
357 }
358 
slotProcessData()359 void MyMoneyQifReader::slotProcessData()
360 {
361   signalProgress(-1, -1);
362 
363   // scan the file and try to determine numeric and date formats
364   m_qifProfile.autoDetect(m_qifLines);
365 
366   // the detection is accurate for numeric values, but it could be
367   // that the dates were too ambiguous so that we have to let the user
368   // decide which one to pick.
369   QStringList dateFormats;
370   m_qifProfile.possibleDateFormats(dateFormats);
371   QString format;
372   if (dateFormats.count() > 1) {
373     bool ok;
374     format = QInputDialog::getItem(0, i18n("Date format selection"), i18n("Pick the date format that suits your input file"), dateFormats, 05, false, &ok);
375     if (!ok) {
376       m_userAbort = true;
377     }
378   } else
379     format = dateFormats.first();
380 
381   if (!format.isEmpty()) {
382     m_qifProfile.setInputDateFormat(format);
383     qDebug("Selected date format: '%s'", qPrintable(format));
384   } else {
385     // cancel the process because there is probably nothing to work with
386     m_userAbort = true;
387   }
388 
389   signalProgress(0, m_qifLines.count(), i18n("Importing QIF..."));
390   QStringList::iterator it;
391   for (it = m_qifLines.begin(); m_userAbort == false && it != m_qifLines.end(); ++it) {
392     ++m_linenumber;
393     // qDebug("Proc: '%s'", (*it).data());
394     if ((*it).startsWith('!')) {
395       processQifSpecial(*it);
396       m_qifEntry.clear();
397     } else if (*it == "^") {
398       if (m_qifEntry.count() > 0) {
399         signalProgress(m_linenumber, 0);
400         processQifEntry();
401         m_qifEntry.clear();
402       }
403     } else {
404       m_qifEntry += *it;
405     }
406   }
407   d->finishStatement();
408 
409   qDebug("%d lines processed", m_linenumber);
410   signalProgress(-1, -1);
411 
412   emit statementsReady(d->statements);
413 }
414 
startImport()415 bool MyMoneyQifReader::startImport()
416 {
417   bool rc = false;
418   d->st = MyMoneyStatement();
419   d->st.m_skipCategoryMatching = !d->mapCategories;
420   m_dontAskAgain.clear();
421   m_accountTranslation.clear();
422   m_userAbort = false;
423   m_pos = 0;
424   m_linenumber = 0;
425   m_filename.clear();
426   m_data.clear();
427 
428   if (m_url.isEmpty()) {
429     return rc;
430   } else if (m_url.isLocalFile()) {
431     m_filename = m_url.toLocalFile();
432   } else {
433     m_filename = QDir::tempPath();
434     if(!m_filename.endsWith(QDir::separator()))
435       m_filename += QDir::separator();
436     m_filename += m_url.fileName();
437     qDebug() << "Source:" << m_url.toDisplayString() << "Destination:" << m_filename;
438     KIO::FileCopyJob *job = KIO::file_copy(m_url, QUrl::fromUserInput(m_filename), -1, KIO::Overwrite);
439 //    KJobWidgets::setWindow(job, kmymoney);
440     if (job->exec() && job->error()) {
441       KMessageBox::detailedError(0, i18n("Error while loading file '%1'.", m_url.toDisplayString()),
442                                  job->errorString(),
443                                  i18n("File access error"));
444       return rc;
445     }
446   }
447 
448   m_file = new QFile(m_filename);
449   if (m_file->open(QIODevice::ReadOnly)) {
450 
451 #ifdef DEBUG_IMPORT
452     qint64 len;
453 
454     while (!m_file->atEnd()) {
455       len = m_file->read(m_buffer, sizeof(m_buffer));
456       if (len == -1) {
457         qWarning("Failed to read block from QIF import file");
458       } else {
459         parseReceivedData(QByteArray(m_buffer, len));
460       }
461     }
462     QTimer::singleShot(0, this, SLOT(slotImportFinished()));
463     rc = true;
464 #else
465     QString program;
466     QStringList arguments;
467     program.clear();
468     arguments.clear();
469     // start filter process, use 'cat -' as the default filter
470     if (m_qifProfile.filterScriptImport().isEmpty()) {
471 #ifdef Q_OS_WIN32                   //krazy:exclude=cpp
472     // this is the Windows equivalent of 'cat -' but since 'type' does not work with stdin
473     // we pass the filename converted to native separators as a parameter
474     program = "cmd.exe";
475     arguments << "/c";
476     arguments << "type";
477     arguments << QDir::toNativeSeparators(m_filename);
478 #else
479     program = "cat";
480     arguments << "-";
481 #endif
482     } else {
483       arguments << m_qifProfile.filterScriptImport().split(' ', QString::KeepEmptyParts);
484       program = arguments.takeFirst();
485     }
486     m_entryType = EntryUnknown;
487 
488     m_filter.setProcessChannelMode(QProcess::MergedChannels);
489     m_filter.start(program, arguments);
490     if (m_filter.waitForStarted()) {
491       signalProgress(0, m_file->size(), i18n("Reading QIF..."));
492       slotSendDataToFilter();
493       rc = true;
494 //      emit statementsReady(d->statements);
495     } else {
496       KMessageBox::detailedError(0, i18n("Error while running the filter '%1'.", m_filter.program()),
497                                  m_filter.errorString(),
498                                  i18n("Filter error"));
499     }
500 #endif
501   }
502   return rc;
503 }
504 
processQifSpecial(const QString & _line)505 void MyMoneyQifReader::processQifSpecial(const QString& _line)
506 {
507   QString line = _line.mid(1);   // get rid of exclamation mark
508   if (line.left(5).toLower() == QString("type:")) {
509     line = line.mid(5);
510 
511     // exportable accounts
512     if (line.toLower() == "ccard" || KMyMoneySettings::qifCreditCard().toLower().contains(line.toLower())) {
513       d->accountType = eMyMoney::Account::Type::CreditCard;
514       d->firstTransaction = true;
515       d->transactionType = m_entryType = EntryTransaction;
516 
517     } else if (line.toLower() == "bank" || KMyMoneySettings::qifBank().toLower().contains(line.toLower())) {
518       d->accountType = eMyMoney::Account::Type::Checkings;
519       d->firstTransaction = true;
520       d->transactionType = m_entryType = EntryTransaction;
521 
522     } else if (line.toLower() == "cash" || KMyMoneySettings::qifCash().toLower().contains(line.toLower())) {
523       d->accountType = eMyMoney::Account::Type::Cash;
524       d->firstTransaction = true;
525       d->transactionType = m_entryType = EntryTransaction;
526 
527     } else if (line.toLower() == "oth a" || KMyMoneySettings::qifAsset().toLower().contains(line.toLower())) {
528       d->accountType = eMyMoney::Account::Type::Asset;
529       d->firstTransaction = true;
530       d->transactionType = m_entryType = EntryTransaction;
531 
532     } else if (line.toLower() == "oth l" || line.toLower() == i18nc("QIF tag for liability account", "Oth L").toLower()) {
533       d->accountType = eMyMoney::Account::Type::Liability;
534       d->firstTransaction = true;
535       d->transactionType = m_entryType = EntryTransaction;
536 
537     } else if (line.toLower() == "invst" || line.toLower() == i18nc("QIF tag for investment account", "Invst").toLower()) {
538       d->accountType = eMyMoney::Account::Type::Investment;
539       d->transactionType = m_entryType = EntryInvestmentTransaction;
540 
541     } else if (line.toLower() == "invoice" || KMyMoneySettings::qifInvoice().toLower().contains(line.toLower())) {
542       m_entryType = EntrySkip;
543 
544     } else if (line.toLower() == "tax") {
545       m_entryType = EntrySkip;
546 
547     } else if (line.toLower() == "bill") {
548       m_entryType = EntrySkip;
549 
550       // exportable lists
551     } else if (line.toLower() == "cat" || line.toLower() == i18nc("QIF tag for category", "Cat").toLower()) {
552       m_entryType = EntryCategory;
553 
554     } else if (line.toLower() == "security" || line.toLower() == i18nc("QIF tag for security", "Security").toLower()) {
555       m_entryType = EntrySecurity;
556 
557     } else if (line.toLower() == "prices" || line.toLower() == i18nc("QIF tag for prices", "Prices").toLower()) {
558       m_entryType = EntryPrice;
559 
560     } else if (line.toLower() == "payee") {
561       m_entryType = EntryPayee;
562 
563     } else if (line.toLower() == "memorized") {
564       m_entryType = EntryMemorizedTransaction;
565 
566     } else if (line.toLower() == "class" || line.toLower() == i18nc("QIF tag for a class", "Class").toLower()) {
567       m_entryType = EntryClass;
568 
569     } else if (line.toLower() == "budget") {
570       m_entryType = EntrySkip;
571 
572     } else if (line.toLower() == "invitem") {
573       m_entryType = EntrySkip;
574 
575     } else if (line.toLower() == "template") {
576       m_entryType = EntrySkip;
577 
578     } else {
579       qWarning("Unknown type code '%s' in QIF file on line %d", qPrintable(line), m_linenumber);
580       m_entryType = EntrySkip;
581     }
582 
583     // option headers
584   } else if (line.toLower() == "account") {
585     m_entryType = EntryAccount;
586 
587   } else if (line.toLower() == "option:autoswitch") {
588     m_entryType = EntryAccount;
589 
590   } else if (line.toLower() == "clear:autoswitch") {
591     m_entryType = d->transactionType;
592   }
593 }
594 
processQifEntry()595 void MyMoneyQifReader::processQifEntry()
596 {
597   // This method processes a 'QIF Entry' which is everything between two caret
598   // signs
599   //
600   try {
601     switch (m_entryType) {
602       case EntryCategory:
603         processCategoryEntry();
604         break;
605 
606       case EntryUnknown:
607         qDebug() << "Line " << m_linenumber << ": Warning: Found an entry without a type being specified. Checking assumed.";
608         processTransactionEntry();
609         break;
610 
611       case EntryTransaction:
612         processTransactionEntry();
613         break;
614 
615       case EntryInvestmentTransaction:
616         processInvestmentTransactionEntry();
617         break;
618 
619       case EntryAccount:
620         processAccountEntry();
621         break;
622 
623       case EntrySecurity:
624         processSecurityEntry();
625         break;
626 
627       case EntryPrice:
628         processPriceEntry();
629         break;
630 
631       case EntryPayee:
632         processPayeeEntry();
633         break;
634 
635       case EntryClass:
636         qDebug() << "Line " << m_linenumber << ": Classes are not yet supported!";
637         break;
638 
639       case EntryMemorizedTransaction:
640         qDebug() << "Line " << m_linenumber << ": Memorized transactions are not yet implemented!";
641         break;
642 
643       case EntrySkip:
644         break;
645 
646       default:
647         qDebug() << "Line " << m_linenumber << ": EntryType " << m_entryType << " not yet implemented!";
648         break;
649     }
650   } catch (const MyMoneyException &e) {
651     if (QString::fromLatin1(e.what()).contains("USERABORT")) {
652       qDebug() << "Line " << m_linenumber << ": Unhandled error: " << e.what();
653     } else {
654       m_userAbort = true;
655     }
656   }
657 }
658 
extractLine(const QChar & id,int cnt)659 const QString MyMoneyQifReader::extractLine(const QChar& id, int cnt)
660 {
661   QStringList::ConstIterator it;
662 
663   m_extractedLine = -1;
664   for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) {
665     ++m_extractedLine;
666     if ((*it)[0] == id) {
667       if (cnt-- == 1) {
668         return (*it).mid(1);
669       }
670     }
671   }
672   m_extractedLine = -1;
673   return QString();
674 }
675 
extractSplits(QList<qSplit> & listqSplits) const676 bool MyMoneyQifReader::extractSplits(QList<qSplit>& listqSplits) const
677 {
678 //     *** With apologies to QString MyMoneyQifReader::extractLine ***
679 
680   QStringList::ConstIterator it;
681   bool ret = false;
682   bool memoPresent = false;
683   int neededCount = 0;
684   qSplit q;
685 
686   for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) {
687     if (((*it)[0] == 'S') || ((*it)[0] == '$') || ((*it)[0] == 'E')) {
688       memoPresent = false;  //                      in case no memo line in this split
689       if ((*it)[0] == 'E') {
690         q.m_strMemo = (*it).mid(1);  //             'E' = Memo
691         d->fixMultiLineMemo(q.m_strMemo);
692         memoPresent = true;  //                     This transaction contains memo
693       } else if ((*it)[0] == 'S') {
694         q.m_strCategoryName = (*it).mid(1);  //   'S' = CategoryName
695         neededCount ++;
696       } else if ((*it)[0] == '$') {
697         q.m_amount = (*it).mid(1);  //            '$' = Amount
698         neededCount ++;
699       }
700       if (neededCount > 1) {  //                         CategoryName & Amount essential
701         listqSplits += q;  //                       Add valid split
702         if (!memoPresent) {  //                     If no memo, clear previous
703           q.m_strMemo.clear();
704         }
705         q = qSplit();  //                               Start new split
706         neededCount = 0;
707         ret = true;
708       }
709     }
710   }
711   return ret;
712 }
713 
714 #if 0
715 void MyMoneyQifReader::processMSAccountEntry(const eMyMoney::Account::Type accountType)
716 {
717   if (extractLine('P').toLower() == m_qifProfile.openingBalanceText().toLower()) {
718     m_account = MyMoneyAccount();
719     m_account.setAccountType(accountType);
720     QString txt = extractLine('T');
721     MyMoneyMoney balance = m_qifProfile.value('T', txt);
722 
723     QDate date = m_qifProfile.date(extractLine('D'));
724     m_account.setOpeningDate(date);
725 
726     QString name = extractLine('L');
727     if (name.left(1) == m_qifProfile.accountDelimiter().left(1)) {
728       name = name.mid(1, name.length() - 2);
729     }
730     d->st_AccountName = name;
731     m_account.setName(name);
732     selectOrCreateAccount(Select, m_account, balance);
733     d->st.m_accountId = m_account.id();
734     if (! balance.isZero()) {
735       MyMoneyFile* file = MyMoneyFile::instance();
736       QString openingtxid = file->openingBalanceTransaction(m_account);
737       MyMoneyFileTransaction ft;
738       if (! openingtxid.isEmpty()) {
739         MyMoneyTransaction openingtx = file->transaction(openingtxid);
740         MyMoneySplit split = openingtx.splitByAccount(m_account.id());
741 
742         if (split.shares() != balance) {
743           const MyMoneySecurity& sec = file->security(m_account.currencyId());
744           if (KMessageBox::questionYesNo(
745                 KMyMoneyUtils::mainWindow(),
746                 i18n("The %1 account currently has an opening balance of %2. This QIF file reports an opening balance of %3. Would you like to overwrite the current balance with the one from the QIF file?", m_account.name(), split.shares().formatMoney(m_account, sec), balance.formatMoney(m_account, sec)),
747                 i18n("Overwrite opening balance"),
748                 KStandardGuiItem::yes(),
749                 KStandardGuiItem::no(),
750                 "OverwriteOpeningBalance")
751               == KMessageBox::Yes) {
752             file->removeTransaction(openingtx);
753             m_account.setOpeningDate(date);
754             file->createOpeningBalanceTransaction(m_account, balance);
755           }
756         }
757 
758       } else {
759         // Add an opening balance
760         m_account.setOpeningDate(date);
761         file->createOpeningBalanceTransaction(m_account, balance);
762       }
763       ft.commit();
764     }
765 
766   } else {
767     // for some unknown reason, Quicken 2001 generates the following (somewhat
768     // misleading) sequence of lines:
769     //
770     //  1: !Account
771     //  2: NAT&T Universal
772     //  3: DAT&T Univers(...xxxx) [CLOSED]
773     //  4: TCCard
774     //  5: ^
775     //  6: !Type:CCard
776     //  7: !Account
777     //  8: NCFCU Visa
778     //  9: DRick's CFCU Visa card (...xxxx)
779     // 10: TCCard
780     // 11: ^
781     // 12: !Type:CCard
782     // 13: D1/ 4' 1
783     //
784     // Lines 1-5 are processed via processQifEntry() and processAccountEntry()
785     // Then Quicken issues line 6 but since the account does not carry any
786     // transaction does not write an end delimiter. Arrrgh! So we end up with
787     // a QIF entry comprising of lines 6-11 and end up in this routine. Actually,
788     // lines 7-11 are the leading for the next account. So we check here if
789     // the !Type:xxx record also contains an !Account line and process the
790     // entry as required.
791     //
792     // (Ace) I think a better solution here is to handle exclamation point
793     // lines separately from entries.  In the above case:
794     // Line 1 would set the mode to "account entries".
795     // Lines 2-5 would be interpreted as an account entry.  This would set m_account.
796     // Line 6 would set the mode to "cc transaction entries".
797     // Line 7 would immediately set the mode to "account entries" again
798     // Lines 8-11 would be interpreted as an account entry.  This would set m_account.
799     // Line 12 would set the mode to "cc transaction entries"
800     // Lines 13+ would be interpreted as cc transaction entries, and life is good
801     int exclamationCnt = 1;
802     QString category;
803     do {
804       category = extractLine('!', exclamationCnt++);
805     } while (!category.isEmpty() && category != "Account");
806 
807     // we have such a weird empty account
808     if (category == "Account") {
809       processAccountEntry();
810     } else {
811       selectOrCreateAccount(Select, m_account);
812 
813       d->st_AccountName = m_account.name();
814       d->st.m_strAccountName = m_account.name();
815       d->st.m_accountId = m_account.id();
816       d->st.m_strAccountNumber = m_account.id();
817       m_account.setNumber(m_account.id());
818       if (m_entryType == EntryInvestmentTransaction)
819         processInvestmentTransactionEntry();
820       else
821         processTransactionEntry();
822     }
823   }
824 }
825 #endif
826 
processPayeeEntry()827 void MyMoneyQifReader::processPayeeEntry()
828 {
829   // TODO
830 }
831 
processCategoryEntry()832 void MyMoneyQifReader::processCategoryEntry()
833 {
834   MyMoneyFile* file = MyMoneyFile::instance();
835   MyMoneyAccount account = MyMoneyAccount();
836   account.setName(extractLine('N'));
837   account.setDescription(extractLine('D'));
838 
839   MyMoneyAccount parentAccount;
840   //The extractline routine will more than likely return 'empty',
841   // so also have to test that either the 'I' or 'E' was detected
842   //and set up accounts accordingly.
843   if ((!extractLine('I').isEmpty()) || (m_extractedLine != -1)) {
844     account.setAccountType(eMyMoney::Account::Type::Income);
845     parentAccount = file->income();
846   } else if ((!extractLine('E').isEmpty()) || (m_extractedLine != -1)) {
847     account.setAccountType(eMyMoney::Account::Type::Expense);
848     parentAccount = file->expense();
849   }
850 
851   // check if we can find the account already in the file
852   auto acc = findAccount(account, MyMoneyAccount());
853 
854   // if not, we just create it
855   if (acc.id().isEmpty()) {
856     MyMoneyAccount brokerage;
857     file->createAccount(account, parentAccount, brokerage, MyMoneyMoney());
858   }
859 }
860 
findAccount(const MyMoneyAccount & acc,const MyMoneyAccount & parent) const861 MyMoneyAccount MyMoneyQifReader::findAccount(const MyMoneyAccount& acc, const MyMoneyAccount& parent) const
862 {
863   static MyMoneyAccount nullAccount;
864 
865   MyMoneyFile* file = MyMoneyFile::instance();
866   QList<MyMoneyAccount> parents;
867   try {
868     // search by id
869     if (!acc.id().isEmpty()) {
870       return file->account(acc.id());
871     }
872     // collect the parents. in case parent does not have an id, we scan the all top-level accounts
873     if (parent.id().isEmpty()) {
874       parents << file->asset();
875       parents << file->liability();
876       parents << file->income();
877       parents << file->expense();
878       parents << file->equity();
879     } else {
880       parents << parent;
881     }
882     QList<MyMoneyAccount>::const_iterator it_p;
883     for (it_p = parents.constBegin(); it_p != parents.constEnd(); ++it_p) {
884       MyMoneyAccount parentAccount = *it_p;
885       // search by name (allow hierarchy)
886       int pos;
887       // check for ':' in the name and use it as separator for a hierarchy
888       QString name = acc.name();
889       bool notFound = false;
890       while ((pos = name.indexOf(MyMoneyFile::AccountSeparator)) != -1) {
891         QString part = name.left(pos);
892         QString remainder = name.mid(pos + 1);
893         const auto existingAccount = file->subAccountByName(parentAccount, part);
894         // if account has not been found, continue with next top level parent
895         if (existingAccount.id().isEmpty()) {
896           notFound = true;
897           break;
898         }
899         parentAccount = existingAccount;
900         name = remainder;
901       }
902       if (notFound)
903         continue;
904       const auto existingAccount = file->subAccountByName(parentAccount, name);
905       if (!existingAccount.id().isEmpty()) {
906         if (acc.accountType() != eMyMoney::Account::Type::Unknown) {
907           if (acc.accountType() != existingAccount.accountType())
908             continue;
909         }
910         return existingAccount;
911       }
912     }
913   } catch (const MyMoneyException &e) {
914     KMessageBox::error(0, i18n("Unable to find account: %1", QString::fromLatin1(e.what())));
915   }
916   return nullAccount;
917 }
918 
transferAccount(const QString & name,bool useBrokerage)919 const QString MyMoneyQifReader::transferAccount(const QString& name, bool useBrokerage)
920 {
921   QString accountId;
922   QStringList tmpEntry = m_qifEntry;   // keep temp copies
923   MyMoneyAccount tmpAccount = m_account;
924 
925   m_qifEntry.clear();               // and construct a temp entry to create/search the account
926   m_qifEntry << QString("N%1").arg(name);
927   m_qifEntry << QString("Tunknown");
928   m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer"));
929   accountId = processAccountEntry(false);
930 
931   // in case we found a reference to an investment account, we need
932   // to switch to the brokerage account instead.
933   MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId);
934   if (useBrokerage && (acc.accountType() == eMyMoney::Account::Type::Investment)) {
935     m_qifEntry.clear();               // and construct a temp entry to create/search the account
936     m_qifEntry << QString("N%1").arg(acc.brokerageName());
937     m_qifEntry << QString("Tunknown");
938     m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer"));
939     accountId = processAccountEntry(false);
940   }
941   m_qifEntry = tmpEntry;               // restore local copies
942   m_account = tmpAccount;
943 
944   return accountId;
945 }
946 
createOpeningBalance(eMyMoney::Account::Type accType)947 void MyMoneyQifReader::createOpeningBalance(eMyMoney::Account::Type accType)
948 {
949   MyMoneyFile* file = MyMoneyFile::instance();
950 
951   // if we don't have a name for the current account we need to extract the name from the L-record
952   if (m_account.name().isEmpty()) {
953     QString name = extractLine('L');
954     if (name.isEmpty()) {
955       name = i18n("QIF imported, no account name supplied");
956     }
957     auto b = d->isTransfer(name, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1));
958     Q_UNUSED(b)
959     QStringList entry = m_qifEntry;   // keep a temp copy
960     m_qifEntry.clear();               // and construct a temp entry to create/search the account
961     m_qifEntry << QString("N%1").arg(name);
962     m_qifEntry << QString("T%1").arg(d->accountTypeToQif(accType));
963     m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer"));
964     processAccountEntry();
965     m_qifEntry = entry;               // restore local copy
966   }
967 
968   MyMoneyFileTransaction ft;
969   try {
970     bool needCreate = true;
971 
972     MyMoneyAccount acc = m_account;
973     // in case we're dealing with an investment account, we better use
974     // the accompanying brokerage account for the opening balance
975     acc = file->accountByName(m_account.brokerageName());
976 
977     // check if we already have an opening balance transaction
978     QString tid = file->openingBalanceTransaction(acc);
979     MyMoneyTransaction ot;
980     if (!tid.isEmpty()) {
981       ot = file->transaction(tid);
982       MyMoneySplit s0 = ot.splitByAccount(acc.id());
983       // if the value is the same, we can silently skip this transaction
984       if (s0.shares() == m_qifProfile.value('T', extractLine('T'))) {
985         needCreate = false;
986       }
987       if (needCreate) {
988         // in case we create it anyway, we issue a warning to the user to check it manually
989         KMessageBox::sorry(0, QString("<qt>%1</qt>").arg(i18n("KMyMoney has imported a second opening balance transaction into account <b>%1</b> which differs from the one found already on file. Please correct this manually once the import is done.", acc.name())), i18n("Opening balance problem"));
990       }
991     }
992 
993     if (needCreate) {
994       acc.setOpeningDate(m_qifProfile.date(extractLine('D')));
995       file->modifyAccount(acc);
996       MyMoneyTransaction t = file->createOpeningBalanceTransaction(acc, m_qifProfile.value('T', extractLine('T')));
997       if (!t.id().isEmpty()) {
998         t.setImported();
999         file->modifyTransaction(t);
1000       }
1001       ft.commit();
1002     }
1003 
1004     // make sure to use the updated version of the account
1005     if (m_account.id() == acc.id())
1006       m_account = acc;
1007 
1008     // remember which account we created
1009     d->st.m_accountId = m_account.id();
1010   } catch (const MyMoneyException &e) {
1011     KMessageBox::detailedError(nullptr,
1012                                i18n("Error while creating opening balance transaction"),
1013                                e.what(),
1014                                i18n("File access error"));
1015   }
1016 }
1017 
processTransactionEntry()1018 void MyMoneyQifReader::processTransactionEntry()
1019 {
1020   ++m_transactionsProcessed;
1021   // in case the user selected to skip the account or the account
1022   // was not found we skip this transaction
1023   /*
1024     if(m_account.id().isEmpty()) {
1025       m_transactionsSkipped++;
1026       return;
1027     }
1028   */
1029   MyMoneyFile* file = MyMoneyFile::instance();
1030   MyMoneyStatement::Split s1;
1031   MyMoneyStatement::Transaction tr;
1032   QString tmp;
1033   QString accountId;
1034   int pos;
1035   QString payee = extractLine('P');
1036   unsigned long h;
1037 
1038   h = MyMoneyTransaction::hash(m_qifEntry.join(";"));
1039 
1040   QString hashBase;
1041   hashBase.sprintf("%s-%07lx", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h);
1042   int idx = 1;
1043   QString hash;
1044   for (;;) {
1045     hash = QString("%1-%2").arg(hashBase).arg(idx);
1046     QMap<QString, bool>::const_iterator it;
1047     it = d->m_hashMap.constFind(hash);
1048     if (it == d->m_hashMap.constEnd()) {
1049       d->m_hashMap[hash] = true;
1050       break;
1051     }
1052     ++idx;
1053   }
1054   tr.m_strBankID = hash;
1055 
1056   if (d->firstTransaction) {
1057     // check if this is an opening balance transaction and process it out of the statement
1058     if (!payee.isEmpty() && ((payee.toLower() == "opening balance") || KMyMoneySettings::qifOpeningBalance().toLower().contains(payee.toLower()))) {
1059       createOpeningBalance(d->accountType);
1060       d->firstTransaction = false;
1061       return;
1062     }
1063   }
1064 
1065   // Process general transaction data
1066 
1067   if (d->st.m_accountId.isEmpty())
1068     d->st.m_accountId = m_account.id();
1069 
1070   s1.m_accountId = d->st.m_accountId;
1071   switch (d->accountType) {
1072   case eMyMoney::Account::Type::Checkings:
1073     d->st.m_eType=eMyMoney::Statement::Type::Checkings;
1074     break;
1075   case eMyMoney::Account::Type::Savings:
1076     d->st.m_eType=eMyMoney::Statement::Type::Savings;
1077     break;
1078   case eMyMoney::Account::Type::Investment:
1079     d->st.m_eType=eMyMoney::Statement::Type::Investment;
1080     break;
1081   case eMyMoney::Account::Type::CreditCard:
1082     d->st.m_eType=eMyMoney::Statement::Type::CreditCard;
1083     break;
1084   default:
1085     d->st.m_eType=eMyMoney::Statement::Type::None;
1086     break;
1087   }
1088 
1089   tr.m_datePosted = (m_qifProfile.date(extractLine('D')));
1090   if (!tr.m_datePosted.isValid()) {
1091     int rc = KMessageBox::warningContinueCancel(0,
1092              i18n("The date entry \"%1\" read from the file cannot be interpreted through the current "
1093                   "date profile setting of \"%2\".\n\nPressing \"Continue\" will "
1094                   "assign todays date to the transaction. Pressing \"Cancel\" will abort "
1095                   "the import operation. You can then restart the import and select a different "
1096                   "QIF profile or create a new one.", extractLine('D'), m_qifProfile.inputDateFormat()),
1097              i18n("Invalid date format"));
1098     switch (rc) {
1099       case KMessageBox::Continue:
1100         tr.m_datePosted = (QDate::currentDate());
1101         break;
1102 
1103       case KMessageBox::Cancel:
1104         throw MYMONEYEXCEPTION_CSTRING("USERABORT");
1105         break;
1106     }
1107   }
1108 
1109   tmp = extractLine('L');
1110   pos = tmp.lastIndexOf("--");
1111   if (tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) {
1112     // it's a transfer, so we wipe the memo
1113 //   tmp = "";         why??
1114 //    st.m_strAccountName = tmp;
1115   } else if (pos != -1) {
1116 //    what's this?
1117 //    t.setValue("Dialog", tmp.mid(pos+2));
1118     tmp = tmp.left(pos);
1119   }
1120 //  t.setMemo(tmp);
1121 
1122   // Assign the "#" field to the transaction's bank id
1123   // This is the custom KMM extension to QIF for a unique ID
1124   tmp = extractLine('#');
1125   if (!tmp.isEmpty()) {
1126     tr.m_strBankID = QString("ID %1").arg(tmp);
1127   }
1128 
1129 #if 0
1130   // Collect data for the account's split
1131   s1.m_accountId = m_account.id();
1132   tmp = extractLine('S');
1133   pos = tmp.findRev("--");
1134   if (pos != -1) {
1135     tmp = tmp.left(pos);
1136   }
1137   if (tmp.left(1) == m_qifProfile.accountDelimiter().left(1))
1138     // it's a transfer, extract the account name
1139     tmp = tmp.mid(1, tmp.length() - 2);
1140   s1.m_strCategoryName = tmp;
1141 #endif
1142   // TODO (Ace) Deal with currencies more gracefully.  QIF cannot deal with multiple
1143   // currencies, so we should assume that transactions imported into a given
1144   // account are in THAT ACCOUNT's currency.  If one of those involves a transfer
1145   // to an account with a different currency, value and shares should be
1146   // different.  (Shares is in the target account's currency, value is in the
1147   // transaction's)
1148 
1149 
1150   s1.m_amount = m_qifProfile.value('T', extractLine('T'));
1151   tr.m_amount = m_qifProfile.value('T', extractLine('T'));
1152   tr.m_shares = m_qifProfile.value('T', extractLine('T'));
1153   tmp = extractLine('N');
1154   if (!tmp.isEmpty())
1155     tr.m_strNumber = tmp;
1156 
1157   if (!payee.isEmpty()) {
1158     tr.m_strPayee = payee;
1159   }
1160 
1161   tr.m_reconcile = d->reconcileState(extractLine('C'));
1162   tr.m_strMemo = extractLine('M');
1163   d->fixMultiLineMemo(tr.m_strMemo);
1164   s1.m_strMemo = tr.m_strMemo;
1165   // tr.m_listSplits.append(s1);
1166 
1167   //             split transaction
1168   //      ****** ensure each field is ******
1169   //      *   attached to correct split    *
1170   QList<qSplit> listqSplits;
1171   if (! extractSplits(listqSplits)) {
1172     MyMoneyAccount account;
1173     // use the same values for the second split, but clear the ID and reverse the value
1174     MyMoneyStatement::Split s2 = s1;
1175     s2.m_reconcile = tr.m_reconcile;
1176     s2.m_amount = (-s1.m_amount);
1177 //    s2.clearId();
1178 
1179     // standard transaction
1180     tmp = extractLine('L');
1181     if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) {
1182       accountId = transferAccount(tmp, false);
1183 
1184     } else {
1185       /*      pos = tmp.findRev("--");
1186             if(pos != -1) {
1187               t.setValue("Dialog", tmp.mid(pos+2));
1188               tmp = tmp.left(pos);
1189             }*/
1190 
1191       // it's an expense / income
1192       tmp = tmp.trimmed();
1193       accountId = file->checkCategory(tmp, s1.m_amount, s2.m_amount);
1194     }
1195 
1196     if (!accountId.isEmpty()) {
1197       try {
1198         account = file->account(accountId);
1199         // FIXME: check that the type matches and ask if not
1200 
1201         if (account.accountType() == eMyMoney::Account::Type::Investment) {
1202           qDebug() << "Line " << m_linenumber << ": Cannot transfer to/from an investment account. Transaction ignored.";
1203           return;
1204         }
1205         if (account.id() == m_account.id()) {
1206           qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored.";
1207           accountId.clear();
1208         }
1209 
1210       } catch (const MyMoneyException &) {
1211         qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found";
1212         accountId.clear();
1213       }
1214     }
1215 
1216     if (!accountId.isEmpty()) {
1217       s2.m_accountId = accountId;
1218       s2.m_strCategoryName = tmp;
1219       tr.m_listSplits.append(s2);
1220     }
1221 
1222   } else {
1223     int   count;
1224     for (count = 1; count <= listqSplits.count(); ++count) {                     // Use true splits count
1225       MyMoneyStatement::Split s2 = s1;
1226       s2.m_amount = (-m_qifProfile.value('$', listqSplits[count-1].m_amount));   // Amount of split
1227       s2.m_strMemo = listqSplits[count-1].m_strMemo;                             // Memo in split
1228       tmp = listqSplits[count-1].m_strCategoryName;                              // Category in split
1229 
1230       if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) {
1231         accountId = transferAccount(tmp, false);
1232 
1233       } else {
1234         pos = tmp.lastIndexOf("--");
1235         if (pos != -1) {
1236           tmp = tmp.left(pos);
1237         }
1238         tmp = tmp.trimmed();
1239         accountId = file->checkCategory(tmp, s1.m_amount, s2.m_amount);
1240       }
1241 
1242       if (!accountId.isEmpty()) {
1243         try {
1244           MyMoneyAccount account = file->account(accountId);
1245           // FIXME: check that the type matches and ask if not
1246 
1247           if (account.accountType() == eMyMoney::Account::Type::Investment) {
1248             qDebug() << "Line " << m_linenumber << ": Cannot convert a split transfer to/from an investment account. Split removed. Total amount adjusted from " << tr.m_amount.formatMoney("", 2) << " to " << (tr.m_amount + s2.m_amount).formatMoney("", 2) << "\n";
1249             tr.m_amount += s2.m_amount;
1250             continue;
1251           }
1252           if (account.id() == m_account.id()) {
1253             qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored.";
1254             accountId.clear();
1255           }
1256 
1257         } catch (const MyMoneyException &) {
1258           qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found";
1259           accountId.clear();
1260         }
1261       }
1262       if (!accountId.isEmpty()) {
1263         s2.m_accountId = accountId;
1264         s2.m_strCategoryName = tmp;
1265         tr.m_listSplits += s2;
1266         // in case the transaction does not have a memo and we
1267         // process the first split just copy the memo over
1268         if (tr.m_listSplits.count() == 1 && tr.m_strMemo.isEmpty())
1269           tr.m_strMemo = s2.m_strMemo;
1270       } else {
1271         // TODO add an option to create a "Unassigned" category
1272         // for now, we just drop the split which will show up as unbalanced
1273         // transaction in the KMyMoney ledger view
1274       }
1275     }
1276   }
1277 
1278   // Add the transaction to the statement
1279   d->st.m_listTransactions += tr;
1280 }
1281 
processInvestmentTransactionEntry()1282 void MyMoneyQifReader::processInvestmentTransactionEntry()
1283 {
1284 //   qDebug() << "Investment Transaction:" << m_qifEntry.count() << " lines";
1285   /*
1286   Items for Investment Accounts
1287   Field   Indicator Explanation
1288   D   Date
1289   N   Action
1290   Y   Security (NAME, not symbol)
1291   I   Price
1292   Q   Quantity (number of shares or split ratio)
1293   T   Transaction amount
1294   C   Cleared status
1295   P   Text in the first line for transfers and reminders (Payee)
1296   M   Memo
1297   O   Commission
1298   L   Account for the transfer
1299   $   Amount transferred
1300   ^   End of the entry
1301 
1302   It will be presumed all transactions are to the associated cash account, if
1303   one exists, unless otherwise noted by the 'L' field.
1304 
1305   Expense/Income categories will be automatically generated, "_Dividend",
1306   "_InterestIncome", etc.
1307 
1308   */
1309 
1310   MyMoneyStatement::Transaction tr;
1311   d->st.m_eType = eMyMoney::Statement::Type::Investment;
1312 
1313 //  t.setCommodity(m_account.currencyId());
1314   // 'D' field: Date
1315   QDate date = m_qifProfile.date(extractLine('D'));
1316   if (date.isValid())
1317     tr.m_datePosted = date;
1318   else {
1319     int rc = KMessageBox::warningContinueCancel(0,
1320              i18n("The date entry \"%1\" read from the file cannot be interpreted through the current "
1321                   "date profile setting of \"%2\".\n\nPressing \"Continue\" will "
1322                   "assign todays date to the transaction. Pressing \"Cancel\" will abort "
1323                   "the import operation. You can then restart the import and select a different "
1324                   "QIF profile or create a new one.", extractLine('D'), m_qifProfile.inputDateFormat()),
1325              i18n("Invalid date format"));
1326     switch (rc) {
1327       case KMessageBox::Continue:
1328         tr.m_datePosted = QDate::currentDate();
1329         break;
1330 
1331       case KMessageBox::Cancel:
1332         throw MYMONEYEXCEPTION_CSTRING("USERABORT");
1333         break;
1334     }
1335   }
1336 
1337   // 'M' field: Memo
1338   QString memo = extractLine('M');
1339   d->fixMultiLineMemo(memo);
1340   tr.m_strMemo = memo;
1341   unsigned long h;
1342 
1343   h = MyMoneyTransaction::hash(m_qifEntry.join(";"));
1344 
1345   QString hashBase;
1346   hashBase.sprintf("%s-%07lx", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h);
1347   int idx = 1;
1348   QString hash;
1349   for (;;) {
1350     hash = QString("%1-%2").arg(hashBase).arg(idx);
1351     QMap<QString, bool>::const_iterator it;
1352     it = d->m_hashMap.constFind(hash);
1353     if (it == d->m_hashMap.constEnd()) {
1354       d->m_hashMap[hash] = true;
1355       break;
1356     }
1357     ++idx;
1358   }
1359   tr.m_strBankID = hash;
1360 
1361   // '#' field: BankID
1362   QString tmp = extractLine('#');
1363   if (! tmp.isEmpty())
1364     tr.m_strBankID = QString("ID %1").arg(tmp);
1365 
1366   // Reconciliation flag
1367   tr.m_reconcile = d->reconcileState(extractLine('C'));
1368 
1369   // 'O' field: Fees
1370   tr.m_fees = m_qifProfile.value('T', extractLine('O'));
1371   // 'T' field: Amount
1372   MyMoneyMoney amount = m_qifProfile.value('T', extractLine('T'));
1373   tr.m_amount = amount;
1374 
1375   MyMoneyStatement::Price price;
1376 
1377   price.m_date = date;
1378   price.m_strSecurity = extractLine('Y');
1379   price.m_amount = m_qifProfile.value('T', extractLine('I'));
1380 
1381 #if 0 // we must check for that later, because certain activities don't need a security
1382   // 'Y' field: Security name
1383 
1384   QString securityname = extractLine('Y').toLower();
1385   if (securityname.isEmpty()) {
1386     qDebug() << "Line " << m_linenumber << ": Investment transaction without a security is not supported.";
1387     return;
1388   }
1389   tr.m_strSecurity = securityname;
1390 #endif
1391 
1392 #if 0
1393 
1394   // For now, we let the statement reader take care of that.
1395 
1396   // The big problem here is that the Y field is not the SYMBOL, it's the NAME.
1397   // The name is not very unique, because people could have used slightly different
1398   // abbreviations or ordered words differently, etc.
1399   //
1400   // If there is a perfect name match with a subordinate stock account, great.
1401   // More likely, we have to rely on the QIF file containing !Type:Security
1402   // records, which tell us the mapping from name to symbol.
1403   //
1404   // Therefore, generally it is not recommended to import a QIF file containing
1405   // investment transactions but NOT containing security records.
1406 
1407   QString securitysymbol = m_investmentMap[securityname];
1408 
1409   // the correct account is the stock account which matches two criteria:
1410   // (1) it is a sub-account of the selected investment account, and either
1411   // (2a) the security name of the transaction matches the name of the security, OR
1412   // (2b) the security name of the transaction maps to a symbol which matches the symbol of the security
1413 
1414   // search through each subordinate account
1415   bool found = false;
1416   MyMoneyAccount thisaccount = m_account;
1417   QStringList accounts = thisaccount.accountList();
1418   QStringList::const_iterator it_account = accounts.begin();
1419   while (!found && it_account != accounts.end()) {
1420     QString currencyid = file->account(*it_account).currencyId();
1421     MyMoneySecurity security = file->security(currencyid);
1422     QString symbol = security.tradingSymbol().toLower();
1423     QString name = security.name().toLower();
1424 
1425     if (securityname == name || securitysymbol == symbol) {
1426       d->st_AccountId = *it_account;
1427       s1.m_accountId = *it_account;
1428       thisaccount = file->account(*it_account);
1429       found = true;
1430 
1431 #if 0
1432       // update the price, while we're here.  in the future, this should be
1433       // an option
1434       QString basecurrencyid = file->baseCurrency().id();
1435       MyMoneyPrice price = file->price(currencyid, basecurrencyid, t_in.m_datePosted, true);
1436       if (!price.isValid()) {
1437         MyMoneyPrice newprice(currencyid, basecurrencyid, t_in.m_datePosted, t_in.m_moneyAmount / t_in.m_dShares, i18n("Statement Importer"));
1438         file->addPrice(newprice);
1439       }
1440 #endif
1441     }
1442 
1443     ++it_account;
1444   }
1445 
1446   if (!found) {
1447     qDebug() << "Line " << m_linenumber << ": Security " << securityname << " not found in this account.  Transaction ignored.";
1448 
1449     // If the security is not known, notify the user
1450     // TODO (Ace) A "SelectOrCreateAccount" interface for investments
1451     KMessageBox::information(0, i18n("This investment account does not contain the \"%1\" security.  "
1452                                      "Transactions involving this security will be ignored.", securityname),
1453                              i18n("Security not found"),
1454                              QString("MissingSecurity%1").arg(securityname.trimmed()));
1455     return;
1456   }
1457 #endif
1458 
1459   // 'Y' field: Security
1460   tr.m_strSecurity = extractLine('Y');
1461 
1462   // 'Q' field: Quantity
1463   MyMoneyMoney quantity = m_qifProfile.value('T', extractLine('Q'));
1464 
1465   // 'N' field: Action
1466   QString action = extractLine('N').toLower();
1467 
1468   // remove trailing X, which seems to have no purpose (?!)
1469   bool xAction = false;
1470   if (action.endsWith('x')) {
1471     action = action.left(action.length() - 1);
1472     xAction = true;
1473   }
1474 
1475   tmp = extractLine('L');
1476   // if the action ends in an X, the L-Record contains the asset account
1477   // to which the dividend should be transferred. In the other cases, it
1478   // may contain a category that identifies the income category for the
1479   // dividend payment
1480   if ((xAction == true)
1481       || (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true)) {
1482     tmp = tmp.remove(QRegExp("[\\[\\]]"));                 //  xAction != true so ignore any'[ and ]'
1483     if (!tmp.isEmpty()) {                                  // use 'L' record name
1484       tr.m_strBrokerageAccount = tmp;
1485       transferAccount(tmp);                                // make sure the account exists
1486     } else {
1487       tr.m_strBrokerageAccount = m_account.brokerageName();// use brokerage account
1488       transferAccount(m_account.brokerageName());          // make sure the account exists
1489     }
1490   } else {
1491     tmp = tmp.remove(QRegExp("[\\[\\]]"));                 //  xAction != true so ignore any'[ and ]'
1492     tr.m_strInterestCategory = tmp;
1493     tr.m_strBrokerageAccount = m_account.brokerageName();
1494   }
1495 
1496   // Whether to create a cash split for the other side of the value
1497   QString accountname; //= extractLine('L');
1498   if (action == "reinvint" || action == "reinvdiv" || action == "reinvlg" || action == "reinvsh") {
1499     d->st.m_listPrices += price;
1500     tr.m_shares = quantity;
1501     tr.m_eAction = (eMyMoney::Transaction::Action::ReinvestDividend);
1502     tr.m_price = m_qifProfile.value('I', extractLine('I'));
1503 
1504     tr.m_strInterestCategory = extractLine('L');
1505     if (tr.m_strInterestCategory.isEmpty()) {
1506       tr.m_strInterestCategory = d->typeToAccountName(action);
1507     }
1508   } else if (action == "div" || action == "cgshort" || action == "cgmid" || action == "cglong" || action == "rtrncap") {
1509     tr.m_eAction = (eMyMoney::Transaction::Action::CashDividend);
1510 
1511     // make sure, we have valid category. Either taken from the L-Record above,
1512     // or derived from the action code
1513     if (tr.m_strInterestCategory.isEmpty()) {
1514       tr.m_strInterestCategory = d->typeToAccountName(action);
1515     }
1516 
1517     // For historic reasons (coming from the OFX importer) the statement
1518     // reader expects the dividend with a reverse sign. So we just do that.
1519     tr.m_amount -= tr.m_fees;
1520 
1521     // We need an extra split which will be the zero-amount investment split
1522     // that serves to mark this transaction as a cash dividend and note which
1523     // stock account it belongs to.
1524     MyMoneyStatement::Split s2;
1525     s2.m_amount = MyMoneyMoney();
1526     s2.m_strCategoryName = extractLine('Y');
1527     tr.m_listSplits.append(s2);
1528   } else if (action == "intinc" || action == "miscinc" || action == "miscexp") {
1529     tr.m_eAction = (eMyMoney::Transaction::Action::Interest);
1530     if (action == "miscexp")
1531       tr.m_eAction = (eMyMoney::Transaction::Action::Fees);
1532 
1533     // make sure, we have a valid category. Either taken from the L-Record above,
1534     // or derived from the action code
1535     if (tr.m_strInterestCategory.isEmpty()) {
1536       tr.m_strInterestCategory = d->typeToAccountName(action);
1537     }
1538 
1539     if (action == "intinc") {
1540       MyMoneyMoney priceValue = m_qifProfile.value('I', extractLine('I'));
1541       tr.m_amount -= tr.m_fees;
1542       if ((!quantity.isZero()) && (!priceValue.isZero()))
1543         tr.m_amount = -(quantity * priceValue);
1544     } else
1545       // For historic reasons (coming from the OFX importer) the statement
1546       // reader expects the dividend with a reverse sign. So we just do that.
1547       if (action != "miscexp")
1548         tr.m_amount = -(amount - tr.m_fees);
1549 
1550     if (tr.m_strMemo.isEmpty())
1551       tr.m_strMemo = (QString("%1 %2").arg(extractLine('Y')).arg(d->typeToAccountName(action))).trimmed();
1552   } else if (action == "xin" || action == "xout") {
1553     QString payee = extractLine('P');
1554     if (!payee.isEmpty() && ((payee.toLower() == "opening balance") || KMyMoneySettings::qifOpeningBalance().toLower().contains(payee.toLower()))) {
1555       createOpeningBalance(eMyMoney::Account::Type::Investment);
1556       return;
1557     }
1558 
1559     tr.m_eAction = (eMyMoney::Transaction::Action::None);
1560     MyMoneyStatement::Split s2;
1561     tmp = extractLine('L');
1562     if (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) {
1563       s2.m_accountId = transferAccount(tmp);
1564       s2.m_strCategoryName = tmp;
1565     } else {
1566       s2.m_strCategoryName = extractLine('L');
1567       if (tr.m_strInterestCategory.isEmpty()) {
1568         s2.m_strCategoryName = d->typeToAccountName(action);
1569       }
1570     }
1571 
1572     if (action == "xout")
1573       tr.m_amount = -tr.m_amount;
1574 
1575     s2.m_amount = -tr.m_amount;
1576     tr.m_listSplits.append(s2);
1577   } else if (action == "buy") {
1578     d->st.m_listPrices += price;
1579     tr.m_price = m_qifProfile.value('I', extractLine('I'));
1580     tr.m_shares = quantity;
1581     tr.m_amount = -amount;
1582     tr.m_eAction = (eMyMoney::Transaction::Action::Buy);
1583   } else if (action == "sell") {
1584     d->st.m_listPrices += price;
1585     tr.m_price = m_qifProfile.value('I', extractLine('I'));
1586     tr.m_shares = -quantity;
1587     tr.m_amount = amount;
1588     tr.m_eAction = (eMyMoney::Transaction::Action::Sell);
1589   } else if (action == "shrsin") {
1590     tr.m_shares = quantity;
1591     tr.m_eAction = (eMyMoney::Transaction::Action::Shrsin);
1592   } else if (action == "shrsout") {
1593     tr.m_shares = -quantity;
1594     tr.m_eAction = (eMyMoney::Transaction::Action::Shrsout);
1595   } else if (action == "stksplit") {
1596     MyMoneyMoney splitfactor = (quantity / MyMoneyMoney(10, 1)).reduce();
1597 
1598     // Stock splits not supported
1599 //     qDebug() << "Line " << m_linenumber << ": Stock split not supported (date=" << date << " security=" << securityname << " factor=" << splitfactor.toString() << ")";
1600 
1601 //    s1.setShares(splitfactor);
1602 //    s1.setValue(0);
1603 //   s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares));
1604 
1605 //     return;
1606   } else {
1607     // Unsupported action type
1608     qDebug() << "Line " << m_linenumber << ": Unsupported transaction action (" << action << ")";
1609     return;
1610   }
1611   d->st.m_strAccountName = accountname;  //  accountname appears not to get set
1612   d->st.m_listTransactions += tr;
1613 
1614   /*************************************************************************
1615    *
1616    * These transactions are natively supported by KMyMoney
1617    *
1618    *************************************************************************/
1619   /*
1620   D1/ 3' 5
1621   NShrsIn
1622   YGENERAL MOTORS CORP 52BR1
1623   I20
1624   Q200
1625   U4,000.00
1626   T4,000.00
1627   M200 shares added to account @ $20/share
1628   ^
1629   */
1630   /*
1631   ^
1632   D1/14' 5
1633   NShrsOut
1634   YTEMPLETON GROWTH 97GJ0
1635   Q50
1636   90  ^
1637   */
1638   /*
1639   D1/28' 5
1640   NBuy
1641   YGENERAL MOTORS CORP 52BR1
1642   I24.35
1643   Q100
1644   U2,435.00
1645   T2,435.00
1646   ^
1647   */
1648   /*
1649   D1/ 5' 5
1650   NSell
1651   YUnited Vanguard
1652   I8.41
1653   Q50
1654   U420.50
1655   T420.50
1656   ^
1657   */
1658   /*
1659   D1/ 7' 5
1660   NReinvDiv
1661   YFRANKLIN INCOME 97GM2
1662   I38
1663   Q1
1664   U38.00
1665   T38.00
1666   ^
1667   */
1668   /*************************************************************************
1669    *
1670    * These transactions are all different kinds of income.  (Anything that
1671    * follows the DNYUT pattern).  They are all handled the same, the only
1672    * difference is which income account the income is placed into.  By
1673    * default, it's placed into _xxx where xxx is the right side of the
1674    * N field.  e.g. NDiv transaction goes into the _Div account
1675    *
1676    *************************************************************************/
1677   /*
1678   D1/10' 5
1679   NDiv
1680   YTEMPLETON GROWTH 97GJ0
1681   U10.00
1682   T10.00
1683   ^
1684   */
1685   /*
1686   D1/10' 5
1687   NIntInc
1688   YTEMPLETON GROWTH 97GJ0
1689   U20.00
1690   T20.00
1691   ^
1692   */
1693   /*
1694   D1/10' 5
1695   NCGShort
1696   YTEMPLETON GROWTH 97GJ0
1697   U111.00
1698   T111.00
1699   ^
1700   */
1701   /*
1702   D1/10' 5
1703   NCGLong
1704   YTEMPLETON GROWTH 97GJ0
1705   U333.00
1706   T333.00
1707   ^
1708   */
1709   /*
1710   D1/10' 5
1711   NCGMid
1712   YTEMPLETON GROWTH 97GJ0
1713   U222.00
1714   T222.00
1715   ^
1716   */
1717   /*
1718   D2/ 2' 5
1719   NRtrnCap
1720   YFRANKLIN INCOME 97GM2
1721   U1,234.00
1722   T1,234.00
1723   ^
1724   */
1725   /*************************************************************************
1726    *
1727    * These transactions deal with miscellaneous activity that KMyMoney
1728    * does not support, but may support in the future.
1729    *
1730    *************************************************************************/
1731   /*   Note the Q field is the split ratio per 10 shares, so Q12.5 is a
1732         12.5:10 split, otherwise known as 5:4.
1733   D1/14' 5
1734   NStkSplit
1735   YIBM
1736   Q12.5
1737   ^
1738   */
1739   /*************************************************************************
1740    *
1741    * These transactions deal with short positions and options, which are
1742    * not supported at all by KMyMoney.  They will be ignored for now.
1743    * There may be a way to hack around this, by creating a new security
1744    * "IBM_Short".
1745    *
1746    *************************************************************************/
1747   /*
1748   D1/21' 5
1749   NShtSell
1750   YIBM
1751   I92.38
1752   Q100
1753   U9,238.00
1754   T9,238.00
1755   ^
1756   */
1757   /*
1758   D1/28' 5
1759   NCvrShrt
1760   YIBM
1761   I92.89
1762   Q100
1763   U9,339.00
1764   T9,339.00
1765   O50.00
1766   ^
1767   */
1768   /*
1769   D6/ 1' 5
1770   NVest
1771   YIBM Option
1772   Q20
1773   ^
1774   */
1775   /*
1776   D6/ 8' 5
1777   NExercise
1778   YIBM Option
1779   I60.952381
1780   Q20
1781   MFrom IBM Option Grant 6/1/2004
1782   ^
1783   */
1784   /*
1785   D6/ 1'14
1786   NExpire
1787   YIBM Option
1788   Q5
1789   ^
1790   */
1791   /*************************************************************************
1792    *
1793    * These transactions do not have an associated investment ("Y" field)
1794    * so presumably they are only valid for the cash account.  Once I
1795    * understand how these are really implemented, they can probably be
1796    * handled without much trouble.
1797    *
1798    *************************************************************************/
1799   /*
1800   D1/14' 5
1801   NCash
1802   U-100.00
1803   T-100.00
1804   LBank Chrg
1805   ^
1806   */
1807   /*
1808   D1/15' 5
1809   NXOut
1810   U500.00
1811   T500.00
1812   L[CU Savings]
1813   $500.00
1814   ^
1815   */
1816   /*
1817   D1/28' 5
1818   NXIn
1819   U1,000.00
1820   T1,000.00
1821   L[CU Checking]
1822   $1,000.00
1823   ^
1824   */
1825   /*
1826   D1/25' 5
1827   NMargInt
1828   U25.00
1829   T25.00
1830   ^
1831   */
1832 }
1833 
findOrCreateIncomeAccount(const QString & searchname)1834 const QString MyMoneyQifReader::findOrCreateIncomeAccount(const QString& searchname)
1835 {
1836   QString result;
1837 
1838   MyMoneyFile *file = MyMoneyFile::instance();
1839 
1840   // First, try to find this account as an income account
1841   MyMoneyAccount acc = file->income();
1842   QStringList list = acc.accountList();
1843   QStringList::ConstIterator it_accid = list.constBegin();
1844   while (it_accid != list.constEnd()) {
1845     acc = file->account(*it_accid);
1846     if (acc.name() == searchname) {
1847       result = *it_accid;
1848       break;
1849     }
1850     ++it_accid;
1851   }
1852 
1853   // If we did not find the account, now we must create one.
1854   if (result.isEmpty()) {
1855     MyMoneyAccount newAccount;
1856     newAccount.setName(searchname);
1857     newAccount.setAccountType(eMyMoney::Account::Type::Income);
1858     MyMoneyAccount income = file->income();
1859     MyMoneyFileTransaction ft;
1860     file->addAccount(newAccount, income);
1861     ft.commit();
1862     result = newAccount.id();
1863   }
1864 
1865   return result;
1866 }
1867 
1868 // TODO (Ace) Combine this and the previous function
1869 
findOrCreateExpenseAccount(const QString & searchname)1870 const QString MyMoneyQifReader::findOrCreateExpenseAccount(const QString& searchname)
1871 {
1872   QString result;
1873 
1874   MyMoneyFile *file = MyMoneyFile::instance();
1875 
1876   // First, try to find this account as an income account
1877   MyMoneyAccount acc = file->expense();
1878   QStringList list = acc.accountList();
1879   QStringList::ConstIterator it_accid = list.constBegin();
1880   while (it_accid != list.constEnd()) {
1881     acc = file->account(*it_accid);
1882     if (acc.name() == searchname) {
1883       result = *it_accid;
1884       break;
1885     }
1886     ++it_accid;
1887   }
1888 
1889   // If we did not find the account, now we must create one.
1890   if (result.isEmpty()) {
1891     MyMoneyAccount newAccount;
1892     newAccount.setName(searchname);
1893     newAccount.setAccountType(eMyMoney::Account::Type::Expense);
1894     MyMoneyFileTransaction ft;
1895     MyMoneyAccount expense = file->expense();
1896     file->addAccount(newAccount, expense);
1897     ft.commit();
1898     result = newAccount.id();
1899   }
1900 
1901   return result;
1902 }
1903 
processAccountEntry(bool resetAccountId)1904 const QString MyMoneyQifReader::processAccountEntry(bool resetAccountId)
1905 {
1906   MyMoneyFile* file = MyMoneyFile::instance();
1907 
1908   MyMoneyAccount account;
1909   QString tmp;
1910 
1911   account.setName(extractLine('N'));
1912   //   qDebug("Process account '%s'", account.name().data());
1913 
1914   account.setDescription(extractLine('D'));
1915 
1916   tmp = extractLine('$');
1917   if (tmp.length() > 0)
1918     account.setValue("lastStatementBalance", tmp);
1919 
1920   tmp = extractLine('/');
1921   if (tmp.length() > 0)
1922     account.setLastReconciliationDate(m_qifProfile.date(tmp));
1923 
1924   QifEntryTypeE transactionType = EntryTransaction;
1925   QString type = extractLine('T').toLower().remove(QRegExp("\\s+"));
1926   if (type == m_qifProfile.profileType().toLower().remove(QRegExp("\\s+"))) {
1927     account.setAccountType(eMyMoney::Account::Type::Checkings);
1928   } else if (type == "ccard" || type == "creditcard") {
1929     account.setAccountType(eMyMoney::Account::Type::CreditCard);
1930   } else if (type == "cash") {
1931     account.setAccountType(eMyMoney::Account::Type::Cash);
1932   } else if (type == "otha") {
1933     account.setAccountType(eMyMoney::Account::Type::Asset);
1934   } else if (type == "othl") {
1935     account.setAccountType(eMyMoney::Account::Type::Liability);
1936   } else if (type == "invst" || type == "port") {
1937     account.setAccountType(eMyMoney::Account::Type::Investment);
1938     transactionType = EntryInvestmentTransaction;
1939   } else if (type == "mutual") { // stock account w/o umbrella investment account
1940     account.setAccountType(eMyMoney::Account::Type::Stock);
1941     transactionType = EntryInvestmentTransaction;
1942   } else if (type == "unknown") {
1943     // don't do anything with the type, leave it unknown
1944   } else {
1945     account.setAccountType(eMyMoney::Account::Type::Checkings);
1946     qDebug() << "Line " << m_linenumber << ": Unknown account type '" << type << "', checkings assumed";
1947   }
1948 
1949   // check if we can find the account already in the file
1950   auto acc = findAccount(account, MyMoneyAccount());
1951   if (acc.id().isEmpty()) {
1952     // in case the account is not found by name and the type is
1953     // unknown, we have to assume something and create a checking account.
1954     // this might be wrong, but we have no choice at this point.
1955     if (account.accountType() == eMyMoney::Account::Type::Unknown)
1956       account.setAccountType(eMyMoney::Account::Type::Checkings);
1957 
1958     MyMoneyAccount parentAccount;
1959     MyMoneyAccount brokerage;
1960     // in case it's a stock account, we need to setup a fix investment account
1961     if (account.isInvest()) {
1962       acc.setName(i18n("%1 (Investment)", account.name()));   // use the same name for the investment account
1963       acc.setDescription(i18n("Autogenerated by QIF importer from type Mutual account entry"));
1964       acc.setAccountType(eMyMoney::Account::Type::Investment);
1965       parentAccount = file->asset();
1966       file->createAccount(acc, parentAccount, brokerage, MyMoneyMoney());
1967       parentAccount = acc;
1968       qDebug("We still need to create the stock account in MyMoneyQifReader::processAccountEntry()");
1969     } else {
1970       // setup parent according the type of the account
1971       switch (account.accountGroup()) {
1972         case eMyMoney::Account::Type::Asset:
1973         default:
1974           parentAccount = file->asset();
1975           break;
1976         case eMyMoney::Account::Type::Liability:
1977           parentAccount = file->liability();
1978           break;
1979         case eMyMoney::Account::Type::Equity:
1980           parentAccount = file->equity();
1981           break;
1982       }
1983     }
1984 
1985     // investment accounts will receive a brokerage account, as KMyMoney
1986     // currently does not allow to store funds in the investment account directly
1987     // but only create it (not here, but later) if it is needed
1988     if (account.accountType() == eMyMoney::Account::Type::Investment) {
1989       brokerage.setName(QString());  //                           brokerage name empty so account not created yet
1990       brokerage.setAccountType(eMyMoney::Account::Type::Checkings);
1991       brokerage.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id());
1992     }
1993     file->createAccount(account, parentAccount, brokerage, MyMoneyMoney());
1994     acc = account;
1995     // qDebug("Account created");
1996   } else {
1997     // qDebug("Existing account found");
1998   }
1999 
2000   if (resetAccountId) {
2001     // possibly start a new statement
2002     d->finishStatement();
2003     m_account = acc;
2004     d->st.m_accountId = m_account.id();  //                      needed here for account selection
2005     d->transactionType = transactionType;
2006   }
2007   return acc.id();
2008 }
2009 
setProgressCallback(void (* callback)(qint64,qint64,const QString &))2010 void MyMoneyQifReader::setProgressCallback(void(*callback)(qint64, qint64, const QString&))
2011 {
2012   m_progressCallback = callback;
2013 }
2014 
signalProgress(qint64 current,qint64 total,const QString & msg)2015 void MyMoneyQifReader::signalProgress(qint64 current, qint64 total, const QString& msg)
2016 {
2017   if (m_progressCallback != 0)
2018     (*m_progressCallback)(current, total, msg);
2019 }
2020 
processPriceEntry()2021 void MyMoneyQifReader::processPriceEntry()
2022 {
2023   /*
2024     !Type:Prices
2025     "IBM",141 9/16,"10/23/98"
2026     ^
2027     !Type:Prices
2028     "GMW",21.28," 3/17' 5"
2029     ^
2030     !Type:Prices
2031     "GMW",71652181.001,"67/128/ 0"
2032     ^
2033 
2034     Note that Quicken will often put in a price with a bogus date and number.  We will ignore
2035     prices with bogus dates.  Hopefully that will catch all of these.
2036 
2037     Also note that prices can be in fractional units, e.g. 141 9/16.
2038 
2039   */
2040 
2041   QStringList::const_iterator it_line = m_qifEntry.constBegin();
2042 
2043   // Make a price for each line
2044   QRegExp priceExp("\"(.*)\",(.*),\"(.*)\"");
2045   while (it_line != m_qifEntry.constEnd()) {
2046     if (priceExp.indexIn(*it_line) != -1) {
2047       MyMoneyStatement::Price price;
2048       price.m_strSecurity = priceExp.cap(1);
2049       QString pricestr = priceExp.cap(2);
2050       QString datestr = priceExp.cap(3);
2051       qDebug() << "Price:" << price.m_strSecurity << " / " << pricestr << " / " << datestr;
2052 
2053       // Only add the price if the date is valid.  If invalid, fail silently.  See note above.
2054       // Also require the price value to not have any slashes.  Old prices will be something like
2055       // "25 9/16", which we do not support.  So we'll skip the price for now.
2056       QDate date = m_qifProfile.date(datestr);
2057       MyMoneyMoney rate(m_qifProfile.value('P', pricestr));
2058       if (date.isValid() && !rate.isZero()) {
2059         price.m_amount = rate;
2060         price.m_date = date;
2061         d->st.m_listPrices += price;
2062       }
2063     }
2064     ++it_line;
2065   }
2066 }
2067 
processSecurityEntry()2068 void MyMoneyQifReader::processSecurityEntry()
2069 {
2070   /*
2071   !Type:Security
2072   NVANGUARD 500 INDEX
2073   SVFINX
2074   TMutual Fund
2075   ^
2076   */
2077 
2078   MyMoneyStatement::Security security;
2079   security.m_strName = extractLine('N');
2080   security.m_strSymbol = extractLine('S');
2081 
2082   d->st.m_listSecurities += security;
2083 }
2084 
statementCount() const2085 int MyMoneyQifReader::statementCount() const
2086 {
2087   return d->statements.count();
2088 }
2089