/*************************************************************************** mymoneyqifreader.cpp ------------------- begin : Mon Jan 27 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyqifreader.h" #include "../mymoney/mymoneyfile.h" #include "../dialogs/kaccountselectdlg.h" #include "../kmymoney2.h" #include "kmymoneyglobalsettings.h" #include "mymoneystatementreader.h" #include // define this to debug the code. Using external filters // while debugging did not work too good for me, so I added // this code. // #define DEBUG_IMPORT #ifdef DEBUG_IMPORT #warning "DEBUG_IMPORT defined --> external filter not available!!!!!!!" #endif class MyMoneyQifReader::Private { public: Private() : accountType(MyMoneyAccount::Checkings), mapCategories(true) {} QString accountTypeToQif(MyMoneyAccount::accountTypeE type) const; /** * finalize the current statement and add it to the statement list */ void finishStatement(void); bool isTransfer(QString& name, const QString& leftDelim, const QString& rightDelim); /** * Converts the QIF specific N-record of investment transactions into * a category name */ QString typeToAccountName(const QString& type) const; /** * Converts the QIF reconcile state to the KMyMoney reconcile state */ MyMoneySplit::reconcileFlagE reconcileState(const QString& state) const; /** */ void fixMultiLineMemo(QString& memo) const; public: /** * the statement that is currently collected/processed */ MyMoneyStatement st; /** * the list of all statements to be sent to MyMoneyStatementReader */ QValueList statements; /** * a list of already used hashes in this file */ QMap m_hashMap; QString st_AccountName; QString st_AccountId; MyMoneyAccount::accountTypeE accountType; bool firstTransaction; bool mapCategories; MyMoneyQifReader::QifEntryTypeE transactionType; }; void MyMoneyQifReader::Private::fixMultiLineMemo(QString& memo) const { memo.replace("\\n", "\n"); } void MyMoneyQifReader::Private::finishStatement(void) { // in case we have collected any data in the statement, we keep it if((st.m_listTransactions.count() + st.m_listPrices.count() + st.m_listSecurities.count()) > 0) { statements += st; qDebug("Statement with %d transactions, %d prices and %d securities added to the statement list", st.m_listTransactions.count(), st.m_listPrices.count(), st.m_listSecurities.count()); } // start with a fresh statement st = MyMoneyStatement(); st.m_skipCategoryMatching = !mapCategories; st.m_eType = (transactionType == MyMoneyQifReader::EntryTransaction) ? MyMoneyStatement::etCheckings : MyMoneyStatement::etInvestment; } QString MyMoneyQifReader::Private::accountTypeToQif(MyMoneyAccount::accountTypeE type) const { QString rc = "Bank"; switch(type) { default: break; case MyMoneyAccount::Cash: rc = "Cash"; break; case MyMoneyAccount::CreditCard: rc = "CCard"; break; case MyMoneyAccount::Asset: rc = "Oth A"; break; case MyMoneyAccount::Liability: rc = "Oth L"; break; case MyMoneyAccount::Investment: rc = "Port"; break; } return rc; } QString MyMoneyQifReader::Private::typeToAccountName(const QString& type) const { if(type == "reinvdiv") return i18n("Category name", "Reinvested dividend"); if(type == "reinvlg") return i18n("Category name", "Reinvested dividend (long term)"); if(type == "reinvsh") return i18n("Category name", "Reinvested dividend (short term)"); if (type == "div") return i18n("Category name", "Dividend"); if(type == "intinc") return i18n("Category name", "Interest"); if(type == "cgshort") return i18n("Category name", "Capital Gain (short term)"); if( type == "cgmid") return i18n("Category name", "Capital Gain (mid term)"); if(type == "cglong") return i18n("Category name", "Capital Gain (long term)"); if(type == "rtrncap") return i18n("Category name", "Returned capital"); if(type == "miscinc") return i18n("Category name", "Miscellaneous income"); if(type == "miscexp") return i18n("Category name", "Miscellaneous expense"); if(type == "sell" || type == "buy") return i18n("Category name", "Investment fees"); return i18n("Unknown QIF type %1").arg(type); } bool MyMoneyQifReader::Private::isTransfer(QString& tmp, const QString& leftDelim, const QString& rightDelim) { // it's a transfer, extract the account name // I've seen entries like this // // S[Mehrwertsteuer]/_VATCode_N_I // // so extracting is a bit more complex and we use a regexp for it QRegExp exp(QString("\\%1(.*)\\%2(.*)").arg(leftDelim, rightDelim)); bool rc; if((rc = (exp.search(tmp) != -1)) == true) { tmp = exp.cap(1)+exp.cap(2); tmp = tmp.stripWhiteSpace(); } return rc; } MyMoneySplit::reconcileFlagE MyMoneyQifReader::Private::reconcileState(const QString& state) const { if(state == "X" || state == "R") // Reconciled return MyMoneySplit::Reconciled; if(state == "*") // Cleared return MyMoneySplit::Cleared; return MyMoneySplit::NotReconciled; } MyMoneyQifReader::MyMoneyQifReader() : d(new Private) { m_skipAccount = false; m_transactionsProcessed = m_transactionsSkipped = 0; m_progressCallback = 0; m_file = 0; m_entryType = EntryUnknown; m_processingData = false; m_userAbort = false; m_warnedInvestment = false; m_warnedSecurity = false; m_warnedPrice = false; connect(&m_filter, SIGNAL(wroteStdin(KProcess*)), this, SLOT(slotSendDataToFilter())); connect(&m_filter, SIGNAL(receivedStdout(KProcess*, char*, int)), this, SLOT(slotReceivedDataFromFilter(KProcess*, char*, int))); connect(&m_filter, SIGNAL(processExited(KProcess*)), this, SLOT(slotImportFinished())); connect(&m_filter, SIGNAL(receivedStderr(KProcess*, char*, int)), this, SLOT(slotReceivedErrorFromFilter(KProcess*, char*, int))); } MyMoneyQifReader::~MyMoneyQifReader() { if(m_file) delete m_file; delete d; } void MyMoneyQifReader::setCategoryMapping(bool map) { d->mapCategories = map; } void MyMoneyQifReader::setURL(const KURL& url) { m_url = url; } void MyMoneyQifReader::setProfile(const QString& profile) { m_qifProfile.loadProfile("Profile-" + profile); } void MyMoneyQifReader::slotSendDataToFilter(void) { Q_LONG len; if(m_file->atEnd()) { // m_filter.flushStdin(); m_filter.closeStdin(); } else { len = m_file->readBlock(m_buffer, sizeof(m_buffer)); if(len == -1) { qWarning("Failed to read block from QIF import file"); m_filter.closeStdin(); m_filter.kill(); } else { m_filter.writeStdin(m_buffer, len); } } } void MyMoneyQifReader::slotReceivedErrorFromFilter(KProcess* /* proc */, char *buff, int len) { QByteArray data; data.duplicate(buff, len); qWarning("%s",static_cast(data)); } void MyMoneyQifReader::slotReceivedDataFromFilter(KProcess* /* proc */, char *buff, int len) { m_pos += len; // signalProgress(m_pos, 0); while(len) { // process char if(*buff == '\n' || *buff == '\r') { // found EOL if(!m_lineBuffer.isEmpty()) { m_qifLines << QString::fromUtf8(m_lineBuffer.stripWhiteSpace()); } m_lineBuffer = QCString(); } else { // collect all others m_lineBuffer += (*buff); } ++buff; --len; } } void MyMoneyQifReader::slotImportFinished(void) { // check if the last EOL char was missing and add the trailing line if(!m_lineBuffer.isEmpty()) { m_qifLines << QString::fromUtf8(m_lineBuffer.stripWhiteSpace()); } qDebug("Read %d bytes", m_pos); QTimer::singleShot(0, this, SLOT(slotProcessData())); } void MyMoneyQifReader::slotProcessData(void) { signalProgress(-1, -1); // scan the file and try to determine numeric and date formats m_qifProfile.autoDetect(m_qifLines); // the detection is accurate for numeric values, but it could be // that the dates were too ambiguous so that we have to let the user // decide which one to pick. QStringList dateFormats; m_qifProfile.possibleDateFormats(dateFormats); QStringList list; if(dateFormats.count() > 1) { list << dateFormats.first(); bool ok; list = KInputDialog::getItemList(i18n("Date format selection"), i18n("Pick the date format that suits your input file"), dateFormats, list, false, &ok); if(!ok) { m_userAbort = true; } } else list = dateFormats; m_qifProfile.setInputDateFormat(list.first()); qDebug("Selected date format: '%s'", list.first().data()); signalProgress(0, m_qifLines.count(), i18n("Importing QIF ...")); QStringList::iterator it; for(it = m_qifLines.begin(); m_userAbort == false && it != m_qifLines.end(); ++it) { ++m_linenumber; // qDebug("Proc: '%s'", (*it).data()); if((*it).startsWith("!")) { processQifSpecial(*it); m_qifEntry.clear(); } else if(*it == "^") { if(m_qifEntry.count() > 0) { signalProgress(m_linenumber, 0); processQifEntry(); m_qifEntry.clear(); } } else { m_qifEntry += *it; } } d->finishStatement(); qDebug("%d lines processed", m_linenumber); signalProgress(-1, -1); emit importFinished(); } bool MyMoneyQifReader::startImport(void) { bool rc = false; d->st = MyMoneyStatement(); d->st.m_skipCategoryMatching = !d->mapCategories; m_dontAskAgain.clear(); m_accountTranslation.clear(); m_userAbort = false; m_pos = 0; m_linenumber = 0; m_filename = QString::null; m_data.clear(); if(!KIO::NetAccess::download(m_url, m_filename, NULL)) { KMessageBox::detailedError(0, i18n("Error while loading file '%1'!").arg(m_url.prettyURL()), KIO::NetAccess::lastErrorString(), i18n("File access error")); return false; } m_file = new QFile(m_filename); if(m_file->open(IO_ReadOnly)) { #ifdef DEBUG_IMPORT Q_LONG len; while(!m_file->atEnd()) { len = m_file->readBlock(m_buffer, sizeof(m_buffer)); if(len == -1) { qWarning("Failed to read block from QIF import file"); } else { slotReceivedDataFromFilter(0, m_buffer, len); } } slotImportFinished(); #else // start filter process, use 'cat -' as the default filter m_filter.clearArguments(); if(m_qifProfile.filterScriptImport().isEmpty()) { m_filter << "cat"; m_filter << "-"; } else { m_filter << QStringList::split(" ", m_qifProfile.filterScriptImport(), true); } m_entryType = EntryUnknown; if(m_filter.start(KProcess::NotifyOnExit, KProcess::All)) { m_filter.resume(); signalProgress(0, m_file->size(), i18n("Reading QIF ...")); slotSendDataToFilter(); rc = true; } else { qDebug("starting filter failed :-("); } #endif } return rc; } bool MyMoneyQifReader::finishImport(void) { bool rc = false; #ifdef DEBUG_IMPORT delete m_file; m_file = 0; // remove the Don't ask again entries KConfig* config = KGlobal::config(); config->setGroup(QString::fromLatin1("Notification Messages")); QStringList::ConstIterator it; for(it = m_dontAskAgain.begin(); it != m_dontAskAgain.end(); ++it) { config->deleteEntry(*it); } config->sync(); m_dontAskAgain.clear(); m_accountTranslation.clear(); signalProgress(-1, -1); rc = !m_userAbort; #else if(!m_filter.isRunning()) { delete m_file; m_file = 0; // remove the Don't ask again entries KConfig* config = KGlobal::config(); config->setGroup(QString::fromLatin1("Notification Messages")); QStringList::ConstIterator it; for(it = m_dontAskAgain.begin(); it != m_dontAskAgain.end(); ++it) { config->deleteEntry(*it); } config->sync(); m_dontAskAgain.clear(); m_accountTranslation.clear(); signalProgress(-1, -1); rc = !m_userAbort && m_filter.normalExit(); } else { qWarning("MyMoneyQifReader::finishImport() must not be called while the filter\n\tprocess is still running."); } #endif // if a temporary file was constructed by NetAccess::download, // then it will be removed with the next call. Otherwise, it // stays untouched on the local filesystem KIO::NetAccess::removeTempFile(m_filename); #if 0 // Add the transaction entries KProgressDialog dlg(0,"transactionaddprogress",i18n("Adding transactions"),i18n("Now adding the transactions to your ledger...")); dlg.progressBar()->setTotalSteps(m_transactionCache.count()); dlg.progressBar()->setTextEnabled(true); dlg.setAllowCancel(true); dlg.show(); kapp->processEvents(); MyMoneyFile* file = MyMoneyFile::instance(); QValueList::iterator it = m_transactionCache.begin(); MyMoneyFileTransaction ft; try { while( it != m_transactionCache.end() ) { if ( dlg.wasCancelled() ) { m_userAbort = true; rc = false; break; } file->addTransaction(*it); dlg.progressBar()->advance(1); ++it; } if(rc) ft.commit(); } catch(MyMoneyException *e) { KMessageBox::detailedSorry(0, i18n("Unable to add transactions"), (e->what() + " " + i18n("thrown in") + " " + e->file()+ ":%1").arg(e->line())); delete e; rc = false; } #endif // Now to import the statements QValueList::const_iterator it_st; for(it_st = d->statements.begin(); it_st != d->statements.end(); ++it_st) kmymoney2->slotStatementImport(*it_st); return rc; } void MyMoneyQifReader::processQifSpecial(const QString& _line) { QString line = _line.mid(1); // get rid of exclamation mark // QString test = line.left(5).lower(); if(line.left(5).lower() == QString("type:")) { line = line.mid(5); // exportable accounts if(line.lower() == "ccard" || KMyMoneyGlobalSettings::qifCreditCard().lower().contains(line.lower())) { d->accountType = MyMoneyAccount::CreditCard; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if(line.lower() == "bank" || KMyMoneyGlobalSettings::qifBank().lower().contains(line.lower())) { d->accountType = MyMoneyAccount::Checkings; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if(line.lower() == "cash" || KMyMoneyGlobalSettings::qifCash().lower().contains(line.lower())) { d->accountType = MyMoneyAccount::Cash; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if(line.lower() == "oth a" || KMyMoneyGlobalSettings::qifAsset().lower().contains(line.lower())) { d->accountType = MyMoneyAccount::Asset; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if(line.lower() == "oth l" || line.lower() == i18n("QIF tag for liability account", "Oth L").lower()) { d->accountType = MyMoneyAccount::Liability; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if(line.lower() == "invst" || line.lower() == i18n("QIF tag for investment account", "Invst").lower()) { d->transactionType = m_entryType = EntryInvestmentTransaction; } else if(line.lower() == "invoice" || KMyMoneyGlobalSettings::qifInvoice().lower().contains(line.lower())) { m_entryType = EntrySkip; } else if(line.lower() == "tax") { m_entryType = EntrySkip; } else if(line.lower() == "bill") { m_entryType = EntrySkip; // exportable lists } else if(line.lower() == "cat" || line.lower() == i18n("QIF tag for category", "Cat").lower()) { m_entryType = EntryCategory; } else if(line.lower() == "security" || line.lower() == i18n("QIF tag for security", "Security").lower()) { m_entryType = EntrySecurity; } else if(line.lower() == "prices" || line.lower() == i18n("QIF tag for prices", "Prices").lower()) { m_entryType = EntryPrice; } else if(line.lower() == "payee") { m_entryType = EntryPayee; } else if(line.lower() == "class" || line.lower() == i18n("QIF tag for a class", "Class").lower()) { m_entryType = EntryClass; } else if(line.lower() == "memorized") { m_entryType = EntryMemorizedTransaction; } else if(line.lower() == "budget") { m_entryType = EntrySkip; } else if(line.lower() == "invitem") { m_entryType = EntrySkip; } else if(line.lower() == "template") { m_entryType = EntrySkip; } else { qWarning("Unknown export header '!Type:%s' in QIF file on line %d: Skipping section.", line.data(), m_linenumber); m_entryType = EntrySkip; } // account headers } else if(line.lower() == "account") { m_entryType = EntryAccount; } else if(line.lower() == "option:autoswitch") { m_entryType = EntryAccount; } else if(line.lower() == "clear:autoswitch") { m_entryType = d->transactionType; } } void MyMoneyQifReader::processQifEntry(void) { // This method processes a 'QIF Entry' which is everything between two caret // signs // try { switch(m_entryType) { case EntryCategory: processCategoryEntry(); break; case EntryUnknown: kdDebug(2) << "Line " << m_linenumber << ": Warning: Found an entry without a type being specified. Checking assumed." << endl; processTransactionEntry(); break; case EntryTransaction: processTransactionEntry(); break; case EntryInvestmentTransaction: processInvestmentTransactionEntry(); break; case EntryAccount: processAccountEntry(); break; case EntrySecurity: processSecurityEntry(); break; case EntryPrice: processPriceEntry(); break; case EntryPayee: processPayeeEntry(); break; case EntryClass: kdDebug(2) << "Line " << m_linenumber << ": Classes are not yet supported!" << endl; break; case EntryMemorizedTransaction: kdDebug(2) << "Line " << m_linenumber << ": Memorized transactions are not yet implemented!" << endl; break; case EntrySkip: break; default: kdDebug(2) << "Line " << m_linenumber<< ": EntryType " << m_entryType <<" not yet implemented!" << endl; break; } } catch(MyMoneyException *e) { if(e->what() != "USERABORT") { kdDebug(2) << "Line " << m_linenumber << ": Unhandled error: " << e->what() << endl; } else { m_userAbort = true; } delete e; } } const QString MyMoneyQifReader::extractLine(const QChar id, int cnt) { QStringList::ConstIterator it; m_extractedLine = -1; for(it = m_qifEntry.begin(); it != m_qifEntry.end(); ++it) { m_extractedLine++; if((*it)[0] == id) { if(cnt-- == 1) { if((*it).mid(1).isEmpty()) return QString(" "); return (*it).mid(1); } } } m_extractedLine = -1; return QString(); } void MyMoneyQifReader::extractSplits(QValueList& listqSplits) const { // *** With apologies to QString MyMoneyQifReader::extractLine *** QStringList::ConstIterator it; for(it = m_qifEntry.begin(); it != m_qifEntry.end(); ++it) { if((*it)[0] == "S") { qSplit q; q.m_strCategoryName = (*it++).mid(1); // 'S' if((*it)[0] == "E") { q.m_strMemo = (*it++).mid(1); // 'E' d->fixMultiLineMemo(q.m_strMemo); } if((*it)[0] == "$") { q.m_amount = (*it).mid(1); // '$' } listqSplits += q; } } } #if 0 void MyMoneyQifReader::processMSAccountEntry(const MyMoneyAccount::accountTypeE accountType) { if(extractLine('P').lower() == m_qifProfile.openingBalanceText().lower()) { m_account = MyMoneyAccount(); m_account.setAccountType(accountType); QString txt = extractLine('T'); MyMoneyMoney balance = m_qifProfile.value('T', txt); QDate date = m_qifProfile.date(extractLine('D')); m_account.setOpeningDate(date); QString name = extractLine('L'); if(name.left(1) == m_qifProfile.accountDelimiter().left(1)) { name = name.mid(1, name.length()-2); } d->st_AccountName = name; m_account.setName(name); selectOrCreateAccount(Select, m_account, balance); d->st.m_accountId = m_account.id(); if ( ! balance.isZero() ) { MyMoneyFile* file = MyMoneyFile::instance(); QString openingtxid = file->openingBalanceTransaction(m_account); MyMoneyFileTransaction ft; if ( ! openingtxid.isEmpty() ) { MyMoneyTransaction openingtx = file->transaction(openingtxid); MyMoneySplit split = openingtx.splitByAccount(m_account.id()); if ( split.shares() != balance ) { const MyMoneySecurity& sec = file->security(m_account.currencyId()); if ( KMessageBox::questionYesNo( qApp->mainWidget(), 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?").arg(m_account.name(), split.shares().formatMoney(m_account, sec),balance.formatMoney(m_account, sec)), i18n("Overwrite opening balance"), KStdGuiItem::yes(), KStdGuiItem::no(), "OverwriteOpeningBalance" ) == KMessageBox::Yes ) { file->removeTransaction( openingtx ); m_account.setOpeningDate( date ); file->createOpeningBalanceTransaction( m_account, balance ); } } } else { // Add an opening balance m_account.setOpeningDate( date ); file->createOpeningBalanceTransaction( m_account, balance ); } ft.commit(); } } else { // for some unknown reason, Quicken 2001 generates the following (somewhat // misleading) sequence of lines: // // 1: !Account // 2: NAT&T Universal // 3: DAT&T Univers(...xxxx) [CLOSED] // 4: TCCard // 5: ^ // 6: !Type:CCard // 7: !Account // 8: NCFCU Visa // 9: DRick's CFCU Visa card (...xxxx) // 10: TCCard // 11: ^ // 12: !Type:CCard // 13: D1/ 4' 1 // // Lines 1-5 are processed via processQifEntry() and processAccountEntry() // Then Quicken issues line 6 but since the account does not carry any // transaction does not write an end delimiter. Arrrgh! So we end up with // a QIF entry comprising of lines 6-11 and end up in this routine. Actually, // lines 7-11 are the leadin for the next account. So we check here if // the !Type:xxx record also contains an !Account line and process the // entry as required. // // (Ace) I think a better solution here is to handle exclamation point // lines separately from entries. In the above case: // Line 1 would set the mode to "account entries". // Lines 2-5 would be interpreted as an account entry. This would set m_account. // Line 6 would set the mode to "cc transaction entries". // Line 7 would immediately set the mode to "account entries" again // Lines 8-11 would be interpreted as an account entry. This would set m_account. // Line 12 would set the mode to "cc transaction entries" // Lines 13+ would be interpreted as cc transaction entries, and life is good int exclamationCnt = 1; QString category; do { category = extractLine('!', exclamationCnt++); } while(!category.isEmpty() && category != "Account"); // we have such a weird empty account if(category == "Account") { processAccountEntry(); } else { selectOrCreateAccount(Select, m_account); d->st_AccountName = m_account.name(); d->st.m_strAccountName = m_account.name(); d->st.m_accountId = m_account.id(); d->st.m_strAccountNumber = m_account.id(); m_account.setNumber(m_account.id()); if ( m_entryType == EntryInvestmentTransaction ) processInvestmentTransactionEntry(); else processTransactionEntry(); } } } #endif void MyMoneyQifReader::processPayeeEntry(void) { // TODO } void MyMoneyQifReader::processCategoryEntry(void) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account = MyMoneyAccount(); account.setName(extractLine('N')); account.setDescription(extractLine('D')); MyMoneyAccount parentAccount; if(!extractLine('I').isEmpty()) { account.setAccountType(MyMoneyAccount::Income); parentAccount = file->income(); } else if(!extractLine('E').isEmpty()) { account.setAccountType(MyMoneyAccount::Expense); parentAccount = file->expense(); } // check if we can find the account already in the file MyMoneyAccount acc = kmymoney2->findAccount(account, MyMoneyAccount()); // if not, we just create it if(acc.id().isEmpty()) { MyMoneyAccount brokerage; MyMoneyMoney balance; kmymoney2->createAccount(account, parentAccount, brokerage, balance); } } QString MyMoneyQifReader::transferAccount(QString name, bool useBrokerage) { QString accountId; QStringList tmpEntry = m_qifEntry; // keep temp copies MyMoneyAccount tmpAccount = m_account; m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("Tunknown"); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); accountId = processAccountEntry(false); // in case we found a reference to an investment account, we need // to switch to the brokerage account instead. MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); if(useBrokerage && (acc.accountType() == MyMoneyAccount::Investment)) { name = acc.brokerageName(); m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("Tunknown"); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); accountId = processAccountEntry(false); } m_qifEntry = tmpEntry; // restore local copies m_account = tmpAccount; return accountId; } void MyMoneyQifReader::createOpeningBalance(MyMoneyAccount::_accountTypeE accType) { MyMoneyFile* file = MyMoneyFile::instance(); // if we don't have a name for the current account we need to extract the name from the L-record if(m_account.name().isEmpty()) { QString name = extractLine('L'); if(name.isEmpty()) { name = i18n("QIF imported, no account name supplied"); } d->isTransfer(name, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1,1)); QStringList entry = m_qifEntry; // keep a temp copy m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("T%1").arg(d->accountTypeToQif(accType)); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); processAccountEntry(); m_qifEntry = entry; // restore local copy } MyMoneyFileTransaction ft; try { bool needCreate = true; MyMoneyAccount acc = m_account; // in case we're dealing with an investment account, we better use // the accompanying brokerage account for the opening balance acc = file->accountByName(m_account.brokerageName()); // check if we already have an opening balance transaction QString tid = file->openingBalanceTransaction(acc); MyMoneyTransaction ot; if(!tid.isEmpty()) { ot = file->transaction(tid); MyMoneySplit s0 = ot.splitByAccount(acc.id()); // if the value is the same, we can silently skip this transaction if(s0.shares() == m_qifProfile.value('T', extractLine('T'))) { needCreate = false; } if(needCreate) { // in case we create it anyway, we issue a warning to the user to check it manually KMessageBox::sorry(0, QString("%1").arg(i18n("KMyMoney has imported a second opening balance transaction into account %1 which differs from the one found already on file. Please correct this manually once the import is done.").arg(acc.name())), i18n("Opening balance problem")); } } if(needCreate) { acc.setOpeningDate(m_qifProfile.date(extractLine('D'))); file->modifyAccount(acc); MyMoneyTransaction t = file->createOpeningBalanceTransaction(acc, m_qifProfile.value('T', extractLine('T'))); if(!t.id().isEmpty()) { t.setImported(); file->modifyTransaction(t); } ft.commit(); } // make sure to use the updated version of the account if(m_account.id() == acc.id()) m_account = acc; // remember which account we created d->st.m_accountId = m_account.id(); } catch(MyMoneyException* e) { KMessageBox::detailedError(0, i18n("Error while creating opening balance transaction"), QString("%1(%2):%3").arg(e->file()).arg(e->line()).arg(e->what()), i18n("File access error")); delete e; } } void MyMoneyQifReader::processTransactionEntry(void) { ++m_transactionsProcessed; // in case the user selected to skip the account or the account // was not found we skip this transaction /* if(m_account.id().isEmpty()) { m_transactionsSkipped++; return; } */ MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyStatement::Split s1; MyMoneyStatement::Transaction tr; QString tmp; QString accountId; int pos; QString payee = extractLine('P'); unsigned long h; h = MyMoneyTransaction::hash(m_qifEntry.join(";")); QString hashBase; hashBase.sprintf("%s-%07lx", m_qifProfile.date(extractLine('D')).toString(Qt::ISODate).data(), h); int idx = 1; QString hash; for(;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.find(hash); if(it == d->m_hashMap.end()) { d->m_hashMap[hash] = true; break; } ++idx; } tr.m_strBankID = hash; if(d->firstTransaction) { // check if this is an opening balance transaction and process it out of the statement if(!payee.isEmpty() && ((payee.lower() == "opening balance") || KMyMoneyGlobalSettings::qifOpeningBalance().lower().contains(payee.lower()))) { createOpeningBalance(); d->firstTransaction = false; return; } } // Process general transaction data if(d->st.m_accountId.isEmpty()) d->st.m_accountId = m_account.id(); s1.m_accountId = d->st.m_accountId; d->st.m_eType = MyMoneyStatement::etCheckings; tr.m_datePosted = (m_qifProfile.date(extractLine('D'))); if(!tr.m_datePosted.isValid()) { int rc = KMessageBox::warningContinueCancel(0, i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " "date profile setting of \"%2\".\n\nPressing \"Continue\" will " "assign todays date to the transaction. Pressing \"Cancel\" will abort " "the import operation. You can then restart the import and select a different " "QIF profile or create a new one.") .arg(extractLine('D')).arg(m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch(rc) { case KMessageBox::Continue: tr.m_datePosted = (QDate::currentDate()); break; case KMessageBox::Cancel: throw new MYMONEYEXCEPTION("USERABORT"); break; } } tmp = extractLine('L'); pos = tmp.findRev("--"); if(tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) { // it's a transfer, so we wipe the memo // tmp = ""; why?? // st.m_strAccountName = tmp; } else if(pos != -1) { // what's this? // t.setValue("Dialog", tmp.mid(pos+2)); tmp = tmp.left(pos); } // t.setMemo(tmp); // Assign the "#" field to the transaction's bank id // This is the custom KMM extension to QIF for a unique ID tmp = extractLine('#'); if(!tmp.isEmpty()) { tr.m_strBankID = QString("ID %1").arg(tmp); } #if 0 // Collect data for the account's split s1.m_accountId = m_account.id(); tmp = extractLine('S'); pos = tmp.findRev("--"); if(pos != -1) { tmp = tmp.left(pos); } if(tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) // it's a transfer, extract the account name tmp = tmp.mid(1, tmp.length()-2); s1.m_strCategoryName = tmp; #endif // TODO (Ace) Deal with currencies more gracefully. QIF cannot deal with multiple // currencies, so we should assume that transactions imported into a given // account are in THAT ACCOUNT's currency. If one of those involves a transfer // to an account with a different currency, value and shares should be // different. (Shares is in the target account's currency, value is in the // transaction's) s1.m_amount = m_qifProfile.value('T', extractLine('T')); tr.m_amount = m_qifProfile.value('T', extractLine('T')); tr.m_shares = m_qifProfile.value('T', extractLine('T')); tmp = extractLine('N'); if (!tmp.isEmpty()) tr.m_strNumber = tmp; if(!payee.isEmpty()) { tr.m_strPayee = payee; } tr.m_reconcile = d->reconcileState(extractLine('C')); tr.m_strMemo = extractLine('M'); d->fixMultiLineMemo(tr.m_strMemo); s1.m_strMemo = tr.m_strMemo; // tr.m_listSplits.append(s1); if(extractLine('$').isEmpty()) { MyMoneyAccount account; // use the same values for the second split, but clear the ID and reverse the value MyMoneyStatement::Split s2 = s1; s2.m_reconcile = tr.m_reconcile; s2.m_amount = (-s1.m_amount); // s2.clearId(); // standard transaction tmp = extractLine('L'); if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { accountId = transferAccount(tmp, false); } else { /* pos = tmp.findRev("--"); if(pos != -1) { t.setValue("Dialog", tmp.mid(pos+2)); tmp = tmp.left(pos); }*/ // it's an expense / income tmp = tmp.stripWhiteSpace(); accountId = checkCategory(tmp, s1.m_amount, s2.m_amount); } if(!accountId.isEmpty()) { try { MyMoneyAccount account = file->account(accountId); // FIXME: check that the type matches and ask if not if ( account.accountType() == MyMoneyAccount::Investment ) { kdDebug(0) << "Line " << m_linenumber << ": Cannot transfer to/from an investment account. Transaction ignored." << endl; return; } if ( account.id() == m_account.id() ) { kdDebug(0) << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored." << endl; accountId = QString(); } } catch (MyMoneyException *e) { kdDebug(0) << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found" << endl; accountId = QString(); delete e; } } if(!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = tmp; tr.m_listSplits.append(s2); } } else { // split transaction QValueList listqSplits; extractSplits(listqSplits); // ****** ensure each field is ****** // * attached to correct split * int count; for(count = 1; !extractLine('$', count).isEmpty(); ++count) { MyMoneyStatement::Split s2 = s1; s2.m_amount = (-m_qifProfile.value('$', listqSplits[count-1].m_amount)); // Amount of split s2.m_strMemo = listqSplits[count-1].m_strMemo; // Memo in split tmp = listqSplits[count-1].m_strCategoryName; // Category in split if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { accountId = transferAccount(tmp, false); } else { pos = tmp.findRev("--"); if(pos != -1) { /// t.setValue("Dialog", tmp.mid(pos+2)); tmp = tmp.left(pos); } tmp = tmp.stripWhiteSpace(); accountId = checkCategory(tmp, s1.m_amount, s2.m_amount); } if(!accountId.isEmpty()) { try { MyMoneyAccount account = file->account(accountId); // FIXME: check that the type matches and ask if not if ( account.accountType() == MyMoneyAccount::Investment ) { kdDebug(0) << "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"; tr.m_amount += s2.m_amount; continue; } if ( account.id() == m_account.id() ) { kdDebug(0) << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored." << endl; accountId = QString(); } } catch (MyMoneyException *e) { kdDebug(0) << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found" << endl; accountId = QString(); delete e; } } if(!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = tmp; tr.m_listSplits += s2; // in case the transaction does not have a memo and we // process the first split just copy the memo over if(tr.m_listSplits.count() == 1 && tr.m_strMemo.isEmpty()) tr.m_strMemo = s2.m_strMemo; } else { // TODO add an option to create a "Unassigned" category // for now, we just drop the split which will show up as unbalanced // transaction in the KMyMoney ledger view } } } // Add the transaction to the statement d->st.m_listTransactions +=tr; } void MyMoneyQifReader::processInvestmentTransactionEntry(void) { // kdDebug(2) << "Investment Transaction:" << m_qifEntry.count() << " lines" << endl; /* Items for Investment Accounts Field Indicator Explanation D Date N Action Y Security (NAME, not symbol) I Price Q Quantity (number of shares or split ratio) T Transaction amount C Cleared status P Text in the first line for transfers and reminders (Payee) M Memo O Commission L Account for the transfer $ Amount transferred ^ End of the entry It will be presumed all transactions are to the associated cash account, if one exists, unless otherwise noted by the 'L' field. Expense/Income categories will be automatically generated, "_Dividend", "_InterestIncome", etc. */ MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyStatement::Transaction tr; d->st.m_eType = MyMoneyStatement::etInvestment; // t.setCommodity(m_account.currencyId()); // 'D' field: Date QDate date = m_qifProfile.date(extractLine('D')); if(date.isValid()) tr.m_datePosted = date; else { int rc = KMessageBox::warningContinueCancel(0, i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " "date profile setting of \"%2\".\n\nPressing \"Continue\" will " "assign todays date to the transaction. Pressing \"Cancel\" will abort " "the import operation. You can then restart the import and select a different " "QIF profile or create a new one.") .arg(extractLine('D')).arg(m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch(rc) { case KMessageBox::Continue: tr.m_datePosted = QDate::currentDate(); break; case KMessageBox::Cancel: throw new MYMONEYEXCEPTION("USERABORT"); break; } } // 'M' field: Memo QString memo = extractLine('M'); d->fixMultiLineMemo(memo); tr.m_strMemo = memo; unsigned long h; h = MyMoneyTransaction::hash(m_qifEntry.join(";")); QString hashBase; hashBase.sprintf("%s-%07lx", m_qifProfile.date(extractLine('D')).toString(Qt::ISODate).data(), h); int idx = 1; QString hash; for(;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.find(hash); if(it == d->m_hashMap.end()) { d->m_hashMap[hash] = true; break; } ++idx; } tr.m_strBankID = hash; // '#' field: BankID QString tmp = extractLine('#'); if ( ! tmp.isEmpty() ) tr.m_strBankID = QString("ID %1").arg(tmp); // Reconciliation flag tr.m_reconcile = d->reconcileState(extractLine('C')); // 'O' field: Fees tr.m_fees = m_qifProfile.value('T', extractLine('O')); // 'T' field: Amount MyMoneyMoney amount = m_qifProfile.value('T', extractLine('T')); tr.m_amount = amount; MyMoneyStatement::Price price; price.m_date = date; price.m_strSecurity = extractLine('Y'); price.m_amount = m_qifProfile.value('T', extractLine('I')); #if 0 // we must check for that later, because certain activities don't need a security // 'Y' field: Security name QString securityname = extractLine('Y').lower(); if ( securityname.isEmpty() ) { kdDebug(2) << "Line " << m_linenumber << ": Investment transaction without a security is not supported." << endl; return; } tr.m_strSecurity = securityname; #endif #if 0 // For now, we let the statement reader take care of that. // The big problem here is that the Y field is not the SYMBOL, it's the NAME. // The name is not very unique, because people could have used slightly different // abbreviations or ordered words differently, etc. // // If there is a perfect name match with a subordinate stock account, great. // More likely, we have to rely on the QIF file containing !Type:Security // records, which tell us the mapping from name to symbol. // // Therefore, generally it is not recommended to import a QIF file containing // investment transactions but NOT containing security records. QString securitysymbol = m_investmentMap[securityname]; // the correct account is the stock account which matches two criteria: // (1) it is a sub-account of the selected investment account, and either // (2a) the security name of the transaction matches the name of the security, OR // (2b) the security name of the transaction maps to a symbol which matches the symbol of the security // search through each subordinate account bool found = false; MyMoneyAccount thisaccount = m_account; QStringList accounts = thisaccount.accountList(); QStringList::const_iterator it_account = accounts.begin(); while( !found && it_account != accounts.end() ) { QString currencyid = file->account(*it_account).currencyId(); MyMoneySecurity security = file->security( currencyid ); QString symbol = security.tradingSymbol().lower(); QString name = security.name().lower(); if ( securityname == name || securitysymbol == symbol ) { d->st_AccountId = *it_account; s1.m_accountId = *it_account; thisaccount = file->account(*it_account); found = true; #if 0 // update the price, while we're here. in the future, this should be // an option QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price( currencyid, basecurrencyid, t_in.m_datePosted, true ); if ( !price.isValid() ) { MyMoneyPrice newprice( currencyid, basecurrencyid, t_in.m_datePosted, t_in.m_moneyAmount / t_in.m_dShares, i18n("Statement Importer") ); file->addPrice(newprice); } #endif } ++it_account; } if (!found) { kdDebug(2) << "Line " << m_linenumber << ": Security " << securityname << " not found in this account. Transaction ignored." << endl; // If the security is not known, notify the user // TODO (Ace) A "SelectOrCreateAccount" interface for investments KMessageBox::information(0, i18n("This investment account does not contain the \"%1\" security. " "Transactions involving this security will be ignored.").arg(securityname), i18n("Security not found"), QString("MissingSecurity%1").arg(securityname.stripWhiteSpace())); return; } #endif // 'Y' field: Security tr.m_strSecurity = extractLine('Y'); // 'Q' field: Quantity MyMoneyMoney quantity = m_qifProfile.value('T', extractLine('Q')); // 'N' field: Action QString action = extractLine('N').lower(); // remove trailing X, which seems to have no purpose (?!) bool xAction = false; if ( action.endsWith("x") ) { action = action.left( action.length() - 1 ); xAction = true; } // Whether to create a cash split for the other side of the value QString accountname ;//= extractLine('L'); if ( action == "reinvdiv" || action == "reinvlg" || action == "reinvsh" ) { d->st.m_listPrices += price; tr.m_shares = quantity; tr.m_eAction = (MyMoneyStatement::Transaction::eaReinvestDividend); tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_strInterestCategory = extractLine('L'); if(tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } } else if ( action == "div" || action == "cgshort" || action == "cgmid" || action == "cglong" || action == "rtrncap") { tr.m_eAction = (MyMoneyStatement::Transaction::eaCashDividend); QString tmp = extractLine('L'); // if the action ends in an X, the L-Record contains the asset account // to which the dividend should be transferred. In the other cases, it // may contain a category that identifies the income category for the // dividend payment if((xAction == true) && (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true)) { tr.m_strBrokerageAccount = tmp; transferAccount(tmp); // make sure the account exists } else { tr.m_strInterestCategory = tmp; } // make sure, we have valid category. Either taken from the L-Record above, // or derived from the action code if(tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } // For historic reasons (coming from the OFX importer) the statement // reader expects the dividend with a reverse sign. So we just do that. tr.m_amount = -(amount - tr.m_fees); // We need an extra split which will be the zero-amount investment split // that serves to mark this transaction as a cash dividend and note which // stock account it belongs to. MyMoneyStatement::Split s2; s2.m_amount = MyMoneyMoney(); s2.m_strCategoryName = extractLine('Y'); tr.m_listSplits.append(s2); } else if ( action == "intinc" || action == "miscinc" || action == "miscexp") { tr.m_eAction = (MyMoneyStatement::Transaction::eaInterest); if(action == "miscexp") tr.m_eAction = (MyMoneyStatement::Transaction::eaFees); QString tmp = extractLine('L'); // if the action ends in an X, the L-Record contains the asset account // to which the dividend should be transferred. In the other cases, it // may contain a category that identifies the income category for the // payment if((xAction == true) && (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true)) { tr.m_strBrokerageAccount = tmp; transferAccount(tmp); // make sure the account exists } else { tr.m_strInterestCategory = tmp; } // make sure, we have a valid category. Either taken from the L-Record above, // or derived from the action code if(tr.m_strInterestCategory.isEmpty()) { tr.m_strInterestCategory = d->typeToAccountName(action); } // For historic reasons (coming from the OFX importer) the statement // reader expects the dividend with a reverse sign. So we just do that. if(action != "miscexp") tr.m_amount = -(amount - tr.m_fees); if(tr.m_strMemo.isEmpty()) tr.m_strMemo = (QString("%1 %2").arg(extractLine('Y')).arg(d->typeToAccountName(action))).stripWhiteSpace(); } else if (action == "xin" || action == "xout") { QString payee = extractLine('P'); if(!payee.isEmpty() && ((payee.lower() == "opening balance") || KMyMoneyGlobalSettings::qifOpeningBalance().lower().contains(payee.lower()))) { createOpeningBalance(MyMoneyAccount::Investment); return; } tr.m_eAction = (MyMoneyStatement::Transaction::eaNone); MyMoneyStatement::Split s2; QString tmp = extractLine('L'); if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { s2.m_accountId = transferAccount(tmp); s2.m_strCategoryName = tmp; } else { s2.m_strCategoryName = extractLine('L'); if(tr.m_strInterestCategory.isEmpty()) { s2.m_strCategoryName = d->typeToAccountName(action); } } if(action == "xout") tr.m_amount = -tr.m_amount; s2.m_amount = -tr.m_amount; tr.m_listSplits.append(s2); } else if (action == "buy") { QString tmp = extractLine('L'); if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true) { tr.m_strBrokerageAccount = tmp; transferAccount(tmp); // make sure the account exists } d->st.m_listPrices += price; tr.m_shares = quantity; tr.m_eAction = (MyMoneyStatement::Transaction::eaBuy); } else if (action == "sell") { QString tmp = extractLine('L'); if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true) { tr.m_strBrokerageAccount = tmp; transferAccount(tmp); // make sure the account exists } d->st.m_listPrices += price; tr.m_shares = -quantity; tr.m_amount = -amount; tr.m_eAction = (MyMoneyStatement::Transaction::eaSell); } else if ( action == "shrsin" ) { tr.m_shares = quantity; tr.m_eAction = (MyMoneyStatement::Transaction::eaShrsin); } else if ( action == "shrsout" ) { tr.m_shares = -quantity; tr.m_eAction = (MyMoneyStatement::Transaction::eaShrsout); } else if ( action == "stksplit" ) { MyMoneyMoney splitfactor = (quantity / MyMoneyMoney(10,1)).reduce(); // Stock splits not supported // kdDebug(2) << "Line " << m_linenumber << ": Stock split not supported (date=" << date << " security=" << securityname << " factor=" << splitfactor.toString() << ")" << endl; // s1.setShares(splitfactor); // s1.setValue(0); // s1.setAction(MyMoneySplit::ActionSplitShares); // return; } else { // Unsupported action type kdDebug(0) << "Line " << m_linenumber << ": Unsupported transaction action (" << action << ")" << endl; return; } d->st.m_strAccountName = accountname; d->st.m_listTransactions +=tr; /************************************************************************* * * These transactions are natively supported by KMyMoney * *************************************************************************/ /* D1/ 3' 5 NShrsIn YGENERAL MOTORS CORP 52BR1 I20 Q200 U4,000.00 T4,000.00 M200 shares added to account @ $20/share ^ */ /* ^ D1/14' 5 NShrsOut YTEMPLETON GROWTH 97GJ0 Q50 90 ^ */ /* D1/28' 5 NBuy YGENERAL MOTORS CORP 52BR1 I24.35 Q100 U2,435.00 T2,435.00 ^ */ /* D1/ 5' 5 NSell YUnited Vanguard I8.41 Q50 U420.50 T420.50 ^ */ /* D1/ 7' 5 NReinvDiv YFRANKLIN INCOME 97GM2 I38 Q1 U38.00 T38.00 ^ */ /************************************************************************* * * These transactions are all different kinds of income. (Anything that * follows the DNYUT pattern). They are all handled the same, the only * difference is which income account the income is placed into. By * default, it's placed into _xxx where xxx is the right side of the * N field. e.g. NDiv transaction goes into the _Div account * *************************************************************************/ /* D1/10' 5 NDiv YTEMPLETON GROWTH 97GJ0 U10.00 T10.00 ^ */ /* D1/10' 5 NIntInc YTEMPLETON GROWTH 97GJ0 U20.00 T20.00 ^ */ /* D1/10' 5 NCGShort YTEMPLETON GROWTH 97GJ0 U111.00 T111.00 ^ */ /* D1/10' 5 NCGLong YTEMPLETON GROWTH 97GJ0 U333.00 T333.00 ^ */ /* D1/10' 5 NCGMid YTEMPLETON GROWTH 97GJ0 U222.00 T222.00 ^ */ /* D2/ 2' 5 NRtrnCap YFRANKLIN INCOME 97GM2 U1,234.00 T1,234.00 ^ */ /************************************************************************* * * These transactions deal with miscellaneous activity that KMyMoney * does not support, but may support in the future. * *************************************************************************/ /* Note the Q field is the split ratio per 10 shares, so Q12.5 is a 12.5:10 split, otherwise known as 5:4. D1/14' 5 NStkSplit YIBM Q12.5 ^ */ /************************************************************************* * * These transactions deal with short positions and options, which are * not supported at all by KMyMoney. They will be ignored for now. * There may be a way to hack around this, by creating a new security * "IBM_Short". * *************************************************************************/ /* D1/21' 5 NShtSell YIBM I92.38 Q100 U9,238.00 T9,238.00 ^ */ /* D1/28' 5 NCvrShrt YIBM I92.89 Q100 U9,339.00 T9,339.00 O50.00 ^ */ /* D6/ 1' 5 NVest YIBM Option Q20 ^ */ /* D6/ 8' 5 NExercise YIBM Option I60.952381 Q20 MFrom IBM Option Grant 6/1/2004 ^ */ /* D6/ 1'14 NExpire YIBM Option Q5 ^ */ /************************************************************************* * * These transactions do not have an associated investment ("Y" field) * so presumably they are only valid for the cash account. Once I * understand how these are really implemented, they can probably be * handled without much trouble. * *************************************************************************/ /* D1/14' 5 NCash U-100.00 T-100.00 LBank Chrg ^ */ /* D1/15' 5 NXOut U500.00 T500.00 L[CU Savings] $500.00 ^ */ /* D1/28' 5 NXIn U1,000.00 T1,000.00 L[CU Checking] $1,000.00 ^ */ /* D1/25' 5 NMargInt U25.00 T25.00 ^ */ } const QString MyMoneyQifReader::findOrCreateIncomeAccount(const QString& searchname) { QString result; MyMoneyFile *file = MyMoneyFile::instance(); // First, try to find this account as an income account MyMoneyAccount acc = file->income(); QStringList list = acc.accountList(); QStringList::ConstIterator it_accid = list.begin(); while ( it_accid != list.end() ) { acc = file->account(*it_accid); if ( acc.name() == searchname ) { result = *it_accid; break; } ++it_accid; } // If we did not find the account, now we must create one. if ( result.isEmpty() ) { MyMoneyAccount acc; acc.setName( searchname ); acc.setAccountType( MyMoneyAccount::Income ); MyMoneyAccount income = file->income(); MyMoneyFileTransaction ft; file->addAccount( acc, income ); ft.commit(); result = acc.id(); } return result; } // TODO (Ace) Combine this and the previous function const QString MyMoneyQifReader::findOrCreateExpenseAccount(const QString& searchname) { QString result; MyMoneyFile *file = MyMoneyFile::instance(); // First, try to find this account as an income account MyMoneyAccount acc = file->expense(); QStringList list = acc.accountList(); QStringList::ConstIterator it_accid = list.begin(); while ( it_accid != list.end() ) { acc = file->account(*it_accid); if ( acc.name() == searchname ) { result = *it_accid; break; } ++it_accid; } // If we did not find the account, now we must create one. if ( result.isEmpty() ) { MyMoneyAccount acc; acc.setName( searchname ); acc.setAccountType( MyMoneyAccount::Expense ); MyMoneyFileTransaction ft; MyMoneyAccount expense = file->expense(); file->addAccount( acc, expense ); ft.commit(); result = acc.id(); } return result; } QString MyMoneyQifReader::checkCategory(const QString& name, const MyMoneyMoney value, const MyMoneyMoney value2) { QString accountId; MyMoneyFile *file = MyMoneyFile::instance(); MyMoneyAccount account; bool found = true; if(!name.isEmpty()) { // The category might be constructed with an arbitraty depth (number of // colon delimited fields). We try to find a parent account within this // hierarchy by searching the following sequence: // // aaaa:bbbb:cccc:ddddd // // 1. search aaaa:bbbb:cccc:dddd, create nothing // 2. search aaaa:bbbb:cccc , create dddd // 3. search aaaa:bbbb , create cccc:dddd // 4. search aaaa , create bbbb:cccc:dddd // 5. don't search , create aaaa:bbbb:cccc:dddd account.setName(name); QString accName; // part to be created (right side in above list) QString parent(name); // a possible parent part (left side in above list) do { accountId = file->categoryToAccount(parent); if(accountId.isEmpty()) { found = false; // prepare next step if(!accName.isEmpty()) accName.prepend(':'); accName.prepend(parent.section(':', -1)); account.setName(accName); parent = parent.section(':', 0, -2); } else if(!accName.isEmpty()) { account.setParentAccountId(accountId); } } while(!parent.isEmpty() && accountId.isEmpty()); // if we did not find the category, we create it if(!found) { MyMoneyAccount parent; if(account.parentAccountId().isEmpty()) { if(!value.isNegative() && value2.isNegative()) parent = file->income(); else parent = file->expense(); } else { parent = file->account(account.parentAccountId()); } account.setAccountType((!value.isNegative() && value2.isNegative()) ? MyMoneyAccount::Income : MyMoneyAccount::Expense); MyMoneyAccount brokerage; // clear out the parent id, because createAccount() does not like that account.setParentAccountId(QString()); kmymoney2->createAccount(account, parent, brokerage, MyMoneyMoney()); accountId = account.id(); } } return accountId; } QString MyMoneyQifReader::processAccountEntry(bool resetAccountId) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; QString tmp; account.setName(extractLine('N')); // qDebug("Process account '%s'", account.name().data()); account.setDescription(extractLine('D')); tmp = extractLine('$'); if(tmp.length() > 0) account.setValue("lastStatementBalance", tmp); tmp = extractLine('/'); if(tmp.length() > 0) account.setValue("lastStatementDate", m_qifProfile.date(tmp).toString("yyyy-MM-dd")); QifEntryTypeE transactionType = EntryTransaction; QString type = extractLine('T').lower().remove(QRegExp("\\s+")); if(type == m_qifProfile.profileType().lower().remove(QRegExp("\\s+"))) { account.setAccountType(MyMoneyAccount::Checkings); } else if(type == "ccard" || type == "creditcard") { account.setAccountType(MyMoneyAccount::CreditCard); } else if(type == "cash") { account.setAccountType(MyMoneyAccount::Cash); } else if(type == "otha") { account.setAccountType(MyMoneyAccount::Asset); } else if(type == "othl") { account.setAccountType(MyMoneyAccount::Liability); } else if(type == "invst" || type == "port") { account.setAccountType(MyMoneyAccount::Investment); transactionType = EntryInvestmentTransaction; } else if(type == "mutual") { // stock account w/o umbrella investment account account.setAccountType(MyMoneyAccount::Stock); transactionType = EntryInvestmentTransaction; } else if(type == "unknown") { // don't do anything with the type, leave it unknown } else { account.setAccountType(MyMoneyAccount::Checkings); kdDebug(2) << "Line " << m_linenumber << ": Unknown account type '" << type << "', checkings assumed" << endl; } // check if we can find the account already in the file MyMoneyAccount acc = kmymoney2->findAccount(account, MyMoneyAccount()); if(acc.id().isEmpty()) { // in case the account is not found by name and the type is // unknown, we have to assume something and create a checking account. // this might be wrong, but we have no choice at this point. if(account.accountType() == MyMoneyAccount::UnknownAccountType) account.setAccountType(MyMoneyAccount::Checkings); MyMoneyAccount parentAccount; MyMoneyAccount brokerage; MyMoneyMoney balance; // in case it's a stock account, we need to setup a fix investment account if(account.isInvest()) { acc.setName(i18n("%1 (Investment)").arg(account.name())); // use the same name for the investment account acc.setDescription(i18n("Autogenerated by QIF importer from type Mutual account entry")); acc.setAccountType(MyMoneyAccount::Investment); parentAccount = file->asset(); kmymoney2->createAccount(acc, parentAccount, brokerage, MyMoneyMoney()); parentAccount = acc; qDebug("We still need to create the stock account in MyMoneyQifReader::processAccountEntry()"); } else { // setup parent according the type of the account switch(account.accountGroup()) { case MyMoneyAccount::Asset: default: parentAccount = file->asset(); break; case MyMoneyAccount::Liability: parentAccount = file->liability(); break; case MyMoneyAccount::Equity: parentAccount = file->equity(); break; } } // investment accounts will receive a brokerage account, as KMyMoney // currently does not allow to store funds in the investment account directly if(account.accountType() == MyMoneyAccount::Investment) { brokerage.setName(account.brokerageName()); brokerage.setAccountType(MyMoneyAccount::Checkings); brokerage.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); } kmymoney2->createAccount(account, parentAccount, brokerage, balance); acc = account; // qDebug("Account created"); } else { // qDebug("Existing account found"); } if(resetAccountId) { // possibly start a new statement d->finishStatement(); m_account = acc; d->st.m_accountId = m_account.id(); d->transactionType = transactionType; } return acc.id(); } void MyMoneyQifReader::selectOrCreateAccount(const SelectCreateMode mode, MyMoneyAccount& account, const MyMoneyMoney& balance) { MyMoneyFile* file = MyMoneyFile::instance(); QString accountId; QString msg; QString typeStr; QString leadIn; KMyMoneyUtils::categoryTypeE type; QMap::ConstIterator it; type = KMyMoneyUtils::none; switch(account.accountGroup()) { default: type = KMyMoneyUtils::asset; type = (KMyMoneyUtils::categoryTypeE) (type | KMyMoneyUtils::liability); typeStr = i18n("account"); leadIn = i18n("al"); break; case MyMoneyAccount::Income: case MyMoneyAccount::Expense: type = KMyMoneyUtils::income; type = (KMyMoneyUtils::categoryTypeE) (type | KMyMoneyUtils::expense); typeStr = i18n("category"); leadIn = i18n("ei"); msg = i18n("Category selection"); break; } KAccountSelectDlg accountSelect(type, "QifImport", kmymoney2); if(!msg.isEmpty()) accountSelect.setCaption(msg); it = m_accountTranslation.find((leadIn + MyMoneyFile::AccountSeperator + account.name()).lower()); if(it != m_accountTranslation.end()) { try { account = file->account(*it); return; } catch (MyMoneyException *e) { QString message(i18n("Account \"%1\" disappeared: ").arg(account.name())); message += e->what(); KMessageBox::error(0, message); delete e; } } if(!account.name().isEmpty()) { if(type & (KMyMoneyUtils::income | KMyMoneyUtils::expense)) { accountId = file->categoryToAccount(account.name()); } else { accountId = file->nameToAccount(account.name()); } if(mode == Create) { if(!accountId.isEmpty()) { account = file->account(accountId); return; } else { switch(KMessageBox::questionYesNo(0, i18n("The %1 '%2' does not exist. Do you " "want to create it?").arg(typeStr).arg(account.name()))) { case KMessageBox::Yes: break; case KMessageBox::No: return; } } } else { accountSelect.setHeader(i18n("Select %1").arg(typeStr)); if(!accountId.isEmpty()) { msg = i18n("The %1 %2 currently exists. Do you want " "to import transactions to this account?") .arg(typeStr).arg(account.name()); } else { msg = i18n("The %1 %2 currently does not exist. You can " "create a new %3 by pressing the Create button " "or select another %4 manually from the selection box.") .arg(typeStr).arg(account.name()).arg(typeStr).arg(typeStr); } } } else { accountSelect.setHeader(i18n("Import transactions to %1").arg(typeStr)); msg = i18n("No %1 information has been found in the selected QIF file. " "Please select an account using the selection box in the dialog or " "create a new %2 by pressing the Create button.") .arg(typeStr).arg(typeStr); } accountSelect.setDescription(msg); accountSelect.setAccount(account, accountId); accountSelect.setMode(mode == Create); accountSelect.showAbortButton(true); // display current entry in widget, the offending line (if any) will be shown in red QStringList::Iterator it_e; int i = 0; for(it_e = m_qifEntry.begin(); it_e != m_qifEntry.end(); ++it_e) { if(m_extractedLine == i) accountSelect.m_qifEntry->setColor(QColor("red")); accountSelect.m_qifEntry->append(*it_e); accountSelect.m_qifEntry->setColor(QColor("black")); ++i; } for(;;) { if(accountSelect.exec() == QDialog::Accepted) { if(!accountSelect.selectedAccount().isEmpty()) { accountId = accountSelect.selectedAccount(); m_accountTranslation[(leadIn + MyMoneyFile::AccountSeperator + account.name()).lower()] = accountId; // MMAccount::openingBalance() is where the accountSelect dialog has // stashed the opening balance that the user chose. MyMoneyAccount importedAccountData(account); // MyMoneyMoney balance = importedAccountData.openingBalance(); account = file->account(accountId); if ( ! balance.isZero() ) { QString openingtxid = file->openingBalanceTransaction(account); MyMoneyFileTransaction ft; if ( ! openingtxid.isEmpty() ) { MyMoneyTransaction openingtx = file->transaction(openingtxid); MyMoneySplit split = openingtx.splitByAccount(account.id()); if ( split.shares() != balance ) { const MyMoneySecurity& sec = file->security(account.currencyId()); if ( KMessageBox::questionYesNo( qApp->mainWidget(), 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?").arg(account.name(), split.shares().formatMoney(account, sec), balance.formatMoney(account, sec)), i18n("Overwrite opening balance"), KStdGuiItem::yes(), KStdGuiItem::no(), "OverwriteOpeningBalance" ) == KMessageBox::Yes ) { file->removeTransaction( openingtx ); file->createOpeningBalanceTransaction( account, balance ); } } } else { // Add an opening balance file->createOpeningBalanceTransaction( account, balance ); } ft.commit(); } break; } } else if(accountSelect.aborted()) throw new MYMONEYEXCEPTION("USERABORT"); if(typeStr == i18n("account")) { KMessageBox::error(0, i18n("You must select or create an account.")); } else { KMessageBox::error(0, i18n("You must select or create a category.")); } } } void MyMoneyQifReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; } void MyMoneyQifReader::signalProgress(int current, int total, const QString& msg) { if(m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void MyMoneyQifReader::processPriceEntry(void) { /* !Type:Prices "IBM",141 9/16,"10/23/98" ^ !Type:Prices "GMW",21.28," 3/17' 5" ^ !Type:Prices "GMW",71652181.001,"67/128/ 0" ^ Note that Quicken will often put in a price with a bogus date and number. We will ignore prices with bogus dates. Hopefully that will catch all of these. Also note that prices can be in fractional units, e.g. 141 9/16. */ QStringList::const_iterator it_line = m_qifEntry.begin(); // Make a price for each line QRegExp priceExp("\"(.*)\",(.*),\"(.*)\""); while ( it_line != m_qifEntry.end() ) { if(priceExp.search(*it_line) != -1) { MyMoneyStatement::Price price; price.m_strSecurity = priceExp.cap(1); QString pricestr = priceExp.cap(2); QString datestr = priceExp.cap(3); kdDebug(0) << "Price:" << price.m_strSecurity << " / " << pricestr << " / " << datestr << endl; // Only add the price if the date is valid. If invalid, fail silently. See note above. // Also require the price value to not have any slashes. Old prices will be something like // "25 9/16", which we do not support. So we'll skip the price for now. QDate date = m_qifProfile.date(datestr); MyMoneyMoney rate(m_qifProfile.value('P', pricestr)); if(date.isValid() && !rate.isZero()) { price.m_amount = rate; price.m_date = date; d->st.m_listPrices += price; } } ++it_line; } } void MyMoneyQifReader::processSecurityEntry(void) { /* !Type:Security NVANGUARD 500 INDEX SVFINX TMutual Fund ^ */ MyMoneyStatement::Security security; security.m_strName = extractLine('N'); security.m_strSymbol = extractLine('S'); d->st.m_listSecurities += security; } #include "mymoneyqifreader.moc"