You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kmymoney/kmymoney2/converter/mymoneyqifreader.cpp

2337 lines
72 KiB

/***************************************************************************
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 <tqfile.h>
#include <tqstringlist.h>
#include <tqtimer.h>
#include <tqtextedit.h>
#include <tqregexp.h>
#include <tqbuffer.h>
// ----------------------------------------------------------------------------
// KDE Headers
#include <klocale.h>
#include <kmessagebox.h>
#include <tdeconfig.h>
#include <kdebug.h>
#include <kprogress.h>
#include <kinputdialog.h>
#include <tdeio/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)
{}
TQString accountTypeToQif(MyMoneyAccount::accountTypeE type) const;
/**
* finalize the current statement and add it to the statement list
*/
void finishStatement(void);
bool isTransfer(TQString& name, const TQString& leftDelim, const TQString& rightDelim);
/**
* Converts the QIF specific N-record of investment transactions into
* a category name
*/
TQString typeToAccountName(const TQString& type) const;
/**
* Converts the QIF reconcile state to the KMyMoney reconcile state
*/
MyMoneySplit::reconcileFlagE reconcileState(const TQString& state) const;
/**
*/
void fixMultiLineMemo(TQString& memo) const;
public:
/**
* the statement that is currently collected/processed
*/
MyMoneyStatement st;
/**
* the list of all statements to be sent to MyMoneyStatementReader
*/
TQValueList<MyMoneyStatement> statements;
/**
* a list of already used hashes in this file
*/
TQMap<TQString, bool> m_hashMap;
TQString st_AccountName;
TQString st_AccountId;
MyMoneyAccount::accountTypeE accountType;
bool firstTransaction;
bool mapCategories;
MyMoneyQifReader::QifEntryTypeE transactionType;
};
void MyMoneyQifReader::Private::fixMultiLineMemo(TQString& 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;
tqDebug("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;
}
TQString MyMoneyQifReader::Private::accountTypeToQif(MyMoneyAccount::accountTypeE type) const
{
TQString 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;
}
TQString MyMoneyQifReader::Private::typeToAccountName(const TQString& 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(TQString& tmp, const TQString& leftDelim, const TQString& 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
TQRegExp exp(TQString("\\%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 TQString& 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, TQT_SIGNAL(wroteStdin(TDEProcess*)), this, TQT_SLOT(slotSendDataToFilter()));
connect(&m_filter, TQT_SIGNAL(receivedStdout(TDEProcess*, char*, int)), this, TQT_SLOT(slotReceivedDataFromFilter(TDEProcess*, char*, int)));
connect(&m_filter, TQT_SIGNAL(processExited(TDEProcess*)), this, TQT_SLOT(slotImportFinished()));
connect(&m_filter, TQT_SIGNAL(receivedStderr(TDEProcess*, char*, int)), this, TQT_SLOT(slotReceivedErrorFromFilter(TDEProcess*, 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 TQString& profile)
{
m_qifProfile.loadProfile("Profile-" + profile);
}
void MyMoneyQifReader::slotSendDataToFilter(void)
{
TQ_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) {
tqWarning("Failed to read block from QIF import file");
m_filter.closeStdin();
m_filter.kill();
} else {
m_filter.writeStdin(m_buffer, len);
}
}
}
void MyMoneyQifReader::slotReceivedErrorFromFilter(TDEProcess* /* proc */, char *buff, int len)
{
TQByteArray data;
data.duplicate(buff, len);
tqWarning("%s",static_cast<const char*>(data));
}
void MyMoneyQifReader::slotReceivedDataFromFilter(TDEProcess* /* 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 << TQString::fromUtf8(m_lineBuffer.stripWhiteSpace());
}
m_lineBuffer = TQCString();
} 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 << TQString::fromUtf8(m_lineBuffer.stripWhiteSpace());
}
tqDebug("Read %d bytes", m_pos);
TQTimer::singleShot(0, this, TQT_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.
TQStringList dateFormats;
m_qifProfile.possibleDateFormats(dateFormats);
TQStringList 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());
tqDebug("Selected date format: '%s'", list.first().data());
signalProgress(0, m_qifLines.count(), i18n("Importing QIF ..."));
TQStringList::iterator it;
for(it = m_qifLines.begin(); m_userAbort == false && it != m_qifLines.end(); ++it) {
++m_linenumber;
// tqDebug("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();
tqDebug("%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 = TQString();
m_data.clear();
if(!TDEIO::NetAccess::download(m_url, m_filename, NULL)) {
KMessageBox::detailedError(0,
i18n("Error while loading file '%1'!").arg(m_url.prettyURL()),
TDEIO::NetAccess::lastErrorString(),
i18n("File access error"));
return false;
}
m_file = new TQFile(m_filename);
if(m_file->open(IO_ReadOnly)) {
#ifdef DEBUG_IMPORT
TQ_LONG len;
while(!m_file->atEnd()) {
len = m_file->readBlock(m_buffer, sizeof(m_buffer));
if(len == -1) {
tqWarning("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 << TQStringList::split(" ", m_qifProfile.filterScriptImport(), true);
}
m_entryType = EntryUnknown;
if(m_filter.start(TDEProcess::NotifyOnExit, TDEProcess::All)) {
m_filter.resume();
signalProgress(0, m_file->size(), i18n("Reading QIF ..."));
slotSendDataToFilter();
rc = true;
} else {
tqDebug("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
TDEConfig* config = TDEGlobal::config();
config->setGroup(TQString::fromLatin1("Notification Messages"));
TQStringList::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
TDEConfig* config = TDEGlobal::config();
config->setGroup(TQString::fromLatin1("Notification Messages"));
TQStringList::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 {
tqWarning("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
TDEIO::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();
TQValueList<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
TQValueList<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 TQString& _line)
{
TQString line = _line.mid(1); // get rid of exclamation mark
// TQString test = line.left(5).lower();
if(line.left(5).lower() == TQString("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 {
tqWarning("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 TQString MyMoneyQifReader::extractLine(const TQChar id, int cnt)
{
TQStringList::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 TQString(" ");
return (*it).mid(1);
}
}
}
m_extractedLine = -1;
return TQString::null;
}
void MyMoneyQifReader::extractSplits(TQValueList<qSplit>& listqSplits) const
{
// *** With apologies to TQString MyMoneyQifReader::extractLine ***
TQStringList::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);
TQString txt = extractLine('T');
MyMoneyMoney balance = m_qifProfile.value('T', txt);
TQDate date = m_qifProfile.date(extractLine('D'));
m_account.setOpeningDate(date);
TQString 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();
TQString 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(
tqApp->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;
TQString 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);
}
}
TQString MyMoneyQifReader::transferAccount(TQString name, bool useBrokerage)
{
TQString accountId;
TQStringList 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 << TQString("N%1").arg(name);
m_qifEntry << TQString("Tunknown");
m_qifEntry << TQString("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 << TQString("N%1").arg(name);
m_qifEntry << TQString("Tunknown");
m_qifEntry << TQString("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()) {
TQString 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));
TQStringList entry = m_qifEntry; // keep a temp copy
m_qifEntry.clear(); // and construct a temp entry to create/search the account
m_qifEntry << TQString("N%1").arg(name);
m_qifEntry << TQString("T%1").arg(d->accountTypeToQif(accType));
m_qifEntry << TQString("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
TQString 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, TQString("<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"),
TQString("%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;
TQString tmp;
TQString accountId;
int pos;
TQString payee = extractLine('P');
unsigned long h;
h = MyMoneyTransaction::hash(m_qifEntry.join(";"));
TQString hashBase;
hashBase.sprintf("%s-%07lx", m_qifProfile.date(extractLine('D')).toString(Qt::ISODate).data(), h);
int idx = 1;
TQString hash;
for(;;) {
hash = TQString("%1-%2").arg(hashBase).arg(idx);
TQMap<TQString, 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 = (TQDate::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 = TQString("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 = TQString();
}
} catch (MyMoneyException *e) {
kdDebug(0) << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found" << endl;
accountId = TQString();
delete e;
}
}
if(!accountId.isEmpty()) {
s2.m_accountId = accountId;
s2.m_strCategoryName = tmp;
tr.m_listSplits.append(s2);
}
} else {
// split transaction
TQValueList<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 = TQString();
}
} catch (MyMoneyException *e) {
kdDebug(0) << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found" << endl;
accountId = TQString();
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
TQDate 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 = TQDate::currentDate();
break;
case KMessageBox::Cancel:
throw new MYMONEYEXCEPTION("USERABORT");
break;
}
}
// 'M' field: Memo
TQString memo = extractLine('M');
d->fixMultiLineMemo(memo);
tr.m_strMemo = memo;
unsigned long h;
h = MyMoneyTransaction::hash(m_qifEntry.join(";"));
TQString hashBase;
hashBase.sprintf("%s-%07lx", m_qifProfile.date(extractLine('D')).toString(Qt::ISODate).data(), h);
int idx = 1;
TQString hash;
for(;;) {
hash = TQString("%1-%2").arg(hashBase).arg(idx);
TQMap<TQString, 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
TQString tmp = extractLine('#');
if ( ! tmp.isEmpty() )
tr.m_strBankID = TQString("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
TQString 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.
TQString 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;
TQStringList accounts = thisaccount.accountList();
TQStringList::const_iterator it_account = accounts.begin();
while( !found && it_account != accounts.end() )
{
TQString currencyid = file->account(*it_account).currencyId();
MyMoneySecurity security = file->security( currencyid );
TQString symbol = security.tradingSymbol().lower();
TQString 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
TQString 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"),
TQString("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
TQString 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
TQString 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);
TQString 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);
TQString 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 = (TQString("%1 %2").arg(extractLine('Y')).arg(d->typeToAccountName(action))).stripWhiteSpace();
}
else if (action == "xin" || action == "xout")
{
TQString 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;
TQString 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")
{
TQString 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")
{
TQString 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 TQString MyMoneyQifReader::findOrCreateIncomeAccount(const TQString& searchname)
{
TQString result;
MyMoneyFile *file = MyMoneyFile::instance();
// First, try to find this account as an income account
MyMoneyAccount acc = file->income();
TQStringList list = acc.accountList();
TQStringList::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 TQString MyMoneyQifReader::findOrCreateExpenseAccount(const TQString& searchname)
{
TQString result;
MyMoneyFile *file = MyMoneyFile::instance();
// First, try to find this account as an income account
MyMoneyAccount acc = file->expense();
TQStringList list = acc.accountList();
TQStringList::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;
}
TQString MyMoneyQifReader::checkCategory(const TQString& name, const MyMoneyMoney value, const MyMoneyMoney value2)
{
TQString 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);
TQString accName; // part to be created (right side in above list)
TQString 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(TQString());
kmymoney2->createAccount(account, parent, brokerage, MyMoneyMoney());
accountId = account.id();
}
}
return accountId;
}
TQString MyMoneyQifReader::processAccountEntry(bool resetAccountId)
{
MyMoneyFile* file = MyMoneyFile::instance();
MyMoneyAccount account;
TQString tmp;
account.setName(extractLine('N'));
// tqDebug("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;
TQString type = extractLine('T').lower().remove(TQRegExp("\\s+"));
if(type == m_qifProfile.profileType().lower().remove(TQRegExp("\\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;
tqDebug("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;
// tqDebug("Account created");
} else {
// tqDebug("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();
TQString accountId;
TQString msg;
TQString typeStr;
TQString leadIn;
KMyMoneyUtils::categoryTypeE type;
TQMap<TQString, TQString>::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) {
TQString 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
TQStringList::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(TQColor("red"));
accountSelect.m_qifEntry->append(*it_e);
accountSelect.m_qifEntry->setColor(TQColor("black"));
++i;
}
for(;;) {
if(accountSelect.exec() == TQDialog::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() )
{
TQString 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(
tqApp->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 TQString&))
{
m_progressCallback = callback;
}
void MyMoneyQifReader::signalProgress(int current, int total, const TQString& 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.
*/
TQStringList::const_iterator it_line = m_qifEntry.begin();
// Make a price for each line
TQRegExp priceExp("\"(.*)\",(.*),\"(.*)\"");
while ( it_line != m_qifEntry.end() )
{
if(priceExp.search(*it_line) != -1) {
MyMoneyStatement::Price price;
price.m_strSecurity = priceExp.cap(1);
TQString pricestr = priceExp.cap(2);
TQString 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.
TQDate 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"