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