diff options
Diffstat (limited to 'kmymoney2/converter/mymoneyqifreader.cpp')
-rw-r--r-- | kmymoney2/converter/mymoneyqifreader.cpp | 2336 |
1 files changed, 2336 insertions, 0 deletions
diff --git a/kmymoney2/converter/mymoneyqifreader.cpp b/kmymoney2/converter/mymoneyqifreader.cpp new file mode 100644 index 0000000..60b0604 --- /dev/null +++ b/kmymoney2/converter/mymoneyqifreader.cpp @@ -0,0 +1,2336 @@ +/*************************************************************************** + mymoneyqifreader.cpp + ------------------- + begin : Mon Jan 27 2003 + copyright : (C) 2000-2003 by Michael Edwardes + email : mte@users.sourceforge.net + Javier Campos Morales <javi_c@users.sourceforge.net> + Felix Rodriguez <frodriguez@users.sourceforge.net> + John C <thetacoturtle@users.sourceforge.net> + Thomas Baumgart <ipwizard@users.sourceforge.net> + Kevin Tambascio <ktambascio@users.sourceforge.net> + Ace Jones <acejones@users.sourceforge.net> + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 <iostream> + +// ---------------------------------------------------------------------------- +// QT Headers + +#include <qfile.h> +#include <qstringlist.h> +#include <qtimer.h> +#include <qtextedit.h> +#include <qregexp.h> +#include <qbuffer.h> + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include <klocale.h> +#include <kmessagebox.h> +#include <kconfig.h> +#include <kdebug.h> +#include <kprogress.h> +#include <kinputdialog.h> +#include <kio/netaccess.h> + +// ---------------------------------------------------------------------------- +// Project Headers + +#include "mymoneyqifreader.h" +#include "../mymoney/mymoneyfile.h" +#include "../dialogs/kaccountselectdlg.h" +#include "../kmymoney2.h" +#include "kmymoneyglobalsettings.h" + +#include "mymoneystatementreader.h" +#include <kmymoney/mymoneystatement.h> + +// 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<MyMoneyStatement> statements; + + /** + * a list of already used hashes in this file + */ + QMap<QString, bool> 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<const char*>(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<MyMoneyTransaction>::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<MyMoneyStatement>::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<qSplit>& 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("<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.").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<QString, bool>::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<qSplit> 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<QString, bool>::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<QString, QString>::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 <b>%2</b> currently exists. Do you want " + "to import transactions to this account?") + .arg(typeStr).arg(account.name()); + + } else { + msg = i18n("The %1 <b>%2</b> currently does not exist. You can " + "create a new %3 by pressing the <b>Create</b> 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 <b>Create</b> 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" |