From dadc34655c3ab961b0b0b94a10eaaba710f0b5e8 Mon Sep 17 00:00:00 2001 From: tpearson Date: Mon, 4 Jul 2011 22:38:03 +0000 Subject: Added kmymoney git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/kmymoney@1239792 283d02a7-25f6-0310-bc7c-ecb5cbfe19da --- kmymoney2/converter/Makefile.am | 24 + kmymoney2/converter/convertertest.cpp | 211 ++ kmymoney2/converter/convertertest.h | 45 + kmymoney2/converter/imymoneyreader.h | 135 ++ kmymoney2/converter/mymoneygncreader.cpp | 2463 ++++++++++++++++++++++++ kmymoney2/converter/mymoneygncreader.h | 904 +++++++++ kmymoney2/converter/mymoneyqifprofile.cpp | 1013 ++++++++++ kmymoney2/converter/mymoneyqifprofile.h | 144 ++ kmymoney2/converter/mymoneyqifreader.cpp | 2336 ++++++++++++++++++++++ kmymoney2/converter/mymoneyqifreader.h | 394 ++++ kmymoney2/converter/mymoneyqifwriter.cpp | 254 +++ kmymoney2/converter/mymoneyqifwriter.h | 138 ++ kmymoney2/converter/mymoneystatementreader.cpp | 1354 +++++++++++++ kmymoney2/converter/mymoneystatementreader.h | 151 ++ kmymoney2/converter/mymoneytemplate.cpp | 420 ++++ kmymoney2/converter/mymoneytemplate.h | 94 + kmymoney2/converter/webpricequote.cpp | 1050 ++++++++++ kmymoney2/converter/webpricequote.h | 252 +++ 18 files changed, 11382 insertions(+) create mode 100644 kmymoney2/converter/Makefile.am create mode 100644 kmymoney2/converter/convertertest.cpp create mode 100644 kmymoney2/converter/convertertest.h create mode 100644 kmymoney2/converter/imymoneyreader.h create mode 100644 kmymoney2/converter/mymoneygncreader.cpp create mode 100644 kmymoney2/converter/mymoneygncreader.h create mode 100644 kmymoney2/converter/mymoneyqifprofile.cpp create mode 100644 kmymoney2/converter/mymoneyqifprofile.h create mode 100644 kmymoney2/converter/mymoneyqifreader.cpp create mode 100644 kmymoney2/converter/mymoneyqifreader.h create mode 100644 kmymoney2/converter/mymoneyqifwriter.cpp create mode 100644 kmymoney2/converter/mymoneyqifwriter.h create mode 100644 kmymoney2/converter/mymoneystatementreader.cpp create mode 100644 kmymoney2/converter/mymoneystatementreader.h create mode 100644 kmymoney2/converter/mymoneytemplate.cpp create mode 100644 kmymoney2/converter/mymoneytemplate.h create mode 100644 kmymoney2/converter/webpricequote.cpp create mode 100644 kmymoney2/converter/webpricequote.h (limited to 'kmymoney2/converter') diff --git a/kmymoney2/converter/Makefile.am b/kmymoney2/converter/Makefile.am new file mode 100644 index 0000000..b54449e --- /dev/null +++ b/kmymoney2/converter/Makefile.am @@ -0,0 +1,24 @@ +KDE_OPTIONS = noautodist + +INCLUDES = $(all_includes) -I$(top_srcdir) -I. -I$(top_srcdir)/kmymoney2 -I$(top_builddir)/kmymoney2 + +instdir=$(includedir)/kmymoney + +noinst_LIBRARIES = libconverter.a +libconverter_a_METASOURCES = AUTO + +libconverter_a_SOURCES = mymoneyqifreader.cpp mymoneyqifwriter.cpp mymoneyqifprofile.cpp mymoneytemplate.cpp mymoneystatementreader.cpp webpricequote.cpp mymoneygncreader.cpp + +EXTRA_DIST = + +inst_HEADERS = mymoneytemplate.h + +noinst_HEADERS = imymoneyreader.h mymoneyqifprofile.h mymoneyqifreader.h mymoneyqifwriter.h mymoneystatementreader.h webpricequote.h mymoneygncreader.h convertertest.h + +if CPPUNIT +check_LIBRARIES = libconvertertest.a + +libconvertertest_a_SOURCES = convertertest.cpp +endif + + diff --git a/kmymoney2/converter/convertertest.cpp b/kmymoney2/converter/convertertest.cpp new file mode 100644 index 0000000..aef63d9 --- /dev/null +++ b/kmymoney2/converter/convertertest.cpp @@ -0,0 +1,211 @@ +/*************************************************************************** + convertertest.cpp + ------------------- + copyright : (C) 2002 by Thomas Baumgart + email : ipwizard@users.sourceforge.net + Ace Jones + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "convertertest.h" + +// uses helper functions from reports tests +#include "../reports/reportstestcommon.h" +using namespace test; + +#include +#include +#include +#include +#include "../mymoney/storage/mymoneystoragexml.h" +#include "../mymoney/storage/mymoneystoragedump.h" + +#define private public +#include "../converter/webpricequote.h" +#undef private + +ConverterTest::ConverterTest() +{ +} + +using namespace convertertest; + +void ConverterTest::setUp () { + + storage = new MyMoneySeqAccessMgr; + file = MyMoneyFile::instance(); + file->attachStorage(storage); + + MyMoneyFileTransaction ft; + + file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); + file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); + file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 100, 1)); + file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); + file->setBaseCurrency(file->currency("USD")); + + MyMoneyPayee payeeTest("Test Payee"); + file->addPayee(payeeTest); + MyMoneyPayee payeeTest2("Thomas Baumgart"); + file->addPayee(payeeTest2); + + acAsset = (MyMoneyFile::instance()->asset().id()); + acLiability = (MyMoneyFile::instance()->liability().id()); + acExpense = (MyMoneyFile::instance()->expense().id()); + acIncome = (MyMoneyFile::instance()->income().id()); + acChecking = makeAccount("Checking Account",MyMoneyAccount::Checkings,moConverterCheckingOpen,QDate(2004,5,15),acAsset); + acCredit = makeAccount("Credit Card",MyMoneyAccount::CreditCard,moConverterCreditOpen,QDate(2004,7,15),acLiability); + acSolo = makeAccount("Solo",MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense); + acParent = makeAccount("Parent",MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense); + acChild = makeAccount("Child",MyMoneyAccount::Expense,0,QDate(2004,2,11),acParent); + acForeign = makeAccount("Foreign",MyMoneyAccount::Expense,0,QDate(2004,1,11),acExpense); + + MyMoneyInstitution i("Bank of the World","","","","","",""); + file->addInstitution(i); + inBank = i.id(); + ft.commit(); +} + +void ConverterTest::tearDown () +{ + file->detachStorage(storage); + delete storage; +} + +void ConverterTest::testWebQuotes() +{ +#ifdef PERFORM_ONLINE_TESTS + try + { + WebPriceQuote q; + QuoteReceiver qr(&q); + + q.launch("DIS"); + +// kdDebug(2) << "ConverterTest::testWebQuotes(): quote for " << q.m_symbol << " on " << qr.m_date.toString() << " is " << qr.m_price.toString() << " errors(" << qr.m_errors.count() << "): " << qr.m_errors.join(" /// ") << endl; + + // No errors allowed + CPPUNIT_ASSERT(qr.m_errors.count() == 0); + + // Quote date should be within the last week, or something bad is going on. + CPPUNIT_ASSERT(qr.m_date <= QDate::currentDate()); + CPPUNIT_ASSERT(qr.m_date >= QDate::currentDate().addDays(-7)); + + // Quote value should at least be positive + CPPUNIT_ASSERT(qr.m_price.isPositive()); + + q.launch("MF8AAUKS.L","Yahoo UK"); + +// kdDebug(2) << "ConverterTest::testWebQuotes(): quote for " << q.m_symbol << " on " << qr.m_date.toString() << " is " << qr.m_price.toString() << " errors(" << qr.m_errors.count() << "): " << qr.m_errors.join(" /// ") << endl; + + CPPUNIT_ASSERT(qr.m_errors.count() == 0); + CPPUNIT_ASSERT(qr.m_date <= QDate::currentDate().addDays(1)); + CPPUNIT_ASSERT(qr.m_date >= QDate::currentDate().addDays(-7)); + CPPUNIT_ASSERT(qr.m_price.isPositive()); + + q.launch("EUR > USD","Yahoo Currency"); + +// kdDebug(2) << "ConverterTest::testWebQuotes(): quote for " << q.m_symbol << " on " << qr.m_date.toString() << " is " << qr.m_price.toString() << " errors(" << qr.m_errors.count() << "): " << qr.m_errors.join(" /// ") << endl; + + CPPUNIT_ASSERT(qr.m_errors.count() == 0); + CPPUNIT_ASSERT(qr.m_date <= QDate::currentDate().addDays(1)); + CPPUNIT_ASSERT(qr.m_date >= QDate::currentDate().addDays(-7)); + CPPUNIT_ASSERT(qr.m_price.isPositive()); + + q.launch("50492","Globe & Mail"); + +// kdDebug(2) << "ConverterTest::testWebQuotes(): quote for " << q.m_symbol << " on " << qr.m_date.toString() << " is " << qr.m_price.toString() << " errors(" << qr.m_errors.count() << "): " << qr.m_errors.join(" /// ") << endl; + + CPPUNIT_ASSERT(qr.m_errors.count() == 0); + CPPUNIT_ASSERT(qr.m_date <= QDate::currentDate().addDays(1)); + CPPUNIT_ASSERT(qr.m_date >= QDate::currentDate().addDays(-7)); + CPPUNIT_ASSERT(qr.m_price.isPositive()); + + q.launch("TDB647","MSN.CA"); + +// kdDebug(2) << "ConverterTest::testWebQuotes(): quote for " << q.m_symbol << " on " << qr.m_date.toString() << " is " << qr.m_price.toString() << " errors(" << qr.m_errors.count() << "): " << qr.m_errors.join(" /// ") << endl; + + CPPUNIT_ASSERT(qr.m_errors.count() == 0); + CPPUNIT_ASSERT(qr.m_date <= QDate::currentDate().addDays(1)); + CPPUNIT_ASSERT(qr.m_date >= QDate::currentDate().addDays(-7)); + CPPUNIT_ASSERT(qr.m_price.isPositive()); + + } + catch (MyMoneyException* e) + { + CPPUNIT_FAIL(e->what()); + } +#endif +} + +void ConverterTest::testDateFormat() +{ + try + { + MyMoneyDateFormat format("%mm-%dd-%yyyy"); + + CPPUNIT_ASSERT(format.convertString("1-5-2005") == QDate(2005,1,5)); + CPPUNIT_ASSERT(format.convertString("jan-15-2005") == QDate(2005,1,15)); + CPPUNIT_ASSERT(format.convertString("august-25-2005") == QDate(2005,8,25)); + + format = MyMoneyDateFormat("%mm/%dd/%yy"); + + CPPUNIT_ASSERT(format.convertString("1/5/05") == QDate(2005,1,5)); + CPPUNIT_ASSERT(format.convertString("jan/15/05") == QDate(2005,1,15)); + CPPUNIT_ASSERT(format.convertString("august/25/05") == QDate(2005,8,25)); + + format = MyMoneyDateFormat("%d\\.%m\\.%yy"); + + CPPUNIT_ASSERT(format.convertString("1.5.05") == QDate(2005,5,1)); + CPPUNIT_ASSERT(format.convertString("15.jan.05") == QDate(2005,1,15)); + CPPUNIT_ASSERT(format.convertString("25.august.05") == QDate(2005,8,25)); + + format = MyMoneyDateFormat("%yyyy\\\\%dddd\\\\%mmmmmmmmmmm"); + + CPPUNIT_ASSERT(format.convertString("2005\\31\\12") == QDate(2005,12,31)); + CPPUNIT_ASSERT(format.convertString("2005\\15\\jan") == QDate(2005,1,15)); + CPPUNIT_ASSERT(format.convertString("2005\\25\\august") == QDate(2005,8,25)); + + format = MyMoneyDateFormat("%m %dd, %yyyy"); + + CPPUNIT_ASSERT(format.convertString("jan 15, 2005") == QDate(2005,1,15)); + CPPUNIT_ASSERT(format.convertString("august 25, 2005") == QDate(2005,8,25)); + CPPUNIT_ASSERT(format.convertString("january 1st, 2005") == QDate(2005,1,1)); + + format = MyMoneyDateFormat("%m %d %y"); + + CPPUNIT_ASSERT(format.convertString("12/31/50",false,2000) == QDate(1950,12,31)); + CPPUNIT_ASSERT(format.convertString("1/1/90",false,2000) == QDate(1990,1,1)); + CPPUNIT_ASSERT(format.convertString("december 31st, 5",false) == QDate(2005,12,31)); + } + catch (MyMoneyException* e) + { + CPPUNIT_FAIL(e->what()); + } +} + +// vim:cin:si:ai:et:ts=2:sw=2: diff --git a/kmymoney2/converter/convertertest.h b/kmymoney2/converter/convertertest.h new file mode 100644 index 0000000..98d4289 --- /dev/null +++ b/kmymoney2/converter/convertertest.h @@ -0,0 +1,45 @@ +/*************************************************************************** + convertertest.h + ------------------- + copyright : (C) 2002 by Thomas Baumgart + email : ipwizard@users.sourceforge.net + Ace Jones + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef CONVERTERTEST_H +#define CONVERTERTEST_H + +#include +#include "../mymoney/mymoneyfile.h" +#include "../mymoney/storage/mymoneyseqaccessmgr.h" + +class ConverterTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(ConverterTest); + CPPUNIT_TEST(testWebQuotes); + CPPUNIT_TEST(testDateFormat); + CPPUNIT_TEST_SUITE_END(); + +private: + MyMoneyAccount *m; + + MyMoneySeqAccessMgr* storage; + MyMoneyFile* file; + +public: + ConverterTest(); + void setUp (); + void tearDown (); + void testWebQuotes(); + void testDateFormat(); +}; + +#endif // CONVERTERTEST_H diff --git a/kmymoney2/converter/imymoneyreader.h b/kmymoney2/converter/imymoneyreader.h new file mode 100644 index 0000000..6222af5 --- /dev/null +++ b/kmymoney2/converter/imymoneyreader.h @@ -0,0 +1,135 @@ + /*************************************************************************** + imymoneyreader.h - description + ------------------- + begin : Wed Feb 25 2004 + copyright : (C) 2000-2004 by Michael Edwardes + email : mte@users.sourceforge.net + Javier Campos Morales + Felix Rodriguez + John C + Thomas Baumgart + Kevin Tambascio + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef IMYMONEYREADER_H +#define IMYMONEYREADER_H + +// ---------------------------------------------------------------------------- +// QT Headers + +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include +#include + +// ---------------------------------------------------------------------------- +// Project Headers + +#include "../mymoney/mymoneyaccount.h" + +/** + * @author Kevin Tambascio + */ + +class IMyMoneyReader : public QObject +{ +public: + IMyMoneyReader() {} + virtual ~IMyMoneyReader() {} + + Q_OBJECT + + /** + * This method is used to store the filename into the object. + * The file should exist. If it does and an external filter + * program is specified with the current selected profile, + * the file is send through this filter and the result + * is stored in the m_tempFile file. + * + * @param name path and name of the file to be imported + */ + virtual void setFilename(const QString& name)=0; + + /** + * This method is used to store the name of the profile into the object. + * The selected profile will be loaded if it exists. If an external + * filter program is specified with the current selected profile, + * the file is send through this filter and the result + * is stored in the m_tempFile file. + * + * @param name QString reference to the name of the profile + */ + virtual void setProfile(const QString& name)=0; + + /** + * This method actually starts the import of data from the selected file + * into the MyMoney engine. + * + * This method also starts the user defined import filter program + * defined in the QIF profile(when a QIF file is selected). If none is + * defined, the file is read as is (actually the UNIX command + * 'cat -' is used as the filter). + * + * If data from the filter program is available, the slot + * slotReceivedDataFromFilter() will be called. + * + * Make sure to connect the signal importFinished() to detect when + * the import actually ended. Call the method finishImport() to clean + * things up and get the overall result of the import. + * + * @retval true the import was started successfully + * @retval false the import could not be started. + */ + virtual const bool startImport(void)=0; + + /** + * This method must be called once the signal importFinished() has + * been emitted. It will clean up the reader state and determines + * the actual return code of the import. + * + * @retval true Import was successful. + * @retval false Import failed because the filter program terminated + * abnormally or the user aborted the import process. + */ + virtual const bool finishImport(void)=0; + + /** + * This method is used to modify the auto payee creation flag. + * If this flag is set, records for payees that are not currently + * found in the engine will be automatically created with no + * further user interaction required. If this flag is no set, + * the user will be asked if the payee should be created or not. + * If the MyMoneyQifReader object is created auto payee creation + * is turned off. + * + * @param create flag if this feature should be turned on (@p true) + * or turned off (@p false) + */ + virtual void setAutoCreatePayee(const bool create)=0; + virtual void setAskPayeeCategory(const bool ask)=0; + + virtual const MyMoneyAccount& account() const { return m_account; }; + virtual void setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; } + +private: + MyMoneyAccount m_account; + void (*m_progressCallback)(int, int, const QString&); + QString m_filename; + +}; + +#endif diff --git a/kmymoney2/converter/mymoneygncreader.cpp b/kmymoney2/converter/mymoneygncreader.cpp new file mode 100644 index 0000000..40933e3 --- /dev/null +++ b/kmymoney2/converter/mymoneygncreader.cpp @@ -0,0 +1,2463 @@ +/*************************************************************************** + mymoneygncreader - description + ------------------- +begin : Wed Mar 3 2004 +copyright : (C) 2000-2004 by Michael Edwardes +email : mte@users.sourceforge.net + Javier Campos Morales + Felix Rodriguez + John C + Thomas Baumgart + Kevin Tambascio +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +// ---------------------------------------------------------------------------- +// QT Includes +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Includes +#ifndef _GNCFILEANON + #include + #include + #include +#endif + +// ---------------------------------------------------------------------------- +// Third party Includes + +// ------------------------------------------------------------Box21---------------- +// Project Includes +#include "mymoneygncreader.h" +#ifndef _GNCFILEANON + #include "config.h" + #include "../mymoney/storage/imymoneystorage.h" + #include "../kmymoneyutils.h" + #include "../mymoney/mymoneyfile.h" + #include "../mymoney/mymoneyprice.h" + #include "../dialogs/kgncimportoptionsdlg.h" + #include "../dialogs/kgncpricesourcedlg.h" + #include "../dialogs/keditscheduledlg.h" + #include "../widgets/kmymoneyedit.h" + #define TRY try { + #define CATCH } catch (MyMoneyException *e) { + #define PASS } catch (MyMoneyException *e) { throw e; } +#else + #include "mymoneymoney.h" + #include + #define i18n QObject::tr + #define TRY + #define CATCH + #define PASS + #define MYMONEYEXCEPTION QString + #define MyMoneyException QString + #define PACKAGE "KMyMoney" +#endif // _GNCFILEANON + +// init static variables +double MyMoneyGncReader::m_fileHideFactor = 0.0; +double GncObject::m_moneyHideFactor; + +// user options +void MyMoneyGncReader::setOptions () { +#ifndef _GNCFILEANON + KGncImportOptionsDlg dlg; // display the dialog to allow the user to set own options + if (dlg.exec()) { + // set users input options + m_dropSuspectSchedules = dlg.scheduleOption(); + m_investmentOption = dlg.investmentOption(); + m_useFinanceQuote = dlg.quoteOption(); + m_useTxNotes = dlg.txNotesOption(); + m_decoder = dlg.decodeOption(); + gncdebug = dlg.generalDebugOption(); + xmldebug = dlg.xmlDebugOption(); + bAnonymize = dlg.anonymizeOption(); + } else { + // user declined, so set some sensible defaults + m_dropSuspectSchedules = false; + // investment option - 0, create investment a/c per stock a/c, 1 = single new investment account, 2 = prompt for each stock + // option 2 doesn't really work too well at present + m_investmentOption = 0; + m_useFinanceQuote = false; + m_useTxNotes = false; + m_decoder = 0; + gncdebug = false; // general debug messages + xmldebug = false; // xml trace + bAnonymize = false; // anonymize input + } + // no dialog option for the following; it will set base currency, and print actual XML data + developerDebug = false; + // set your fave currency here to save getting that enormous dialog each time you run a test + // especially if you have to scroll down to USD... + if (developerDebug) m_storage->setValue ("kmm-baseCurrency", "GBP"); +#endif // _GNCFILEANON +} + +GncObject::GncObject () { + m_v.setAutoDelete (true); +} + +// Check that the current element is of a version we are coded for +void GncObject::checkVersion (const QString& elName, const QXmlAttributes& elAttrs, const map_elementVersions& map) { + TRY + if (map.contains(elName)) { // if it's not in the map, there's nothing to check + if (!map[elName].contains(elAttrs.value("version"))) { + QString em = i18n("%1: Sorry. This importer cannot handle version %2 of element %3") + .arg(__func__).arg(elAttrs.value("version")).arg(elName); + throw new MYMONEYEXCEPTION (em); + } + } + return ; + PASS +} + +// Check if this element is in the current object's sub element list +GncObject *GncObject::isSubElement (const QString& elName, const QXmlAttributes& elAttrs) { + TRY + uint i; + GncObject *next = 0; + for (i = 0; i < m_subElementListCount; i++) { + if (elName == m_subElementList[i]) { + m_state = i; + next = startSubEl(); // go create the sub object + if (next != 0) { + next->initiate(elName, elAttrs); // initialize it + next->m_elementName = elName; // save it's name so we can identify the end + } + break; + } + } + return (next); + PASS +} + +// Check if this element is in the current object's data element list +bool GncObject::isDataElement (const QString &elName, const QXmlAttributes& elAttrs) { + TRY + uint i; + for (i = 0; i < m_dataElementListCount; i++) { + if (elName == m_dataElementList[i]) { + m_state = i; + dataEl(elAttrs); // go set the pointer so the data can be stored + return (true); + } + } + m_dataPtr = 0; // we don't need this, so make sure we don't store extraneous data + return (false); + PASS +} + +// return the variable string, decoded if required +QString GncObject::var (int i) const { + return (pMain->m_decoder == 0 + ? *(m_v.at(i)) + : pMain->m_decoder->toUnicode (*(m_v.at(i)))); +} + +void GncObject::adjustHideFactor () { + m_moneyHideFactor = pMain->m_fileHideFactor * (1.0 + (int)(200.0 * rand()/(RAND_MAX+1.0))) / 100.0; +} + +// data anonymizer +QString GncObject::hide (QString data, unsigned int anonClass) { + TRY + if (!pMain->bAnonymize) return (data); // no anonymizing required + // counters used to generate names for anonymizer + static int nextAccount; + static int nextEquity; + static int nextPayee; + static int nextSched; + static QMap anonPayees; // to check for duplicate payee names + static QMap anonStocks; // for reference to equities + + QString result (data); + QMap::Iterator it; + MyMoneyMoney in, mresult; + switch (anonClass) { + case ASIS: break; // this is not personal data + case SUPPRESS: result = ""; break; // this is personal and is not essential + case NXTACC: result = i18n("Account%1").arg(++nextAccount, -6); break; // generate account name + case NXTEQU: // generate/return an equity name + it = anonStocks.find (data); + if (it == anonStocks.end()) { + result = i18n("Stock%1").arg(++nextEquity, -6); + anonStocks.insert (data, result); + } else { + result = (*it).data(); + } + break; + case NXTPAY: // genearet/return a payee name + it = anonPayees.find (data); + if (it == anonPayees.end()) { + result = i18n("Payee%1").arg(++nextPayee, -6); + anonPayees.insert (data, result); + } else { + result = (*it).data(); + } + break; + case NXTSCHD: result = i18n("Schedule%1").arg(++nextSched, -6); break; // generate a schedule name + case MONEY1: + in = MyMoneyMoney(data); + if (data == "-1/0") in = MyMoneyMoney (0); // spurious gnucash data - causes a crash sometimes + mresult = MyMoneyMoney(m_moneyHideFactor) * in; + mresult.convert(10000); + result = mresult.toString(); + break; + case MONEY2: + in = MyMoneyMoney(data); + if (data == "-1/0") in = MyMoneyMoney (0); + mresult = MyMoneyMoney(m_moneyHideFactor) * in; + mresult.convert(10000); + mresult.setThousandSeparator (' '); + result = mresult.formatMoney("", 2); + break; + } + return (result); + PASS +} + +// dump current object data values // only called if gncdebug set +void GncObject::debugDump () { + uint i; + qDebug ("Object %s", m_elementName.latin1()); + for (i = 0; i < m_dataElementListCount; i++) { + qDebug ("%s = %s", m_dataElementList[i].latin1(), m_v.at(i)->latin1()); + } +} +//***************************************************************** +GncFile::GncFile () { + static const QString subEls[] = {"gnc:book", "gnc:count-data", "gnc:commodity", "price", + "gnc:account", "gnc:transaction", "gnc:template-transactions", + "gnc:schedxaction" + }; + m_subElementList = subEls; + m_subElementListCount = END_FILE_SELS; + m_dataElementListCount = 0; + m_processingTemplates = false; + m_bookFound = false; +} + +GncFile::~GncFile () {} + +GncObject *GncFile::startSubEl() { + TRY + if (pMain->xmldebug) qDebug ("File start subel m_state %d", m_state); + GncObject *next = 0; + switch (m_state) { + case BOOK: + if (m_bookFound) throw new MYMONEYEXCEPTION (i18n("This version of the importer cannot handle multi-book files.")); + m_bookFound = true; + break; + case COUNT: next = new GncCountData; break; + case CMDTY: next = new GncCommodity; break; + case PRICE: next = new GncPrice; break; + case ACCT: + // accounts within the template section are ignored + if (!m_processingTemplates) next = new GncAccount; + break; + case TX: next = new GncTransaction (m_processingTemplates); break; + case TEMPLATES: m_processingTemplates = true; break; + case SCHEDULES: m_processingTemplates = false; next = new GncSchedule; break; + default: throw new MYMONEYEXCEPTION ("GncFile rcvd invalid state"); + } + return (next); + PASS +} + +void GncFile::endSubEl(GncObject *subObj) { + if (pMain->xmldebug) qDebug ("File end subel"); + if (!m_processingTemplates) delete subObj; // template txs must be saved awaiting schedules + m_dataPtr = 0; + return ; +} +//****************************************** GncDate ********************************************* +GncDate::GncDate () { + m_subElementListCount = 0; + static const QString dEls[] = {"ts:date", "gdate"}; + m_dataElementList = dEls; + m_dataElementListCount = END_Date_DELS; + static const unsigned int anonClasses[] = {ASIS, ASIS}; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); +} + +GncDate::~GncDate() {} +//*************************************GncCmdtySpec*************************************** +GncCmdtySpec::GncCmdtySpec () { + m_subElementListCount = 0; + static const QString dEls[] = {"cmdty:space", "cmdty:id"}; + m_dataElementList = dEls; + m_dataElementListCount = END_CmdtySpec_DELS; + static const unsigned int anonClasses[] = {ASIS, ASIS}; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); +} + +GncCmdtySpec::~GncCmdtySpec () {} + +QString GncCmdtySpec::hide(QString data, unsigned int) { + // hide equity names, but not currency names + unsigned int newClass = ASIS; + switch (m_state) { + case CMDTYID: + if (!isCurrency()) newClass = NXTEQU; + } + return (GncObject::hide (data, newClass)); +} +//************* GncKvp******************************************** +GncKvp::GncKvp () { + m_subElementListCount = END_Kvp_SELS; + static const QString subEls[] = {"slot"}; // kvp's may be nested + m_subElementList = subEls; + m_dataElementListCount = END_Kvp_DELS; + static const QString dataEls[] = {"slot:key", "slot:value"}; + m_dataElementList = dataEls; + static const unsigned int anonClasses[] = {ASIS, ASIS}; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); + m_kvpList.setAutoDelete (true); +} + +GncKvp::~GncKvp () {} + +void GncKvp::dataEl (const QXmlAttributes& elAttrs) { + switch (m_state) { + case VALUE: + m_kvpType = elAttrs.value("type"); + } + m_dataPtr = m_v.at(m_state); + if (key().contains ("formula")) { + m_anonClass = MONEY2; + } else { + m_anonClass = ASIS; + } + return ; +} + +GncObject *GncKvp::startSubEl() { + if (pMain->xmldebug) qDebug ("Kvp start subel m_state %d", m_state); + TRY + GncObject *next = 0; + switch (m_state) { + case KVP: next = new GncKvp; break; + default: throw new MYMONEYEXCEPTION ("GncKvp rcvd invalid m_state "); + } + return (next); + PASS +} + +void GncKvp::endSubEl(GncObject *subObj) { + if (pMain->xmldebug) qDebug ("Kvp end subel"); + m_kvpList.append (subObj); + m_dataPtr = 0; + return ; +} +//*********************************GncLot********************************************* +GncLot::GncLot() { + m_subElementListCount = 0; + m_dataElementListCount = 0; +} + +GncLot::~GncLot() {} + +//*********************************GncCountData*************************************** +GncCountData::GncCountData() { + m_subElementListCount = 0; + m_dataElementListCount = 0; + m_v.append (new QString ("")); // only 1 data item +} + +GncCountData::~GncCountData () {} + +void GncCountData::initiate (const QString&, const QXmlAttributes& elAttrs) { + m_countType = elAttrs.value ("cd:type"); + m_dataPtr = m_v.at(0); + return ; +} + +void GncCountData::terminate () { + int i = m_v.at(0)->toInt(); + if (m_countType == "commodity") { + pMain->setGncCommodityCount(i); return ; + } + if (m_countType == "account") { + pMain->setGncAccountCount(i); return ; + } + if (m_countType == "transaction") { + pMain->setGncTransactionCount(i); return ; + } + if (m_countType == "schedxaction") { + pMain->setGncScheduleCount(i); return ; + } + if (i != 0) { + if (m_countType == "budget") pMain->setBudgetsFound(true); + else if (m_countType.left(7) == "gnc:Gnc") pMain->setSmallBusinessFound(true); + else if (pMain->xmldebug) qDebug ("Unknown count type %s", m_countType.latin1()); + } + return ; +} +//*********************************GncCommodity*************************************** +GncCommodity::GncCommodity () { + m_subElementListCount = 0; + static const QString dEls[] = {"cmdty:space", "cmdty:id", "cmdty:name", "cmdty:fraction"}; + m_dataElementList = dEls; + m_dataElementListCount = END_Commodity_DELS; + static const unsigned int anonClasses[] = {ASIS, NXTEQU, SUPPRESS, ASIS}; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); +} + +GncCommodity::~GncCommodity () {} + +void GncCommodity::terminate() { + TRY + pMain->convertCommodity (this); + return ; + PASS +} +//************* GncPrice******************************************** +GncPrice::GncPrice () { + static const QString subEls[] = {"price:commodity", "price:currency", "price:time"}; + m_subElementList = subEls; + m_subElementListCount = END_Price_SELS; + m_dataElementListCount = END_Price_DELS; + static const QString dataEls[] = {"price:value"}; + m_dataElementList = dataEls; + static const unsigned int anonClasses[] = {ASIS}; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); + m_vpCommodity = NULL; + m_vpCurrency = NULL; + m_vpPriceDate = NULL; +} + +GncPrice::~GncPrice () { + delete m_vpCommodity; delete m_vpCurrency; delete m_vpPriceDate; +} + +GncObject *GncPrice::startSubEl() { + TRY + GncObject *next = 0; + switch (m_state) { + case CMDTY: next = new GncCmdtySpec; break; + case CURR: next = new GncCmdtySpec; break; + case PRICEDATE: next = new GncDate; break; + default: throw new MYMONEYEXCEPTION ("GncPrice rcvd invalid m_state"); + } + return (next); + PASS +} + +void GncPrice::endSubEl(GncObject *subObj) { + TRY + switch (m_state) { + case CMDTY: m_vpCommodity = static_cast(subObj); break; + case CURR: m_vpCurrency = static_cast(subObj); break; + case PRICEDATE: m_vpPriceDate = static_cast(subObj); break; + default: throw new MYMONEYEXCEPTION ("GncPrice rcvd invalid m_state"); + } + return; + PASS +} + +void GncPrice::terminate() { + TRY + pMain->convertPrice (this); + return ; + PASS +} +//************* GncAccount******************************************** +GncAccount::GncAccount () { + m_subElementListCount = END_Account_SELS; + static const QString subEls[] = {"act:commodity", "slot", "act:lots"}; + m_subElementList = subEls; + m_dataElementListCount = END_Account_DELS; + static const QString dataEls[] = {"act:id", "act:name", "act:description", + "act:type", "act:parent"}; + m_dataElementList = dataEls; + static const unsigned int anonClasses[] = {ASIS, NXTACC, SUPPRESS, ASIS, ASIS}; + m_anonClassList = anonClasses; + m_kvpList.setAutoDelete (true); + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); + m_vpCommodity = NULL; +} + +GncAccount::~GncAccount () { + delete m_vpCommodity; +} + +GncObject *GncAccount::startSubEl() { + TRY + if (pMain->xmldebug) qDebug ("Account start subel m_state %d", m_state); + GncObject *next = 0; + switch (m_state) { + case CMDTY: next = new GncCmdtySpec; break; + case KVP: next = new GncKvp; break; + case LOTS: next = new GncLot(); + pMain->setLotsFound(true); // we don't handle lots; just set flag to report + break; + default: throw new MYMONEYEXCEPTION ("GncAccount rcvd invalid m_state"); + } + return (next); + PASS +} + +void GncAccount::endSubEl(GncObject *subObj) { + if (pMain->xmldebug) qDebug ("Account end subel"); + switch (m_state) { + case CMDTY: m_vpCommodity = static_cast(subObj); break; + case KVP: m_kvpList.append (subObj); + } + return ; +} + +void GncAccount::terminate() { + TRY + pMain->convertAccount (this); + return ; + PASS +} +//************* GncTransaction******************************************** +GncTransaction::GncTransaction (bool processingTemplates) { + m_subElementListCount = END_Transaction_SELS; + static const QString subEls[] = {"trn:currency", "trn:date-posted", "trn:date-entered", + "trn:split", "slot"}; + m_subElementList = subEls; + m_dataElementListCount = END_Transaction_DELS; + static const QString dataEls[] = {"trn:id", "trn:num", "trn:description"}; + m_dataElementList = dataEls; + static const unsigned int anonClasses[] = {ASIS, SUPPRESS, NXTPAY}; + m_anonClassList = anonClasses; + adjustHideFactor(); + m_template = processingTemplates; + m_splitList.setAutoDelete (true); + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); + m_vpCurrency = NULL; + m_vpDateEntered = m_vpDatePosted = NULL; +} + +GncTransaction::~GncTransaction () { + delete m_vpCurrency; delete m_vpDatePosted; delete m_vpDateEntered; +} + +GncObject *GncTransaction::startSubEl() { + TRY + if (pMain->xmldebug) qDebug ("Transaction start subel m_state %d", m_state); + GncObject *next = 0; + switch (m_state) { + case CURRCY: next = new GncCmdtySpec; break; + case POSTED: + case ENTERED: + next = new GncDate; break; + case SPLIT: + if (isTemplate()) { + next = new GncTemplateSplit; + } else { + next = new GncSplit; + } + break; + case KVP: next = new GncKvp; break; + default: throw new MYMONEYEXCEPTION ("GncTransaction rcvd invalid m_state"); + } + return (next); + PASS +} + +void GncTransaction::endSubEl(GncObject *subObj) { + if (pMain->xmldebug) qDebug ("Transaction end subel"); + switch (m_state) { + case CURRCY: m_vpCurrency = static_cast(subObj); break; + case POSTED: m_vpDatePosted = static_cast(subObj); break; + case ENTERED: m_vpDateEntered = static_cast(subObj); break; + case SPLIT: m_splitList.append (subObj); break; + case KVP: m_kvpList.append (subObj); + } + return ; +} + +void GncTransaction::terminate() { + TRY + if (isTemplate()) { + pMain->saveTemplateTransaction(this); + } else { + pMain->convertTransaction (this); + } + return ; + PASS +} +//************* GncSplit******************************************** +GncSplit::GncSplit () { + m_subElementListCount = END_Split_SELS; + static const QString subEls[] = {"split:reconcile-date"}; + m_subElementList = subEls; + m_dataElementListCount = END_Split_DELS; + static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", + "split:quantity", "split:account"}; + m_dataElementList = dataEls; + static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); + m_vpDateReconciled = NULL; +} + +GncSplit::~GncSplit () { + delete m_vpDateReconciled; +} + +GncObject *GncSplit::startSubEl () { + TRY + GncObject *next = 0; + switch (m_state) { + case RECDATE: next = new GncDate; break; + default: throw new MYMONEYEXCEPTION ("GncTemplateSplit rcvd invalid m_state "); + } + return (next); + PASS +} + +void GncSplit::endSubEl(GncObject *subObj) { + if (pMain->xmldebug) qDebug ("Split end subel"); + switch (m_state) { + case RECDATE: m_vpDateReconciled = static_cast(subObj); break; + } + return ; +} +//************* GncTemplateSplit******************************************** +GncTemplateSplit::GncTemplateSplit () { + m_subElementListCount = END_TemplateSplit_SELS; + static const QString subEls[] = {"slot"}; + m_subElementList = subEls; + m_dataElementListCount = END_TemplateSplit_DELS; + static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", + "split:quantity", "split:account"}; + m_dataElementList = dataEls; + static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); + m_kvpList.setAutoDelete (true); +} + +GncTemplateSplit::~GncTemplateSplit () {} + +GncObject *GncTemplateSplit::startSubEl() { + if (pMain->xmldebug) qDebug ("TemplateSplit start subel m_state %d", m_state); + TRY + GncObject *next = 0; + switch (m_state) { + case KVP: next = new GncKvp; break; + default: throw new MYMONEYEXCEPTION ("GncTemplateSplit rcvd invalid m_state"); + } + return (next); + PASS +} + +void GncTemplateSplit::endSubEl(GncObject *subObj) { + if (pMain->xmldebug) qDebug ("TemplateSplit end subel"); + m_kvpList.append (subObj); + m_dataPtr = 0; + return ; +} +//************* GncSchedule******************************************** +GncSchedule::GncSchedule () { + m_subElementListCount = END_Schedule_SELS; + static const QString subEls[] = {"sx:start", "sx:last", "sx:end", "gnc:freqspec", "gnc:recurrence","sx:deferredInstance"}; + m_subElementList = subEls; + m_dataElementListCount = END_Schedule_DELS; + static const QString dataEls[] = {"sx:name", "sx:enabled", "sx:autoCreate", "sx:autoCreateNotify", + "sx:autoCreateDays", "sx:advanceCreateDays", "sx:advanceRemindDays", + "sx:instanceCount", "sx:num-occur", + "sx:rem-occur", "sx:templ-acct"}; + m_dataElementList = dataEls; + static const unsigned int anonClasses[] = {NXTSCHD, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS}; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); + m_vpStartDate = m_vpLastDate = m_vpEndDate = NULL; + m_vpFreqSpec = NULL; + m_vpRecurrence.clear(); + m_vpRecurrence.setAutoDelete(true); + m_vpSchedDef = NULL; +} + +GncSchedule::~GncSchedule () { + delete m_vpStartDate; delete m_vpLastDate; delete m_vpEndDate; delete m_vpFreqSpec; delete m_vpSchedDef; +} + +GncObject *GncSchedule::startSubEl() { + if (pMain->xmldebug) qDebug ("Schedule start subel m_state %d", m_state); + TRY + GncObject *next = 0; + switch (m_state) { + case STARTDATE: + case LASTDATE: + case ENDDATE: next = new GncDate; break; + case FREQ: next = new GncFreqSpec; break; + case RECURRENCE: next = new GncRecurrence; break; + case DEFINST: next = new GncSchedDef; break; + default: throw new MYMONEYEXCEPTION ("GncSchedule rcvd invalid m_state"); + } + return (next); + PASS +} + +void GncSchedule::endSubEl(GncObject *subObj) { + if (pMain->xmldebug) qDebug ("Schedule end subel"); + switch (m_state) { + case STARTDATE: m_vpStartDate = static_cast(subObj); break; + case LASTDATE: m_vpLastDate = static_cast(subObj); break; + case ENDDATE: m_vpEndDate = static_cast(subObj); break; + case FREQ: m_vpFreqSpec = static_cast(subObj); break; + case RECURRENCE: m_vpRecurrence.append(static_cast(subObj)); break; + case DEFINST: m_vpSchedDef = static_cast(subObj); break; + } + return ; +} + +void GncSchedule::terminate() { + TRY + pMain->convertSchedule (this); + return ; + PASS +} +//************* GncFreqSpec******************************************** +GncFreqSpec::GncFreqSpec () { + m_subElementListCount = END_FreqSpec_SELS; + static const QString subEls[] = {"gnc:freqspec"}; + m_subElementList = subEls; + m_dataElementListCount = END_FreqSpec_DELS; + static const QString dataEls[] = {"fs:ui_type", "fs:monthly", "fs:daily", "fs:weekly", "fs:interval", + "fs:offset", "fs:day"}; + m_dataElementList = dataEls; + static const unsigned int anonClasses[] = {ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS }; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); + m_fsList.setAutoDelete (true); +} + +GncFreqSpec::~GncFreqSpec () {} + +GncObject *GncFreqSpec::startSubEl() { + TRY + if (pMain->xmldebug) qDebug ("FreqSpec start subel m_state %d", m_state); + + GncObject *next = 0; + switch (m_state) { + case COMPO: next = new GncFreqSpec; break; + default: throw new MYMONEYEXCEPTION ("GncFreqSpec rcvd invalid m_state"); + } + return (next); + PASS +} + +void GncFreqSpec::endSubEl(GncObject *subObj) { + if (pMain->xmldebug) qDebug ("FreqSpec end subel"); + switch (m_state) { + case COMPO: m_fsList.append (subObj); break; + } + m_dataPtr = 0; + return ; +} + +void GncFreqSpec::terminate() { + pMain->convertFreqSpec (this); + return ; +} +//************* GncRecurrence******************************************** +GncRecurrence::GncRecurrence () { + m_subElementListCount = END_Recurrence_SELS; + static const QString subEls[] = {"recurrence:start"}; + m_subElementList = subEls; + m_dataElementListCount = END_Recurrence_DELS; + static const QString dataEls[] = {"recurrence:mult", "recurrence:period_type"}; + m_dataElementList = dataEls; + static const unsigned int anonClasses[] = {ASIS, ASIS}; + m_anonClassList = anonClasses; + for (uint i = 0; i < m_dataElementListCount; i++) m_v.append (new QString ("")); +} + +GncRecurrence::~GncRecurrence () { + delete m_vpStartDate; +} + +GncObject *GncRecurrence::startSubEl() { + TRY + if (pMain->xmldebug) qDebug ("Recurrence start subel m_state %d", m_state); + + GncObject *next = 0; + switch (m_state) { + case STARTDATE: next = new GncDate; break; + default: throw new MYMONEYEXCEPTION ("GncRecurrence rcvd invalid m_state"); + } + return (next); + PASS +} + +void GncRecurrence::endSubEl(GncObject *subObj) { + if (pMain->xmldebug) qDebug ("Recurrence end subel"); + switch (m_state) { + case STARTDATE: m_vpStartDate = static_cast(subObj); break; + } + m_dataPtr = 0; + return ; +} + +void GncRecurrence::terminate() { + pMain->convertRecurrence (this); + return ; +} + +QString GncRecurrence::getFrequency() const { + // This function converts a gnucash 2.2 recurrence specification into it's previous equivalent + // This will all need re-writing when MTE finishes the schedule re-write + if (periodType() == "once") return("once"); + if ((periodType() == "day") and (mult() == "1")) return("daily"); + if (periodType() == "week") { + if (mult() == "1") return ("weekly"); + if (mult() == "2") return ("bi_weekly"); + if (mult() == "4") return ("four-weekly"); + } + if (periodType() == "month") { + if (mult() == "1") return ("monthly"); + if (mult() == "2") return ("two-monthly"); + if (mult() == "3") return ("quarterly"); + if (mult() == "4") return ("tri_annually"); + if (mult() == "6") return ("semi_yearly"); + if (mult() == "12") return ("yearly"); + if (mult() == "24") return ("two-yearly"); + } + return ("unknown"); +} + +//************* GncSchedDef******************************************** +GncSchedDef::GncSchedDef () { + // process ing for this sub-object is undefined at the present time + m_subElementListCount = 0; + m_dataElementListCount = 0; +} + +GncSchedDef::~GncSchedDef () {} + +/************************************************************************************************ + XML Reader +************************************************************************************************/ +void XmlReader::processFile (QIODevice* pDevice) { + m_source = new QXmlInputSource (pDevice); // set up the Qt XML reader + m_reader = new QXmlSimpleReader; + m_reader->setContentHandler (this); + // go read the file + if (!m_reader->parse (m_source)) { + throw new MYMONEYEXCEPTION (i18n("Input file cannot be parsed; may be corrupt\n%s", errorString().latin1())); + } + delete m_reader; + delete m_source; + return ; +} + +// XML handling routines +bool XmlReader::startDocument() { + m_os.setAutoDelete (true); + m_co = new GncFile; // create initial object, push to stack , pass it the 'main' pointer + m_os.push (m_co); + m_co->setPm (pMain); + m_headerFound = false; +#ifdef _GNCFILEANON + pMain->oStream << ""; + lastType = -1; + indentCount = 0; +#endif // _GNCFILEANON + return (true); +} + +bool XmlReader::startElement (const QString&, const QString&, const QString& elName , + const QXmlAttributes& elAttrs) { + try { + if (pMain->gncdebug) qDebug ("XML start - %s", elName.latin1()); +#ifdef _GNCFILEANON + int i; + QString spaces; + // anonymizer - write data + if (elName == "gnc:book" || elName == "gnc:count-data" || elName == "book:id") lastType = -1; + pMain->oStream << endl; + switch (lastType) { + case 0: + indentCount += 2; + // tricky fall through here + + case 2: + spaces.fill (' ', indentCount); + pMain->oStream << spaces.latin1(); + break; + } + pMain->oStream << '<' << elName; + for (i = 0; i < elAttrs.count(); i++) { + pMain->oStream << ' ' << elAttrs.qName(i) << '=' << '"' << elAttrs.value(i) << '"'; + } + pMain->oStream << '>'; + lastType = 0; +#else + if ((!m_headerFound) && (elName != "gnc-v2")) + throw new MYMONEYEXCEPTION (i18n("Invalid header for file. Should be 'gnc-v2'")); + m_headerFound = true; +#endif // _GNCFILEANON + m_co->checkVersion (elName, elAttrs, pMain->m_versionList); + // check if this is a sub object element; if so, push stack and initialize + GncObject *temp = m_co->isSubElement (elName, elAttrs); + if (temp != 0) { + m_os.push (temp); + m_co = m_os.top(); + m_co->setVersion(elAttrs.value("version")); + m_co->setPm (pMain); // pass the 'main' pointer to the sub object + // return true; // removed, as we hit a return true anyway + } +#if 0 + // check for a data element + if (m_co->isDataElement (elName, elAttrs)) + return (true); +#endif + else { + // reduced the above to + m_co->isDataElement(elName, elAttrs); + } + } catch (MyMoneyException *e) { +#ifndef _GNCFILEANON + // we can't pass on exceptions here coz the XML reader won't catch them and we just abort + KMessageBox::error(0, i18n("Import failed:\n\n%1").arg(e->what()), PACKAGE); + qFatal ("%s", e->what().latin1()); +#else + qFatal ("%s", e->latin1()); +#endif // _GNCFILEANON + } + return true; // to keep compiler happy +} + +bool XmlReader::endElement( const QString&, const QString&, const QString&elName ) { + try { + if (pMain->xmldebug) qDebug ("XML end - %s", elName.latin1()); +#ifdef _GNCFILEANON + QString spaces; + switch (lastType) { + case 2: + indentCount -= 2; spaces.fill (' ', indentCount); pMain->oStream << endl << spaces.latin1(); break; + } + pMain->oStream << "' ; + lastType = 2; +#endif // _GNCFILEANON + m_co->resetDataPtr(); // so we don't get extraneous data loaded into the variables + if (elName == m_co->getElName()) { // check if this is the end of the current object + if (pMain->gncdebug) m_co->debugDump(); // dump the object data (temp) + // call the terminate routine, pop the stack, and advise the parent that it's done + m_co->terminate(); + GncObject *temp = m_co; + m_os.pop(); + m_co = m_os.top(); + m_co->endSubEl (temp); + } + return (true); + } catch (MyMoneyException *e) { +#ifndef _GNCFILEANON + // we can't pass on exceptions here coz the XML reader won't catch them and we just abort + KMessageBox::error(0, i18n("Import failed:\n\n%1").arg(e->what()), PACKAGE); + qFatal ("%s", e->what().latin1()); +#else + qFatal ("%s", e->latin1()); +#endif // _GNCFILEANON + } + return (true); // to keep compiler happy +} + +bool XmlReader::characters (const QString &data) { + if (pMain->xmldebug) qDebug ("XML Data received - %d bytes", data.length()); + QString pData = data.stripWhiteSpace(); // data may contain line feeds and indentation spaces + if (!pData.isEmpty()) { + if (pMain->developerDebug) qDebug ("XML Data - %s", pData.latin1()); + m_co->storeData (pData); //go store it +#ifdef _GNCFILEANON + QString anonData = m_co->getData (); + if (anonData.isEmpty()) anonData = pData; + // there must be a Qt standard way of doing the following but I can't ... find it + anonData.replace ('<', "<"); + anonData.replace ('>', ">"); + anonData.replace ('&', "&"); + pMain->oStream << anonData; // write original data + lastType = 1; +#endif // _GNCFILEANON + } + return (true); +} + +bool XmlReader::endDocument() { +#ifdef _GNCFILEANON + pMain->oStream << endl << endl; + pMain->oStream << "" << endl; + pMain->oStream << "" << endl; + pMain->oStream << "" << endl; +#endif // _GNCFILEANON + return (true); +} + +/******************************************************************************************* + Main class for this module + Controls overall operation of the importer +********************************************************************************************/ +//***************** Constructor *********************** +MyMoneyGncReader::MyMoneyGncReader() { +#ifndef _GNCFILEANON + m_storage = NULL; + m_messageList.setAutoDelete (true); + m_templateList.setAutoDelete (true); +#endif // _GNCFILEANON +// to hold gnucash count data (only used for progress bar) + m_gncCommodityCount = m_gncAccountCount = m_gncTransactionCount = m_gncScheduleCount = 0; + m_smallBusinessFound = m_budgetsFound = m_lotsFound = false; + m_commodityCount = m_priceCount = m_accountCount = m_transactionCount = m_templateCount = m_scheduleCount = 0; + m_decoder = 0; + // build a list of valid versions + static const QString versionList[] = {"gnc:book 2.0.0", "gnc:commodity 2.0.0", "gnc:pricedb 1", + "gnc:account 2.0.0", "gnc:transaction 2.0.0", "gnc:schedxaction 1.0.0", + "gnc:schedxaction 2.0.0", // for gnucash 2.2 onward + "gnc:freqspec 1.0.0", "zzz" // zzz = stopper + }; + unsigned int i; + for (i = 0; versionList[i] != "zzz"; ++i) + m_versionList[versionList[i].section (' ', 0, 0)].append(versionList[i].section (' ', 1, 1)); +} + +//***************** Destructor ************************* +MyMoneyGncReader::~MyMoneyGncReader() {} + +//**************************** Main Entry Point ************************************ +#ifndef _GNCFILEANON +void MyMoneyGncReader::readFile(QIODevice* pDevice, IMyMoneySerialize* storage) { + + Q_CHECK_PTR (pDevice); + Q_CHECK_PTR (storage); + + m_storage = dynamic_cast(storage); + qDebug ("Entering gnucash importer"); + setOptions (); + // get a file anonymization factor from the user + if (bAnonymize) setFileHideFactor (); + //m_defaultPayee = createPayee (i18n("Unknown payee")); + + MyMoneyFileTransaction ft; + m_xr = new XmlReader (this); + try { + m_xr->processFile (pDevice); + terminate (); // do all the wind-up things + ft.commit(); + } catch (MyMoneyException *e) { + KMessageBox::error(0, i18n("Import failed:\n\n%1").arg(e->what()), PACKAGE); + qFatal ("%s", e->what().latin1()); + } // end catch + signalProgress (0, 1, i18n("Import complete")); // switch off progress bar + delete m_xr; + qDebug ("Exiting gnucash importer"); + return ; +} +#else +// Control code for the file anonymizer +void MyMoneyGncReader::readFile(QString in, QString out) { + QFile pDevice (in); + if (!pDevice.open (IO_ReadOnly)) qFatal ("Can't open input file"); + QFile outFile (out); + if (!outFile.open (IO_WriteOnly)) qFatal ("Can't open output file"); + oStream.setDevice (&outFile); + bAnonymize = true; + // get a file anonymization factor from the user + setFileHideFactor (); + m_xr = new XmlReader (this); + try { + m_xr->processFile (&pDevice); + } catch (MyMoneyException *e) { + qFatal ("%s", e->latin1()); + } // end catch + delete m_xr; + pDevice.close(); + outFile.close(); + return ; +} + +#include +int main (int argc, char ** argv) { + QApplication a (argc, argv); + MyMoneyGncReader m; + QString inFile, outFile; + + if (argc > 0) inFile = a.argv()[1]; + if (argc > 1) outFile = a.argv()[2]; + if (inFile.isEmpty()) { + inFile = QFileDialog::getOpenFileName("", + "Gnucash files(*.nc *)", + 0); + } + if (inFile.isEmpty()) qFatal ("Input file required"); + if (outFile.isEmpty()) outFile = inFile + ".anon"; + m.readFile (inFile, outFile); + exit (0); +} +#endif // _GNCFILEANON + +void MyMoneyGncReader::setFileHideFactor () { +#define MINFILEHIDEF 0.01 +#define MAXFILEHIDEF 99.99 + srand (QTime::currentTime().second()); // seed randomizer for anonymize + m_fileHideFactor = 0.0; + while (m_fileHideFactor == 0.0) { + m_fileHideFactor = QInputDialog::getDouble ( + i18n ("Disguise your wealth"), + i18n ("Each monetary value on your file will be multiplied by a random number between 0.01 and 1.99\n" + "with a different value used for each transaction. In addition, to further disguise the true\n" + "values, you may enter a number between %1 and %2 which will be applied to all values.\n" + "These numbers will not be stored in the file.").arg(MINFILEHIDEF).arg(MAXFILEHIDEF), + (1.0 + (int)(1000.0 * rand() / (RAND_MAX + 1.0))) / 100.0, + MINFILEHIDEF, MAXFILEHIDEF, 2); + } +} +#ifndef _GNCFILEANON + +//********************************* convertCommodity ******************************************* +void MyMoneyGncReader::convertCommodity (const GncCommodity *gcm) { + Q_CHECK_PTR (gcm); + MyMoneySecurity equ; + if (m_commodityCount == 0) signalProgress (0, m_gncCommodityCount, i18n("Loading commodities...")); + if (!gcm->isCurrency()) { // currencies should not be present here but... + equ.setName (gcm->name()); + equ.setTradingSymbol (gcm->id()); + equ.setTradingMarket (gcm->space()); // the 'space' may be market or quote source, dep on what the user did + // don't set the source here since he may not want quotes + //equ.setValue ("kmm-online-source", gcm->space()); // we don't know, so use it as both + equ.setTradingCurrency (""); // not available here, will set from pricedb or transaction + equ.setSecurityType (MyMoneySecurity::SECURITY_STOCK); // default to it being a stock + //tell the storage objects we have a new equity object. + equ.setSmallestAccountFraction(gcm->fraction().toInt()); + m_storage->addSecurity(equ); + + //assign the gnucash id as the key into the map to find our id + if (gncdebug) qDebug ("mapping, key = %s, id = %s", gcm->id().latin1(), equ.id().data()); + m_mapEquities[gcm->id().utf8()] = equ.id(); + } + signalProgress (++m_commodityCount, 0); + return ; +} + +//******************************* convertPrice ************************************************ +void MyMoneyGncReader::convertPrice (const GncPrice *gpr) { + Q_CHECK_PTR (gpr); + // add this to our price history + if (m_priceCount == 0) signalProgress (0, 1, i18n("Loading prices...")); + MyMoneyMoney rate = convBadValue (gpr->value()); + if (gpr->commodity()->isCurrency()) { + MyMoneyPrice exchangeRate (gpr->commodity()->id().utf8(), gpr->currency()->id().utf8(), + gpr->priceDate(), rate, i18n("Imported History")); + m_storage->addPrice (exchangeRate); + } else { + MyMoneySecurity e = m_storage->security(m_mapEquities[gpr->commodity()->id().utf8()]); + if (gncdebug) qDebug ("Searching map, key = %s, found id = %s", + gpr->commodity()->id().latin1(), e.id().data()); + e.setTradingCurrency (gpr->currency()->id().utf8()); + MyMoneyPrice stockPrice(e.id(), gpr->currency()->id().utf8(), gpr->priceDate(), rate, i18n("Imported History")); + m_storage->addPrice (stockPrice); + m_storage->modifySecurity(e); + } + signalProgress (++m_priceCount, 0); + return ; +} + +//*********************************convertAccount **************************************** +void MyMoneyGncReader::convertAccount (const GncAccount* gac) { + Q_CHECK_PTR (gac); + TRY + // we don't care about the GNC root account + if("ROOT" == gac->type()) { + m_rootId = gac->id().utf8(); + return; + } + + MyMoneyAccount acc; + if (m_accountCount == 0) signalProgress (0, m_gncAccountCount, i18n("Loading accounts...")); + acc.setName(gac->name()); + + acc.setDescription(gac->desc()); + + QDate currentDate = QDate::currentDate(); + acc.setOpeningDate(currentDate); + acc.setLastModified(currentDate); + acc.setLastReconciliationDate(currentDate); + if (gac->commodity()->isCurrency()) { + acc.setCurrencyId (gac->commodity()->id().utf8()); + m_currencyCount[gac->commodity()->id()]++; + } + + acc.setParentAccountId (gac->parent().utf8()); + // now determine the account type and its parent id + /* This list taken from +# Feb 2006: A RELAX NG Compact schema for gnucash "v2" XML files. +# Copyright (C) 2006 Joshua Sled +"NO_TYPE" "BANK" "CASH" "CREDIT" "ASSET" "LIABILITY" "STOCK" "MUTUAL" "CURRENCY" +"INCOME" "EXPENSE" "EQUITY" "RECEIVABLE" "PAYABLE" "CHECKING" "SAVINGS" "MONEYMRKT" "CREDITLINE" + Some don't seem to be used in practice. Not sure what CREDITLINE s/be converted as. + */ + if ("BANK" == gac->type() || "CHECKING" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Checkings); + } else if ("SAVINGS" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Savings); + } else if ("ASSET" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Asset); + } else if ("CASH" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Cash); + } else if ("CURRENCY" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Cash); + } else if ("STOCK" == gac->type() || "MUTUAL" == gac->type() ) { + // gnucash allows a 'broker' account to be denominated as type STOCK, but with + // a currency balance. We do not need to create a stock account for this + // actually, the latest version of gnc (1.8.8) doesn't seem to allow you to do + // this any more, though I do have one in my own account... + if (gac->commodity()->isCurrency()) { + acc.setAccountType(MyMoneyAccount::Investment); + } else { + acc.setAccountType(MyMoneyAccount::Stock); + } + } else if ("EQUITY" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Equity); + } else if ("LIABILITY" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Liability); + } else if ("CREDIT" == gac->type()) { + acc.setAccountType(MyMoneyAccount::CreditCard); + } else if ("INCOME" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Income); + } else if ("EXPENSE" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Expense); + } else if ("RECEIVABLE" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Asset); + } else if ("PAYABLE" == gac->type()) { + acc.setAccountType(MyMoneyAccount::Liability); + } else if ("MONEYMRKT" == gac->type()) { + acc.setAccountType(MyMoneyAccount::MoneyMarket); + } else { // we have here an account type we can't currently handle + QString em = + i18n("Current importer does not recognize GnuCash account type %1").arg(gac->type()); + throw new MYMONEYEXCEPTION (em); + } + // if no parent account is present, assign to one of our standard accounts + if ((acc.parentAccountId().isEmpty()) || (acc.parentAccountId() == m_rootId)) { + switch (acc.accountGroup()) { + case MyMoneyAccount::Asset: acc.setParentAccountId (m_storage->asset().id()); break; + case MyMoneyAccount::Liability: acc.setParentAccountId (m_storage->liability().id()); break; + case MyMoneyAccount::Income: acc.setParentAccountId (m_storage->income().id()); break; + case MyMoneyAccount::Expense: acc.setParentAccountId (m_storage->expense().id()); break; + case MyMoneyAccount::Equity: acc.setParentAccountId (m_storage->equity().id()); break; + default: break; // not necessary but avoids compiler warnings + } + } + + // extra processing for a stock account + if (acc.accountType() == MyMoneyAccount::Stock) { + // save the id for later linking to investment account + m_stockList.append (gac->id()); + // set the equity type + MyMoneySecurity e = m_storage->security (m_mapEquities[gac->commodity()->id().utf8()]); + if (gncdebug) qDebug ("Acct equity search, key = %s, found id = %s", + gac->commodity()->id().latin1(), e.id().data()); + acc.setCurrencyId (e.id()); // actually, the security id + if ("MUTUAL" == gac->type()) { + e.setSecurityType (MyMoneySecurity::SECURITY_MUTUALFUND); + if (gncdebug) qDebug ("Setting %s to mutual", e.name().latin1()); + m_storage->modifySecurity (e); + } + // See if he wants online quotes for this account + // NB: In gnc, this selection is per account, in KMM, per security + // This is unlikely to cause problems in practice. If it does, + // we probably need to introduce a 'pricing basis' in the account class + QPtrListIterator kvpi (gac->m_kvpList); + GncKvp *k; + while ((k = static_cast(kvpi.current())) != 0) { + if (k->key().contains("price-source") && k->type() == "string") { + getPriceSource (e, k->value()); + break; + } else { + ++kvpi; + } + } + } + + // check for tax-related status + QPtrListIterator kvpi (gac->m_kvpList); + GncKvp *k; + while ((k = static_cast(kvpi.current())) != 0) { + if (k->key().contains("tax-related") && k->type() == "integer" && k->value() == "1") { + acc.setValue ("Tax", "Yes"); + break; + } else { + ++kvpi; + } + } + + // all the details from the file about the account should be known by now. + // calling addAccount will automatically fill in the account ID. + m_storage->addAccount(acc); + m_mapIds[gac->id().utf8()] = acc.id(); // to link gnucash id to ours for tx posting + + if (gncdebug) qDebug("Gnucash account %s has id of %s, type of %s, parent is %s", + gac->id().latin1(), acc.id().data(), + KMyMoneyUtils::accountTypeToString(acc.accountType()).latin1(), acc.parentAccountId().data()); + + signalProgress (++m_accountCount, 0); + return ; + PASS +} + +//********************************************** convertTransaction ***************************** +void MyMoneyGncReader::convertTransaction (const GncTransaction *gtx) { + Q_CHECK_PTR (gtx); + MyMoneyTransaction tx; + MyMoneySplit split; + unsigned int i; + + if (m_transactionCount == 0) signalProgress (0, m_gncTransactionCount, i18n("Loading transactions...")); + // initialize class variables related to transactions + m_txCommodity = ""; + m_txPayeeId = ""; + m_potentialTransfer = true; + m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); + // payee, dates, commodity + if (!gtx->desc().isEmpty()) m_txPayeeId = createPayee (gtx->desc()); + tx.setEntryDate (gtx->dateEntered()); + tx.setPostDate (gtx->datePosted()); + m_txDatePosted = tx.postDate(); // save for use in splits + m_txChequeNo = gtx->no(); // ditto + tx.setCommodity (gtx->currency().utf8()); + m_txCommodity = tx.commodity(); // save in storage, maybe needed for Orphan accounts + // process splits + for (i = 0; i < gtx->splitCount(); i++) { + convertSplit (static_cast(gtx->getSplit (i))); + } + // handle the odd case of just one split, which gnc allows, + // by just duplicating the split + // of course, we should change the sign but this case has only ever been seen + // when the balance is zero, and can cause kmm to crash, so... + if (gtx->splitCount() == 1) { + convertSplit (static_cast(gtx->getSplit (0))); + } + m_splitList += m_liabilitySplitList += m_otherSplitList; + // the splits are in order in splitList. Link them to the tx. also, determine the + // action type, and fill in some fields which gnc holds at transaction level + // first off, is it a transfer (can only have 2 splits?) + // also, a tx with just 2 splits is shown by GnuCash as non-split + bool nonSplitTx = true; + if (m_splitList.count() != 2) { + m_potentialTransfer = false; + nonSplitTx = false; + } + for (i = 0; i < gtx->kvpCount(); i++ ) { + const GncKvp *slot = gtx->getKvp(i); + if (slot->key() == "notes") tx.setMemo(slot->value()); + } + QValueList::iterator it = m_splitList.begin(); + while (!m_splitList.isEmpty()) { + split = *it; + // at this point, if m_potentialTransfer is still true, it is actually one! + if (m_potentialTransfer) split.setAction(MyMoneySplit::ActionTransfer); + if ((m_useTxNotes) // if use txnotes option is set + && (nonSplitTx) // and it's a (GnuCash) non-split transaction + && (!tx.memo().isEmpty())) // and tx notes are present + split.setMemo(tx.memo()); // use the tx notes as memo + tx.addSplit(split); + it = m_splitList.remove(it); + } + // memo - set from split - not any more + //tx.setMemo(txMemo); + m_storage->addTransaction(tx, true); // all done, add the transaction to storage + signalProgress (++m_transactionCount, 0); + return ; +} +//******************************************convertSplit******************************** +void MyMoneyGncReader::convertSplit (const GncSplit *gsp) { + Q_CHECK_PTR (gsp); + MyMoneySplit split; + MyMoneyAccount splitAccount; + // find the kmm account id coresponding to the gnc id + QString kmmAccountId; + map_accountIds::Iterator id = m_mapIds.find(gsp->acct().utf8()); + if (id != m_mapIds.end()) { + kmmAccountId = id.data(); + } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name + kmmAccountId = createOrphanAccount (gsp->acct()); + } + // find the account pointer and save for later + splitAccount = m_storage->account (kmmAccountId); + // print some data so we can maybe identify this split later + // TODO : prints personal data + //if (gncdebug) qDebug ("Split data - gncid %s, kmmid %s, memo %s, value %s, recon state %s", + // gsp->acct().latin1(), kmmAccountId.data(), gsp->memo().latin1(), gsp->value().latin1(), + // gsp->recon().latin1()); + // payee id + split.setPayeeId (m_txPayeeId.utf8()); + // reconciled state and date + switch (gsp->recon().at(0).latin1()) { + case 'n': + split.setReconcileFlag(MyMoneySplit::NotReconciled); break; + case 'c': + split.setReconcileFlag(MyMoneySplit::Cleared); break; + case 'y': + split.setReconcileFlag(MyMoneySplit::Reconciled); break; + } + split.setReconcileDate(gsp->reconDate()); + // memo + split.setMemo(gsp->memo()); + // accountId + split.setAccountId (kmmAccountId); + // cheque no + split.setNumber (m_txChequeNo); + // value and quantity + MyMoneyMoney splitValue (convBadValue (gsp->value())); + if (gsp->value() == "-1/0") { // treat gnc invalid value as zero + // it's not quite a consistency check, but easier to treat it as such + postMessage ("CC", 4, splitAccount.name().latin1(), m_txDatePosted.toString(Qt::ISODate).latin1()); + } + MyMoneyMoney splitQuantity(convBadValue(gsp->qty())); + split.setValue (splitValue); + // if split currency = tx currency, set shares = value (14/10/05) + if (splitAccount.currencyId() == m_txCommodity) { + split.setShares (splitValue); + } else { + split.setShares (splitQuantity); + } + + // in kmm, the first split is important. in this routine we will + // save the splits in our split list with the priority: + // 1. assets + // 2. liabilities + // 3. others (categories) + // but keeping each in same order as gnucash + MyMoneySecurity e; + MyMoneyMoney price, newPrice(0); + + switch (splitAccount.accountGroup()) { + case MyMoneyAccount::Asset: + if (splitAccount.accountType() == MyMoneyAccount::Stock) { + split.value() == MyMoneyMoney(0) ? + split.setAction (MyMoneySplit::ActionAddShares) : // free shares? + split.setAction (MyMoneySplit::ActionBuyShares); + m_potentialTransfer = false; // ? + // add a price history entry + e = m_storage->security(splitAccount.currencyId()); + // newPrice fix supplied by Phil Longstaff + price = split.value() / split.shares(); +#define NEW_DENOM 10000 + if (!split.shares().isZero()) // patch to fix divide by zero? + newPrice = MyMoneyMoney ( price.toDouble(), (signed64)NEW_DENOM ); + if (!newPrice.isZero()) { + TRY + // we can't use m_storage->security coz security list is not built yet + m_storage->currency(m_txCommodity); // will throw exception if not currency + e.setTradingCurrency (m_txCommodity); + if (gncdebug) qDebug ("added price for %s, %s date %s", + e.name().latin1(), newPrice.toString().latin1(), + m_txDatePosted.toString(Qt::ISODate).latin1()); + m_storage->modifySecurity(e); + MyMoneyPrice dealPrice (e.id(), m_txCommodity, m_txDatePosted, newPrice, i18n("Imported Transaction")); + m_storage->addPrice (dealPrice); + CATCH // stock transfer; treat like free shares? + split.setAction (MyMoneySplit::ActionAddShares); + delete e; + } + } + } else { // not stock + if (split.value().isNegative()) { + bool isNumeric = false; + if (!split.number().isEmpty()) { + split.number().toLong(&isNumeric); // No QString.isNumeric()?? + } + if (isNumeric) { + split.setAction (MyMoneySplit::ActionCheck); + } else { + split.setAction (MyMoneySplit::ActionWithdrawal); + } + } else { + split.setAction (MyMoneySplit::ActionDeposit); + } + } + m_splitList.append(split); + break; + case MyMoneyAccount::Liability: + split.value().isNegative() ? + split.setAction (MyMoneySplit::ActionWithdrawal) : + split.setAction (MyMoneySplit::ActionDeposit); + m_liabilitySplitList.append(split); + break; + default: + m_potentialTransfer = false; + m_otherSplitList.append (split); + } + // backdate the account opening date if necessary + if (m_txDatePosted < splitAccount.openingDate()) { + splitAccount.setOpeningDate(m_txDatePosted); + m_storage->modifyAccount(splitAccount); + } + return ; +} +//********************************* convertTemplateTransaction ********************************************** +MyMoneyTransaction MyMoneyGncReader::convertTemplateTransaction (const QString& schedName, const GncTransaction *gtx) { + + Q_CHECK_PTR (gtx); + MyMoneyTransaction tx; + MyMoneySplit split; + unsigned int i; + if (m_templateCount == 0) signalProgress (0, 1, i18n("Loading templates...")); + + // initialize class variables related to transactions + m_txCommodity = ""; + m_txPayeeId = ""; + m_potentialTransfer = true; + m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); + + // payee, dates, commodity + if (!gtx->desc().isEmpty()) { + m_txPayeeId = createPayee (gtx->desc()); + } else { + m_txPayeeId = createPayee (i18n("Unknown payee")); // schedules require a payee tho normal tx's don't. not sure why... + } + tx.setEntryDate(gtx->dateEntered()); + tx.setPostDate(gtx->datePosted()); + m_txDatePosted = tx.postDate(); + tx.setCommodity (gtx->currency().utf8()); + m_txCommodity = tx.commodity(); // save for possible use in orphan account + // process splits + for (i = 0; i < gtx->splitCount(); i++) { + convertTemplateSplit (schedName, static_cast(gtx->getSplit (i))); + } + // determine the action type for the splits and link them to the template tx + /*QString negativeActionType, positiveActionType; + if (!m_splitList.isEmpty()) { // if there are asset splits + positiveActionType = MyMoneySplit::ActionDeposit; + negativeActionType = MyMoneySplit::ActionWithdrawal; + } else { // if there are liability splits + positiveActionType = MyMoneySplit::ActionWithdrawal; + negativeActionType = MyMoneySplit::ActionDeposit; +} */ + if (!m_otherSplitList.isEmpty()) m_potentialTransfer = false; // tfrs can occur only between assets and asset/liabilities + m_splitList += m_liabilitySplitList += m_otherSplitList; + // the splits are in order in splitList. Transfer them to the tx + // also, determine the action type. first off, is it a transfer (can only have 2 splits?) + if (m_splitList.count() != 2) m_potentialTransfer = false; + // at this point, if m_potentialTransfer is still true, it is actually one! + QString txMemo = ""; + QValueList::iterator it = m_splitList.begin(); + while (!m_splitList.isEmpty()) { + split = *it; + if (m_potentialTransfer) { + split.setAction(MyMoneySplit::ActionTransfer); + } else { + if (split.value().isNegative()) { + //split.setAction (negativeActionType); + split.setAction (MyMoneySplit::ActionWithdrawal); + } else { + //split.setAction (positiveActionType); + split.setAction (MyMoneySplit::ActionDeposit); + } + } + split.setNumber(gtx->no()); // set cheque no (or equivalent description) + // Arbitrarily, save the first non-null split memo as the memo for the whole tx + // I think this is necessary because txs with just 2 splits (the majority) + // are not viewable as split transactions in kmm so the split memo is not seen + if ((txMemo.isEmpty()) && (!split.memo().isEmpty())) txMemo = split.memo(); + tx.addSplit(split); + it = m_splitList.remove(it); + } + // memo - set from split + tx.setMemo (txMemo); + signalProgress (++m_templateCount, 0); + return (tx); +} +//********************************* convertTemplateSplit **************************************************** +void MyMoneyGncReader::convertTemplateSplit (const QString& schedName, const GncTemplateSplit *gsp) { + Q_CHECK_PTR (gsp); + // convertTemplateSplit + MyMoneySplit split; + MyMoneyAccount splitAccount; + unsigned int i, j; + bool nonNumericFormula = false; + + // action, value and account will be set from slots + // reconcile state, always Not since it hasn't even been posted yet (?) + split.setReconcileFlag(MyMoneySplit::NotReconciled); + // memo + split.setMemo(gsp->memo()); + // payee id + split.setPayeeId (m_txPayeeId.utf8()); + // read split slots (KVPs) + int xactionCount = 0; + int validSlotCount = 0; + QString gncAccountId; + for (i = 0; i < gsp->kvpCount(); i++ ) { + const GncKvp *slot = gsp->getKvp(i); + if ((slot->key() == "sched-xaction") && (slot->type() == "frame")) { + bool bFoundStringCreditFormula = false; + bool bFoundStringDebitFormula = false; + bool bFoundGuidAccountId = false; + QString gncCreditFormula, gncDebitFormula; + for (j = 0; j < slot->kvpCount(); j++) { + const GncKvp *subSlot = slot->getKvp (j); + // again, see comments above. when we have a full specification + // of all the options available to us, we can no doubt improve on this + if ((subSlot->key() == "credit-formula") && (subSlot->type() == "string")) { + gncCreditFormula = subSlot->value(); + bFoundStringCreditFormula = true; + } + if ((subSlot->key() == "debit-formula") && (subSlot->type() == "string")) { + gncDebitFormula = subSlot->value(); + bFoundStringDebitFormula = true; + } + if ((subSlot->key() == "account") && (subSlot->type() == "guid")) { + gncAccountId = subSlot->value(); + bFoundGuidAccountId = true; + } + } + // all data read, now check we have everything + if ((bFoundStringCreditFormula) && (bFoundStringDebitFormula) && (bFoundGuidAccountId)) { + if (gncdebug) qDebug ("Found valid slot; credit %s, debit %s, acct %s", + gncCreditFormula.latin1(), gncDebitFormula.latin1(), gncAccountId.latin1()); + validSlotCount++; + } + // validate numeric, work out sign + MyMoneyMoney exFormula (0); + exFormula.setNegativeMonetarySignPosition (MyMoneyMoney::BeforeQuantityMoney); + QString numericTest; + char crdr=0 ; + if (!gncCreditFormula.isEmpty()) { + crdr = 'C'; + numericTest = gncCreditFormula; + } else if (!gncDebitFormula.isEmpty()) { + crdr = 'D'; + numericTest = gncDebitFormula; + } + kMyMoneyMoneyValidator v (0); + int pos; // useless, but required for validator + if (v.validate (numericTest, pos) == QValidator::Acceptable) { + switch (crdr) { + case 'C': + exFormula = QString ("-" + numericTest); break; + case 'D': + exFormula = numericTest; + } + } else { + if (gncdebug) qDebug ("%s is not numeric", numericTest.latin1()); + nonNumericFormula = true; + } + split.setValue (exFormula); + xactionCount++; + } else { + postMessage ("SC", 3, schedName.latin1(), slot->key().latin1(), slot->type().latin1()); + m_suspectSchedule = true; + } + } + // report this as untranslatable tx + if (xactionCount > 1) { + postMessage ("SC", 4, schedName.latin1()); + m_suspectSchedule = true; + } + if (validSlotCount == 0) { + postMessage ("SC", 5, schedName.latin1()); + m_suspectSchedule = true; + } + if (nonNumericFormula) { + postMessage ("SC", 6, schedName.latin1()); + m_suspectSchedule = true; + } + // find the kmm account id coresponding to the gnc id + QString kmmAccountId; + map_accountIds::Iterator id = m_mapIds.find(gncAccountId.utf8()); + if (id != m_mapIds.end()) { + kmmAccountId = id.data(); + } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name + kmmAccountId = createOrphanAccount (gncAccountId); + } + splitAccount = m_storage->account (kmmAccountId); + split.setAccountId (kmmAccountId); + // if split currency = tx currency, set shares = value (14/10/05) + if (splitAccount.currencyId() == m_txCommodity) { + split.setShares (split.value()); + } /* else { //FIXME: scheduled currency or investment tx needs to be investigated + split.setShares (splitQuantity); + } */ + // add the split to one of the lists + switch (splitAccount.accountGroup()) { + case MyMoneyAccount::Asset: + m_splitList.append (split); break; + case MyMoneyAccount::Liability: + m_liabilitySplitList.append (split); break; + default: + m_otherSplitList.append (split); + } + // backdate the account opening date if necessary + if (m_txDatePosted < splitAccount.openingDate()) { + splitAccount.setOpeningDate(m_txDatePosted); + m_storage->modifyAccount(splitAccount); + } + return ; +} +//********************************* convertSchedule ******************************************************** +void MyMoneyGncReader::convertSchedule (const GncSchedule *gsc) { + TRY + Q_CHECK_PTR (gsc); + MyMoneySchedule sc; + MyMoneyTransaction tx; + m_suspectSchedule = false; + QDate startDate, nextDate, lastDate, endDate; // for date calculations + QDate today = QDate::currentDate(); + int numOccurs, remOccurs; + + if (m_scheduleCount == 0) signalProgress (0, m_gncScheduleCount, i18n("Loading schedules...")); + // schedule name + sc.setName(gsc->name()); + // find the transaction template as stored earlier + QPtrListIterator itt (m_templateList); + GncTransaction *ttx; + while ((ttx = itt.current()) != 0) { + // the id to match against is the split:account value in the splits + if (static_cast(ttx->getSplit(0))->acct() == gsc->templId()) break; + ++itt; + } + if (itt == 0) { + throw new MYMONEYEXCEPTION (i18n("Can't find template transaction for schedule %1").arg(sc.name())); + } else { + tx = convertTemplateTransaction (sc.name(), *itt); + } + tx.clearId(); + +// define the conversion table for intervals + struct convIntvl { + QString gncType; // the gnucash name + unsigned char interval; // for date calculation + unsigned int intervalCount; + MyMoneySchedule::occurenceE occ; // equivalent occurence code + MyMoneySchedule::weekendOptionE wo; + }; +/* other intervals supported by gnc according to Josh Sled's schema (see above) + "none" "semi_monthly" + */ + /* some of these type names do not appear in gnucash and are difficult to generate for + pre 2.2 files.They can be generated for 2.2 however, by GncRecurrence::getFrequency() */ + static convIntvl vi [] = { + {"once", 'o', 1, MyMoneySchedule::OCCUR_ONCE, MyMoneySchedule::MoveNothing }, + {"daily" , 'd', 1, MyMoneySchedule::OCCUR_DAILY, MyMoneySchedule::MoveNothing }, + //{"daily_mf", 'd', 1, MyMoneySchedule::OCCUR_DAILY, MyMoneySchedule::MoveMonday }, doesn't work, need new freq in kmm + {"30-days" , 'd', 30, MyMoneySchedule::OCCUR_EVERYTHIRTYDAYS, MyMoneySchedule::MoveNothing }, + {"weekly", 'w', 1, MyMoneySchedule::OCCUR_WEEKLY, MyMoneySchedule::MoveNothing }, + {"bi_weekly", 'w', 2, MyMoneySchedule::OCCUR_EVERYOTHERWEEK, MyMoneySchedule::MoveNothing }, + {"three-weekly", 'w', 3, MyMoneySchedule::OCCUR_EVERYTHREEWEEKS, MyMoneySchedule::MoveNothing }, + {"four-weekly", 'w', 4, MyMoneySchedule::OCCUR_EVERYFOURWEEKS, + MyMoneySchedule::MoveNothing }, + {"eight-weekly", 'w', 8, MyMoneySchedule::OCCUR_EVERYEIGHTWEEKS, MyMoneySchedule::MoveNothing }, + {"monthly", 'm', 1, MyMoneySchedule::OCCUR_MONTHLY, MyMoneySchedule::MoveNothing }, + {"two-monthly", 'm', 2, MyMoneySchedule::OCCUR_EVERYOTHERMONTH, + MyMoneySchedule::MoveNothing }, + {"quarterly", 'm', 3, MyMoneySchedule::OCCUR_QUARTERLY, MyMoneySchedule::MoveNothing }, + {"tri_annually", 'm', 4, MyMoneySchedule::OCCUR_EVERYFOURMONTHS, MyMoneySchedule::MoveNothing }, + {"semi_yearly", 'm', 6, MyMoneySchedule::OCCUR_TWICEYEARLY, MyMoneySchedule::MoveNothing }, + {"yearly", 'y', 1, MyMoneySchedule::OCCUR_YEARLY, MyMoneySchedule::MoveNothing }, + {"two-yearly", 'y', 2, MyMoneySchedule::OCCUR_EVERYOTHERYEAR, + MyMoneySchedule::MoveNothing }, + {"zzz", 'y', 1, MyMoneySchedule::OCCUR_YEARLY, MyMoneySchedule::MoveNothing} + // zzz = stopper, may cause problems. what else can we do? + }; + + QString frequency = "unknown"; // set default to unknown frequency + bool unknownOccurs = false; // may have zero, or more than one frequency/recurrence spec + QString schedEnabled; + if (gsc->version() == "2.0.0") { + if (gsc->m_vpRecurrence.count() != 1) { + unknownOccurs = true; + } else { + const GncRecurrence *gre = gsc->m_vpRecurrence.first(); + //qDebug (QString("Sched %1, pt %2, mu %3, sd %4").arg(gsc->name()).arg(gre->periodType()) + // .arg(gre->mult()).arg(gre->startDate().toString(Qt::ISODate))); + frequency = gre->getFrequency(); + schedEnabled = gsc->enabled(); + } + sc.setOccurence(MyMoneySchedule::OCCUR_ONCE); // FIXME - how to convert + } else { + // find this interval + const GncFreqSpec *fs = gsc->getFreqSpec(); + if (fs == NULL) { + unknownOccurs = true; + } else { + frequency = fs->intervalType(); + if (!fs->m_fsList.isEmpty()) unknownOccurs = true; // nested freqspec + } + schedEnabled = "y"; // earlier versions did not have an enable flag + } + + int i; + for (i = 0; vi[i].gncType != "zzz"; i++) { + if (frequency == vi[i].gncType) break; + } + if (vi[i].gncType == "zzz") { + postMessage ("SC", 1, sc.name().latin1(), frequency.latin1()); + i = 0; // treat as single occurrence + m_suspectSchedule = true; + } + if (unknownOccurs) { + postMessage ("SC", 7, sc.name().latin1()); + m_suspectSchedule = true; + } + // set the occurrence interval, weekend option, start date + sc.setOccurence (vi[i].occ); + sc.setWeekendOption (vi[i].wo); + sc.setStartDate (gsc->startDate()); + // if a last date was specified, use it, otherwise try to work out the last date + sc.setLastPayment(gsc->lastDate()); + numOccurs = gsc->numOccurs().toInt(); + if (sc.lastPayment() == QDate()) { + nextDate = lastDate = gsc->startDate(); + while ((nextDate < today) && (numOccurs-- != 0)) { + lastDate = nextDate; + nextDate = incrDate (lastDate, vi[i].interval, vi[i].intervalCount); + } + sc.setLastPayment(lastDate); + } + // under Tom's new regime, the tx dates are the next due date (I think) + tx.setPostDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); + tx.setEntryDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); + // if an end date was specified, use it, otherwise if the input file had a number + // of occurs remaining, work out the end date + sc.setEndDate(gsc->endDate()); + numOccurs = gsc->numOccurs().toInt(); + remOccurs = gsc->remOccurs().toInt(); + if ((sc.endDate() == QDate()) && (remOccurs > 0)) { + endDate = sc.lastPayment(); + while (remOccurs-- > 0) { + endDate = incrDate (endDate, vi[i].interval, vi[i].intervalCount); + } + sc.setEndDate(endDate); + } + // Check for sched deferred interval. Don't know how/if we can handle it, or even what it means... + if (gsc->getSchedDef() != NULL) { + postMessage ("SC", 8, sc.name().latin1()); + m_suspectSchedule = true; + } + // payment type, options + sc.setPaymentType((MyMoneySchedule::paymentTypeE)MyMoneySchedule::STYPE_OTHER); + sc.setFixed (!m_suspectSchedule); // if any probs were found, set it as variable so user will always be prompted + // we don't currently have a 'disable' option, but just make sure auto-enter is off if not enabled + //qDebug(QString("%1 and %2").arg(gsc->autoCreate()).arg(schedEnabled)); + sc.setAutoEnter ((gsc->autoCreate() == "y") && (schedEnabled == "y")); + //qDebug(QString("autoEnter set to %1").arg(sc.autoEnter())); + // type + QString actionType = tx.splits().first().action(); + if (actionType == MyMoneySplit::ActionDeposit) { + sc.setType((MyMoneySchedule::typeE)MyMoneySchedule::TYPE_DEPOSIT); + } else if (actionType == MyMoneySplit::ActionTransfer) { + sc.setType((MyMoneySchedule::typeE)MyMoneySchedule::TYPE_TRANSFER); + } else { + sc.setType((MyMoneySchedule::typeE)MyMoneySchedule::TYPE_BILL); + } + // finally, set the transaction pointer + sc.setTransaction(tx); + //tell the storage objects we have a new schedule object. + if (m_suspectSchedule && m_dropSuspectSchedules) { + postMessage ("SC", 2, sc.name().latin1()); + } else { + m_storage->addSchedule(sc); + if (m_suspectSchedule) + m_suspectList.append (sc.id()); + } + signalProgress (++m_scheduleCount, 0); + return ; + PASS +} +//********************************* convertFreqSpec ******************************************************** +void MyMoneyGncReader::convertFreqSpec (const GncFreqSpec *) { + // Nowt to do here at the moment, convertSched only retrieves the interval type + // but we will probably need to look into the nested freqspec when we properly implement semi-monthly and stuff + return ; +} +//********************************* convertRecurrence ******************************************************** +void MyMoneyGncReader::convertRecurrence (const GncRecurrence *) { + return ; +} + +//********************************************************************************************************** +//************************************* terminate ********************************************************** +void MyMoneyGncReader::terminate () { + TRY + // All data has been converted and added to storage + // this code is just temporary to show us what is in the file. + if (gncdebug) qDebug("%d accounts found in the GnuCash file", (unsigned int)m_mapIds.count()); + for (map_accountIds::Iterator it = m_mapIds.begin(); it != m_mapIds.end(); ++it) { + if (gncdebug) qDebug("key = %s, value = %s", it.key().data(), it.data().data()); + } + // first step is to implement the users investment option, now we + // have all the accounts available + QValueList::iterator stocks; + for (stocks = m_stockList.begin(); stocks != m_stockList.end(); ++stocks) { + checkInvestmentOption (*stocks); + } + // Next step is to walk the list and assign the parent/child relationship between the objects. + unsigned int i = 0; + signalProgress (0, m_accountCount, i18n ("Reorganizing accounts...")); + QValueList list; + QValueList::Iterator acc; + m_storage->accountList(list); + for (acc = list.begin(); acc != list.end(); ++acc) { + if ((*acc).parentAccountId() == m_storage->asset().id()) { + MyMoneyAccount assets = m_storage->asset(); + m_storage->addAccount(assets, (*acc)); + if (gncdebug) qDebug("Account id %s is a child of the main asset account", (*acc).id().data()); + } else if ((*acc).parentAccountId() == m_storage->liability().id()) { + MyMoneyAccount liabilities = m_storage->liability(); + m_storage->addAccount(liabilities, (*acc)); + if (gncdebug) qDebug("Account id %s is a child of the main liability account", (*acc).id().data()); + } else if ((*acc).parentAccountId() == m_storage->income().id()) { + MyMoneyAccount incomes = m_storage->income(); + m_storage->addAccount(incomes, (*acc)); + if (gncdebug) qDebug("Account id %s is a child of the main income account", (*acc).id().data()); + } else if ((*acc).parentAccountId() == m_storage->expense().id()) { + MyMoneyAccount expenses = m_storage->expense(); + m_storage->addAccount(expenses, (*acc)); + if (gncdebug) qDebug("Account id %s is a child of the main expense account", (*acc).id().data()); + } else if ((*acc).parentAccountId() == m_storage->equity().id()) { + MyMoneyAccount equity = m_storage->equity(); + m_storage->addAccount(equity, (*acc)); + if (gncdebug) qDebug("Account id %s is a child of the main equity account", (*acc).id().data()); + } else if ((*acc).parentAccountId() == m_rootId) { + if (gncdebug) qDebug("Account id %s is a child of root", (*acc).id().data()); + } else { + // it is not under one of the main accounts, so find gnucash parent + QString parentKey = (*acc).parentAccountId(); + if (gncdebug) qDebug ("acc %s, parent %s", (*acc).id().data(), + (*acc).parentAccountId().data()); + map_accountIds::Iterator id = m_mapIds.find(parentKey); + if (id != m_mapIds.end()) { + if (gncdebug) qDebug("Setting account id %s's parent account id to %s", + (*acc).id().data(), id.data().data()); + MyMoneyAccount parent = m_storage->account(id.data()); + parent = checkConsistency (parent, (*acc)); + m_storage->addAccount (parent, (*acc)); + } else { + throw new MYMONEYEXCEPTION ("terminate() could not find account id"); + } + } + signalProgress (++i, 0); + } // end for account + signalProgress (0, 1, (".")); // debug - get rid of reorg message + // offer the most common account currency as a default + QString mainCurrency = ""; + unsigned int maxCount = 0; + QMap::ConstIterator it; + for (it = m_currencyCount.begin(); it != m_currencyCount.end(); ++it) { + if (it.data() > maxCount) { + maxCount = it.data(); + mainCurrency = it.key(); + } + } + + if (mainCurrency != "") { + /* fix for qt3.3.4?. According to Qt docs, this should return the enum id of the button pressed, and + indeed it used to do so. However now it seems to return the index of the button. In this case it doesn't matter, + since for Yes, the id is 3 and the index is 0, whereas the No button will return 4 or 1. So we test for either Yes case */ + /* and now it seems to have changed again, returning 259 for a Yes??? so use KMessagebox */ + QString question = i18n("Your main currency seems to be %1 (%2); do you want to set this as your base currency?") + .arg(mainCurrency).arg(m_storage->currency(mainCurrency.utf8()).name()); + if(KMessageBox::questionYesNo(0, question, PACKAGE) == KMessageBox::Yes) { + m_storage->setValue ("kmm-baseCurrency", mainCurrency); + } + } + // now produce the end of job reports - first, work out which ones are required + m_ccCount = 0, m_orCount = 0, m_scCount = 0; + for (i = 0; i < m_messageList.count(); i++) { + if ((*m_messageList.at(i)).source == "CC") m_ccCount++; + if ((*m_messageList.at(i)).source == "OR") m_orCount++; + if ((*m_messageList.at(i)).source == "SC") m_scCount++; + } + QValueList sectionsToReport; // list of sections needing report + sectionsToReport.append ("MN"); // always build the main section + if (m_ccCount > 0) sectionsToReport.append ("CC"); + if (m_orCount > 0) sectionsToReport.append ("OR"); + if (m_scCount > 0) sectionsToReport.append ("SC"); + // produce the sections in message boxes + bool exit = false; + for (i = 0; (i < sectionsToReport.count()) && !exit; i++) { + QString button0Text = i18n("More"); + if (i + 1 == sectionsToReport.count()) + button0Text = i18n("Done"); // last section + KGuiItem yesItem(button0Text, QIconSet(), "", ""); + KGuiItem noItem(i18n("Save Report"), QIconSet(), "", ""); + + switch(KMessageBox::questionYesNoCancel(0, + buildReportSection (*sectionsToReport.at(i)), + PACKAGE, + yesItem, noItem)) { + case KMessageBox::Yes: + break; + case KMessageBox::No: + exit = writeReportToFile (sectionsToReport); + break; + default: + exit = true; + break; + } + } + + for (i = 0; i < m_suspectList.count(); i++) { + MyMoneySchedule sc = m_storage->schedule(m_suspectList[i]); + KEditScheduleDlg *s; + switch(KMessageBox::warningYesNo(0, i18n("Problems were encountered in converting schedule '%1'.\nDo you want to review or edit it now?").arg(sc.name()), PACKAGE)) { + case KMessageBox::Yes: + s = new KEditScheduleDlg (sc); + // FIXME: connect newCategory to something useful, so that we + // can create categories from within the dialog + if (s->exec()) + m_storage->modifySchedule (s->schedule()); + delete s; + break; + + default: + break; + } + } + PASS +} +//************************************ buildReportSection************************************ +QString MyMoneyGncReader::buildReportSection (const QString& source) { + TRY + QString s = ""; + bool more = false; + if (source == "MN") { + s.append (i18n("Found:\n\n")); + s.append (QString::number(m_commodityCount) + i18n(" commodities (equities)\n")); + s.append (QString::number(m_priceCount) + i18n(" prices\n")); + s.append (QString::number(m_accountCount) + i18n(" accounts\n")); + s.append (QString::number(m_transactionCount) + i18n(" transactions\n")); + s.append (QString::number(m_scheduleCount) + i18n(" schedules\n")); + s.append ("\n\n"); + if (m_ccCount == 0) { + s.append (i18n("No inconsistencies were detected")); + } else { + s.append (QString::number(m_ccCount) + i18n(" inconsistencies were detected and corrected\n")); + more = true; + } + if (m_orCount > 0) { + s.append ("\n\n"); + s.append (QString::number(m_orCount) + i18n(" orphan accounts were created\n")); + more = true; + } + if (m_scCount > 0) { + s.append ("\n\n"); + s.append (QString::number(m_scCount) + i18n(" possible schedule problems were noted\n")); + more = true; + } + QString unsupported (""); + QString lineSep ("\n - "); + if (m_smallBusinessFound) unsupported.append(lineSep + i18n("Small Business Features (Customers, Invoices, etc.)")); + if (m_budgetsFound) unsupported.append(lineSep + i18n("Budgets")); + if (m_lotsFound) unsupported.append(lineSep + i18n("Lots")); + if (!unsupported.isEmpty()) { + unsupported.prepend(i18n("The following features found in your file are not currently supported:")); + s.append(unsupported); + } + if (more) s.append (i18n("\n\nPress More for further information")); + } else { // we need to retrieve the posted messages for this source + if (gncdebug) qDebug("Building messages for source %s", source.latin1()); + unsigned int i, j; + for (i = 0; i < m_messageList.count(); i++) { + GncMessageArgs *m = m_messageList.at(i); + if (m->source == source) { + if (gncdebug) qDebug("%s", QString("build text source %1, code %2, argcount %3") + .arg(m->source).arg(m->code).arg(m->args.count()).data()); + QString ss = GncMessages::text (m->source, m->code); + // add variable args. the .arg function seems always to replace the + // lowest numbered placeholder it finds, so translating messages + // with variables in a different order should still work okay (I think...) + for (j = 0; j < m->args.count(); j++) ss = ss.arg (*m->args.at(j)); + s.append (ss + "\n"); + } + } + } + if (gncdebug) qDebug ("%s", s.latin1()); + return (static_cast(s)); + PASS +} +//************************ writeReportToFile********************************* +bool MyMoneyGncReader::writeReportToFile (const QValueList& sectionsToReport) { + TRY + unsigned int i; + QFileDialog* fd = new QFileDialog (0, "Save report as", TRUE); + fd->setMode (QFileDialog::AnyFile); + if (fd->exec() != QDialog::Accepted) { + delete fd; + return (false); + } + QFile reportFile(fd->selectedFile()); + QFileInfo fi (reportFile); + if (!reportFile.open (IO_WriteOnly)) { + delete fd; + return (false); + } + QTextStream stream (&reportFile); + for (i = 0; i < sectionsToReport.count(); i++) { + stream << buildReportSection (*sectionsToReport.at(i)).latin1() << endl; + } + reportFile.close(); + delete fd; + return (true); + PASS +} +/**************************************************************************** + Utility routines +*****************************************************************************/ +//************************ createPayee *************************** + +QString MyMoneyGncReader::createPayee (const QString& gncDescription) { + MyMoneyPayee payee; + try { + payee = m_storage->payeeByName (gncDescription); + } catch (MyMoneyException *e) { // payee not found, create one + delete e; + payee.setName (gncDescription); + m_storage->addPayee (payee); + } + return (payee.id()); +} +//************************************** createOrphanAccount ******************************* +QString MyMoneyGncReader::createOrphanAccount (const QString& gncName) { + MyMoneyAccount acc; + + acc.setName ("orphan_" + gncName); + acc.setDescription (i18n("Orphan created from unknown gnucash account")); + + QDate today = QDate::currentDate(); + + acc.setOpeningDate (today); + acc.setLastModified (today); + acc.setLastReconciliationDate (today); + acc.setCurrencyId (m_txCommodity); + acc.setAccountType (MyMoneyAccount::Asset); + acc.setParentAccountId (m_storage->asset().id()); + m_storage->addAccount (acc); + // assign the gnucash id as the key into the map to find our id + m_mapIds[gncName.utf8()] = acc.id(); + postMessage ("OR", 1, acc.name().data()); + return (acc.id()); +} +//****************************** incrDate ********************************************* +QDate MyMoneyGncReader::incrDate (QDate lastDate, unsigned char interval, unsigned int intervalCount) { + TRY + switch (interval) { + case 'd': + return (lastDate.addDays(intervalCount)); + case 'w': + return (lastDate.addDays(intervalCount * 7)); + case 'm': + return (lastDate.addMonths(intervalCount)); + case 'y': + return (lastDate.addYears(intervalCount)); + case 'o': // once-only + return (lastDate); + } + throw new MYMONEYEXCEPTION (i18n("Internal error - invalid interval char in incrDate")); + QDate r = QDate(); return (r); // to keep compiler happy + PASS +} +//********************************* checkConsistency ********************************** +MyMoneyAccount MyMoneyGncReader::checkConsistency (MyMoneyAccount& parent, MyMoneyAccount& child) { + TRY + // gnucash is flexible/weird enough to allow various inconsistencies + // these are a couple I found in my file, no doubt more will be discovered + if ((child.accountType() == MyMoneyAccount::Investment) && + (parent.accountType() != MyMoneyAccount::Asset)) { + postMessage ("CC", 1, child.name().latin1()); + return m_storage->asset(); + } + if ((child.accountType() == MyMoneyAccount::Income) && + (parent.accountType() != MyMoneyAccount::Income)) { + postMessage ("CC", 2, child.name().latin1()); + return m_storage->income(); + } + if ((child.accountType() == MyMoneyAccount::Expense) && + (parent.accountType() != MyMoneyAccount::Expense)) { + postMessage ("CC", 3, child.name().latin1()); + return m_storage->expense(); + } + return (parent); + PASS +} +//*********************************** checkInvestmentOption ************************* +void MyMoneyGncReader::checkInvestmentOption (QString stockId) { + // implement the investment option for stock accounts + // first check whether the parent account (gnucash id) is actually an + // investment account. if it is, no further action is needed + MyMoneyAccount stockAcc = m_storage->account (m_mapIds[stockId.utf8()]); + MyMoneyAccount parent; + QString parentKey = stockAcc.parentAccountId(); + map_accountIds::Iterator id = m_mapIds.find (parentKey); + if (id != m_mapIds.end()) { + parent = m_storage->account (id.data()); + if (parent.accountType() == MyMoneyAccount::Investment) return ; + } + // so now, check the investment option requested by the user + // option 0 creates a separate investment account for each stock account + if (m_investmentOption == 0) { + MyMoneyAccount invAcc (stockAcc); + invAcc.setAccountType (MyMoneyAccount::Investment); + invAcc.setCurrencyId (QString("")); // we don't know what currency it is!! + invAcc.setParentAccountId (parentKey); // intersperse it between old parent and child stock acct + m_storage->addAccount (invAcc); + m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later + if (gncdebug) qDebug ("Created investment account %s as id %s, parent %s", invAcc.name().data(), invAcc.id().data(), + invAcc.parentAccountId().data()); + if (gncdebug) qDebug ("Setting stock %s, id %s, as child of %s", stockAcc.name().data(), stockAcc.id().data(), invAcc.id().data()); + stockAcc.setParentAccountId (invAcc.id()); + m_storage->addAccount(invAcc, stockAcc); + // investment option 1 creates a single investment account for all stocks + } else if (m_investmentOption == 1) { + static QString singleInvAccId = ""; + MyMoneyAccount singleInvAcc; + bool ok = false; + if (singleInvAccId.isEmpty()) { // if the account has not yet been created + QString invAccName; + while (!ok) { + invAccName = QInputDialog::getText (PACKAGE, + i18n("Enter the investment account name "), QLineEdit::Normal, + i18n("My Investments"), &ok); + } + singleInvAcc.setName (invAccName); + singleInvAcc.setAccountType (MyMoneyAccount::Investment); + singleInvAcc.setCurrencyId (QString("")); + singleInvAcc.setParentAccountId (m_storage->asset().id()); + m_storage->addAccount (singleInvAcc); + m_mapIds [singleInvAcc.id()] = singleInvAcc.id(); // so stock account gets parented (again) to investment account later + if (gncdebug) qDebug ("Created investment account %s as id %s, parent %s, reparenting stock", + singleInvAcc.name().data(), singleInvAcc.id().data(), singleInvAcc.parentAccountId().data()); + singleInvAccId = singleInvAcc.id(); + } else { // the account has already been created + singleInvAcc = m_storage->account (singleInvAccId); + } + m_storage->addAccount(singleInvAcc, stockAcc); // add stock as child + // the original intention of option 2 was to allow any asset account to be converted to an investment (broker) account + // however, since we have already stored the accounts as asset, we have no way at present of changing their type + // the only alternative would be to hold all the gnucash data in memory, then implement this option, then convert all the data + // that would mean a major overhaul of the code. Perhaps I'll think of another way... + } else if (m_investmentOption == 2) { + static int lastSelected = 0; + MyMoneyAccount invAcc (stockAcc); + QStringList accList; + QValueList list; + QValueList::Iterator acc; + m_storage->accountList(list); + // build a list of candidates for the input box + for (acc = list.begin(); acc != list.end(); ++acc) { + // if (((*acc).accountGroup() == MyMoneyAccount::Asset) && ((*acc).accountType() != MyMoneyAccount::Stock)) accList.append ((*acc).name()); + if ((*acc).accountType() == MyMoneyAccount::Investment) accList.append ((*acc).name()); + } + //if (accList.isEmpty()) qFatal ("No available accounts"); + bool ok = false; + while (!ok) { // keep going till we have a valid investment parent + QString invAccName = QInputDialog::getItem ( + PACKAGE, i18n("Select parent investment account or enter new name. Stock %1").arg(stockAcc.name ()), + accList, lastSelected, true, &ok); + if (ok) { + lastSelected = accList.findIndex (invAccName); // preserve selection for next time + for (acc = list.begin(); acc != list.end(); ++acc) { + if ((*acc).name() == invAccName) break; + } + if (acc != list.end()) { // an account was selected + invAcc = *acc; + } else { // a new account name was entered + invAcc.setAccountType (MyMoneyAccount::Investment); + invAcc.setName (invAccName); + invAcc.setCurrencyId (QString("")); + invAcc.setParentAccountId (m_storage->asset().id()); + m_storage->addAccount (invAcc); + ok = true; + } + if (invAcc.accountType() == MyMoneyAccount::Investment) { + ok = true; + } else { + // this code is probably not going to be implemented coz we can't change account types (??) +#if 0 + QMessageBox mb (PACKAGE, + i18n ("%1 is not an Investment Account. Do you wish to make it one?").arg(invAcc.name()), + QMessageBox::Question, + QMessageBox::Yes | QMessageBox::Default, + QMessageBox::No | QMessageBox::Escape, + QMessageBox::NoButton); + switch (mb.exec()) { + case QMessageBox::No : + ok = false; break; + default: + // convert it - but what if it has splits??? + qFatal ("Not yet implemented"); + ok = true; + break; + } +#endif + switch(KMessageBox::questionYesNo(0, i18n ("%1 is not an Investment Account. Do you wish to make it one?").arg(invAcc.name(), PACKAGE))) { + case KMessageBox::Yes: + // convert it - but what if it has splits??? + qFatal ("Not yet implemented"); + ok = true; + break; + default: + ok = false; + break; + } + } + } // end if ok - user pressed Cancel + } // end while !ok + m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later + m_storage->addAccount(invAcc, stockAcc); + } else { // investment option != 0, 1, 2 + qFatal ("Invalid investment option %d", m_investmentOption); + } +} + +// get the price source for a stock (gnc account) where online quotes are requested +void MyMoneyGncReader::getPriceSource (MyMoneySecurity stock, QString gncSource) { + // if he wants to use Finance::Quote, no conversion of source name is needed + if (m_useFinanceQuote) { + stock.setValue ("kmm-online-quote-system", "Finance::Quote"); + stock.setValue ("kmm-online-source", gncSource.lower()); + m_storage->modifySecurity(stock); + return; + } + // first check if we have already asked about this source + // (mapSources is initialy empty. We may be able to pre-fill it with some equivalent + // sources, if such things do exist. User feedback may help here.) + QMap::Iterator it; + for (it = m_mapSources.begin(); it != m_mapSources.end(); it++) { + if (it.key() == gncSource) { + stock.setValue("kmm-online-source", it.data()); + m_storage->modifySecurity(stock); + return; + } + } + // not found in map, so ask the user + KGncPriceSourceDlg *dlg = new KGncPriceSourceDlg (stock.name(), gncSource); + dlg->exec(); + QString s = dlg->selectedSource(); + if (!s.isEmpty()) { + stock.setValue("kmm-online-source", s); + m_storage->modifySecurity(stock); + } + if (dlg->alwaysUse()) m_mapSources[gncSource] = s; + delete dlg; + return; +} + +// functions to control the progress bar +//*********************** setProgressCallback ***************************** +void MyMoneyGncReader::setProgressCallback(void(*callback)(int, int, const QString&)) { + m_progressCallback = callback; return ; +} +//************************** signalProgress ******************************* +void MyMoneyGncReader::signalProgress(int current, int total, const QString& msg) { + if (m_progressCallback != 0) + (*m_progressCallback)(current, total, msg); + return ; +} +// error and information reporting +//***************************** Information and error messages ********************* +void MyMoneyGncReader::postMessage (const QString& source, const unsigned int code, const char* arg1) { + postMessage (source, code, QStringList(arg1)); +} +void MyMoneyGncReader::postMessage (const QString& source, const unsigned int code, const char* arg1, const char* arg2) { + QStringList argList(arg1); + argList.append(arg2); + postMessage(source, code, argList); +} +void MyMoneyGncReader::postMessage (const QString& source, const unsigned int code, const char* arg1, const char* arg2, const char* arg3) { + QStringList argList(arg1); + argList.append(arg2); + argList.append(arg3); + postMessage(source, code, argList); +} +void MyMoneyGncReader::postMessage (const QString& source, const unsigned int code, const QStringList& argList) { + unsigned int i; + GncMessageArgs *m = new GncMessageArgs; + + m->source = source; + m->code = code; + // get the number of args this message requires + const unsigned int argCount = GncMessages::argCount (source, code); + if ((gncdebug) && (argCount != argList.count())) + qDebug("%s", QString("MyMoneyGncReader::postMessage debug: Message %1, code %2, requires %3 arguments, got %4") + .arg(source).arg(code).arg(argCount).arg(argList.count()).data()); + // store the arguments + for (i = 0; i < argCount; i++) { + if (i > argList.count()) m->args.append(QString()); + else m->args.append (argList[i]); //Adds the next argument to the list + } + m_messageList.append (m); + return ; +} +//********************************** Message texts ********************************************** +GncMessages::messText GncMessages::texts [] = { + {"CC", 1, i18n("An Investment account must be a child of an Asset account\n" + "Account %1 will be stored under the main Asset account")}, + {"CC", 2, i18n("An Income account must be a child of an Income account\n" + "Account %1 will be stored under the main Income account")}, + {"CC", 3, i18n("An Expense account must be a child of an Expense account\n" + "Account %1 will be stored under the main Expense account")}, + {"OR", 1, i18n("One or more transactions contain a reference to an otherwise unknown account\n" + "An asset account with the name %1 has been created to hold the data")}, + {"SC", 1, i18n("Schedule %1 has interval of %2 which is not currently available")}, + {"SC", 2, i18n("Schedule %1 dropped at user request")}, + {"SC", 3, i18n("Schedule %1 contains unknown action (key = %2, type = %3)")}, + {"SC", 4, i18n("Schedule %1 contains multiple actions; only one has been imported")}, + {"SC", 5, i18n("Schedule %1 contains no valid splits")}, + {"SC", 6, i18n("Schedule %1 appears to contain a formula. GnuCash formulae are not convertible")}, + {"SC", 7, i18n("Schedule %1 contains unknown interval specification; please check for correct operation")}, + {"SC", 8, i18n("Schedule %1 contains a deferred interval specification; please check for correct operation")}, + {"CC", 4, i18n("Account or Category %1, transaction date %2; split contains invalid value; please check")}, + {"ZZ", 0, ""} // stopper + }; +// +QString GncMessages::text (const QString source, const unsigned int code) { + TRY + unsigned int i; + for (i = 0; texts[i].source != "ZZ"; i++) { + if ((source == texts[i].source) && (code == texts[i].code)) break; + } + if (texts[i].source == "ZZ") { + QString mess = QString().sprintf("Internal error - unknown message - source %s, code %d", source.latin1(), code); + throw new MYMONEYEXCEPTION (mess); + } + return (texts[i].text); + PASS +} +// +unsigned int GncMessages::argCount (const QString source, const unsigned int code) { + TRY + unsigned int i; + for (i = 0; texts[i].source != "ZZ"; i++) { + if ((source == texts[i].source) && (code == texts[i].code)) break; + } + if (texts[i].source == "ZZ") { + QString mess = QString().sprintf("Internal error - unknown message - source %s, code %d", source.latin1(), code); + throw new MYMONEYEXCEPTION (mess); + } + QRegExp argConst ("%\\d"); + int offset = 0; + unsigned int argCount = 0; + while ((offset = argConst.search (texts[i].text, offset)) != -1) { + argCount++; + offset += 2; + } + return (argCount); + PASS +} +#endif // _GNCFILEANON diff --git a/kmymoney2/converter/mymoneygncreader.h b/kmymoney2/converter/mymoneygncreader.h new file mode 100644 index 0000000..df08913 --- /dev/null +++ b/kmymoney2/converter/mymoneygncreader.h @@ -0,0 +1,904 @@ +/*************************************************************************** + mymoneygncreader - description + ------------------- + begin : Wed Mar 3 2004 + copyright : (C) 2000-2004 by Michael Edwardes + email : mte@users.sourceforge.net + Javier Campos Morales + Felix Rodriguez + John C + Thomas Baumgart + Kevin Tambascio +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +/* +The main class of this module, MyMoneyGncReader, contains only a readFile() +function, which controls the import of data from an XML file created by the +current GnuCash version (1.8.8). + +The XML is processed in class XmlReader, which is an implementation of the Qt +SAX2 reader class. + +Data in the input file is processed as a set of objects which fortunately, +though perhaps not surprisingly, have almost a one-for-one correspondence with +KMyMoney objects. These objects are bounded by start and end XML elements, and +may contain both nested objects (described as sub objects in the code), and data +items, also delimited by start and end elements. For example: + * start of sub object within file + Account Name * data string with start and end elements + ... + * end of sub objects + +A GnuCash file may consist of more than one 'book', or set of data. It is not +clear how we could currently implement this, so only the first book in a file is +processed. This should satisfy most user situations. + +GnuCash is somewhat inconsistent in its division of the major sections of the +file. For example, multiple price history entries are delimited by +elements, while each account starts with its own top-level element. In general, +the 'container' elements are ignored. + +XmlReader + +This is an implementation of the Qt QXmlDefaultHandler class, which provides +three main function calls in addition to start and end of document. The +startElement() and endElement() calls are self-explanatory, the characters() +function provides data strings. Thus in the above example, the sequence of calls +would be + startElement() for gnc:account + startElement() for act:name + characters() for 'Account Name' + endElement() for act:name + ... + endElement() for gnc:account + +Objects + +Since the processing requirements of XML for most elements are very similar, the +common code is implemented in a GncObject class, from which the others are +derived, with virtual function calls to cater for any differences. The +'grandfather' object, GncFile representing the file (or more correctly, 'book') +as a whole, is created in the startDocument() function call. + +The constructor function of each object is responsible for providing two lists +for the XmlReader to scan, a list of element names which represent sub objects +(called sub elements in the code), and a similar list of names representing data +elements. In addition, an array of variables (m_v) is provided and initialized, +to contain the actual data strings. + +Implementation + +Since objects may be nested, a stack is used, with the top element pointing to +the 'current object'. The startDocument() call creates the first, GncFile, +object at the top of the stack. + +As each startElement() call occurs, the two element lists created by the current +object are scanned. +If this element represents the start of a sub object, the current object's subEl() +function is called to create an instance of the appropriate type. This is then +pushed to the top of the stack, and the new object's initiate() function is +called. This is used to process any XML attributes attached to the element; +GnuCash makes little use of these. +If this represents the start of a data element, a pointer (m_dataPointer) is set +to point to an entry in the array (m_v) in which a subsequent characters() call +can store the actual data. + +When an endElement() call occurs, a check is made to see if it matches the +element name which started the current object. If so, the object's terminate() +function is called. If the object represents a similar KMM object, this will +normally result in a call to a conversion routine in the main +(MyMoneyGncReader) class to convert the data to native format and place it in +storage. The stack is then popped, and the parent (now current) object notified +by a call to its endSubEl() function. Again depending on the type of object, +this will either delete the instance, or save it in its own storage for later +processing. +For example, a GncSplit object makes little sense outside the context of its +transaction, so will be saved by the transaction. A GncTransaction object on the +other hand will be converted, along with its attendant splits, and then deleted +by its parent. + +Since at any one time an object will only be processing either a subobject or a +data element, a single object variable, m_state, is used to determine the actual +type. In effect, it acts as the current index into either the subElement or +dataElement list. As an object variable, it will be saved on the stack across +subobject processing. + +Exceptions and Problems + +Fatal exceptions are processed via the standard MyMoneyException method. +Due to differences in implementation between GnuCash and KMM, it is not always +possible to provide an absolutely correct conversion. When such a problem +situation is recognized, a message, along with any relevant variable data, is +passed to the main class, and used to produce a report when processing +terminates. The GncMessages and GncMessageArg classes implement this. + +Anonymizer + +When debugging problems, it is often useful to have a trace of what is happening +within the module. However, in view of the sensitive nature of personal finance +data, most users will be reluctant to provide this. Accordingly, an anonymize +(hide()) function is provided to handle data strings. These may either be passed +through asis (non-personal data), blanked out (non-critical but possibly personal +data), replaced with a generated version (required, but possibly personal), or +randomized (monetary amounts). The action for each data item is determined in +the object's constructor function along with the creation of the data element +list. +This module will later be used as the basis of a file anonymizer, which will +enable users to safely provide us with a copy of their GnuCash files, and will +allow us to test the structure, if not the data content, of the file. +*/ + +#ifndef MYMONEYSTORAGEGNC_H +#define MYMONEYSTORAGEGNC_H + +// Some STL headers in GCC4.3 contain operator new. Memory checker mangles these +#ifdef _CHECK_MEMORY + #undef new +#endif +// system includes +#include + +// ---------------------------------------------------------------------------- +// QT Includes + +#include +class QIODevice; +#include +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes +#ifdef _CHECK_MEMORY + #include +#endif + +#ifndef _GNCFILEANON +#include "../mymoney/storage/imymoneyserialize.h" // not used any more, but call interface requires it +#include "../mymoney/storage/imymoneystorageformat.h" +#endif // _GNCFILEANON + +// not sure what these are for, but leave them in +#define VERSION_0_60_XML 0x10000010 // Version 0.5 file version info +#define VERSION_0_61_XML 0x10000011 // use 8 bytes for MyMoneyMoney objects +#define GNUCASH_ID_KEY "GNUCASH_ID" + +typedef QMap map_accountIds; +typedef map_accountIds::iterator map_accountIds_iter; +typedef map_accountIds::const_iterator map_accountIds_citer; + +typedef QMap map_elementVersions; + +class MyMoneyGncReader; + +/** GncObject is the base class for the various objects in the gnucash file + Beyond the first level XML objects, elements will be of one of three types: + 1. Sub object elements, which require creation of another object to process + 2. Data object elements, which are only followed by data to be stored in a variable (m_v array) + 3. Ignored objects, data not needed and not included herein +*/ +class GncObject { +public: + GncObject(); + ; // to save delete loop when finished + virtual ~GncObject() {} // make sure to have impl of all virtual rtns to avoid vtable errors? +protected: + friend class XmlReader; + friend class MyMoneyGncReader; + + // check for sub object element; if it is, create the object + GncObject *isSubElement (const QString &elName, const QXmlAttributes& elAttrs); + // check for data element; if so, set data pointer + bool isDataElement (const QString &elName, const QXmlAttributes& elAttrs); + // process start element for 'this'; normally for attribute checking; other initialization done in constructor + virtual void initiate (const QString&, const QXmlAttributes&) { return ;}; + // a sub object has completed; process the data it gathered + virtual void endSubEl(GncObject *) {m_dataPtr = 0; return ;}; + // store data for data element + void storeData (const QString& pData) // NB - data MAY come in chunks, and may need to be anonymized + {if (m_dataPtr != 0) + m_dataPtr->append (hide (pData, m_anonClass)); return ;} + // following is provided only for a future file anonymizer + QString getData () const { return ((m_dataPtr != 0) ? *m_dataPtr : "");}; + void resetDataPtr() {m_dataPtr = 0;}; + // process end element for 'this'; usually to convert to KMM format + virtual void terminate() { return ;}; + void setVersion (const QString& v) {m_version = v; return; }; + QString version() const {return (m_version);}; + + // some gnucash elements have version attribute; check it + void checkVersion (const QString&, const QXmlAttributes&, const map_elementVersions&); + // get name of element processed by 'this' + QString getElName () const { return (m_elementName);}; + // pass 'main' pointer to object + void setPm (MyMoneyGncReader *pM) {pMain = pM;}; + // debug only + void debugDump(); + + // called by isSubElement to create appropriate sub object + virtual GncObject *startSubEl() { return (0);}; + // called by isDataElement to set variable pointer + virtual void dataEl(const QXmlAttributes&) {m_dataPtr = m_v.at(m_state); m_anonClass = m_anonClassList[m_state];}; + // return gnucash data string variable pointer + virtual QString var (int i) const; + // anonymize data + virtual QString hide (QString, unsigned int); + + MyMoneyGncReader *pMain; // pointer to 'main' class + // used at start of each transaction so same money hide factor is applied to all splits + void adjustHideFactor(); + + QString m_elementName; // save 'this' element's name + QString m_version; // and it's gnucash version + const QString *m_subElementList; // list of sub object element names for 'this' + unsigned int m_subElementListCount; // count of above + const QString *m_dataElementList; // ditto for data elements + unsigned int m_dataElementListCount; + QString *m_dataPtr; // pointer to m_v variable for current data item + mutable QPtrList m_v; // storage for variable pointers + + unsigned int m_state; // effectively, the index to subElementList or dataElementList, whichever is currently in use + + const unsigned int *m_anonClassList; + enum anonActions {ASIS, SUPPRESS, NXTACC, NXTEQU, NXTPAY, NXTSCHD, MAYBEQ, MONEY1, MONEY2}; // anonymize actions - see hide() + unsigned int m_anonClass; // class of current data item for anonymizer + static double m_moneyHideFactor; // a per-transaction factor +}; + +// ***************************************************************************** +// This is the 'grandfather' object representing the gnucash file as a whole +class GncFile : public GncObject { +public: + GncFile (); + ~GncFile(); +private: + enum iSubEls {BOOK, COUNT, CMDTY, PRICE, ACCT, TX, TEMPLATES, SCHEDULES, END_FILE_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + + bool m_processingTemplates; // gnc uses same transaction element for ordinary and template tx's; this will distinguish + bool m_bookFound; // to detect multi-book files +}; +// The following are 'utility' objects, which occur within several other object types +// **************************************************************************** +// commodity specification. consists of +// cmdty:space - either ISO4217 if this cmdty is a currency, or, usually, the name of a stock exchange +// cmdty:id - ISO4217 currency symbol, or 'ticker symbol' +class GncCmdtySpec : public GncObject { +public: + GncCmdtySpec (); + ~GncCmdtySpec (); +protected: + friend class MyMoneyGncReader; + friend class GncTransaction; + bool isCurrency() const { return (*m_v.at(CMDTYSPC) == QString("ISO4217"));}; + QString id() const { return (*m_v.at(CMDTYID));}; + QString space() const { return (*m_v.at(CMDTYSPC));}; +private: + // data elements + enum CmdtySpecDataEls {CMDTYSPC, CMDTYID, END_CmdtySpec_DELS}; + virtual QString hide (QString, unsigned int); +}; +// ********************************************************************* +// date; maybe one of two types, ts:date which is date/time, gdate which is date only +// we do not preserve time data (at present) +class GncDate : public GncObject { +public: + GncDate (); + ~GncDate(); +protected: + friend class MyMoneyGncReader; + friend class GncPrice; + friend class GncTransaction; + friend class GncSplit; + friend class GncSchedule; + friend class GncRecurrence; + const QDate date() const { return (QDate::fromString(m_v.at(TSDATE)->section(' ', 0, 0), Qt::ISODate));}; +private: + // data elements + enum DateDataEls {TSDATE, GDATE, END_Date_DELS}; + virtual void dataEl(const QXmlAttributes&) {m_dataPtr = m_v.at(TSDATE); m_anonClass = GncObject::ASIS;} + ; // treat both date types the same +}; +// ************* GncKvp******************************************** +// Key/value pairs, which are introduced by the 'slot' element +// Consist of slot:key (the 'name' of the kvp), and slot:value (the data value) +// the slot value also contains a slot type (string, integer, etc) implemented as an XML attribute +// kvp's may be nested +class GncKvp : public GncObject { +public: + GncKvp (); + ~GncKvp(); +protected: + friend class MyMoneyGncReader; + + QString key() const { return (var(KEY));}; + QString value() const { return (var(VALUE));}; + QString type() const { return (m_kvpType);}; + unsigned int kvpCount() const { return (m_kvpList.count());}; + const GncKvp *getKvp(unsigned int i) const { return (static_cast(m_kvpList.at(i)));}; +private: + // subsidiary objects/elements + enum KvpSubEls {KVP, END_Kvp_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + // data elements + enum KvpDataEls {KEY, VALUE, END_Kvp_DELS }; + virtual void dataEl (const QXmlAttributes&); + mutable QPtrList m_kvpList; + QString m_kvpType; // type is an XML attribute +}; +// ************* GncLot******************************************** +// KMM doesn't have support for lots as yet +class GncLot : public GncObject { + public: + GncLot (); + ~GncLot(); + protected: + friend class MyMoneyGncReader; + private: +}; + +/** Following are the main objects within the gnucash file, which correspond largely one-for-one + with similar objects in the kmymoney structure, apart from schedules which gnc splits between + template (transaction data) and schedule (date data) +*/ +//******************************************************************** +class GncCountData : public GncObject { +public: + GncCountData (); + ~GncCountData (); +private: + virtual void initiate (const QString&, const QXmlAttributes&); + virtual void terminate(); + QString m_countType; // type of element being counted +}; +//******************************************************************** +class GncCommodity : public GncObject { +public: + GncCommodity (); + ~GncCommodity(); +protected: + friend class MyMoneyGncReader; + // access data values + bool isCurrency() const { return (var(SPACE) == QString("ISO4217"));}; + QString space() const { return (var(SPACE));}; + QString id() const { return (var(ID));}; + QString name() const { return (var(NAME));}; + QString fraction() const { return (var(FRACTION));}; +private: + virtual void terminate(); + // data elements + enum {SPACE, ID, NAME, FRACTION, END_Commodity_DELS}; +}; +// ************* GncPrice******************************************** +class GncPrice : public GncObject { +public: + GncPrice (); + ~GncPrice(); +protected: + friend class MyMoneyGncReader; + // access data values + const GncCmdtySpec *commodity() const { return (m_vpCommodity);}; + const GncCmdtySpec *currency() const { return (m_vpCurrency);}; + QString value() const { return (var(VALUE));}; + QDate priceDate () const { return (m_vpPriceDate->date());}; +private: + virtual void terminate(); + // sub object elements + enum PriceSubEls {CMDTY, CURR, PRICEDATE, END_Price_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + // data elements + enum PriceDataEls {VALUE, END_Price_DELS }; + GncCmdtySpec *m_vpCommodity, *m_vpCurrency; + GncDate *m_vpPriceDate; +}; +// ************* GncAccount******************************************** +class GncAccount : public GncObject { +public: + GncAccount (); + ~GncAccount(); +protected: + friend class MyMoneyGncReader; + // access data values + GncCmdtySpec *commodity() const { return (m_vpCommodity);}; + QString id () const { return (var(ID));}; + QString name () const { return (var(NAME));}; + QString desc () const { return (var(DESC));}; + QString type () const { return (var(TYPE));}; + QString parent () const { return (var(PARENT));}; +private: + // subsidiary objects/elements + enum AccountSubEls {CMDTY, KVP, LOTS, END_Account_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + virtual void terminate(); + // data elements + enum AccountDataEls {ID, NAME, DESC, TYPE, PARENT, END_Account_DELS }; + GncCmdtySpec *m_vpCommodity; + QPtrList m_kvpList; +}; +// ************* GncSplit******************************************** +class GncSplit : public GncObject { +public: + GncSplit (); + ~GncSplit(); +protected: + friend class MyMoneyGncReader; + // access data values + QString id() const { return (var(ID));}; + QString memo() const { return (var(MEMO));}; + QString recon() const { return (var(RECON));}; + QString value() const { return (var(VALUE));}; + QString qty() const { return (var(QTY));}; + QString acct() const { return (var(ACCT));}; +const QDate reconDate() const {QDate x = QDate(); return (m_vpDateReconciled == NULL ? x : m_vpDateReconciled->date());}; +private: + // subsidiary objects/elements + enum TransactionSubEls {RECDATE, END_Split_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + // data elements + enum SplitDataEls {ID, MEMO, RECON, VALUE, QTY, ACCT, END_Split_DELS }; + GncDate *m_vpDateReconciled; +}; +// ************* GncTransaction******************************************** +class GncTransaction : public GncObject { +public: + GncTransaction (bool processingTemplates); + ~GncTransaction(); +protected: + friend class MyMoneyGncReader; + // access data values + QString id() const { return (var(ID));}; + QString no() const { return (var(NO));}; + QString desc() const { return (var(DESC));}; + QString currency() const { return (m_vpCurrency == NULL ? QString () : m_vpCurrency->id());}; + QDate dateEntered() const { return (m_vpDateEntered->date());}; + QDate datePosted() const { return (m_vpDatePosted->date());}; + bool isTemplate() const { return (m_template);}; + unsigned int splitCount() const { return (m_splitList.count());}; + unsigned int kvpCount() const { return (m_kvpList.count());}; + const GncObject *getSplit (unsigned int i) const { return (m_splitList.at(i));}; + const GncKvp *getKvp(unsigned int i) const { return (static_cast(m_kvpList.at(i)));}; +private: + // subsidiary objects/elements + enum TransactionSubEls {CURRCY, POSTED, ENTERED, SPLIT, KVP, END_Transaction_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + virtual void terminate(); + // data elements + enum TransactionDataEls {ID, NO, DESC, END_Transaction_DELS }; + GncCmdtySpec *m_vpCurrency; + GncDate *m_vpDateEntered, *m_vpDatePosted; + mutable QPtrList m_splitList; + bool m_template; // true if this is a template for scheduled transaction + mutable QPtrList m_kvpList; +}; + +// ************* GncTemplateSplit******************************************** +class GncTemplateSplit : public GncObject { +public: + GncTemplateSplit (); + ~GncTemplateSplit(); +protected: + friend class MyMoneyGncReader; + // access data values + QString id() const { return (var(ID));}; + QString memo() const { return (var(MEMO));}; + QString recon() const { return (var(RECON));}; + QString value() const { return (var(VALUE));}; + QString qty() const { return (var(QTY));}; + QString acct() const { return (var(ACCT));}; + unsigned int kvpCount() const { return (m_kvpList.count());}; + const GncKvp *getKvp(unsigned int i) const { return (static_cast(m_kvpList.at(i)));}; +private: + // subsidiary objects/elements + enum TemplateSplitSubEls {KVP, END_TemplateSplit_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + // data elements + enum TemplateSplitDataEls {ID, MEMO, RECON, VALUE, QTY, ACCT, END_TemplateSplit_DELS }; + mutable QPtrList m_kvpList; +}; +// ************* GncSchedule******************************************** +class GncFreqSpec; +class GncRecurrence; +class GncSchedDef; +class GncSchedule : public GncObject { +public: + GncSchedule (); + ~GncSchedule(); +protected: + friend class MyMoneyGncReader; + // access data values + QString name() const { return (var(NAME));}; + QString enabled() const {return var(ENABLED);}; + QString autoCreate() const { return (var(AUTOC));}; + QString autoCrNotify() const { return (var(AUTOCN));}; + QString autoCrDays() const { return (var(AUTOCD));}; + QString advCrDays() const { return (var(ADVCD));}; + QString advCrRemindDays() const { return (var(ADVRD));}; + QString instanceCount() const { return (var(INSTC));}; + QString numOccurs() const { return (var(NUMOCC));}; + QString remOccurs() const { return (var(REMOCC));}; + QString templId() const { return (var(TEMPLID));}; + QDate startDate () const + {QDate x = QDate(); return (m_vpStartDate == NULL ? x : m_vpStartDate->date());}; + QDate lastDate () const + {QDate x = QDate(); return (m_vpLastDate == NULL ? x : m_vpLastDate->date());}; + QDate endDate() const + {QDate x = QDate(); return (m_vpEndDate == NULL ? x : m_vpEndDate->date());}; + const GncFreqSpec *getFreqSpec() const { return (m_vpFreqSpec);}; + const GncSchedDef *getSchedDef() const { return (m_vpSchedDef);}; +private: + // subsidiary objects/elements + enum ScheduleSubEls {STARTDATE, LASTDATE, ENDDATE, FREQ, RECURRENCE, DEFINST, END_Schedule_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + virtual void terminate(); + // data elements + enum ScheduleDataEls {NAME, ENABLED, AUTOC, AUTOCN, AUTOCD, ADVCD, ADVRD, INSTC, + NUMOCC, REMOCC, TEMPLID, END_Schedule_DELS }; + GncDate *m_vpStartDate, *m_vpLastDate, *m_vpEndDate; + GncFreqSpec *m_vpFreqSpec; + mutable QPtrList m_vpRecurrence; // gnc handles multiple occurrences + GncSchedDef *m_vpSchedDef; +}; +// ************* GncFreqSpec******************************************** +class GncFreqSpec : public GncObject { +public: + GncFreqSpec (); + ~GncFreqSpec(); +protected: + friend class MyMoneyGncReader; + // access data values (only interval type used at present) + QString intervalType() const { return (var(INTVT));}; +private: + // subsidiary objects/elements + enum FreqSpecSubEls {COMPO, END_FreqSpec_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + // data elements + enum FreqSpecDataEls {INTVT, MONTHLY, DAILY, WEEKLY, INTVI, INTVO, INTVD, END_FreqSpec_DELS}; + virtual void terminate(); + mutable QPtrList m_fsList; +}; + +// ************* GncRecurrence******************************************** +// this object replaces GncFreqSpec from Gnucash 2.2 onwards +class GncRecurrence : public GncObject { +public: + GncRecurrence (); + ~GncRecurrence(); +protected: + friend class MyMoneyGncReader; + // access data values + QDate startDate () const + {QDate x = QDate(); return (m_vpStartDate == NULL ? x : m_vpStartDate->date());}; + QString mult() const {return (var(MULT));}; + QString periodType() const {return (var(PERIODTYPE));}; + QString getFrequency() const; +private: + // subsidiary objects/elements + enum RecurrenceSubEls {STARTDATE, END_Recurrence_SELS }; + virtual GncObject *startSubEl(); + virtual void endSubEl(GncObject *); + // data elements + enum RecurrenceDataEls {MULT, PERIODTYPE, END_Recurrence_DELS}; + virtual void terminate(); + GncDate *m_vpStartDate; +}; + +// ************* GncSchedDef******************************************** +// This is a sub-object of GncSchedule, (sx:deferredInstance) function currently unknown +class GncSchedDef : public GncObject { + public: + GncSchedDef (); + ~GncSchedDef(); + protected: + friend class MyMoneyGncReader; + private: + // subsidiary objects/elements +}; + +// **************************************************************************************** +/** + XML Reader + The XML reader is an implementation of the Qt SAX2 XML parser. It determines the type + of object represented by the XMl, and calls the appropriate object functions +*/ +// ***************************************************************************************** +class XmlReader : public QXmlDefaultHandler { +protected: + friend class MyMoneyGncReader; + XmlReader (MyMoneyGncReader *pM) : pMain(pM) {} // keep pointer to 'main' + void processFile (QIODevice*); // main entry point of reader + // define xml content handler functions + bool startDocument (); + bool startElement (const QString&, const QString&, const QString&, const QXmlAttributes&); + bool endElement (const QString&, const QString&, const QString&); + bool characters (const QString &); + bool endDocument(); +private: + QXmlInputSource *m_source; + QXmlSimpleReader *m_reader; + QPtrStack m_os; // stack of sub objects + GncObject *m_co; // current object, for ease of coding (=== m_os.top) + MyMoneyGncReader *pMain; // the 'main' pointer, to pass on to objects + bool m_headerFound; // check for gnc-v2 header +#ifdef _GNCFILEANON + int lastType; // 0 = start element, 1 = data, 2 = end element + int indentCount; +#endif // _GNCFILEANON +}; + +/** + * private classes to define messages to be held in list for final report + */ +class GncMessageArgs { +protected: + friend class MyMoneyGncReader; + QString source; // 'type of message + unsigned int code; // to identify actual message + QValueList args; // variable arguments +}; + +class GncMessages { +protected: + friend class MyMoneyGncReader; + static QString text (const QString, const unsigned int); // returns text of identified message + static unsigned int argCount (const QString, const unsigned int); // returns no. of args required +private: + typedef struct { + const QString source; + const unsigned int code; + QString text; + } + messText; + static messText texts []; +}; + +/** + MyMoneyGncReader - Main class for this module + Controls overall operation of the importer + */ + +#ifndef _GNCFILEANON +class MyMoneyGncReader : public IMyMoneyStorageFormat { +#else +class MyMoneyGncReader { +#endif // _GNCFILEANON +public: + MyMoneyGncReader(); + virtual ~MyMoneyGncReader(); + /** + * Import a GnuCash XML file + * + * @param pDevice : pointer to GnuCash file + * @param storage : pointer to MyMoneySerialize storage + * + * @return void + * + */ +#ifndef _GNCFILEANON + void readFile (QIODevice* pDevice, IMyMoneySerialize* storage); // main entry point, IODevice is gnucash file + void writeFile (QIODevice*, IMyMoneySerialize*) { return ;}; // dummy entry needed by kmymoneywiew. we will not be writing +#else + void readFile (QString, QString); +#endif // _GNCFILEANON + QTextCodec *m_decoder; +protected: + friend class GncObject; // pity we can't just say GncObject. And compiler doesn't like multiple friends on one line... + friend class GncFile; // there must be a better way... + friend class GncDate; + friend class GncCmdtySpec; + friend class GncKvp; + friend class GncLot; + friend class GncCountData; + friend class GncCommodity; + friend class GncPrice; + friend class GncAccount; + friend class GncTransaction; + friend class GncSplit; + friend class GncTemplateTransaction; + friend class GncTemplateSplit; + friend class GncSchedule; + friend class GncFreqSpec; + friend class GncRecurrence; + friend class XmlReader; +#ifndef _GNCFILEANON + /** functions to convert gnc objects to our equivalent */ + void convertCommodity (const GncCommodity *); + void convertPrice (const GncPrice *); + void convertAccount (const GncAccount *); + void convertTransaction (const GncTransaction *); + void convertSplit (const GncSplit *); + void saveTemplateTransaction (GncTransaction *t) {m_templateList.append (t);}; + void convertSchedule (const GncSchedule *); + void convertFreqSpec (const GncFreqSpec *); + void convertRecurrence (const GncRecurrence *); +#else + /** functions to convert gnc objects to our equivalent */ + void convertCommodity (const GncCommodity *) {return;}; + void convertPrice (const GncPrice *) {return;}; + void convertAccount (const GncAccount *) {return;}; + void convertTransaction (const GncTransaction *) {return;}; + void convertSplit (const GncSplit *) {return;}; + void saveTemplateTransaction (GncTransaction *t) {return;}; + void convertSchedule (const GncSchedule *) {return;}; + void convertFreqSpec (const GncFreqSpec *) {return;}; +#endif // _GNCFILEANON +/** to post messages for final report */ + void postMessage (const QString&, const unsigned int, const char *); + void postMessage (const QString&, const unsigned int, const char *, const char *); + void postMessage (const QString&, const unsigned int, const char *, const char *, const char *); + void postMessage (const QString&, const unsigned int, const QStringList&); + void setProgressCallback (void(*callback)(int, int, const QString&)); + void signalProgress (int current, int total, const QString& = ""); + /** user options */ + /** + Scheduled Transactions + Due to differences in implementation, it is not always possible to import scheduled + transactions correctly. Though best efforts are made, it may be that some + imported transactions cause problems within kmymoney. + An attempt is made within the importer to identify potential problem transactions, + and setting this option will cause them to be dropped from the file. + A report of which were dropped, and why, will be produced. + m_dropSuspectSchedules - drop suspect scheduled transactions + */ + bool m_dropSuspectSchedules; + /** + Investments + In kmymoney, all accounts representing investments (stocks, shares, bonds, etc.) must + have an associated investment account (e.g. a broker account). The stock account holds + the share balance, the investment account a money balance. + Gnucash does not do this, so we cannot automate this function. If you have investments, + you must select one of the following options. + 0 - create a separate investment account for each stock with the same name as the stock + 1 - create a single investment account to hold all stocks - you will be asked for a name + 2 - create multiple investment accounts - you will be asked for a name for each stock + N.B. :- option 2 doesn't really work quite as desired at present + */ + unsigned int m_investmentOption; + /** Online quotes + The user has the option to use the Finance::Quote system, as used by GnuCash, to + retrieve online share price quotes + */ + bool m_useFinanceQuote; + /** Tx Notes handling + Under some usage conditions, non-split GnuCash transactions may contain residual, usually incorrect, memo + data which is not normally visible to the user. When imported into KMyMoney however, due to display + differences, this data can become visible. Often, these transactions will have a Notes field describing + the real purpose of the transaction. If this option is selected, these notes, if present, will be used to + override the extraneous memo data." */ + bool m_useTxNotes; + // set gnucash counts (not always accurate!) + void setGncCommodityCount(int i) { m_gncCommodityCount = i;}; + void setGncAccountCount (int i) { m_gncAccountCount = i;}; + void setGncTransactionCount (int i) { m_gncTransactionCount = i;}; + void setGncScheduleCount (int i) { m_gncScheduleCount = i;}; + void setSmallBusinessFound (bool b) { m_smallBusinessFound = b;}; + void setBudgetsFound (bool b) { m_budgetsFound = b;}; + void setLotsFound (bool b) { m_lotsFound = b;}; + /* Debug Options + If you don't know what these are, best leave them alone. + gncdebug - produce general debug messages + xmldebug - produce a trace of the gnucash file XML + bAnonymize - hide personal data (account names, payees, etc., randomize money amounts) + */ + bool gncdebug; // general debug messages + bool xmldebug; // xml trace + bool bAnonymize; // anonymize input + static double m_fileHideFactor; // an overall anonymization factor to be applied to all items + bool developerDebug; +private: + void setOptions (); // to set user options from dialog + void setFileHideFactor (); + // the following handles the gnucash indicator for a bad value (-1/0) which causes us probs + QString convBadValue (QString gncValue) const {return (gncValue == "-1/0" ? "0/1" : gncValue); }; +#ifndef _GNCFILEANON + MyMoneyTransaction convertTemplateTransaction (const QString&, const GncTransaction *); + void convertTemplateSplit (const QString&, const GncTemplateSplit *); +#endif // _GNCFILEANON + // wind up when all done + void terminate(); + QString buildReportSection (const QString&); + bool writeReportToFile (const QValueList&); + // main storage +#ifndef _GNCFILEANON + IMyMoneyStorage *m_storage; +#else + QTextStream oStream; +#endif // _GNCFILEANON + XmlReader *m_xr; + /** to hold the callback pointer for the progress bar */ + void (*m_progressCallback)(int, int, const QString&); + // a map of which versions of the various elements (objects) we can import + map_elementVersions m_versionList; + // counters holding count data from the Gnc 'count-data' section + int m_gncCommodityCount; + int m_gncAccountCount; + int m_gncTransactionCount; + int m_gncScheduleCount; + + // flags indicating detection of features not (yet?) supported + bool m_smallBusinessFound; + bool m_budgetsFound; + bool m_lotsFound; + + /** counters for reporting */ + int m_commodityCount; + int m_priceCount; + int m_accountCount; + int m_transactionCount; + int m_templateCount; + int m_scheduleCount; +#ifndef _GNCFILEANON + // counters for error reporting + int m_ccCount, m_orCount, m_scCount; + // currency counter + QMap m_currencyCount; + /** + * Map gnucash vs. Kmm ids for accounts, equities, schedules, price sources + */ + QMap m_mapIds; + QString m_rootId; // save the root id for terminate() + QMap m_mapEquities; + QMap m_mapSchedules; + QMap m_mapSources; + /** + * A list of stock accounts (gnc ids) which will be held till the end + so we can implement the user's investment option + */ + QValueList m_stockList; + /** + * Temporary storage areas for transaction processing + */ + QString m_txCommodity; // save commodity for current transaction + QString m_txPayeeId; // gnc has payee at tx level, we need it at split level + QDate m_txDatePosted; // ditto for post date + QString m_txChequeNo; // ditto for cheque number + /** In kmm, the order of splits is critical to some operations. These + * areas will hold the splits until we've read them all */ + QValueList m_splitList, m_liabilitySplitList, m_otherSplitList; + bool m_potentialTransfer; // to determine whether this might be a transfer + /** Schedules are processed through 3 different functions, any of which may set this flag */ + bool m_suspectSchedule; + /** + * A holding area for template txs while we're waiting for the schedules + */ + QPtrList m_templateList; + /** Hold a list of suspect schedule ids for later processing? */ + QValueList m_suspectList; + /** + * To hold message data till final report + */ + QPtrList m_messageList; + GncMessages *m_messageTexts; + /** + * Internal utility functions + */ + QString createPayee (const QString&); // create a payee and return it's id + QString createOrphanAccount (const QString&); // create unknown account and return the id + QDate incrDate (QDate lastDate, unsigned char interval, unsigned int intervalCount); // for date calculations + MyMoneyAccount checkConsistency (MyMoneyAccount& parent, MyMoneyAccount& child); // gnucash is sometimes TOO flexible + void checkInvestmentOption (QString stockId); // implement user investment option + void getPriceSource (MyMoneySecurity stock, QString gncSource); +#endif // _GNCFILEANON +}; + +#endif // MYMONEYSTORAGEGNC_H diff --git a/kmymoney2/converter/mymoneyqifprofile.cpp b/kmymoney2/converter/mymoneyqifprofile.cpp new file mode 100644 index 0000000..b8fe97c --- /dev/null +++ b/kmymoney2/converter/mymoneyqifprofile.cpp @@ -0,0 +1,1013 @@ +/*************************************************************************** + mymoneyqifprofile.cpp - description + ------------------- + begin : Tue Dec 24 2002 + copyright : (C) 2002 by Thomas Baumgart + email : thb@net-bembel.de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +// ---------------------------------------------------------------------------- +// QT Includes + +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Includes + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes + +#include "mymoneyqifprofile.h" +#include "../mymoney/mymoneyexception.h" +#include "../mymoney/mymoneymoney.h" + +/* + * CENTURY_BREAK is used to identfy the century for a two digit year + * + * if yr is < CENTURY_BREAK it is in 2000 + * if yr is >= CENTURY_BREAK it is in 1900 + * + * so with CENTURY_BREAK being 70 the following will happen: + * + * 00..69 -> 2000..2069 + * 70..99 -> 1970..1999 + */ +#define CENTURY_BREAK 70 + +class MyMoneyQifProfile::Private { + public: + Private() { + m_changeCount.resize(3, 0); + m_lastValue.resize(3, 0); + m_largestValue.resize(3, 0); + } + + void getThirdPosition(void); + void dissectDate(QValueVector& parts, const QString& txt) const; + + QValueVector m_changeCount; + QValueVector m_lastValue; + QValueVector m_largestValue; + QMap m_partPos; +}; + +void MyMoneyQifProfile::Private::dissectDate(QValueVector& parts, const QString& txt) const +{ + QRegExp nonDelimChars("[ 0-9a-zA-Z]"); + int part = 0; // the current part we scan + unsigned int pos; // the current scan position + unsigned int maxPartSize = txt.length() > 6 ? 4 : 2; + // the maximum size of a part + // some fu... up MS-Money versions write two delimiter in a row + // so we need to keep track of them. Example: D14/12/'08 + bool lastWasDelim = false; + + // separate the parts of the date and keep the locations of the delimiters + for(pos = 0; pos < txt.length() && part < 3; ++pos) { + if(nonDelimChars.search(txt[pos]) == -1) { + if(!lastWasDelim) { + ++part; + maxPartSize = 0; // make sure to pick the right one depending if next char is numeric or not + lastWasDelim = true; + } + } else { + lastWasDelim = false; + // check if the part is over and we did not see a delimiter + if((maxPartSize != 0) && (parts[part].length() == maxPartSize)) { + ++part; + maxPartSize = 0; + } + if(maxPartSize == 0) { + maxPartSize = txt[pos].isDigit() ? 2 : 3; + if(part == 2) + maxPartSize = 4; + } + if(part < 3) + parts[part] += txt[pos]; + } + } + + if(part == 3) { // invalid date + for(int i = 0; i < 3; ++i) { + parts[i] = "0"; + } + } +} + + +void MyMoneyQifProfile::Private::getThirdPosition(void) +{ + // if we have detected two parts we can calculate the third and its position + if(m_partPos.count() == 2) { + QValueList partsPresent = m_partPos.keys(); + QStringList partsAvail = QStringList::split(",", "d,m,y"); + int missingIndex = -1; + int value = 0; + for(int i = 0; i < 3; ++i) { + if(!partsPresent.contains(partsAvail[i][0])) { + missingIndex = i; + } else { + value += m_partPos[partsAvail[i][0]]; + } + } + m_partPos[partsAvail[missingIndex][0]] = 3 - value; + } +} + + + +MyMoneyQifProfile::MyMoneyQifProfile() : + d(new Private), + m_isDirty(false) +{ + clear(); +} + +MyMoneyQifProfile::MyMoneyQifProfile(const QString& name) : + d(new Private), + m_isDirty(false) +{ + loadProfile(name); +} + +MyMoneyQifProfile::~MyMoneyQifProfile() +{ + delete d; +} + +void MyMoneyQifProfile::clear(void) +{ + m_dateFormat = "%d.%m.%yyyy"; + m_apostropheFormat = "2000-2099"; + m_valueMode = ""; + m_filterScriptImport = ""; + m_filterScriptExport = ""; + m_filterFileType = "*.qif"; + + m_decimal.clear(); + m_decimal['$'] = + m_decimal['Q'] = + m_decimal['T'] = + m_decimal['O'] = + m_decimal['I'] = KGlobal::locale()->monetaryDecimalSymbol()[0]; + + m_thousands.clear(); + m_thousands['$'] = + m_thousands['Q'] = + m_thousands['T'] = + m_thousands['O'] = + m_thousands['I'] = KGlobal::locale()->monetaryThousandsSeparator()[0]; + + m_openingBalanceText = "Opening Balance"; + m_voidMark = "VOID "; + m_accountDelimiter = "["; + + m_profileName = ""; + m_profileDescription = ""; + m_profileType = "Bank"; + + m_attemptMatchDuplicates = true; +} + +void MyMoneyQifProfile::loadProfile(const QString& name) +{ + KConfig* config = KGlobal::config(); + config->setGroup(name); + + clear(); + + m_profileName = name; + m_profileDescription = config->readEntry("Description", m_profileDescription); + m_profileType = config->readEntry("Type", m_profileType); + m_dateFormat = config->readEntry("DateFormat", m_dateFormat); + m_apostropheFormat = config->readEntry("ApostropheFormat", m_apostropheFormat); + m_accountDelimiter = config->readEntry("AccountDelimiter", m_accountDelimiter); + m_openingBalanceText = config->readEntry("OpeningBalance", m_openingBalanceText); + m_voidMark = config->readEntry("VoidMark", m_voidMark); + m_filterScriptImport = config->readEntry("FilterScriptImport", m_filterScriptImport); + m_filterScriptExport = config->readEntry("FilterScriptExport", m_filterScriptExport); + m_filterFileType = config->readEntry("FilterFileType",m_filterFileType); + + m_attemptMatchDuplicates = config->readBoolEntry("AttemptMatchDuplicates", m_attemptMatchDuplicates); + + // make sure, we remove any old stuff for now + config->deleteEntry("FilterScript"); + + QString tmp = QString(m_decimal['Q']) + m_decimal['T'] + m_decimal['I'] + + m_decimal['$'] + m_decimal['O']; + tmp = config->readEntry("Decimal", tmp); + m_decimal['Q'] = tmp[0]; + m_decimal['T'] = tmp[1]; + m_decimal['I'] = tmp[2]; + m_decimal['$'] = tmp[3]; + m_decimal['O'] = tmp[4]; + + tmp = QString(m_thousands['Q']) + m_thousands['T'] + m_thousands['I'] + + m_thousands['$'] + m_thousands['O']; + tmp = config->readEntry("Thousand", tmp); + m_thousands['Q'] = tmp[0]; + m_thousands['T'] = tmp[1]; + m_thousands['I'] = tmp[2]; + m_thousands['$'] = tmp[3]; + m_thousands['O'] = tmp[4]; + + m_isDirty = false; +} + +void MyMoneyQifProfile::saveProfile(void) +{ + if(m_isDirty == true) { + KConfig* config = KGlobal::config(); + config->setGroup(m_profileName); + + config->writeEntry("Description", m_profileDescription); + config->writeEntry("Type", m_profileType); + config->writeEntry("DateFormat", m_dateFormat); + config->writeEntry("ApostropheFormat", m_apostropheFormat); + config->writeEntry("AccountDelimiter", m_accountDelimiter); + config->writeEntry("OpeningBalance", m_openingBalanceText); + config->writeEntry("VoidMark", m_voidMark); + config->writeEntry("FilterScriptImport", m_filterScriptImport); + config->writeEntry("FilterScriptExport", m_filterScriptExport); + config->writeEntry("FilterFileType", m_filterFileType); + config->writeEntry("AttemptMatchDuplicates", m_attemptMatchDuplicates); + + QString tmp; + + tmp = QString(m_decimal['Q']) + m_decimal['T'] + m_decimal['I'] + + m_decimal['$'] + m_decimal['O']; + config->writeEntry("Decimal", tmp); + tmp = QString(m_thousands['Q']) + m_thousands['T'] + m_thousands['I'] + + m_thousands['$'] + m_thousands['O']; + config->writeEntry("Thousand", tmp); + } + m_isDirty = false; +} + +void MyMoneyQifProfile::setProfileName(const QString& name) +{ + if(m_profileName != name) + m_isDirty = true; + + m_profileName = name; +} + +void MyMoneyQifProfile::setProfileDescription(const QString& desc) +{ + if(m_profileDescription != desc) + m_isDirty = true; + + m_profileDescription = desc; +} + +void MyMoneyQifProfile::setProfileType(const QString& type) +{ + if(m_profileType != type) + m_isDirty = true; + m_profileType = type; +} + +void MyMoneyQifProfile::setOutputDateFormat(const QString& dateFormat) +{ + if(m_dateFormat != dateFormat) + m_isDirty = true; + + m_dateFormat = dateFormat; +} + +void MyMoneyQifProfile::setInputDateFormat(const QString& dateFormat) +{ + int j = -1; + if(dateFormat.length() > 0) { + for(unsigned int i = 0; i < dateFormat.length()-1; ++i) { + if(dateFormat[i] == '%') { + d->m_partPos[dateFormat[++i]] = ++j; + } + } + } +} + +void MyMoneyQifProfile::setApostropheFormat(const QString& apostropheFormat) +{ + if(m_apostropheFormat != apostropheFormat) + m_isDirty = true; + + m_apostropheFormat = apostropheFormat; +} + +void MyMoneyQifProfile::setAmountDecimal(const QChar& def, const QChar& chr) +{ + QChar ch(chr); + if(ch == QChar()) + ch = ' '; + + if(m_decimal[def] != ch) + m_isDirty = true; + + m_decimal[def] = ch; +} + +void MyMoneyQifProfile::setAmountThousands(const QChar& def, const QChar& chr) +{ + QChar ch(chr); + if(ch == QChar()) + ch = ' '; + + if(m_thousands[def] != ch) + m_isDirty = true; + + m_thousands[def] = ch; +} + +QChar MyMoneyQifProfile::amountDecimal(const QChar& def) const +{ + QChar chr = m_decimal[def]; + return chr; +} + +QChar MyMoneyQifProfile::amountThousands(const QChar& def) const +{ + QChar chr = m_thousands[def]; + return chr; +} + +void MyMoneyQifProfile::setAccountDelimiter(const QString& delim) +{ + QString txt(delim); + + if(txt.isEmpty()) + txt = " "; + else if(txt[0] != '[') + txt = "["; + + if(m_accountDelimiter[0] != txt[0]) + m_isDirty = true; + m_accountDelimiter = txt[0]; +} + +void MyMoneyQifProfile::setOpeningBalanceText(const QString& txt) +{ + if(m_openingBalanceText != txt) + m_isDirty = true; + m_openingBalanceText = txt; +} + +void MyMoneyQifProfile::setVoidMark(const QString& txt) +{ + if(m_voidMark != txt) + m_isDirty = true; + m_voidMark = txt; +} + +QString MyMoneyQifProfile::accountDelimiter(void) const +{ + QString rc; + + switch(m_accountDelimiter[0]) { + case ' ': + rc = " "; + break; + default: + rc = "[]"; + break; + } + return rc; +} + +QString MyMoneyQifProfile::date(const QDate& datein) const +{ + const char* format = m_dateFormat.latin1(); + QString buffer; + QChar delim; + int maskLen; + char maskChar; + + while(*format) { + switch(*format) { + case '%': + maskLen = 0; + maskChar = *++format; + while(*format && *format == maskChar) { + ++maskLen; + ++format; + } + + switch(maskChar) { + case 'd': + if(delim) + buffer += delim; + buffer += QString::number(datein.day()).rightJustify(2, '0'); + break; + + case 'm': + if(delim) + buffer += delim; + if(maskLen == 3) + buffer += KGlobal::locale()->calendar()->monthName(datein.month(), datein.year(), true); + else + buffer += QString::number(datein.month()).rightJustify(2, '0'); + break; + + case 'y': + if(maskLen == 2) { + buffer += twoDigitYear(delim, datein.year()); + } else { + if(delim) + buffer += delim; + buffer += QString::number(datein.year()); + } + break; + default: + throw new MYMONEYEXCEPTION("Invalid char in QifProfile date field"); + break; + } + delim = 0; + break; + + default: + if(delim) + buffer += delim; + delim = *format++; + break; + } + } + return buffer; +} + +const QDate MyMoneyQifProfile::date(const QString& datein) const +{ + // in case we don't know the format, we return an invalid date + if(d->m_partPos.count() != 3) + return QDate(); + + QValueVector scannedParts(3); + d->dissectDate(scannedParts, datein); + + int yr, mon, day; + bool ok; + yr = scannedParts[d->m_partPos['y']].toInt(); + mon = scannedParts[d->m_partPos['m']].toInt(&ok); + if(!ok) { + QStringList monthNames = QStringList::split(",", "jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec"); + int j; + for(j = 1; j <= 12; ++j) { + if((KGlobal::locale()->calendar()->monthName(j, 2000, true).lower() == scannedParts[d->m_partPos['m']].lower()) + || (monthNames[j-1] == scannedParts[d->m_partPos['m']].lower())) { + mon = j; + break; + } + } + if(j == 13) { + qWarning("Unknown month '%s'", scannedParts[d->m_partPos['m']].data()); + return QDate(); + } + } + + day = scannedParts[d->m_partPos['d']].toInt(); + if(yr < 100) { // two digit year information? + if(yr < CENTURY_BREAK) // less than the CENTURY_BREAK we assume this century + yr += 2000; + else + yr += 1900; + } + return QDate(yr, mon, day); + +#if 0 + QString scannedDelim[2]; + QString formatParts[3]; + QString formatDelim[2]; + int part; + int delim; + unsigned int i,j; + + part = -1; + delim = 0; + for(i = 0; i < m_dateFormat.length(); ++i) { + if(m_dateFormat[i] == '%') { + ++part; + if(part == 3) { + qWarning("MyMoneyQifProfile::date(const QString& datein) Too many parts in date format"); + return QDate(); + } + ++i; + } + switch(m_dateFormat[i].latin1()) { + case 'm': + case 'd': + case 'y': + formatParts[part] += m_dateFormat[i]; + break; + case '/': + case '-': + case '.': + case '\'': + if(delim == 2) { + qWarning("MyMoneyQifProfile::date(const QString& datein) Too many delimiters in date format"); + return QDate(); + } + formatDelim[delim] = m_dateFormat[i]; + ++delim; + break; + default: + qWarning("MyMoneyQifProfile::date(const QString& datein) Invalid char in date format"); + return QDate(); + } + } + + + part = 0; + delim = 0; + bool prevWasChar = false; + for(i = 0; i < datein.length(); ++i) { + switch(datein[i].latin1()) { + case '/': + case '.': + case '-': + case '\'': + if(delim == 2) { + qWarning("MyMoneyQifProfile::date(const QString& datein) Too many delimiters in date field"); + return QDate(); + } + scannedDelim[delim] = datein[i]; + ++delim; + ++part; + prevWasChar = false; + break; + + default: + if(prevWasChar && datein[i].isDigit()) { + ++part; + prevWasChar = false; + } + if(datein[i].isLetter()) + prevWasChar = true; + // replace blank with 0 + scannedParts[part] += (datein[i] == ' ') ? QChar('0') : datein[i]; + break; + } + } + + int day = 1, + mon = 1, + yr = 1900; + bool ok = false; + for(i = 0; i < 2; ++i) { + if(scannedDelim[i] != formatDelim[i] + && scannedDelim[i] != QChar('\'')) { + qWarning("MyMoneyQifProfile::date(const QString& datein) Invalid delimiter '%s' when '%s' was expected", + scannedDelim[i].latin1(), formatDelim[i].latin1()); + return QDate(); + } + } + + QString msg; + for(i = 0; i < 3; ++i) { + switch(formatParts[i][0].latin1()) { + case 'd': + day = scannedParts[i].toUInt(&ok); + if (!ok) + msg = "Invalid numeric character in day string"; + break; + case 'm': + if(formatParts[i].length() != 3) { + mon = scannedParts[i].toUInt(&ok); + if (!ok) + msg = "Invalid numeric character in month string"; + } else { + for(j = 1; j <= 12; ++j) { + if(KGlobal::locale()->calendar()->monthName(j, 2000, true).lower() == formatParts[i].lower()) { + mon = j; + ok = true; + break; + } + } + if(j == 13) { + msg = "Unknown month '" + scannedParts[i] + "'"; + } + } + break; + case 'y': + ok = false; + if(scannedParts[i].length() == formatParts[i].length()) { + yr = scannedParts[i].toUInt(&ok); + if (!ok) + msg = "Invalid numeric character in month string"; + if(yr < 100) { // two digit year info + if(i > 1) { + ok = true; + if(scannedDelim[i-1] == QChar('\'')) { + if(m_apostropheFormat == "1900-1949") { + if(yr < 50) + yr += 1900; + else + yr += 2000; + } else if(m_apostropheFormat == "1900-1999") { + yr += 1900; + } else if(m_apostropheFormat == "2000-2099") { + yr += 2000; + } else { + msg = "Unsupported apostropheFormat!"; + ok = false; + } + } else { + if(m_apostropheFormat == "1900-1949") { + if(yr < 50) + yr += 2000; + else + yr += 1900; + } else if(m_apostropheFormat == "1900-1999") { + yr += 2000; + } else if(m_apostropheFormat == "2000-2099") { + yr += 1900; + } else { + msg = "Unsupported apostropheFormat!"; + ok = false; + } + } + } else { + msg = "Year as first parameter is not supported!"; + } + } else if(yr < 1900) { + msg = "Year not in range < 100 or >= 1900!"; + } else { + ok = true; + } + } else { + msg = QString("Length of year (%1) does not match expected length (%2).") + .arg(scannedParts[i].length()).arg(formatParts[i].length()); + } + break; + } + if(!msg.isEmpty()) { + qWarning("MyMoneyQifProfile::date(const QString& datein) %s",msg.latin1()); + return QDate(); + } + } + return QDate(yr, mon, day); +#endif +} + +QString MyMoneyQifProfile::twoDigitYear(const QChar delim, int yr) const +{ + QChar realDelim = delim; + QString buffer; + + if(delim) { + if((m_apostropheFormat == "1900-1949" && yr <= 1949) + || (m_apostropheFormat == "1900-1999" && yr <= 1999) + || (m_apostropheFormat == "2000-2099" && yr >= 2000)) + realDelim = '\''; + buffer += realDelim; + } + yr -= 1900; + if(yr > 100) + yr -= 100; + + if(yr < 10) + buffer += "0"; + + buffer += QString::number(yr); + return buffer; +} + +QString MyMoneyQifProfile::value(const QChar& def, const MyMoneyMoney& valuein) const +{ + unsigned char _decimalSeparator; + unsigned char _thousandsSeparator; + QString res; + + _decimalSeparator = MyMoneyMoney::decimalSeparator(); + _thousandsSeparator = MyMoneyMoney::thousandSeparator(); + MyMoneyMoney::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition(); + + MyMoneyMoney::setDecimalSeparator(amountDecimal(def)); + MyMoneyMoney::setThousandSeparator(amountThousands(def)); + MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + + res = valuein.formatMoney("", 2); + + MyMoneyMoney::setDecimalSeparator(_decimalSeparator); + MyMoneyMoney::setThousandSeparator(_thousandsSeparator); + MyMoneyMoney::setNegativeMonetarySignPosition(_signPosition); + + return res; +} + +MyMoneyMoney MyMoneyQifProfile::value(const QChar& def, const QString& valuein) const +{ + unsigned char _decimalSeparator; + unsigned char _thousandsSeparator; + MyMoneyMoney res; + + _decimalSeparator = MyMoneyMoney::decimalSeparator(); + _thousandsSeparator = MyMoneyMoney::thousandSeparator(); + MyMoneyMoney::signPosition _signPosition = MyMoneyMoney::negativeMonetarySignPosition(); + + MyMoneyMoney::setDecimalSeparator(amountDecimal(def)); + MyMoneyMoney::setThousandSeparator(amountThousands(def)); + MyMoneyMoney::setNegativeMonetarySignPosition(MyMoneyMoney::BeforeQuantityMoney); + + res = MyMoneyMoney(valuein); + + MyMoneyMoney::setDecimalSeparator(_decimalSeparator); + MyMoneyMoney::setThousandSeparator(_thousandsSeparator); + MyMoneyMoney::setNegativeMonetarySignPosition(_signPosition); + + return res; +} + +void MyMoneyQifProfile::setFilterScriptImport(const QString& script) +{ + if(m_filterScriptImport != script) + m_isDirty = true; + + m_filterScriptImport = script; +} + +void MyMoneyQifProfile::setFilterScriptExport(const QString& script) +{ + if(m_filterScriptExport != script) + m_isDirty = true; + + m_filterScriptExport = script; +} + +void MyMoneyQifProfile::setFilterFileType(const QString& txt) +{ + if(m_filterFileType != txt) + m_isDirty = true; + + m_filterFileType = txt; +} + +void MyMoneyQifProfile::setAttemptMatchDuplicates(bool f) +{ + if ( m_attemptMatchDuplicates != f ) + m_isDirty = true; + + m_attemptMatchDuplicates = f; +} + +QString MyMoneyQifProfile::inputDateFormat(void) const +{ + QStringList list; + possibleDateFormats(list); + if(list.count() == 1) + return list.first(); + return QString(); +} + +void MyMoneyQifProfile::possibleDateFormats(QStringList& list) const +{ + QStringList defaultList = QStringList::split(":", "y,m,d:y,d,m:m,d,y:m,y,d:d,m,y:d,y,m"); + list.clear(); + QStringList::const_iterator it_d; + for(it_d = defaultList.begin(); it_d != defaultList.end(); ++it_d) { + QStringList parts = QStringList::split(",", *it_d); + int i; + for(i = 0; i < 3; ++i) { + if(d->m_partPos.contains(parts[i][0])) { + if(d->m_partPos[parts[i][0]] != i) + break; + } + // months can't be larger than 12 + if(parts[i] == "m" && d->m_largestValue[i] > 12) + break; + // days can't be larger than 31 + if(parts[i] == "d" && d->m_largestValue[i] > 31) + break; + } + // matches all tests + if(i == 3) { + QString format = *it_d; + format.replace('y', "%y"); + format.replace('m', "%m"); + format.replace('d', "%d"); + format.replace(',', " "); + list << format; + } + } + // if we haven't found any, then there's something wrong. + // in this case, we present the full list and let the user decide + if(list.count() == 0) { + for(it_d = defaultList.begin(); it_d != defaultList.end(); ++it_d) { + QString format = *it_d; + format.replace('y', "%y"); + format.replace('m', "%m"); + format.replace('d', "%d"); + format.replace(',', " "); + list << format; + } + } +} + +void MyMoneyQifProfile::autoDetect(const QStringList& lines) +{ + m_dateFormat = QString(); + m_decimal.clear(); + m_thousands.clear(); + + QString numericRecords = "BT$OIQ"; + QStringList::const_iterator it; + int datesScanned = 0; + // section: used to switch between different QIF sections, + // because the Record identifiers are ambigous between sections + // eg. in transaction records, T identifies a total amount, in + // account sections it's the type. + // + // 0 - unknown + // 1 - account + // 2 - transactions + // 3 - prices + int section = 0; + QRegExp price("\"(.*)\",(.*),\"(.*)\""); + for(it = lines.begin(); it != lines.end(); ++it) { + QChar c((*it)[0]); + if(c == '!') { + QString sname = (*it).lower(); + section = 0; + if(sname.startsWith("!account")) + section = 1; + else if(sname.startsWith("!type")) { + if(sname.startsWith("!type:cat") + || sname.startsWith("!type:payee") + || sname.startsWith("!type:security") + || sname.startsWith("!type:class")) { + section = 0; + } else if(sname.startsWith("!type:price")) { + section = 3; + } else + section = 2; + } + } + + switch(section) { + case 1: + if(c == 'B') { + scanNumeric((*it).mid(1), m_decimal[c], m_thousands[c]); + } + break; + case 2: + if(numericRecords.contains(c)) { + scanNumeric((*it).mid(1), m_decimal[c], m_thousands[c]); + } else if((c == 'D') && (m_dateFormat.isEmpty())) { + if(d->m_partPos.count() != 3) { + scanDate((*it).mid(1)); + ++datesScanned; + if(d->m_partPos.count() == 2) { + // if we have detected two parts we can calculate the third and its position + d->getThirdPosition(); + } + } + } + break; + case 3: + if(price.search(*it) != -1) { + scanNumeric(price.cap(2), m_decimal['P'], m_thousands['P']); + scanDate(price.cap(3)); + ++datesScanned; + } + break; + } + } + + // the following algorithm is only applied if we have more + // than 20 dates found. Smaller numbers have shown that the + // results are inaccurate which leads to a reduced number of + // date formats presented to choose from. + if(d->m_partPos.count() != 3 && datesScanned > 20) { + QMap sortedPos; + // make sure to reset the known parts for the following algorithm + if(d->m_partPos.contains('y')) { + d->m_changeCount[d->m_partPos['y']] = -1; + for(int i = 0; i < 3; ++i) { + if(d->m_partPos['y'] == i) + continue; + // can we say for sure that we hit the day field? + if(d->m_largestValue[i] > 12) { + d->m_partPos['d'] = i; + } + } + } + if(d->m_partPos.contains('d')) + d->m_changeCount[d->m_partPos['d']] = -1; + if(d->m_partPos.contains('m')) + d->m_changeCount[d->m_partPos['m']] = -1; + + for(int i = 0; i < 3; ++i) { + if(d->m_changeCount[i] != -1) { + sortedPos[d->m_changeCount[i]] = i; + } + } + + QMap::const_iterator it_a; + QMap::const_iterator it_b; + switch(sortedPos.count()) { + case 1: // all the same + // let the user decide, we can't figure it out + break; + + case 2: // two are the same, we treat the largest as the day + // if it's 20% larger than the other one and let the + // user pick the other two + { + it_b = sortedPos.begin(); + it_a = it_b; + ++it_b; + double a = d->m_changeCount[*it_a]; + double b = d->m_changeCount[*it_b]; + if(b > (a * 1.2)) { + d->m_partPos['d'] = *it_b; + } + } + break; + + case 3: // three different, we check if they are 20% apart each + it_b = sortedPos.begin(); + for(int i = 0; i < 2; ++i) { + it_a = it_b; + ++it_b; + double a = d->m_changeCount[*it_a]; + double b = d->m_changeCount[*it_b]; + if(b > (a * 1.2)) { + switch(i) { + case 0: + d->m_partPos['y'] = *it_a; + break; + case 1: + d->m_partPos['d'] = *it_b; + break; + } + } + } + break; + } + // extract the last if necessary and possible date position + d->getThirdPosition(); + } +} + +void MyMoneyQifProfile::scanNumeric(const QString& txt, QChar& decimal, QChar& thousands) const +{ + QChar first, second; + QRegExp numericChars("[0-9-()]"); + for(unsigned int i = 0; i < txt.length(); ++i) { + if(numericChars.search(txt[i]) == -1) { + first = second; + second = txt[i]; + } + } + if(!second.isNull()) + decimal = second; + if(!first.isNull()) + thousands = first; +} + +void MyMoneyQifProfile::scanDate(const QString& txt) const +{ + // extract the parts from the txt + QValueVector parts(3); // the various parts of the date + d->dissectDate(parts, txt); + + // now analyse the parts + for(int i = 0; i < 3; ++i) { + bool ok; + int value = parts[i].toInt(&ok); + if(!ok) { // this should happen only if the part is non-numeric -> month + d->m_partPos['m'] = i; + } else if(value != 0) { + if(value != d->m_lastValue[i]) { + d->m_changeCount[i]++; + d->m_lastValue[i] = value; + if(value > d->m_largestValue[i]) + d->m_largestValue[i] = value; + } + // if it's > 31 it can only be years + if(value > 31) { + d->m_partPos['y'] = i; + } + // and if it's in between 12 and 32 and we already identified the + // position for the year it must be days + if((value > 12) && (value < 32) && d->m_partPos.contains('y')) { + d->m_partPos['d'] = i; + } + } + } +} + +#include "mymoneyqifprofile.moc" diff --git a/kmymoney2/converter/mymoneyqifprofile.h b/kmymoney2/converter/mymoneyqifprofile.h new file mode 100644 index 0000000..bd6b328 --- /dev/null +++ b/kmymoney2/converter/mymoneyqifprofile.h @@ -0,0 +1,144 @@ +/*************************************************************************** + mymoneyqifprofile.h - description + ------------------- + begin : Tue Dec 24 2002 + copyright : (C) 2002 by Thomas Baumgart + email : thb@net-bembel.de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef MYMONEYQIFPROFILE_H +#define MYMONEYQIFPROFILE_H + +// ---------------------------------------------------------------------------- +// QT Includes + +#include +#include +class QDate; + +// ---------------------------------------------------------------------------- +// KDE Includes + +// ---------------------------------------------------------------------------- +// Project Includes + +class MyMoneyMoney; + +/** + * @author Thomas Baumgart + */ + +class MyMoneyQifProfile : public QObject +{ + Q_OBJECT + +public: + MyMoneyQifProfile(); + MyMoneyQifProfile(const QString& name); + ~MyMoneyQifProfile(); + + const QString& profileName(void) const { return m_profileName; } + void setProfileName(const QString& name); + + void loadProfile(const QString& name); + void saveProfile(void); + + const QDate date(const QString& datein) const; + QString date(const QDate& datein) const; + + MyMoneyMoney value(const QChar& def, const QString& valuein) const; + QString value(const QChar& def, const MyMoneyMoney& valuein) const; + + const QString& outputDateFormat(void) const { return m_dateFormat; } + QString inputDateFormat(void) const; + const QString& apostropheFormat(void) const { return m_apostropheFormat; } + QChar amountDecimal(const QChar& def) const; + QChar amountThousands(const QChar& def) const; + const QString& profileDescription(void) const { return m_profileDescription; } + const QString& profileType(void) const { return m_profileType; } + const QString& openingBalanceText(void) const { return m_openingBalanceText; } + QString accountDelimiter(void) const; + const QString& voidMark(void) const { return m_voidMark; } + const QString& filterScriptImport(void) const { return m_filterScriptImport; } + const QString& filterScriptExport(void) const { return m_filterScriptExport; } + const QString& filterFileType(void) const { return m_filterFileType; } + bool attemptMatchDuplicates(void) const { return m_attemptMatchDuplicates; } + + /** + * This method scans all strings contained in @a lines and tries to figure + * out the settings for m_decimal, m_thousands and m_dateFormat + */ + void autoDetect(const QStringList& lines); + + /** + * This method returns a list of possible date formats the user + * can choose from. If autoDetect() has not been run, the @a list + * contains all possible date formats, in the other case, the @a list + * is adjusted to those that will match the data scanned. + */ + void possibleDateFormats(QStringList& list) const; + + /** + * This method presets the member variables with the default values. + */ + void clear(void); + + /** + * This method is used to determine, if a profile has been changed or not + */ + bool isDirty(void) const { return m_isDirty; }; + +public slots: + void setProfileDescription(const QString& desc); + void setProfileType(const QString& type); + void setOutputDateFormat(const QString& dateFormat); + void setInputDateFormat(const QString& dateFormat); + void setApostropheFormat(const QString& apostropheFormat); + void setAmountDecimal(const QChar& def, const QChar& chr); + void setAmountThousands(const QChar& def, const QChar& chr); + void setAccountDelimiter(const QString& delim); + void setOpeningBalanceText(const QString& text); + void setVoidMark(const QString& txt); + void setFilterScriptImport(const QString& txt); + void setFilterScriptExport(const QString& txt); + void setFilterFileType(const QString& txt); + void setAttemptMatchDuplicates(bool); + +private: + QString twoDigitYear(const QChar delim, int yr) const; + void scanNumeric(const QString& txt, QChar& decimal, QChar& thousands) const; + void scanDate(const QString& txt) const; + +private: + /// \internal d-pointer class. + class Private; + /// \internal d-pointer instance. + Private* const d; + bool m_isDirty; + QString m_profileName; + QString m_profileDescription; + QString m_dateFormat; + QString m_apostropheFormat; + QString m_valueMode; + QString m_profileType; + QString m_openingBalanceText; + QString m_voidMark; + QString m_accountDelimiter; + QString m_filterScriptImport; + QString m_filterScriptExport; + QString m_filterFileType; /*< The kind of input files the filter will expect, e.g. "*.qif" */ + QMap m_decimal; + QMap m_thousands; + bool m_attemptMatchDuplicates; +}; + +#endif 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 + Felix Rodriguez + John C + Thomas Baumgart + Kevin Tambascio + Ace Jones + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include + +// ---------------------------------------------------------------------------- +// QT Headers + +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Headers + +#include "mymoneyqifreader.h" +#include "../mymoney/mymoneyfile.h" +#include "../dialogs/kaccountselectdlg.h" +#include "../kmymoney2.h" +#include "kmymoneyglobalsettings.h" + +#include "mymoneystatementreader.h" +#include + +// define this to debug the code. Using external filters +// while debugging did not work too good for me, so I added +// this code. +// #define DEBUG_IMPORT + +#ifdef DEBUG_IMPORT +#warning "DEBUG_IMPORT defined --> external filter not available!!!!!!!" +#endif + +class MyMoneyQifReader::Private { + public: + Private() : + accountType(MyMoneyAccount::Checkings), + mapCategories(true) + {} + + QString accountTypeToQif(MyMoneyAccount::accountTypeE type) const; + + /** + * finalize the current statement and add it to the statement list + */ + void finishStatement(void); + + bool isTransfer(QString& name, const QString& leftDelim, const QString& rightDelim); + + /** + * Converts the QIF specific N-record of investment transactions into + * a category name + */ + QString typeToAccountName(const QString& type) const; + + /** + * Converts the QIF reconcile state to the KMyMoney reconcile state + */ + MyMoneySplit::reconcileFlagE reconcileState(const QString& state) const; + + /** + */ + void fixMultiLineMemo(QString& memo) const; + + public: + /** + * the statement that is currently collected/processed + */ + MyMoneyStatement st; + /** + * the list of all statements to be sent to MyMoneyStatementReader + */ + QValueList statements; + + /** + * a list of already used hashes in this file + */ + QMap m_hashMap; + + QString st_AccountName; + QString st_AccountId; + MyMoneyAccount::accountTypeE accountType; + bool firstTransaction; + bool mapCategories; + MyMoneyQifReader::QifEntryTypeE transactionType; +}; + +void MyMoneyQifReader::Private::fixMultiLineMemo(QString& memo) const +{ + memo.replace("\\n", "\n"); +} + +void MyMoneyQifReader::Private::finishStatement(void) +{ + // in case we have collected any data in the statement, we keep it + if((st.m_listTransactions.count() + st.m_listPrices.count() + st.m_listSecurities.count()) > 0) { + statements += st; + qDebug("Statement with %d transactions, %d prices and %d securities added to the statement list", + st.m_listTransactions.count(), st.m_listPrices.count(), st.m_listSecurities.count()); + } + // start with a fresh statement + st = MyMoneyStatement(); + st.m_skipCategoryMatching = !mapCategories; + st.m_eType = (transactionType == MyMoneyQifReader::EntryTransaction) ? MyMoneyStatement::etCheckings : MyMoneyStatement::etInvestment; +} + +QString MyMoneyQifReader::Private::accountTypeToQif(MyMoneyAccount::accountTypeE type) const +{ + QString rc = "Bank"; + + switch(type) { + default: + break; + case MyMoneyAccount::Cash: + rc = "Cash"; + break; + case MyMoneyAccount::CreditCard: + rc = "CCard"; + break; + case MyMoneyAccount::Asset: + rc = "Oth A"; + break; + case MyMoneyAccount::Liability: + rc = "Oth L"; + break; + case MyMoneyAccount::Investment: + rc = "Port"; + break; + } + return rc; +} + +QString MyMoneyQifReader::Private::typeToAccountName(const QString& type) const +{ + if(type == "reinvdiv") + return i18n("Category name", "Reinvested dividend"); + + if(type == "reinvlg") + return i18n("Category name", "Reinvested dividend (long term)"); + + if(type == "reinvsh") + return i18n("Category name", "Reinvested dividend (short term)"); + + if (type == "div") + return i18n("Category name", "Dividend"); + + if(type == "intinc") + return i18n("Category name", "Interest"); + + if(type == "cgshort") + return i18n("Category name", "Capital Gain (short term)"); + + if( type == "cgmid") + return i18n("Category name", "Capital Gain (mid term)"); + + if(type == "cglong") + return i18n("Category name", "Capital Gain (long term)"); + + if(type == "rtrncap") + return i18n("Category name", "Returned capital"); + + if(type == "miscinc") + return i18n("Category name", "Miscellaneous income"); + + if(type == "miscexp") + return i18n("Category name", "Miscellaneous expense"); + + if(type == "sell" || type == "buy") + return i18n("Category name", "Investment fees"); + + return i18n("Unknown QIF type %1").arg(type); +} + +bool MyMoneyQifReader::Private::isTransfer(QString& tmp, const QString& leftDelim, const QString& rightDelim) +{ + // it's a transfer, extract the account name + // I've seen entries like this + // + // S[Mehrwertsteuer]/_VATCode_N_I + // + // so extracting is a bit more complex and we use a regexp for it + QRegExp exp(QString("\\%1(.*)\\%2(.*)").arg(leftDelim, rightDelim)); + + bool rc; + if((rc = (exp.search(tmp) != -1)) == true) { + tmp = exp.cap(1)+exp.cap(2); + tmp = tmp.stripWhiteSpace(); + } + return rc; +} + +MyMoneySplit::reconcileFlagE MyMoneyQifReader::Private::reconcileState(const QString& state) const +{ + if(state == "X" || state == "R") // Reconciled + return MyMoneySplit::Reconciled; + + if(state == "*") // Cleared + return MyMoneySplit::Cleared; + + return MyMoneySplit::NotReconciled; +} + + +MyMoneyQifReader::MyMoneyQifReader() : + d(new Private) +{ + m_skipAccount = false; + m_transactionsProcessed = + m_transactionsSkipped = 0; + m_progressCallback = 0; + m_file = 0; + m_entryType = EntryUnknown; + m_processingData = false; + m_userAbort = false; + m_warnedInvestment = false; + m_warnedSecurity = false; + m_warnedPrice = false; + + connect(&m_filter, SIGNAL(wroteStdin(KProcess*)), this, SLOT(slotSendDataToFilter())); + connect(&m_filter, SIGNAL(receivedStdout(KProcess*, char*, int)), this, SLOT(slotReceivedDataFromFilter(KProcess*, char*, int))); + connect(&m_filter, SIGNAL(processExited(KProcess*)), this, SLOT(slotImportFinished())); + connect(&m_filter, SIGNAL(receivedStderr(KProcess*, char*, int)), this, SLOT(slotReceivedErrorFromFilter(KProcess*, char*, int))); +} + +MyMoneyQifReader::~MyMoneyQifReader() +{ + if(m_file) + delete m_file; + delete d; +} + +void MyMoneyQifReader::setCategoryMapping(bool map) +{ + d->mapCategories = map; +} + +void MyMoneyQifReader::setURL(const KURL& url) +{ + m_url = url; +} + +void MyMoneyQifReader::setProfile(const QString& profile) +{ + m_qifProfile.loadProfile("Profile-" + profile); +} + +void MyMoneyQifReader::slotSendDataToFilter(void) +{ + Q_LONG len; + + if(m_file->atEnd()) { + // m_filter.flushStdin(); + m_filter.closeStdin(); + } else { + len = m_file->readBlock(m_buffer, sizeof(m_buffer)); + if(len == -1) { + qWarning("Failed to read block from QIF import file"); + m_filter.closeStdin(); + m_filter.kill(); + } else { + m_filter.writeStdin(m_buffer, len); + } + } +} + +void MyMoneyQifReader::slotReceivedErrorFromFilter(KProcess* /* proc */, char *buff, int len) +{ + QByteArray data; + data.duplicate(buff, len); + qWarning("%s",static_cast(data)); +} + +void MyMoneyQifReader::slotReceivedDataFromFilter(KProcess* /* proc */, char *buff, int len) +{ + m_pos += len; + // signalProgress(m_pos, 0); + + while(len) { + // process char + if(*buff == '\n' || *buff == '\r') { + // found EOL + if(!m_lineBuffer.isEmpty()) { + m_qifLines << QString::fromUtf8(m_lineBuffer.stripWhiteSpace()); + } + m_lineBuffer = QCString(); + } else { + // collect all others + m_lineBuffer += (*buff); + } + ++buff; + --len; + } +} + +void MyMoneyQifReader::slotImportFinished(void) +{ + // check if the last EOL char was missing and add the trailing line + if(!m_lineBuffer.isEmpty()) { + m_qifLines << QString::fromUtf8(m_lineBuffer.stripWhiteSpace()); + } + qDebug("Read %d bytes", m_pos); + QTimer::singleShot(0, this, SLOT(slotProcessData())); +} + +void MyMoneyQifReader::slotProcessData(void) +{ + signalProgress(-1, -1); + + // scan the file and try to determine numeric and date formats + m_qifProfile.autoDetect(m_qifLines); + + // the detection is accurate for numeric values, but it could be + // that the dates were too ambiguous so that we have to let the user + // decide which one to pick. + QStringList dateFormats; + m_qifProfile.possibleDateFormats(dateFormats); + QStringList list; + if(dateFormats.count() > 1) { + list << dateFormats.first(); + bool ok; + list = KInputDialog::getItemList(i18n("Date format selection"), i18n("Pick the date format that suits your input file"), dateFormats, list, false, &ok); + if(!ok) { + m_userAbort = true; + } + } else + list = dateFormats; + + m_qifProfile.setInputDateFormat(list.first()); + + qDebug("Selected date format: '%s'", list.first().data()); + + signalProgress(0, m_qifLines.count(), i18n("Importing QIF ...")); + QStringList::iterator it; + for(it = m_qifLines.begin(); m_userAbort == false && it != m_qifLines.end(); ++it) { + ++m_linenumber; + // qDebug("Proc: '%s'", (*it).data()); + if((*it).startsWith("!")) { + processQifSpecial(*it); + m_qifEntry.clear(); + } else if(*it == "^") { + if(m_qifEntry.count() > 0) { + signalProgress(m_linenumber, 0); + processQifEntry(); + m_qifEntry.clear(); + } + } else { + m_qifEntry += *it; + } + } + d->finishStatement(); + + qDebug("%d lines processed", m_linenumber); + signalProgress(-1, -1); + + emit importFinished(); +} + +bool MyMoneyQifReader::startImport(void) +{ + bool rc = false; + d->st = MyMoneyStatement(); + d->st.m_skipCategoryMatching = !d->mapCategories; + m_dontAskAgain.clear(); + m_accountTranslation.clear(); + m_userAbort = false; + m_pos = 0; + m_linenumber = 0; + m_filename = QString::null; + m_data.clear(); + + if(!KIO::NetAccess::download(m_url, m_filename, NULL)) { + KMessageBox::detailedError(0, + i18n("Error while loading file '%1'!").arg(m_url.prettyURL()), + KIO::NetAccess::lastErrorString(), + i18n("File access error")); + return false; + } + + m_file = new QFile(m_filename); + if(m_file->open(IO_ReadOnly)) { + +#ifdef DEBUG_IMPORT + Q_LONG len; + + while(!m_file->atEnd()) { + len = m_file->readBlock(m_buffer, sizeof(m_buffer)); + if(len == -1) { + qWarning("Failed to read block from QIF import file"); + } else { + slotReceivedDataFromFilter(0, m_buffer, len); + } + } + slotImportFinished(); + +#else + // start filter process, use 'cat -' as the default filter + m_filter.clearArguments(); + if(m_qifProfile.filterScriptImport().isEmpty()) { + m_filter << "cat"; + m_filter << "-"; + } else { + m_filter << QStringList::split(" ", m_qifProfile.filterScriptImport(), true); + } + m_entryType = EntryUnknown; + + if(m_filter.start(KProcess::NotifyOnExit, KProcess::All)) { + m_filter.resume(); + signalProgress(0, m_file->size(), i18n("Reading QIF ...")); + slotSendDataToFilter(); + rc = true; + } else { + qDebug("starting filter failed :-("); + } +#endif + } + return rc; +} + +bool MyMoneyQifReader::finishImport(void) +{ + bool rc = false; + +#ifdef DEBUG_IMPORT + delete m_file; + m_file = 0; + + // remove the Don't ask again entries + KConfig* config = KGlobal::config(); + config->setGroup(QString::fromLatin1("Notification Messages")); + QStringList::ConstIterator it; + + for(it = m_dontAskAgain.begin(); it != m_dontAskAgain.end(); ++it) { + config->deleteEntry(*it); + } + config->sync(); + m_dontAskAgain.clear(); + m_accountTranslation.clear(); + + signalProgress(-1, -1); + rc = !m_userAbort; + +#else + if(!m_filter.isRunning()) { + delete m_file; + m_file = 0; + + // remove the Don't ask again entries + KConfig* config = KGlobal::config(); + config->setGroup(QString::fromLatin1("Notification Messages")); + QStringList::ConstIterator it; + + for(it = m_dontAskAgain.begin(); it != m_dontAskAgain.end(); ++it) { + config->deleteEntry(*it); + } + config->sync(); + m_dontAskAgain.clear(); + m_accountTranslation.clear(); + + signalProgress(-1, -1); + rc = !m_userAbort && m_filter.normalExit(); + } else { + qWarning("MyMoneyQifReader::finishImport() must not be called while the filter\n\tprocess is still running."); + } +#endif + + // if a temporary file was constructed by NetAccess::download, + // then it will be removed with the next call. Otherwise, it + // stays untouched on the local filesystem + KIO::NetAccess::removeTempFile(m_filename); + +#if 0 + // Add the transaction entries + KProgressDialog dlg(0,"transactionaddprogress",i18n("Adding transactions"),i18n("Now adding the transactions to your ledger...")); + dlg.progressBar()->setTotalSteps(m_transactionCache.count()); + dlg.progressBar()->setTextEnabled(true); + dlg.setAllowCancel(true); + dlg.show(); + kapp->processEvents(); + MyMoneyFile* file = MyMoneyFile::instance(); + QValueList::iterator it = m_transactionCache.begin(); + MyMoneyFileTransaction ft; + try + { + while( it != m_transactionCache.end() ) + { + if ( dlg.wasCancelled() ) + { + m_userAbort = true; + rc = false; + break; + } + file->addTransaction(*it); + dlg.progressBar()->advance(1); + ++it; + } + if(rc) + ft.commit(); + } catch(MyMoneyException *e) { + KMessageBox::detailedSorry(0, i18n("Unable to add transactions"), + (e->what() + " " + i18n("thrown in") + " " + e->file()+ ":%1").arg(e->line())); + delete e; + rc = false; + } +#endif + // Now to import the statements + QValueList::const_iterator it_st; + for(it_st = d->statements.begin(); it_st != d->statements.end(); ++it_st) + kmymoney2->slotStatementImport(*it_st); + return rc; +} + +void MyMoneyQifReader::processQifSpecial(const QString& _line) +{ + QString line = _line.mid(1); // get rid of exclamation mark + // QString test = line.left(5).lower(); + if(line.left(5).lower() == QString("type:")) { + line = line.mid(5); + + // exportable accounts + if(line.lower() == "ccard" || KMyMoneyGlobalSettings::qifCreditCard().lower().contains(line.lower())) { + d->accountType = MyMoneyAccount::CreditCard; + d->firstTransaction = true; + d->transactionType = m_entryType = EntryTransaction; + + } else if(line.lower() == "bank" || KMyMoneyGlobalSettings::qifBank().lower().contains(line.lower())) { + d->accountType = MyMoneyAccount::Checkings; + d->firstTransaction = true; + d->transactionType = m_entryType = EntryTransaction; + + } else if(line.lower() == "cash" || KMyMoneyGlobalSettings::qifCash().lower().contains(line.lower())) { + d->accountType = MyMoneyAccount::Cash; + d->firstTransaction = true; + d->transactionType = m_entryType = EntryTransaction; + + } else if(line.lower() == "oth a" || KMyMoneyGlobalSettings::qifAsset().lower().contains(line.lower())) { + d->accountType = MyMoneyAccount::Asset; + d->firstTransaction = true; + d->transactionType = m_entryType = EntryTransaction; + + } else if(line.lower() == "oth l" || line.lower() == i18n("QIF tag for liability account", "Oth L").lower()) { + d->accountType = MyMoneyAccount::Liability; + d->firstTransaction = true; + d->transactionType = m_entryType = EntryTransaction; + + } else if(line.lower() == "invst" || line.lower() == i18n("QIF tag for investment account", "Invst").lower()) { + d->transactionType = m_entryType = EntryInvestmentTransaction; + + } else if(line.lower() == "invoice" || KMyMoneyGlobalSettings::qifInvoice().lower().contains(line.lower())) { + m_entryType = EntrySkip; + + } else if(line.lower() == "tax") { + m_entryType = EntrySkip; + + } else if(line.lower() == "bill") { + m_entryType = EntrySkip; + + // exportable lists + } else if(line.lower() == "cat" || line.lower() == i18n("QIF tag for category", "Cat").lower()) { + m_entryType = EntryCategory; + + } else if(line.lower() == "security" || line.lower() == i18n("QIF tag for security", "Security").lower()) { + m_entryType = EntrySecurity; + + } else if(line.lower() == "prices" || line.lower() == i18n("QIF tag for prices", "Prices").lower()) { + m_entryType = EntryPrice; + + } else if(line.lower() == "payee") { + m_entryType = EntryPayee; + + } else if(line.lower() == "class" || line.lower() == i18n("QIF tag for a class", "Class").lower()) { + m_entryType = EntryClass; + + } else if(line.lower() == "memorized") { + m_entryType = EntryMemorizedTransaction; + + } else if(line.lower() == "budget") { + m_entryType = EntrySkip; + + } else if(line.lower() == "invitem") { + m_entryType = EntrySkip; + + } else if(line.lower() == "template") { + m_entryType = EntrySkip; + + } else { + qWarning("Unknown export header '!Type:%s' in QIF file on line %d: Skipping section.", line.data(), m_linenumber); + m_entryType = EntrySkip; + } + + // account headers + } else if(line.lower() == "account") { + m_entryType = EntryAccount; + + } else if(line.lower() == "option:autoswitch") { + m_entryType = EntryAccount; + + } else if(line.lower() == "clear:autoswitch") { + m_entryType = d->transactionType; + } +} + +void MyMoneyQifReader::processQifEntry(void) +{ + // This method processes a 'QIF Entry' which is everything between two caret + // signs + // + try { + switch(m_entryType) { + case EntryCategory: + processCategoryEntry(); + break; + + case EntryUnknown: + kdDebug(2) << "Line " << m_linenumber << ": Warning: Found an entry without a type being specified. Checking assumed." << endl; + processTransactionEntry(); + break; + + case EntryTransaction: + processTransactionEntry(); + break; + + case EntryInvestmentTransaction: + processInvestmentTransactionEntry(); + break; + + case EntryAccount: + processAccountEntry(); + break; + + case EntrySecurity: + processSecurityEntry(); + break; + + case EntryPrice: + processPriceEntry(); + break; + + case EntryPayee: + processPayeeEntry(); + break; + + case EntryClass: + kdDebug(2) << "Line " << m_linenumber << ": Classes are not yet supported!" << endl; + break; + + case EntryMemorizedTransaction: + kdDebug(2) << "Line " << m_linenumber << ": Memorized transactions are not yet implemented!" << endl; + break; + + case EntrySkip: + break; + + default: + kdDebug(2) << "Line " << m_linenumber<< ": EntryType " << m_entryType <<" not yet implemented!" << endl; + break; + } + } catch(MyMoneyException *e) { + if(e->what() != "USERABORT") { + kdDebug(2) << "Line " << m_linenumber << ": Unhandled error: " << e->what() << endl; + } else { + m_userAbort = true; + } + delete e; + } +} + +const QString MyMoneyQifReader::extractLine(const QChar id, int cnt) +{ + QStringList::ConstIterator it; + + m_extractedLine = -1; + for(it = m_qifEntry.begin(); it != m_qifEntry.end(); ++it) { + m_extractedLine++; + if((*it)[0] == id) { + if(cnt-- == 1) { + if((*it).mid(1).isEmpty()) + return QString(" "); + return (*it).mid(1); + } + } + } + m_extractedLine = -1; + return QString(); +} + +void MyMoneyQifReader::extractSplits(QValueList& listqSplits) const +{ +// *** With apologies to QString MyMoneyQifReader::extractLine *** + + QStringList::ConstIterator it; + + for(it = m_qifEntry.begin(); it != m_qifEntry.end(); ++it) { + if((*it)[0] == "S") { + qSplit q; + q.m_strCategoryName = (*it++).mid(1); // 'S' + if((*it)[0] == "E") { + q.m_strMemo = (*it++).mid(1); // 'E' + d->fixMultiLineMemo(q.m_strMemo); + } + if((*it)[0] == "$") { + q.m_amount = (*it).mid(1); // '$' + } + listqSplits += q; + } + } +} +#if 0 +void MyMoneyQifReader::processMSAccountEntry(const MyMoneyAccount::accountTypeE accountType) +{ + if(extractLine('P').lower() == m_qifProfile.openingBalanceText().lower()) { + m_account = MyMoneyAccount(); + m_account.setAccountType(accountType); + QString txt = extractLine('T'); + MyMoneyMoney balance = m_qifProfile.value('T', txt); + + QDate date = m_qifProfile.date(extractLine('D')); + m_account.setOpeningDate(date); + + QString name = extractLine('L'); + if(name.left(1) == m_qifProfile.accountDelimiter().left(1)) { + name = name.mid(1, name.length()-2); + } + d->st_AccountName = name; + m_account.setName(name); + selectOrCreateAccount(Select, m_account, balance); + d->st.m_accountId = m_account.id(); + if ( ! balance.isZero() ) + { + MyMoneyFile* file = MyMoneyFile::instance(); + QString openingtxid = file->openingBalanceTransaction(m_account); + MyMoneyFileTransaction ft; + if ( ! openingtxid.isEmpty() ) + { + MyMoneyTransaction openingtx = file->transaction(openingtxid); + MyMoneySplit split = openingtx.splitByAccount(m_account.id()); + + if ( split.shares() != balance ) + { + const MyMoneySecurity& sec = file->security(m_account.currencyId()); + if ( KMessageBox::questionYesNo( + qApp->mainWidget(), + i18n("The %1 account currently has an opening balance of %2. This QIF file reports an opening balance of %3. Would you like to overwrite the current balance with the one from the QIF file?").arg(m_account.name(), split.shares().formatMoney(m_account, sec),balance.formatMoney(m_account, sec)), + i18n("Overwrite opening balance"), + KStdGuiItem::yes(), + KStdGuiItem::no(), + "OverwriteOpeningBalance" ) + == KMessageBox::Yes ) + { + file->removeTransaction( openingtx ); + m_account.setOpeningDate( date ); + file->createOpeningBalanceTransaction( m_account, balance ); + } + } + + } + else + { + // Add an opening balance + m_account.setOpeningDate( date ); + file->createOpeningBalanceTransaction( m_account, balance ); + } + ft.commit(); + } + + } else { + // for some unknown reason, Quicken 2001 generates the following (somewhat + // misleading) sequence of lines: + // + // 1: !Account + // 2: NAT&T Universal + // 3: DAT&T Univers(...xxxx) [CLOSED] + // 4: TCCard + // 5: ^ + // 6: !Type:CCard + // 7: !Account + // 8: NCFCU Visa + // 9: DRick's CFCU Visa card (...xxxx) + // 10: TCCard + // 11: ^ + // 12: !Type:CCard + // 13: D1/ 4' 1 + // + // Lines 1-5 are processed via processQifEntry() and processAccountEntry() + // Then Quicken issues line 6 but since the account does not carry any + // transaction does not write an end delimiter. Arrrgh! So we end up with + // a QIF entry comprising of lines 6-11 and end up in this routine. Actually, + // lines 7-11 are the leadin for the next account. So we check here if + // the !Type:xxx record also contains an !Account line and process the + // entry as required. + // + // (Ace) I think a better solution here is to handle exclamation point + // lines separately from entries. In the above case: + // Line 1 would set the mode to "account entries". + // Lines 2-5 would be interpreted as an account entry. This would set m_account. + // Line 6 would set the mode to "cc transaction entries". + // Line 7 would immediately set the mode to "account entries" again + // Lines 8-11 would be interpreted as an account entry. This would set m_account. + // Line 12 would set the mode to "cc transaction entries" + // Lines 13+ would be interpreted as cc transaction entries, and life is good + int exclamationCnt = 1; + QString category; + do { + category = extractLine('!', exclamationCnt++); + } while(!category.isEmpty() && category != "Account"); + + // we have such a weird empty account + if(category == "Account") { + processAccountEntry(); + } else + { + selectOrCreateAccount(Select, m_account); + + d->st_AccountName = m_account.name(); + d->st.m_strAccountName = m_account.name(); + d->st.m_accountId = m_account.id(); + d->st.m_strAccountNumber = m_account.id(); + m_account.setNumber(m_account.id()); + if ( m_entryType == EntryInvestmentTransaction ) + processInvestmentTransactionEntry(); + else + processTransactionEntry(); + } + } +} +#endif + +void MyMoneyQifReader::processPayeeEntry(void) +{ + // TODO +} + +void MyMoneyQifReader::processCategoryEntry(void) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + MyMoneyAccount account = MyMoneyAccount(); + account.setName(extractLine('N')); + account.setDescription(extractLine('D')); + + MyMoneyAccount parentAccount; + if(!extractLine('I').isEmpty()) { + account.setAccountType(MyMoneyAccount::Income); + parentAccount = file->income(); + } else if(!extractLine('E').isEmpty()) { + account.setAccountType(MyMoneyAccount::Expense); + parentAccount = file->expense(); + } + + // check if we can find the account already in the file + MyMoneyAccount acc = kmymoney2->findAccount(account, MyMoneyAccount()); + + // if not, we just create it + if(acc.id().isEmpty()) { + MyMoneyAccount brokerage; + MyMoneyMoney balance; + kmymoney2->createAccount(account, parentAccount, brokerage, balance); + } +} + +QString MyMoneyQifReader::transferAccount(QString name, bool useBrokerage) +{ + QString accountId; + QStringList tmpEntry = m_qifEntry; // keep temp copies + MyMoneyAccount tmpAccount = m_account; + + m_qifEntry.clear(); // and construct a temp entry to create/search the account + m_qifEntry << QString("N%1").arg(name); + m_qifEntry << QString("Tunknown"); + m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); + accountId = processAccountEntry(false); + + // in case we found a reference to an investment account, we need + // to switch to the brokerage account instead. + MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); + if(useBrokerage && (acc.accountType() == MyMoneyAccount::Investment)) { + name = acc.brokerageName(); + m_qifEntry.clear(); // and construct a temp entry to create/search the account + m_qifEntry << QString("N%1").arg(name); + m_qifEntry << QString("Tunknown"); + m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); + accountId = processAccountEntry(false); + } + m_qifEntry = tmpEntry; // restore local copies + m_account = tmpAccount; + + return accountId; +} + +void MyMoneyQifReader::createOpeningBalance(MyMoneyAccount::_accountTypeE accType) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + + // if we don't have a name for the current account we need to extract the name from the L-record + if(m_account.name().isEmpty()) { + QString name = extractLine('L'); + if(name.isEmpty()) { + name = i18n("QIF imported, no account name supplied"); + } + d->isTransfer(name, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1,1)); + QStringList entry = m_qifEntry; // keep a temp copy + m_qifEntry.clear(); // and construct a temp entry to create/search the account + m_qifEntry << QString("N%1").arg(name); + m_qifEntry << QString("T%1").arg(d->accountTypeToQif(accType)); + m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); + processAccountEntry(); + m_qifEntry = entry; // restore local copy + } + + MyMoneyFileTransaction ft; + try { + bool needCreate = true; + + MyMoneyAccount acc = m_account; + // in case we're dealing with an investment account, we better use + // the accompanying brokerage account for the opening balance + acc = file->accountByName(m_account.brokerageName()); + + // check if we already have an opening balance transaction + QString tid = file->openingBalanceTransaction(acc); + MyMoneyTransaction ot; + if(!tid.isEmpty()) { + ot = file->transaction(tid); + MyMoneySplit s0 = ot.splitByAccount(acc.id()); + // if the value is the same, we can silently skip this transaction + if(s0.shares() == m_qifProfile.value('T', extractLine('T'))) { + needCreate = false; + } + if(needCreate) { + // in case we create it anyway, we issue a warning to the user to check it manually + KMessageBox::sorry(0, QString("%1").arg(i18n("KMyMoney has imported a second opening balance transaction into account %1 which differs from the one found already on file. Please correct this manually once the import is done.").arg(acc.name())), i18n("Opening balance problem")); + } + } + + if(needCreate) { + acc.setOpeningDate(m_qifProfile.date(extractLine('D'))); + file->modifyAccount(acc); + MyMoneyTransaction t = file->createOpeningBalanceTransaction(acc, m_qifProfile.value('T', extractLine('T'))); + if(!t.id().isEmpty()) { + t.setImported(); + file->modifyTransaction(t); + } + ft.commit(); + } + + // make sure to use the updated version of the account + if(m_account.id() == acc.id()) + m_account = acc; + + // remember which account we created + d->st.m_accountId = m_account.id(); + } catch(MyMoneyException* e) { + KMessageBox::detailedError(0, + i18n("Error while creating opening balance transaction"), + QString("%1(%2):%3").arg(e->file()).arg(e->line()).arg(e->what()), + i18n("File access error")); + delete e; + } +} + +void MyMoneyQifReader::processTransactionEntry(void) +{ + ++m_transactionsProcessed; + // in case the user selected to skip the account or the account + // was not found we skip this transaction +/* + if(m_account.id().isEmpty()) { + m_transactionsSkipped++; + return; + } +*/ + MyMoneyFile* file = MyMoneyFile::instance(); + MyMoneyStatement::Split s1; + MyMoneyStatement::Transaction tr; + QString tmp; + QString accountId; + int pos; + QString payee = extractLine('P'); + unsigned long h; + + h = MyMoneyTransaction::hash(m_qifEntry.join(";")); + + QString hashBase; + hashBase.sprintf("%s-%07lx", m_qifProfile.date(extractLine('D')).toString(Qt::ISODate).data(), h); + int idx = 1; + QString hash; + for(;;) { + hash = QString("%1-%2").arg(hashBase).arg(idx); + QMap::const_iterator it; + it = d->m_hashMap.find(hash); + if(it == d->m_hashMap.end()) { + d->m_hashMap[hash] = true; + break; + } + ++idx; + } + tr.m_strBankID = hash; + + if(d->firstTransaction) { + // check if this is an opening balance transaction and process it out of the statement + if(!payee.isEmpty() && ((payee.lower() == "opening balance") || KMyMoneyGlobalSettings::qifOpeningBalance().lower().contains(payee.lower()))) { + createOpeningBalance(); + d->firstTransaction = false; + return; + } + } + + // Process general transaction data + + if(d->st.m_accountId.isEmpty()) + d->st.m_accountId = m_account.id(); + + s1.m_accountId = d->st.m_accountId; + + d->st.m_eType = MyMoneyStatement::etCheckings; + tr.m_datePosted = (m_qifProfile.date(extractLine('D'))); + if(!tr.m_datePosted.isValid()) + { + int rc = KMessageBox::warningContinueCancel(0, + i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " + "date profile setting of \"%2\".\n\nPressing \"Continue\" will " + "assign todays date to the transaction. Pressing \"Cancel\" will abort " + "the import operation. You can then restart the import and select a different " + "QIF profile or create a new one.") + .arg(extractLine('D')).arg(m_qifProfile.inputDateFormat()), + i18n("Invalid date format")); + switch(rc) { + case KMessageBox::Continue: + tr.m_datePosted = (QDate::currentDate()); + break; + + case KMessageBox::Cancel: + throw new MYMONEYEXCEPTION("USERABORT"); + break; + } + } + + tmp = extractLine('L'); + pos = tmp.findRev("--"); + if(tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) { + // it's a transfer, so we wipe the memo +// tmp = ""; why?? +// st.m_strAccountName = tmp; + } else if(pos != -1) { +// what's this? +// t.setValue("Dialog", tmp.mid(pos+2)); + tmp = tmp.left(pos); + } +// t.setMemo(tmp); + + // Assign the "#" field to the transaction's bank id + // This is the custom KMM extension to QIF for a unique ID + tmp = extractLine('#'); + if(!tmp.isEmpty()) + { + tr.m_strBankID = QString("ID %1").arg(tmp); + } + +#if 0 + // Collect data for the account's split + s1.m_accountId = m_account.id(); + tmp = extractLine('S'); + pos = tmp.findRev("--"); + if(pos != -1) { + tmp = tmp.left(pos); + } + if(tmp.left(1) == m_qifProfile.accountDelimiter().left(1)) + // it's a transfer, extract the account name + tmp = tmp.mid(1, tmp.length()-2); + s1.m_strCategoryName = tmp; +#endif + // TODO (Ace) Deal with currencies more gracefully. QIF cannot deal with multiple + // currencies, so we should assume that transactions imported into a given + // account are in THAT ACCOUNT's currency. If one of those involves a transfer + // to an account with a different currency, value and shares should be + // different. (Shares is in the target account's currency, value is in the + // transaction's) + + + s1.m_amount = m_qifProfile.value('T', extractLine('T')); + tr.m_amount = m_qifProfile.value('T', extractLine('T')); + tr.m_shares = m_qifProfile.value('T', extractLine('T')); + tmp = extractLine('N'); + if (!tmp.isEmpty()) + tr.m_strNumber = tmp; + + if(!payee.isEmpty()) { + tr.m_strPayee = payee; + } + + tr.m_reconcile = d->reconcileState(extractLine('C')); + tr.m_strMemo = extractLine('M'); + d->fixMultiLineMemo(tr.m_strMemo); + s1.m_strMemo = tr.m_strMemo; + // tr.m_listSplits.append(s1); + + if(extractLine('$').isEmpty()) { + MyMoneyAccount account; + // use the same values for the second split, but clear the ID and reverse the value + MyMoneyStatement::Split s2 = s1; + s2.m_reconcile = tr.m_reconcile; + s2.m_amount = (-s1.m_amount); +// s2.clearId(); + + // standard transaction + tmp = extractLine('L'); + if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { + accountId = transferAccount(tmp, false); + + } else { +/* pos = tmp.findRev("--"); + if(pos != -1) { + t.setValue("Dialog", tmp.mid(pos+2)); + tmp = tmp.left(pos); + }*/ + + // it's an expense / income + tmp = tmp.stripWhiteSpace(); + accountId = checkCategory(tmp, s1.m_amount, s2.m_amount); + } + + if(!accountId.isEmpty()) { + try { + MyMoneyAccount account = file->account(accountId); + // FIXME: check that the type matches and ask if not + + if ( account.accountType() == MyMoneyAccount::Investment ) + { + kdDebug(0) << "Line " << m_linenumber << ": Cannot transfer to/from an investment account. Transaction ignored." << endl; + return; + } + if ( account.id() == m_account.id() ) + { + kdDebug(0) << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored." << endl; + accountId = QString(); + } + + } catch (MyMoneyException *e) { + kdDebug(0) << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found" << endl; + accountId = QString(); + delete e; + } + } + + if(!accountId.isEmpty()) { + s2.m_accountId = accountId; + s2.m_strCategoryName = tmp; + tr.m_listSplits.append(s2); + } + + } else { + // split transaction + QValueList listqSplits; + + extractSplits(listqSplits); // ****** ensure each field is ****** + // * attached to correct split * + int count; + + for(count = 1; !extractLine('$', count).isEmpty(); ++count) + { + MyMoneyStatement::Split s2 = s1; + s2.m_amount = (-m_qifProfile.value('$', listqSplits[count-1].m_amount)); // Amount of split + s2.m_strMemo = listqSplits[count-1].m_strMemo; // Memo in split + tmp = listqSplits[count-1].m_strCategoryName; // Category in split + + if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) + { + accountId = transferAccount(tmp, false); + + } else { + pos = tmp.findRev("--"); + if(pos != -1) { +/// t.setValue("Dialog", tmp.mid(pos+2)); + tmp = tmp.left(pos); + } + tmp = tmp.stripWhiteSpace(); + accountId = checkCategory(tmp, s1.m_amount, s2.m_amount); + } + + if(!accountId.isEmpty()) { + try { + MyMoneyAccount account = file->account(accountId); + // FIXME: check that the type matches and ask if not + + if ( account.accountType() == MyMoneyAccount::Investment ) + { + kdDebug(0) << "Line " << m_linenumber << ": Cannot convert a split transfer to/from an investment account. Split removed. Total amount adjusted from " << tr.m_amount.formatMoney("", 2) << " to " << (tr.m_amount + s2.m_amount).formatMoney("", 2) << "\n"; + tr.m_amount += s2.m_amount; + continue; + } + if ( account.id() == m_account.id() ) + { + kdDebug(0) << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored." << endl; + accountId = QString(); + } + + } catch (MyMoneyException *e) { + kdDebug(0) << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found" << endl; + accountId = QString(); + delete e; + } + } + if(!accountId.isEmpty()) + { + s2.m_accountId = accountId; + s2.m_strCategoryName = tmp; + tr.m_listSplits += s2; + // in case the transaction does not have a memo and we + // process the first split just copy the memo over + if(tr.m_listSplits.count() == 1 && tr.m_strMemo.isEmpty()) + tr.m_strMemo = s2.m_strMemo; + } + else + { + // TODO add an option to create a "Unassigned" category + // for now, we just drop the split which will show up as unbalanced + // transaction in the KMyMoney ledger view + } + } + } + + // Add the transaction to the statement + d->st.m_listTransactions +=tr; +} + +void MyMoneyQifReader::processInvestmentTransactionEntry(void) +{ +// kdDebug(2) << "Investment Transaction:" << m_qifEntry.count() << " lines" << endl; + /* + Items for Investment Accounts + Field Indicator Explanation + D Date + N Action + Y Security (NAME, not symbol) + I Price + Q Quantity (number of shares or split ratio) + T Transaction amount + C Cleared status + P Text in the first line for transfers and reminders (Payee) + M Memo + O Commission + L Account for the transfer + $ Amount transferred + ^ End of the entry + + It will be presumed all transactions are to the associated cash account, if + one exists, unless otherwise noted by the 'L' field. + + Expense/Income categories will be automatically generated, "_Dividend", + "_InterestIncome", etc. + + */ + + MyMoneyFile* file = MyMoneyFile::instance(); + + MyMoneyStatement::Transaction tr; + d->st.m_eType = MyMoneyStatement::etInvestment; + +// t.setCommodity(m_account.currencyId()); + // 'D' field: Date + QDate date = m_qifProfile.date(extractLine('D')); + if(date.isValid()) + tr.m_datePosted = date; + else + { + int rc = KMessageBox::warningContinueCancel(0, + i18n("The date entry \"%1\" read from the file cannot be interpreted through the current " + "date profile setting of \"%2\".\n\nPressing \"Continue\" will " + "assign todays date to the transaction. Pressing \"Cancel\" will abort " + "the import operation. You can then restart the import and select a different " + "QIF profile or create a new one.") + .arg(extractLine('D')).arg(m_qifProfile.inputDateFormat()), + i18n("Invalid date format")); + switch(rc) { + case KMessageBox::Continue: + tr.m_datePosted = QDate::currentDate(); + break; + + case KMessageBox::Cancel: + throw new MYMONEYEXCEPTION("USERABORT"); + break; + } + } + + // 'M' field: Memo + QString memo = extractLine('M'); + d->fixMultiLineMemo(memo); + tr.m_strMemo = memo; + unsigned long h; + + h = MyMoneyTransaction::hash(m_qifEntry.join(";")); + + QString hashBase; + hashBase.sprintf("%s-%07lx", m_qifProfile.date(extractLine('D')).toString(Qt::ISODate).data(), h); + int idx = 1; + QString hash; + for(;;) { + hash = QString("%1-%2").arg(hashBase).arg(idx); + QMap::const_iterator it; + it = d->m_hashMap.find(hash); + if(it == d->m_hashMap.end()) { + d->m_hashMap[hash] = true; + break; + } + ++idx; + } + tr.m_strBankID = hash; + + // '#' field: BankID + QString tmp = extractLine('#'); + if ( ! tmp.isEmpty() ) + tr.m_strBankID = QString("ID %1").arg(tmp); + + // Reconciliation flag + tr.m_reconcile = d->reconcileState(extractLine('C')); + + // 'O' field: Fees + tr.m_fees = m_qifProfile.value('T', extractLine('O')); + // 'T' field: Amount + MyMoneyMoney amount = m_qifProfile.value('T', extractLine('T')); + tr.m_amount = amount; + + MyMoneyStatement::Price price; + + price.m_date = date; + price.m_strSecurity = extractLine('Y'); + price.m_amount = m_qifProfile.value('T', extractLine('I')); + +#if 0 // we must check for that later, because certain activities don't need a security + // 'Y' field: Security name + + QString securityname = extractLine('Y').lower(); + if ( securityname.isEmpty() ) + { + kdDebug(2) << "Line " << m_linenumber << ": Investment transaction without a security is not supported." << endl; + return; + } + tr.m_strSecurity = securityname; +#endif + +#if 0 + + // For now, we let the statement reader take care of that. + + // The big problem here is that the Y field is not the SYMBOL, it's the NAME. + // The name is not very unique, because people could have used slightly different + // abbreviations or ordered words differently, etc. + // + // If there is a perfect name match with a subordinate stock account, great. + // More likely, we have to rely on the QIF file containing !Type:Security + // records, which tell us the mapping from name to symbol. + // + // Therefore, generally it is not recommended to import a QIF file containing + // investment transactions but NOT containing security records. + + QString securitysymbol = m_investmentMap[securityname]; + + // the correct account is the stock account which matches two criteria: + // (1) it is a sub-account of the selected investment account, and either + // (2a) the security name of the transaction matches the name of the security, OR + // (2b) the security name of the transaction maps to a symbol which matches the symbol of the security + + // search through each subordinate account + bool found = false; + MyMoneyAccount thisaccount = m_account; + QStringList accounts = thisaccount.accountList(); + QStringList::const_iterator it_account = accounts.begin(); + while( !found && it_account != accounts.end() ) + { + QString currencyid = file->account(*it_account).currencyId(); + MyMoneySecurity security = file->security( currencyid ); + QString symbol = security.tradingSymbol().lower(); + QString name = security.name().lower(); + + if ( securityname == name || securitysymbol == symbol ) + { + d->st_AccountId = *it_account; + s1.m_accountId = *it_account; + thisaccount = file->account(*it_account); + found = true; + +#if 0 + // update the price, while we're here. in the future, this should be + // an option + QString basecurrencyid = file->baseCurrency().id(); + MyMoneyPrice price = file->price( currencyid, basecurrencyid, t_in.m_datePosted, true ); + if ( !price.isValid() ) + { + MyMoneyPrice newprice( currencyid, basecurrencyid, t_in.m_datePosted, t_in.m_moneyAmount / t_in.m_dShares, i18n("Statement Importer") ); + file->addPrice(newprice); + } +#endif + } + + ++it_account; + } + + if (!found) + { + kdDebug(2) << "Line " << m_linenumber << ": Security " << securityname << " not found in this account. Transaction ignored." << endl; + + // If the security is not known, notify the user + // TODO (Ace) A "SelectOrCreateAccount" interface for investments + KMessageBox::information(0, i18n("This investment account does not contain the \"%1\" security. " + "Transactions involving this security will be ignored.").arg(securityname), + i18n("Security not found"), + QString("MissingSecurity%1").arg(securityname.stripWhiteSpace())); + return; + } +#endif + + // 'Y' field: Security + tr.m_strSecurity = extractLine('Y'); + + // 'Q' field: Quantity + MyMoneyMoney quantity = m_qifProfile.value('T', extractLine('Q')); + + // 'N' field: Action + QString action = extractLine('N').lower(); + + // remove trailing X, which seems to have no purpose (?!) + bool xAction = false; + if ( action.endsWith("x") ) { + action = action.left( action.length() - 1 ); + xAction = true; + } + + // Whether to create a cash split for the other side of the value + QString accountname ;//= extractLine('L'); + if ( action == "reinvdiv" || action == "reinvlg" || action == "reinvsh" ) + { + d->st.m_listPrices += price; + tr.m_shares = quantity; + tr.m_eAction = (MyMoneyStatement::Transaction::eaReinvestDividend); + tr.m_price = m_qifProfile.value('I', extractLine('I')); + + tr.m_strInterestCategory = extractLine('L'); + if(tr.m_strInterestCategory.isEmpty()) { + tr.m_strInterestCategory = d->typeToAccountName(action); + } + } + else if ( action == "div" || action == "cgshort" || action == "cgmid" || action == "cglong" || action == "rtrncap") + { + tr.m_eAction = (MyMoneyStatement::Transaction::eaCashDividend); + + QString tmp = extractLine('L'); + // if the action ends in an X, the L-Record contains the asset account + // to which the dividend should be transferred. In the other cases, it + // may contain a category that identifies the income category for the + // dividend payment + if((xAction == true) + && (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true)) { + tr.m_strBrokerageAccount = tmp; + transferAccount(tmp); // make sure the account exists + } else { + tr.m_strInterestCategory = tmp; + } + + // make sure, we have valid category. Either taken from the L-Record above, + // or derived from the action code + if(tr.m_strInterestCategory.isEmpty()) { + tr.m_strInterestCategory = d->typeToAccountName(action); + } + + // For historic reasons (coming from the OFX importer) the statement + // reader expects the dividend with a reverse sign. So we just do that. + tr.m_amount = -(amount - tr.m_fees); + + // We need an extra split which will be the zero-amount investment split + // that serves to mark this transaction as a cash dividend and note which + // stock account it belongs to. + MyMoneyStatement::Split s2; + s2.m_amount = MyMoneyMoney(); + s2.m_strCategoryName = extractLine('Y'); + tr.m_listSplits.append(s2); + } + else if ( action == "intinc" || action == "miscinc" || action == "miscexp") + { + tr.m_eAction = (MyMoneyStatement::Transaction::eaInterest); + if(action == "miscexp") + tr.m_eAction = (MyMoneyStatement::Transaction::eaFees); + + QString tmp = extractLine('L'); + // if the action ends in an X, the L-Record contains the asset account + // to which the dividend should be transferred. In the other cases, it + // may contain a category that identifies the income category for the + // payment + if((xAction == true) + && (d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true)) { + tr.m_strBrokerageAccount = tmp; + transferAccount(tmp); // make sure the account exists + } else { + tr.m_strInterestCategory = tmp; + } + + // make sure, we have a valid category. Either taken from the L-Record above, + // or derived from the action code + if(tr.m_strInterestCategory.isEmpty()) { + tr.m_strInterestCategory = d->typeToAccountName(action); + } + + + // For historic reasons (coming from the OFX importer) the statement + // reader expects the dividend with a reverse sign. So we just do that. + if(action != "miscexp") + tr.m_amount = -(amount - tr.m_fees); + + if(tr.m_strMemo.isEmpty()) + tr.m_strMemo = (QString("%1 %2").arg(extractLine('Y')).arg(d->typeToAccountName(action))).stripWhiteSpace(); + } + else if (action == "xin" || action == "xout") + { + QString payee = extractLine('P'); + if(!payee.isEmpty() && ((payee.lower() == "opening balance") || KMyMoneyGlobalSettings::qifOpeningBalance().lower().contains(payee.lower()))) { + createOpeningBalance(MyMoneyAccount::Investment); + return; + } + + tr.m_eAction = (MyMoneyStatement::Transaction::eaNone); + MyMoneyStatement::Split s2; + QString tmp = extractLine('L'); + if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1))) { + s2.m_accountId = transferAccount(tmp); + s2.m_strCategoryName = tmp; + } else { + s2.m_strCategoryName = extractLine('L'); + if(tr.m_strInterestCategory.isEmpty()) { + s2.m_strCategoryName = d->typeToAccountName(action); + } + } + + if(action == "xout") + tr.m_amount = -tr.m_amount; + + s2.m_amount = -tr.m_amount; + tr.m_listSplits.append(s2); + } + else if (action == "buy") + { + QString tmp = extractLine('L'); + if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true) { + tr.m_strBrokerageAccount = tmp; + transferAccount(tmp); // make sure the account exists + } + + d->st.m_listPrices += price; + tr.m_shares = quantity; + tr.m_eAction = (MyMoneyStatement::Transaction::eaBuy); + } + else if (action == "sell") + { + QString tmp = extractLine('L'); + if(d->isTransfer(tmp, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)) == true) { + tr.m_strBrokerageAccount = tmp; + transferAccount(tmp); // make sure the account exists + } + + d->st.m_listPrices += price; + tr.m_shares = -quantity; + tr.m_amount = -amount; + tr.m_eAction = (MyMoneyStatement::Transaction::eaSell); + } + else if ( action == "shrsin" ) + { + tr.m_shares = quantity; + tr.m_eAction = (MyMoneyStatement::Transaction::eaShrsin); + } + else if ( action == "shrsout" ) + { + tr.m_shares = -quantity; + tr.m_eAction = (MyMoneyStatement::Transaction::eaShrsout); + } + else if ( action == "stksplit" ) + { + MyMoneyMoney splitfactor = (quantity / MyMoneyMoney(10,1)).reduce(); + + // Stock splits not supported +// kdDebug(2) << "Line " << m_linenumber << ": Stock split not supported (date=" << date << " security=" << securityname << " factor=" << splitfactor.toString() << ")" << endl; + +// s1.setShares(splitfactor); +// s1.setValue(0); +// s1.setAction(MyMoneySplit::ActionSplitShares); + +// return; + } + else + { + // Unsupported action type + kdDebug(0) << "Line " << m_linenumber << ": Unsupported transaction action (" << action << ")" << endl; + return; + } + d->st.m_strAccountName = accountname; + d->st.m_listTransactions +=tr; + + /************************************************************************* + * + * These transactions are natively supported by KMyMoney + * + *************************************************************************/ + /* + D1/ 3' 5 + NShrsIn + YGENERAL MOTORS CORP 52BR1 + I20 + Q200 + U4,000.00 + T4,000.00 + M200 shares added to account @ $20/share + ^ + */ + /* + ^ + D1/14' 5 + NShrsOut + YTEMPLETON GROWTH 97GJ0 + Q50 +90 ^ + */ + /* + D1/28' 5 + NBuy + YGENERAL MOTORS CORP 52BR1 + I24.35 + Q100 + U2,435.00 + T2,435.00 + ^ + */ + /* + D1/ 5' 5 + NSell + YUnited Vanguard + I8.41 + Q50 + U420.50 + T420.50 + ^ + */ + /* + D1/ 7' 5 + NReinvDiv + YFRANKLIN INCOME 97GM2 + I38 + Q1 + U38.00 + T38.00 + ^ + */ + /************************************************************************* + * + * These transactions are all different kinds of income. (Anything that + * follows the DNYUT pattern). They are all handled the same, the only + * difference is which income account the income is placed into. By + * default, it's placed into _xxx where xxx is the right side of the + * N field. e.g. NDiv transaction goes into the _Div account + * + *************************************************************************/ + /* + D1/10' 5 + NDiv + YTEMPLETON GROWTH 97GJ0 + U10.00 + T10.00 + ^ + */ + /* + D1/10' 5 + NIntInc + YTEMPLETON GROWTH 97GJ0 + U20.00 + T20.00 + ^ + */ + /* + D1/10' 5 + NCGShort + YTEMPLETON GROWTH 97GJ0 + U111.00 + T111.00 + ^ + */ + /* + D1/10' 5 + NCGLong + YTEMPLETON GROWTH 97GJ0 + U333.00 + T333.00 + ^ + */ + /* + D1/10' 5 + NCGMid + YTEMPLETON GROWTH 97GJ0 + U222.00 + T222.00 + ^ + */ + /* + D2/ 2' 5 + NRtrnCap + YFRANKLIN INCOME 97GM2 + U1,234.00 + T1,234.00 + ^ + */ + /************************************************************************* + * + * These transactions deal with miscellaneous activity that KMyMoney + * does not support, but may support in the future. + * + *************************************************************************/ + /* Note the Q field is the split ratio per 10 shares, so Q12.5 is a + 12.5:10 split, otherwise known as 5:4. + D1/14' 5 + NStkSplit + YIBM + Q12.5 + ^ + */ + /************************************************************************* + * + * These transactions deal with short positions and options, which are + * not supported at all by KMyMoney. They will be ignored for now. + * There may be a way to hack around this, by creating a new security + * "IBM_Short". + * + *************************************************************************/ + /* + D1/21' 5 + NShtSell + YIBM + I92.38 + Q100 + U9,238.00 + T9,238.00 + ^ + */ + /* + D1/28' 5 + NCvrShrt + YIBM + I92.89 + Q100 + U9,339.00 + T9,339.00 + O50.00 + ^ + */ + /* + D6/ 1' 5 + NVest + YIBM Option + Q20 + ^ + */ + /* + D6/ 8' 5 + NExercise + YIBM Option + I60.952381 + Q20 + MFrom IBM Option Grant 6/1/2004 + ^ + */ + /* + D6/ 1'14 + NExpire + YIBM Option + Q5 + ^ + */ + /************************************************************************* + * + * These transactions do not have an associated investment ("Y" field) + * so presumably they are only valid for the cash account. Once I + * understand how these are really implemented, they can probably be + * handled without much trouble. + * + *************************************************************************/ + /* + D1/14' 5 + NCash + U-100.00 + T-100.00 + LBank Chrg + ^ + */ + /* + D1/15' 5 + NXOut + U500.00 + T500.00 + L[CU Savings] + $500.00 + ^ + */ + /* + D1/28' 5 + NXIn + U1,000.00 + T1,000.00 + L[CU Checking] + $1,000.00 + ^ + */ + /* + D1/25' 5 + NMargInt + U25.00 + T25.00 + ^ + */ +} + +const QString MyMoneyQifReader::findOrCreateIncomeAccount(const QString& searchname) +{ + QString result; + + MyMoneyFile *file = MyMoneyFile::instance(); + + // First, try to find this account as an income account + MyMoneyAccount acc = file->income(); + QStringList list = acc.accountList(); + QStringList::ConstIterator it_accid = list.begin(); + while ( it_accid != list.end() ) + { + acc = file->account(*it_accid); + if ( acc.name() == searchname ) + { + result = *it_accid; + break; + } + ++it_accid; + } + + // If we did not find the account, now we must create one. + if ( result.isEmpty() ) + { + MyMoneyAccount acc; + acc.setName( searchname ); + acc.setAccountType( MyMoneyAccount::Income ); + MyMoneyAccount income = file->income(); + MyMoneyFileTransaction ft; + file->addAccount( acc, income ); + ft.commit(); + result = acc.id(); + } + + return result; +} + +// TODO (Ace) Combine this and the previous function + +const QString MyMoneyQifReader::findOrCreateExpenseAccount(const QString& searchname) +{ + QString result; + + MyMoneyFile *file = MyMoneyFile::instance(); + + // First, try to find this account as an income account + MyMoneyAccount acc = file->expense(); + QStringList list = acc.accountList(); + QStringList::ConstIterator it_accid = list.begin(); + while ( it_accid != list.end() ) + { + acc = file->account(*it_accid); + if ( acc.name() == searchname ) + { + result = *it_accid; + break; + } + ++it_accid; + } + + // If we did not find the account, now we must create one. + if ( result.isEmpty() ) + { + MyMoneyAccount acc; + acc.setName( searchname ); + acc.setAccountType( MyMoneyAccount::Expense ); + MyMoneyFileTransaction ft; + MyMoneyAccount expense = file->expense(); + file->addAccount( acc, expense ); + ft.commit(); + result = acc.id(); + } + + return result; +} + +QString MyMoneyQifReader::checkCategory(const QString& name, const MyMoneyMoney value, const MyMoneyMoney value2) +{ + QString accountId; + MyMoneyFile *file = MyMoneyFile::instance(); + MyMoneyAccount account; + bool found = true; + + if(!name.isEmpty()) { + // The category might be constructed with an arbitraty depth (number of + // colon delimited fields). We try to find a parent account within this + // hierarchy by searching the following sequence: + // + // aaaa:bbbb:cccc:ddddd + // + // 1. search aaaa:bbbb:cccc:dddd, create nothing + // 2. search aaaa:bbbb:cccc , create dddd + // 3. search aaaa:bbbb , create cccc:dddd + // 4. search aaaa , create bbbb:cccc:dddd + // 5. don't search , create aaaa:bbbb:cccc:dddd + + account.setName(name); + QString accName; // part to be created (right side in above list) + QString parent(name); // a possible parent part (left side in above list) + do { + accountId = file->categoryToAccount(parent); + if(accountId.isEmpty()) { + found = false; + // prepare next step + if(!accName.isEmpty()) + accName.prepend(':'); + accName.prepend(parent.section(':', -1)); + account.setName(accName); + parent = parent.section(':', 0, -2); + } else if(!accName.isEmpty()) { + account.setParentAccountId(accountId); + } + } + while(!parent.isEmpty() && accountId.isEmpty()); + + // if we did not find the category, we create it + if(!found) { + MyMoneyAccount parent; + if(account.parentAccountId().isEmpty()) { + if(!value.isNegative() && value2.isNegative()) + parent = file->income(); + else + parent = file->expense(); + } else { + parent = file->account(account.parentAccountId()); + } + account.setAccountType((!value.isNegative() && value2.isNegative()) ? MyMoneyAccount::Income : MyMoneyAccount::Expense); + MyMoneyAccount brokerage; + // clear out the parent id, because createAccount() does not like that + account.setParentAccountId(QString()); + kmymoney2->createAccount(account, parent, brokerage, MyMoneyMoney()); + accountId = account.id(); + } + } + + return accountId; +} + +QString MyMoneyQifReader::processAccountEntry(bool resetAccountId) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + + MyMoneyAccount account; + QString tmp; + + account.setName(extractLine('N')); + // qDebug("Process account '%s'", account.name().data()); + + account.setDescription(extractLine('D')); + + tmp = extractLine('$'); + if(tmp.length() > 0) + account.setValue("lastStatementBalance", tmp); + + tmp = extractLine('/'); + if(tmp.length() > 0) + account.setValue("lastStatementDate", m_qifProfile.date(tmp).toString("yyyy-MM-dd")); + + QifEntryTypeE transactionType = EntryTransaction; + QString type = extractLine('T').lower().remove(QRegExp("\\s+")); + if(type == m_qifProfile.profileType().lower().remove(QRegExp("\\s+"))) { + account.setAccountType(MyMoneyAccount::Checkings); + } else if(type == "ccard" || type == "creditcard") { + account.setAccountType(MyMoneyAccount::CreditCard); + } else if(type == "cash") { + account.setAccountType(MyMoneyAccount::Cash); + } else if(type == "otha") { + account.setAccountType(MyMoneyAccount::Asset); + } else if(type == "othl") { + account.setAccountType(MyMoneyAccount::Liability); + } else if(type == "invst" || type == "port") { + account.setAccountType(MyMoneyAccount::Investment); + transactionType = EntryInvestmentTransaction; + } else if(type == "mutual") { // stock account w/o umbrella investment account + account.setAccountType(MyMoneyAccount::Stock); + transactionType = EntryInvestmentTransaction; + } else if(type == "unknown") { + // don't do anything with the type, leave it unknown + } else { + account.setAccountType(MyMoneyAccount::Checkings); + kdDebug(2) << "Line " << m_linenumber << ": Unknown account type '" << type << "', checkings assumed" << endl; + } + + // check if we can find the account already in the file + MyMoneyAccount acc = kmymoney2->findAccount(account, MyMoneyAccount()); + if(acc.id().isEmpty()) { + // in case the account is not found by name and the type is + // unknown, we have to assume something and create a checking account. + // this might be wrong, but we have no choice at this point. + if(account.accountType() == MyMoneyAccount::UnknownAccountType) + account.setAccountType(MyMoneyAccount::Checkings); + + MyMoneyAccount parentAccount; + MyMoneyAccount brokerage; + MyMoneyMoney balance; + // in case it's a stock account, we need to setup a fix investment account + if(account.isInvest()) { + acc.setName(i18n("%1 (Investment)").arg(account.name())); // use the same name for the investment account + acc.setDescription(i18n("Autogenerated by QIF importer from type Mutual account entry")); + acc.setAccountType(MyMoneyAccount::Investment); + parentAccount = file->asset(); + kmymoney2->createAccount(acc, parentAccount, brokerage, MyMoneyMoney()); + parentAccount = acc; + qDebug("We still need to create the stock account in MyMoneyQifReader::processAccountEntry()"); + } else { + // setup parent according the type of the account + switch(account.accountGroup()) { + case MyMoneyAccount::Asset: + default: + parentAccount = file->asset(); + break; + case MyMoneyAccount::Liability: + parentAccount = file->liability(); + break; + case MyMoneyAccount::Equity: + parentAccount = file->equity(); + break; + } + } + + // investment accounts will receive a brokerage account, as KMyMoney + // currently does not allow to store funds in the investment account directly + if(account.accountType() == MyMoneyAccount::Investment) { + brokerage.setName(account.brokerageName()); + brokerage.setAccountType(MyMoneyAccount::Checkings); + brokerage.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); + } + kmymoney2->createAccount(account, parentAccount, brokerage, balance); + acc = account; + // qDebug("Account created"); + } else { + // qDebug("Existing account found"); + } + + if(resetAccountId) { + // possibly start a new statement + d->finishStatement(); + m_account = acc; + d->st.m_accountId = m_account.id(); + d->transactionType = transactionType; + } + return acc.id(); +} + +void MyMoneyQifReader::selectOrCreateAccount(const SelectCreateMode mode, MyMoneyAccount& account, const MyMoneyMoney& balance) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + + QString accountId; + QString msg; + QString typeStr; + QString leadIn; + KMyMoneyUtils::categoryTypeE type; + + QMap::ConstIterator it; + + type = KMyMoneyUtils::none; + switch(account.accountGroup()) { + default: + type = KMyMoneyUtils::asset; + type = (KMyMoneyUtils::categoryTypeE) (type | KMyMoneyUtils::liability); + typeStr = i18n("account"); + leadIn = i18n("al"); + break; + + case MyMoneyAccount::Income: + case MyMoneyAccount::Expense: + type = KMyMoneyUtils::income; + type = (KMyMoneyUtils::categoryTypeE) (type | KMyMoneyUtils::expense); + typeStr = i18n("category"); + leadIn = i18n("ei"); + msg = i18n("Category selection"); + break; + } + + KAccountSelectDlg accountSelect(type, "QifImport", kmymoney2); + if(!msg.isEmpty()) + accountSelect.setCaption(msg); + + it = m_accountTranslation.find((leadIn + MyMoneyFile::AccountSeperator + account.name()).lower()); + if(it != m_accountTranslation.end()) { + try { + account = file->account(*it); + return; + + } catch (MyMoneyException *e) { + QString message(i18n("Account \"%1\" disappeared: ").arg(account.name())); + message += e->what(); + KMessageBox::error(0, message); + delete e; + } + } + + if(!account.name().isEmpty()) { + if(type & (KMyMoneyUtils::income | KMyMoneyUtils::expense)) { + accountId = file->categoryToAccount(account.name()); + } else { + accountId = file->nameToAccount(account.name()); + } + + if(mode == Create) { + if(!accountId.isEmpty()) { + account = file->account(accountId); + return; + + } else { + switch(KMessageBox::questionYesNo(0, + i18n("The %1 '%2' does not exist. Do you " + "want to create it?").arg(typeStr).arg(account.name()))) { + case KMessageBox::Yes: + break; + case KMessageBox::No: + return; + } + } + } else { + accountSelect.setHeader(i18n("Select %1").arg(typeStr)); + if(!accountId.isEmpty()) { + msg = i18n("The %1 %2 currently exists. Do you want " + "to import transactions to this account?") + .arg(typeStr).arg(account.name()); + + } else { + msg = i18n("The %1 %2 currently does not exist. You can " + "create a new %3 by pressing the Create button " + "or select another %4 manually from the selection box.") + .arg(typeStr).arg(account.name()).arg(typeStr).arg(typeStr); + } + } + } else { + accountSelect.setHeader(i18n("Import transactions to %1").arg(typeStr)); + msg = i18n("No %1 information has been found in the selected QIF file. " + "Please select an account using the selection box in the dialog or " + "create a new %2 by pressing the Create button.") + .arg(typeStr).arg(typeStr); + } + + accountSelect.setDescription(msg); + accountSelect.setAccount(account, accountId); + accountSelect.setMode(mode == Create); + accountSelect.showAbortButton(true); + + // display current entry in widget, the offending line (if any) will be shown in red + QStringList::Iterator it_e; + int i = 0; + for(it_e = m_qifEntry.begin(); it_e != m_qifEntry.end(); ++it_e) { + if(m_extractedLine == i) + accountSelect.m_qifEntry->setColor(QColor("red")); + accountSelect.m_qifEntry->append(*it_e); + accountSelect.m_qifEntry->setColor(QColor("black")); + ++i; + } + + for(;;) { + if(accountSelect.exec() == QDialog::Accepted) { + if(!accountSelect.selectedAccount().isEmpty()) { + accountId = accountSelect.selectedAccount(); + + m_accountTranslation[(leadIn + MyMoneyFile::AccountSeperator + account.name()).lower()] = accountId; + + // MMAccount::openingBalance() is where the accountSelect dialog has + // stashed the opening balance that the user chose. + MyMoneyAccount importedAccountData(account); + // MyMoneyMoney balance = importedAccountData.openingBalance(); + account = file->account(accountId); + if ( ! balance.isZero() ) + { + QString openingtxid = file->openingBalanceTransaction(account); + MyMoneyFileTransaction ft; + if ( ! openingtxid.isEmpty() ) + { + MyMoneyTransaction openingtx = file->transaction(openingtxid); + MyMoneySplit split = openingtx.splitByAccount(account.id()); + + if ( split.shares() != balance ) + { + const MyMoneySecurity& sec = file->security(account.currencyId()); + if ( KMessageBox::questionYesNo( + qApp->mainWidget(), + i18n("The %1 account currently has an opening balance of %2. This QIF file reports an opening balance of %3. Would you like to overwrite the current balance with the one from the QIF file?").arg(account.name(), split.shares().formatMoney(account, sec), balance.formatMoney(account, sec)), + i18n("Overwrite opening balance"), + KStdGuiItem::yes(), + KStdGuiItem::no(), + "OverwriteOpeningBalance" ) + == KMessageBox::Yes ) + { + file->removeTransaction( openingtx ); + file->createOpeningBalanceTransaction( account, balance ); + } + } + } + else + { + // Add an opening balance + file->createOpeningBalanceTransaction( account, balance ); + } + ft.commit(); + } + break; + } + + } else if(accountSelect.aborted()) + throw new MYMONEYEXCEPTION("USERABORT"); + + if(typeStr == i18n("account")) { + KMessageBox::error(0, i18n("You must select or create an account.")); + } else { + KMessageBox::error(0, i18n("You must select or create a category.")); + } + } +} + +void MyMoneyQifReader::setProgressCallback(void(*callback)(int, int, const QString&)) +{ + m_progressCallback = callback; +} + +void MyMoneyQifReader::signalProgress(int current, int total, const QString& msg) +{ + if(m_progressCallback != 0) + (*m_progressCallback)(current, total, msg); +} + +void MyMoneyQifReader::processPriceEntry(void) +{ +/* + !Type:Prices + "IBM",141 9/16,"10/23/98" + ^ + !Type:Prices + "GMW",21.28," 3/17' 5" + ^ + !Type:Prices + "GMW",71652181.001,"67/128/ 0" + ^ + + Note that Quicken will often put in a price with a bogus date and number. We will ignore + prices with bogus dates. Hopefully that will catch all of these. + + Also note that prices can be in fractional units, e.g. 141 9/16. + +*/ + + QStringList::const_iterator it_line = m_qifEntry.begin(); + + // Make a price for each line + QRegExp priceExp("\"(.*)\",(.*),\"(.*)\""); + while ( it_line != m_qifEntry.end() ) + { + if(priceExp.search(*it_line) != -1) { + MyMoneyStatement::Price price; + price.m_strSecurity = priceExp.cap(1); + QString pricestr = priceExp.cap(2); + QString datestr = priceExp.cap(3); + kdDebug(0) << "Price:" << price.m_strSecurity << " / " << pricestr << " / " << datestr << endl; + + // Only add the price if the date is valid. If invalid, fail silently. See note above. + // Also require the price value to not have any slashes. Old prices will be something like + // "25 9/16", which we do not support. So we'll skip the price for now. + QDate date = m_qifProfile.date(datestr); + MyMoneyMoney rate(m_qifProfile.value('P', pricestr)); + if(date.isValid() && !rate.isZero()) + { + price.m_amount = rate; + price.m_date = date; + d->st.m_listPrices += price; + } + } + ++it_line; + } +} + +void MyMoneyQifReader::processSecurityEntry(void) +{ + /* + !Type:Security + NVANGUARD 500 INDEX + SVFINX + TMutual Fund + ^ + */ + + MyMoneyStatement::Security security; + security.m_strName = extractLine('N'); + security.m_strSymbol = extractLine('S'); + + d->st.m_listSecurities += security; +} + +#include "mymoneyqifreader.moc" diff --git a/kmymoney2/converter/mymoneyqifreader.h b/kmymoney2/converter/mymoneyqifreader.h new file mode 100644 index 0000000..77bf5ad --- /dev/null +++ b/kmymoney2/converter/mymoneyqifreader.h @@ -0,0 +1,394 @@ +/*************************************************************************** + mymoneyqifreader.h - description + ------------------- + begin : Mon Jan 27 2003 + copyright : (C) 2000-2003 by Michael Edwardes + email : mte@users.sourceforge.net + Javier Campos Morales + Felix Rodriguez + John C + Thomas Baumgart + Kevin Tambascio + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef MYMONEYQIFREADER_H +#define MYMONEYQIFREADER_H + +// ---------------------------------------------------------------------------- +// QT Headers + +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Headers + +#include "mymoneyqifprofile.h" +#include "../mymoney/mymoneyaccount.h" +#include "../mymoney/mymoneytransaction.h" + +class MyMoneyFileTransaction; + +/** + * @author Thomas Baumgart + */ +class MyMoneyQifReader : public QObject +{ + Q_OBJECT + friend class Private; + +private: + typedef enum { + EntryUnknown = 0, + EntryAccount, + EntryTransaction, + EntryCategory, + EntryMemorizedTransaction, + EntryInvestmentTransaction, + EntrySecurity, + EntryPrice, + EntryPayee, + EntryClass, + EntrySkip + } QifEntryTypeE; + + struct qSplit + { + QString m_strCategoryName; + QString m_strMemo; + QString m_amount; + }; + + +public: + MyMoneyQifReader(); + ~MyMoneyQifReader(); + + /** + * This method is used to store the filename into the object. + * The file should exist. If it does and an external filter + * program is specified with the current selected profile, + * the file is send through this filter and the result + * is stored in the m_tempFile file. + * + * @param url URL of the file to be imported + */ + void setURL(const KURL& url); + + /** + * This method is used to store the name of the profile into the object. + * The selected profile will be loaded if it exists. If an external + * filter program is specified with the current selected profile, + * the file is send through this filter and the result + * is stored in the m_tempFile file. + * + * @param name QString reference to the name of the profile + */ + void setProfile(const QString& name); + + /** + * This method actually starts the import of data from the selected file + * into the MyMoney engine. + * + * This method also starts the user defined import filter program + * defined in the QIF profile. If none is defined, the file is read + * as is (actually the UNIX command 'cat -' is used as the filter). + * + * If data from the filter program is available, the slot + * slotReceivedDataFromFilter() will be called. + * + * Make sure to connect the signal importFinished() to detect when + * the import actually ended. Call the method finishImport() to clean + * things up and get the overall result of the import. + * + * @retval true the import was started successfully + * @retval false the import could not be started. + */ + bool startImport(void); + + /** + * This method must be called once the signal importFinished() has + * been emitted. It will clean up the reader state and determines + * the actual return code of the import. + * + * @retval true Import was successful. + * @retval false Import failed because the filter program terminated + * abnormally or the user aborted the import process. + */ + bool finishImport(void); + + void setCategoryMapping(bool map); + + const MyMoneyAccount& account() const { return m_account; }; + + void setProgressCallback(void(*callback)(int, int, const QString&)); + +private: + /** + * This method is used to update the progress information. It + * checks if an appropriate function is known and calls it. + * + * For a parameter description see KMyMoneyView::progressCallback(). + */ + void signalProgress(int current, int total, const QString& = ""); + + /** + * This method scans a transaction contained in + * a QIF file formatted as an account record. This + * format is used by MS-Money. If the specific data + * is not found, then the data in the entry is treated + * as a transaction. In this case, the user will be asked to + * specify the account to which the transactions should be imported. + * The entry data is found in m_qifEntry. + * + * @param accountType see MyMoneyAccount() for details. Defaults to MyMoneyAccount::Checkings + */ + void processMSAccountEntry(const MyMoneyAccount::accountTypeE accountType = MyMoneyAccount::Checkings); + + /** + * This method scans the m_qifEntry object as a payee record specified by Quicken + */ + void processPayeeEntry(void); + + /** + * This method scans the m_qifEntry object as an account record specified + * by Quicken. In case @p resetAccountId is @p true (the default), the + * global account id will be reset. + * + * The id of the account will be returned. + */ + QString processAccountEntry(bool resetAccountId = true); + + /** + * This method scans the m_qifEntry object as a category record specified + * by Quicken. + */ + void processCategoryEntry(void); + + /** + * This method scans the m_qifEntry object as a transaction record specified + * by Quicken. + */ + void processTransactionEntry(void); + + /** + * This method scans the m_qifEntry object as an investment transaction + * record specified by Quicken. + */ + void processInvestmentTransactionEntry(void); + + /** + * This method scans the m_qifEntry object as a price record specified + * by Quicken. + */ + void processPriceEntry(void); + + /** + * This method scans the m_qifEntry object as a security record specified + * by Quicken. + */ + void processSecurityEntry(void); + + /** + * This method processes the lines previously collected in + * the member variable m_qifEntry. If further information + * by the user is required to process the entry it will + * be collected. + */ + void processQifEntry(void); + + /** + * This method process a line starting with an exclamation mark + */ + void processQifSpecial(const QString& _line); + + /** + * This method is used to get the account id of the split for + * a transaction from the text found in the QIF $ or L record. + * If an account with the name is not found, the user is asked + * if it should be created. + * + * @param name name of account as found in the QIF file + * @param value value found in the T record + * @param value2 value found in the $ record for splitted transactions + * + * @return id of the account for the split. If no name is specified + * or the account was not found and not created the + * return value will be "". + */ + QString checkCategory(const QString& name, const MyMoneyMoney value, const MyMoneyMoney value2); + + /** + * This method extracts the line beginning with the letter @p id + * from the lines contained in the QStringList object @p m_qifEntry. + * An empty QString is returned, if the line is not found. + * + * @param id QChar containing the letter to be found + * @param cnt return cnt'th of occurance of id in lines. cnt defaults to 1. + * + * @return QString with the remainder of the line or empty if + * @p id is not found in @p lines + */ + const QString extractLine(const QChar id, int cnt = 1); + + /** + * This method examines each line in the QStringList object @p m_qifEntry, + * searching for split entries, which it extracts into a struct qSplit and + * stores all splits found in @p listqSplits . + */ + void extractSplits(QValueList& listqSplits) const; + + enum SelectCreateMode { + Create = 0, + Select + }; + /** + * This method is used to find an account using the account's name + * stored in @p account in the current MyMoneyFile object. If it does not + * exist, the user has the chance to create it or to skip processing + * of this account. + * + * If an account has been selected, account will be set to contain it's data. + * If the skip operation was requested, account will be empty. + * + * Depending on @p mode the bahaviour of this method is slightly different. + * The following table shows the dependencies: + * + * @code + * case mode operation + * ----------------------------------------------------------------------------- + * account with same name exists Create returns immediately + * m_account contains data + * of existing account + * + * account does not exist Create immediately calls dialog + * to create account + * + * account with same name exists Select User will be asked if + * he wants to use the existing + * account or create a new one + * + * account does not exist Select User will be asked to + * select a different account + * or create a new one + * + * @endcode + * + * @param mode Is either Create or Select depending on the above table + * @param account Reference to MyMoneyAccount object + */ + + void selectOrCreateAccount(const SelectCreateMode mode, MyMoneyAccount& account, const MyMoneyMoney& openingBalance = MyMoneyMoney()); + + /** + * This method looks up the @p searchname account by name and returns its id + * if it was found. If it was not found, it creates a new income account using + * @p searchname as a name, and returns the id if the newly created account + * + * @param searchname The name of the account to find or create + * @return QString id of the found or created account + */ + static const QString findOrCreateIncomeAccount(const QString& searchname); + + /** + * This method looks up the @p searchname account by name and returns its id + * if it was found. If it was not found, it creates a new expense account using + * @p searchname as a name, and returns the id if the newly created account + * + * @param searchname The name of the account to find or create + * @return QString id of the found or created account + */ + static const QString findOrCreateExpenseAccount(const QString& searchname); + + /** + * This method returns the account id for a given account @a name. In + * case @a name references an investment account and @a useBrokerage is @a true + * (the default), the id of the corresponding brokerage account will be + * returned. In case an account is not existant, it will be created. + */ + QString transferAccount(QString name, bool useBrokerage = true); + + // void processQifLine(void); + void createOpeningBalance(MyMoneyAccount::_accountTypeE accType = MyMoneyAccount::Checkings); + +signals: + /** + * This signal will be emitted when the import is finished. + */ + void importFinished(void); + +private slots: + void slotSendDataToFilter(void); + void slotReceivedDataFromFilter(KProcess* /* proc */, char *buff, int len); + void slotReceivedErrorFromFilter(KProcess* /* proc */, char *buff, int len); + // void slotReceivedDataFromFilter(void); + // void slotReceivedErrorFromFilter(void); + void slotProcessData(void); + + /** + * This slot is used to be informed about the end of the filtering process. + * It emits the signal importFinished() + */ + void slotImportFinished(void); + + +private: + /// \internal d-pointer class. + class Private; + /// \internal d-pointer instance. + Private* const d; + + KProcess m_filter; + QString m_filename; + KURL m_url; + MyMoneyQifProfile m_qifProfile; + MyMoneyAccount m_account; + unsigned long m_transactionsSkipped; + unsigned long m_transactionsProcessed; + QStringList m_dontAskAgain; + QMap m_accountTranslation; + QMap m_investmentMap; + QFile *m_file; + char m_buffer[1024]; + QCString m_lineBuffer; + QStringList m_qifEntry; + int m_extractedLine; + QString m_qifLine; + QStringList m_qifLines; + QifEntryTypeE m_entryType; + bool m_skipAccount; + bool m_processingData; + bool m_userAbort; + bool m_autoCreatePayee; + unsigned long m_pos; + unsigned m_linenumber; + bool m_warnedInvestment; + bool m_warnedSecurity; + bool m_warnedPrice; + QValueList m_transactionCache; + + QValueList m_data; + + void (*m_progressCallback)(int, int, const QString&); + + MyMoneyFileTransaction* m_ft; +}; + +#endif diff --git a/kmymoney2/converter/mymoneyqifwriter.cpp b/kmymoney2/converter/mymoneyqifwriter.cpp new file mode 100644 index 0000000..9526acd --- /dev/null +++ b/kmymoney2/converter/mymoneyqifwriter.cpp @@ -0,0 +1,254 @@ +/*************************************************************************** + mymoneyqifwriter.cpp - description + ------------------- + begin : Sun Jan 5 2003 + copyright : (C) 2000-2003 by Michael Edwardes + email : mte@users.sourceforge.net + Javier Campos Morales + Felix Rodriguez + John C + Thomas Baumgart + Kevin Tambascio + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +// ---------------------------------------------------------------------------- +// QT Headers + +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include +#include + +// ---------------------------------------------------------------------------- +// Project Headers + +#include "mymoneyqifwriter.h" +#include "../mymoney/mymoneyfile.h" + +MyMoneyQifWriter::MyMoneyQifWriter() +{ +} + +MyMoneyQifWriter::~MyMoneyQifWriter() +{ +} + +void MyMoneyQifWriter::write(const QString& filename, const QString& profile, + const QString& accountId, const bool accountData, + const bool categoryData, + const QDate& startDate, const QDate& endDate) +{ + m_qifProfile.loadProfile("Profile-" + profile); + + QFile qifFile(filename); + if(qifFile.open(IO_WriteOnly)) { + QTextStream s(&qifFile); + + try { + if(categoryData) { + writeCategoryEntries(s); + } + + if(accountData) { + writeAccountEntry(s, accountId, startDate, endDate); + } + emit signalProgress(-1, -1); + + } catch(MyMoneyException *e) { + QString errMsg = i18n("Unexpected exception '%1' thrown in %2, line %3 " + "caught in MyMoneyQifWriter::write()") + .arg(e->what()).arg(e->file()).arg(e->line()); + + KMessageBox::error(0, errMsg); + delete e; + } + + qifFile.close(); + } else { + KMessageBox::error(0, i18n("Unable to open file '%1' for writing").arg(filename)); + } +} + +void MyMoneyQifWriter::writeAccountEntry(QTextStream &s, const QString& accountId, const QDate& startDate, const QDate& endDate) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + MyMoneyAccount account; + + account = file->account(accountId); + MyMoneyTransactionFilter filter(accountId); + filter.setDateFilter(startDate, endDate); + QValueList list = file->transactionList(filter); + QString openingBalanceTransactionId; + + s << "!Type:" << m_qifProfile.profileType() << endl; + if(!startDate.isValid() || startDate <= account.openingDate()) { + s << "D" << m_qifProfile.date(account.openingDate()) << endl; + openingBalanceTransactionId = file->openingBalanceTransaction(account); + MyMoneySplit split; + if(!openingBalanceTransactionId.isEmpty()) { + MyMoneyTransaction openingBalanceTransaction = file->transaction(openingBalanceTransactionId); + split = openingBalanceTransaction.splitByAccount(account.id(), true /* match */); + } + s << "T" << m_qifProfile.value('T', split.value()) << endl; + } else { + s << "D" << m_qifProfile.date(startDate) << endl; + s << "T" << m_qifProfile.value('T', file->balance(accountId, startDate.addDays(-1))) << endl; + } + s << "CX" << endl; + s << "P" << m_qifProfile.openingBalanceText() << endl; + s << "L"; + if(m_qifProfile.accountDelimiter().length()) + s << m_qifProfile.accountDelimiter()[0]; + s << account.name(); + if(m_qifProfile.accountDelimiter().length() > 1) + s << m_qifProfile.accountDelimiter()[1]; + s << endl; + s << "^" << endl; + + QValueList::ConstIterator it; + signalProgress(0, list.count()); + int count = 0; + for(it = list.begin(); it != list.end(); ++it) { + // don't include the openingBalanceTransaction again + if((*it).id() != openingBalanceTransactionId) + writeTransactionEntry(s, *it, accountId); + signalProgress(++count, 0); + } +} + +void MyMoneyQifWriter::writeCategoryEntries(QTextStream &s) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + MyMoneyAccount income; + MyMoneyAccount expense; + + income = file->income(); + expense = file->expense(); + + s << "!Type:Cat" << endl; + QStringList list = income.accountList() + expense.accountList(); + emit signalProgress(0, list.count()); + QStringList::Iterator it; + int count = 0; + for(it = list.begin(); it != list.end(); ++it) { + writeCategoryEntry(s, *it, ""); + emit signalProgress(++count, 0); + } +} + +void MyMoneyQifWriter::writeCategoryEntry(QTextStream &s, const QString& accountId, const QString& leadIn) +{ + MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); + QString name = acc.name(); + + s << "N" << leadIn << name << endl; + s << (MyMoneyAccount::accountGroup(acc.accountType()) == MyMoneyAccount::Expense ? "E" : "I") << endl; + s << "^" << endl; + + QStringList list = acc.accountList(); + QStringList::Iterator it; + name += ":"; + for(it = list.begin(); it != list.end(); ++it) { + writeCategoryEntry(s, *it, name); + } +} + +void MyMoneyQifWriter::writeTransactionEntry(QTextStream &s, const MyMoneyTransaction& t, const QString& accountId) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + MyMoneySplit split = t.splitByAccount(accountId); + + s << "D" << m_qifProfile.date(t.postDate()) << endl; + + switch(split.reconcileFlag()) { + case MyMoneySplit::Cleared: + s << "C*" << endl; + break; + + case MyMoneySplit::Reconciled: + case MyMoneySplit::Frozen: + s << "CX" << endl; + break; + + default: + break; + } + + if(split.memo().length() > 0) { + QString m = split.memo(); + m.replace('\n', "\\n"); + s << "M" << m << endl; + } + + s << "T" << m_qifProfile.value('T', split.value()) << endl; + + if(split.number().length() > 0) + s << "N" << split.number() << endl; + + if(!split.payeeId().isEmpty()) { + MyMoneyPayee payee = file->payee(split.payeeId()); + s << "P" << payee.name() << endl; + } + + QValueList list = t.splits(); + if(list.count() > 1) { + MyMoneySplit sp = t.splitByAccount(accountId, false); + MyMoneyAccount acc = file->account(sp.accountId()); + if(acc.accountGroup() != MyMoneyAccount::Income + && acc.accountGroup() != MyMoneyAccount::Expense) { + s << "L" << m_qifProfile.accountDelimiter()[0] + << MyMoneyFile::instance()->accountToCategory(sp.accountId()) + << m_qifProfile.accountDelimiter()[1] << endl; + } else { + s << "L" << file->accountToCategory(sp.accountId()) << endl; + } + if(list.count() > 2) { + QValueList::ConstIterator it; + for(it = list.begin(); it != list.end(); ++it) { + if(!((*it) == split)) { + writeSplitEntry(s, *it); + } + } + } + } + s << "^" << endl; +} + +void MyMoneyQifWriter::writeSplitEntry(QTextStream& s, const MyMoneySplit& split) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + + s << "S"; + MyMoneyAccount acc = file->account(split.accountId()); + if(acc.accountGroup() != MyMoneyAccount::Income + && acc.accountGroup() != MyMoneyAccount::Expense) { + s << m_qifProfile.accountDelimiter()[0] + << file->accountToCategory(split.accountId()) + << m_qifProfile.accountDelimiter()[1]; + } else { + s << file->accountToCategory(split.accountId()); + } + s << endl; + + if(split.memo().length() > 0) { + QString m = split.memo(); + m.replace('\n', "\\n"); + s << "E" << m << endl; + } + + s << "$" << m_qifProfile.value('$', -split.value()) << endl; +} + +#include "mymoneyqifwriter.moc" diff --git a/kmymoney2/converter/mymoneyqifwriter.h b/kmymoney2/converter/mymoneyqifwriter.h new file mode 100644 index 0000000..f77e612 --- /dev/null +++ b/kmymoney2/converter/mymoneyqifwriter.h @@ -0,0 +1,138 @@ +/*************************************************************************** + mymoneyqifwriter.h - description + ------------------- + begin : Sun Jan 5 2003 + copyright : (C) 2000-2003 by Michael Edwardes + email : mte@users.sourceforge.net + Javier Campos Morales + Felix Rodriguez + John C + Thomas Baumgart + Kevin Tambascio + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef MYMONEYQIFWRITER_H +#define MYMONEYQIFWRITER_H + +// ---------------------------------------------------------------------------- +// QT Headers + +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +// ---------------------------------------------------------------------------- +// Project Headers + +class MyMoneyTransaction; +class MyMoneySplit; +#include "mymoneyqifprofile.h" + +/** + * @author Thomas Baumgart + */ + +/** + * This class represents the QIF writer. All conversion between the + * internal representation of accounts, transactions is handled in this + * object. The conversion is controlled using a MyMoneyQifProfile to allow + * the user to control the conversion. + */ +class MyMoneyQifWriter : public QObject +{ + Q_OBJECT + +public: + MyMoneyQifWriter(); + ~MyMoneyQifWriter(); + + /** + * This method is used to start the conversion. The parameters control + * the destination of the data and the parts that will be exported. + * Individual errors will be reported using message boxes. + * + * @param filename The name of the output file with full path information + * @param profile The name of the profile to be used for conversion + * @param accountId The id of the account that will be exported + * @param accountData If true, the transactions will be exported + * @param categoryData If true, the categories will be exported as well + * @param startDate Transations before this date will not be exported + * @param endDate Transactions after this date will not be exported + */ + void write(const QString& filename, const QString& profile, + const QString& accountId, const bool accountData, + const bool categoryData, + const QDate& startDate, const QDate& endDate); + +private: + /** + * This method writes the entries necessary for an account. First + * the leadin, and then the transactions that are in the account + * specified by @p accountId in the range from @p startDate to @p + * endDate. + * + * @param s reference to textstream + * @param accountId id of the account to be written + * @param startDate date from which entries are written + * @param endDate date until which entries are written + */ + void writeAccountEntry(QTextStream& s, const QString& accountId, const QDate& startDate, const QDate& endDate); + + /** + * This method writes the category entries to the stream + * @p s. It writes the leadin and uses writeCategoryEntries() + * to write the entries and emits signalProgess() where needed. + * + * @param s reference to textstream + */ + void writeCategoryEntries(QTextStream& s); + + /** + * This method writes the category entry for account with + * the ID @p accountId to the stream @p s. All subaccounts + * are processed as well. + * + * @param s reference to textstream + * @param accountId id of the account to be written + * @param leadIn constant text that will be prepended to the account's name + */ + void writeCategoryEntry(QTextStream& s, const QString& accountId, const QString& leadIn); + + void writeTransactionEntry(QTextStream &s, const MyMoneyTransaction& t, const QString& accountId); + void writeSplitEntry(QTextStream &s, const MyMoneySplit& t); + +signals: + /** + * This signal is emitted while the operation progresses. + * When the operation starts, the signal is emitted with + * @p current being 0 and @p max having the maximum value. + * + * During the operation, the signal is emitted with @p current + * containing the current value on the way to the maximum value. + * @p max will be 0 in this case. + * + * When the operation is finished, the signal is emitted with + * @p current and @p max set to -1 to identify the end of the + * operation. + * + * @param current see above + * @param max see above + */ + void signalProgress(int current, int max); + +private: + MyMoneyQifProfile m_qifProfile; +}; + +#endif diff --git a/kmymoney2/converter/mymoneystatementreader.cpp b/kmymoney2/converter/mymoneystatementreader.cpp new file mode 100644 index 0000000..b804a59 --- /dev/null +++ b/kmymoney2/converter/mymoneystatementreader.cpp @@ -0,0 +1,1354 @@ +/*************************************************************************** + mymoneystatementreader.cpp + ------------------- + begin : Mon Aug 30 2004 + copyright : (C) 2000-2004 by Michael Edwardes + email : mte@users.sourceforge.net + Javier Campos Morales + Felix Rodriguez + John C + Thomas Baumgart + Kevin Tambascio + Ace Jones + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include + +// ---------------------------------------------------------------------------- +// QT Headers + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Headers + +#include "mymoneystatementreader.h" +#include +#include +#include +#include +#include +#include "../dialogs/kaccountselectdlg.h" +#include "../dialogs/transactionmatcher.h" +#include "../dialogs/kenterscheduledlg.h" +#include "../kmymoney2.h" +#include + +class MyMoneyStatementReader::Private +{ + public: + Private() : + transactionsCount(0), + transactionsAdded(0), + transactionsMatched(0), + transactionsDuplicate(0), + scannedCategories(false) + {} + + const QString& feeId(const MyMoneyAccount& invAcc); + const QString& interestId(const MyMoneyAccount& invAcc); + QString interestId(const QString& name); + QString feeId(const QString& name); + void assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in); + + MyMoneyAccount lastAccount; + QValueList transactions; + QValueList payees; + int transactionsCount; + int transactionsAdded; + int transactionsMatched; + int transactionsDuplicate; + QMap uniqIds; + QMap securitiesBySymbol; + QMap securitiesByName; + bool m_skipCategoryMatching; + private: + void scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName); + QString nameToId(const QString&name, MyMoneyAccount& parent); + private: + QString m_feeId; + QString m_interestId; + bool scannedCategories; +}; + + +const QString& MyMoneyStatementReader::Private::feeId(const MyMoneyAccount& invAcc) +{ + scanCategories(m_feeId, invAcc, MyMoneyFile::instance()->expense(), i18n("_Fees")); + return m_feeId; +} + +const QString& MyMoneyStatementReader::Private::interestId(const MyMoneyAccount& invAcc) +{ + scanCategories(m_interestId, invAcc, MyMoneyFile::instance()->income(), i18n("_Dividend")); + return m_interestId; +} + +QString MyMoneyStatementReader::Private::nameToId(const QString&name, MyMoneyAccount& parent) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + MyMoneyAccount acc = file->accountByName(name); + // if it does not exist, we have to create it + if(acc.id().isEmpty()) { + acc.setName( name ); + acc.setAccountType( parent.accountType() ); + acc.setCurrencyId(parent.currencyId()); + file->addAccount(acc, parent); + } + return acc.id(); +} + +QString MyMoneyStatementReader::Private::interestId(const QString& name) +{ + MyMoneyAccount parent = MyMoneyFile::instance()->income(); + return nameToId(name, parent); +} + +QString MyMoneyStatementReader::Private::feeId(const QString& name) +{ + MyMoneyAccount parent = MyMoneyFile::instance()->expense(); + return nameToId(name, parent); +} + + +void MyMoneyStatementReader::Private::scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName) +{ + if(!scannedCategories) { + KMyMoneyUtils::previouslyUsedCategories(invAcc.id(), m_feeId, m_interestId); + scannedCategories = true; + } + + if(id.isEmpty()) { + MyMoneyFile* file = MyMoneyFile::instance(); + MyMoneyAccount acc = file->accountByName(defaultName); + // if it does not exist, we have to create it + if(acc.id().isEmpty()) { + MyMoneyAccount parent = parentAccount; + acc.setName( defaultName ); + acc.setAccountType( parent.accountType() ); + acc.setCurrencyId(parent.currencyId()); + file->addAccount(acc, parent); + } + id = acc.id(); + } +} + +void MyMoneyStatementReader::Private::assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in) +{ + if( ! t_in.m_strBankID.isEmpty() ) { + // make sure that id's are unique from this point on by appending a -# + // postfix if needed + QString base(t_in.m_strBankID); + QString hash(base); + int idx = 1; + for(;;) { + QMap::const_iterator it; + it = uniqIds.find(hash); + if(it == uniqIds.end()) { + uniqIds[hash] = true; + break; + } + hash = QString("%1-%2").arg(base).arg(idx); + ++idx; + } + + s.setBankID(hash); + } +} + + +MyMoneyStatementReader::MyMoneyStatementReader() : + d(new Private), + m_userAbort(false), + m_autoCreatePayee(false), + m_ft(0), + m_progressCallback(0) +{ + m_askPayeeCategory = KMyMoneyGlobalSettings::askForPayeeCategory(); +} + +MyMoneyStatementReader::~MyMoneyStatementReader() +{ + delete d; +} + +bool MyMoneyStatementReader::anyTransactionAdded(void) const +{ + return (d->transactionsAdded != 0) ? true : false; +} + +void MyMoneyStatementReader::setAutoCreatePayee(bool create) +{ + m_autoCreatePayee = create; +} + +void MyMoneyStatementReader::setAskPayeeCategory(bool ask) +{ + m_askPayeeCategory = ask; +} + +bool MyMoneyStatementReader::import(const MyMoneyStatement& s, QStringList& messages) +{ + // + // For testing, save the statement to an XML file + // (uncomment this line) + // + //MyMoneyStatement::writeXMLFile(s,"Imported.Xml"); + + // + // Select the account + // + + m_account = MyMoneyAccount(); + + m_ft = new MyMoneyFileTransaction(); + d->m_skipCategoryMatching = s.m_skipCategoryMatching; + + // if the statement source left some information about + // the account, we use it to get the current data of it + if(!s.m_accountId.isEmpty()) { + try { + m_account = MyMoneyFile::instance()->account(s.m_accountId); + } catch(MyMoneyException* e) { + qDebug("Received reference '%s' to unknown account in statement", s.m_accountId.data()); + delete e; + } + } + + if(m_account.id().isEmpty()) + { + m_account.setName(s.m_strAccountName); + m_account.setNumber(s.m_strAccountNumber); + + switch ( s.m_eType ) + { + case MyMoneyStatement::etCheckings: + m_account.setAccountType(MyMoneyAccount::Checkings); + break; + case MyMoneyStatement::etSavings: + m_account.setAccountType(MyMoneyAccount::Savings); + break; + case MyMoneyStatement::etInvestment: + //testing support for investment statements! + //m_userAbort = true; + //KMessageBox::error(kmymoney2, i18n("This is an investment statement. These are not supported currently."), i18n("Critical Error")); + m_account.setAccountType(MyMoneyAccount::Investment); + break; + case MyMoneyStatement::etCreditCard: + m_account.setAccountType(MyMoneyAccount::CreditCard); + break; + default: + m_account.setAccountType(MyMoneyAccount::Checkings); + break; + } + + + // we ask the user only if we have some transactions to process + if ( !m_userAbort && s.m_listTransactions.count() > 0) + m_userAbort = ! selectOrCreateAccount(Select, m_account); + } + + // see if we need to update some values stored with the account + if(m_account.value("lastStatementBalance") != s.m_closingBalance.toString() + || m_account.value("lastImportedTransactionDate") != s.m_dateEnd.toString(Qt::ISODate)) { + if(s.m_closingBalance != MyMoneyMoney::autoCalc) { + m_account.setValue("lastStatementBalance", s.m_closingBalance.toString()); + if ( s.m_dateEnd.isValid() ) { + m_account.setValue("lastImportedTransactionDate", s.m_dateEnd.toString(Qt::ISODate)); + } + } + + try { + MyMoneyFile::instance()->modifyAccount(m_account); + } catch(MyMoneyException* e) { + qDebug("Updating account in MyMoneyStatementReader::startImport failed"); + delete e; + } + } + + + if(!m_account.name().isEmpty()) + messages += i18n("Importing statement for account %1").arg(m_account.name()); + else if(s.m_listTransactions.count() == 0) + messages += i18n("Importing statement without transactions"); + + qDebug("Importing statement for '%s'", m_account.name().data()); + + // + // Process the securities + // + signalProgress(0, s.m_listSecurities.count(), "Importing Statement ..."); + int progress = 0; + QValueList::const_iterator it_s = s.m_listSecurities.begin(); + while ( it_s != s.m_listSecurities.end() ) + { + processSecurityEntry(*it_s); + signalProgress(++progress, 0); + ++it_s; + } + signalProgress(-1, -1); + + // + // Process the transactions + // + + if ( !m_userAbort ) + { + try { + qDebug("Processing transactions (%s)", m_account.name().data()); + signalProgress(0, s.m_listTransactions.count(), "Importing Statement ..."); + int progress = 0; + QValueList::const_iterator it_t = s.m_listTransactions.begin(); + while ( it_t != s.m_listTransactions.end() ) + { + processTransactionEntry(*it_t); + signalProgress(++progress, 0); + ++it_t; + } + qDebug("Processing transactions done (%s)", m_account.name().data()); + + } catch(MyMoneyException* e) { + if(e->what() == "USERABORT") + m_userAbort = true; + else + qDebug("Caught exception from processTransactionEntry() not caused by USERABORT: %s", e->what().data()); + delete e; + } + signalProgress(-1, -1); + } + + // + // process price entries + // + if ( !m_userAbort ) + { + try { + signalProgress(0, s.m_listPrices.count(), "Importing Statement ..."); + QValueList slist = MyMoneyFile::instance()->securityList(); + QValueList::const_iterator it_s; + for(it_s = slist.begin(); it_s != slist.end(); ++it_s) { + d->securitiesBySymbol[(*it_s).tradingSymbol()] = *it_s; + d->securitiesByName[(*it_s).name()] = *it_s; + } + + int progress = 0; + QValueList::const_iterator it_p = s.m_listPrices.begin(); + while(it_p != s.m_listPrices.end()) { + processPriceEntry(*it_p); + signalProgress(++progress, 0); + ++it_p; + } + } catch(MyMoneyException* e) { + if(e->what() == "USERABORT") + m_userAbort = true; + else + qDebug("Caught exception from processPriceEntry() not caused by USERABORT: %s", e->what().data()); + delete e; + } + signalProgress(-1, -1); + } + + bool rc = false; + + // delete all payees created in vain + int payeeCount = d->payees.count(); + QValueList::const_iterator it_p; + for(it_p = d->payees.begin(); it_p != d->payees.end(); ++it_p) { + try { + MyMoneyFile::instance()->removePayee(*it_p); + --payeeCount; + } catch(MyMoneyException* e) { + // if we can't delete it, it must be in use which is ok for us + delete e; + } + } + + if(s.m_closingBalance.isAutoCalc()) { + messages += i18n(" Statement balance is not contained in statement."); + } else { + messages += i18n(" Statement balance on %1 is reported to be %2").arg(s.m_dateEnd.toString(Qt::ISODate)).arg(s.m_closingBalance.formatMoney("",2)); + } + messages += i18n(" Transactions"); + messages += i18n(" %1 processed").arg(d->transactionsCount); + messages += i18n(" %1 added").arg(d->transactionsAdded); + messages += i18n(" %1 matched").arg(d->transactionsMatched); + messages += i18n(" %1 duplicates").arg(d->transactionsDuplicate); + messages += i18n(" Payees"); + messages += i18n(" %1 created").arg(payeeCount); + messages += QString(); + + // 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(); + + rc = !m_userAbort; + + // finish the transaction + if(rc) + m_ft->commit(); + delete m_ft; + m_ft = 0; + + qDebug("Importing statement for '%s' done", m_account.name().data()); + + return rc; +} + +void MyMoneyStatementReader::processPriceEntry(const MyMoneyStatement::Price& p_in) +{ + if(d->securitiesBySymbol.contains(p_in.m_strSecurity)) { + + MyMoneyPrice price(d->securitiesBySymbol[p_in.m_strSecurity].id(), + MyMoneyFile::instance()->baseCurrency().id(), + p_in.m_date, + p_in.m_amount, "QIF"); + MyMoneyFile::instance()->addPrice(price); + + } else if(d->securitiesByName.contains(p_in.m_strSecurity)) { + + MyMoneyPrice price(d->securitiesByName[p_in.m_strSecurity].id(), + MyMoneyFile::instance()->baseCurrency().id(), + p_in.m_date, + p_in.m_amount, "QIF"); + MyMoneyFile::instance()->addPrice(price); + } + +} + +void MyMoneyStatementReader::processSecurityEntry(const MyMoneyStatement::Security& sec_in) +{ + // For a security entry, we will just make sure the security exists in the + // file. It will not get added to the investment account until it's called + // for in a transaction. + MyMoneyFile* file = MyMoneyFile::instance(); + + // check if we already have the security + // In a statement, we do not know what type of security this is, so we will + // not use type as a matching factor. + MyMoneySecurity security; + QValueList list = file->securityList(); + QValueList::ConstIterator it = list.begin(); + while ( it != list.end() && security.id().isEmpty() ) + { + if(sec_in.m_strSymbol.isEmpty()) { + if((*it).name() == sec_in.m_strName) + security = *it; + } else if((*it).tradingSymbol() == sec_in.m_strSymbol) + security = *it; + ++it; + } + + // if the security was not found, we have to create it while not forgetting + // to setup the type + if(security.id().isEmpty()) + { + security.setName(sec_in.m_strName); + security.setTradingSymbol(sec_in.m_strSymbol); + security.setSmallestAccountFraction(1000); + security.setTradingCurrency(file->baseCurrency().id()); + security.setValue("kmm-security-id", sec_in.m_strId); + security.setValue("kmm-online-source", "Yahoo"); + security.setSecurityType(MyMoneySecurity::SECURITY_STOCK); + MyMoneyFileTransaction ft; + try { + file->addSecurity(security); + ft.commit(); + kdDebug(0) << "Created " << security.name() << " with id " << security.id() << endl; + } catch(MyMoneyException *e) { + KMessageBox::error(0, i18n("Error creating security record: %1").arg(e->what()), i18n("Error")); + } + } else { + kdDebug(0) << "Found " << security.name() << " with id " << security.id() << endl; + } +} + +void MyMoneyStatementReader::processTransactionEntry(const MyMoneyStatement::Transaction& t_in) +{ + MyMoneyFile* file = MyMoneyFile::instance(); + + MyMoneyTransaction t; + +#if 0 + QString dbgMsg; + dbgMsg = QString("Process %1, '%3', %2").arg(t_in.m_datePosted.toString(Qt::ISODate)).arg(t_in.m_amount.formatMoney("", 2)).arg(t_in.m_strBankID); + qDebug("%s", dbgMsg.data()); +#endif + + // mark it imported for the view + t.setImported(); + + // TODO (Ace) We can get the commodity from the statement!! + // Although then we would need UI to verify + t.setCommodity(m_account.currencyId()); + + t.setPostDate(t_in.m_datePosted); + t.setMemo(t_in.m_strMemo); + +#if 0 + // (acejones) removing this code. keeping it around for reference. + // + // this is the OLD way of handling bank ID's, which unfortunately was wrong. + // bank ID's actually need to go on the split which corresponds with the + // account we're importing into. + // + // thus anywhere "this account" is put into a split is also where we need + // to put the bank ID in. + // + if ( ! t_in.m_strBankID.isEmpty() ) + t.setBankID(t_in.m_strBankID); +#endif + + MyMoneySplit s1; + + s1.setMemo(t_in.m_strMemo); + s1.setValue(t_in.m_amount - t_in.m_fees); + s1.setShares(s1.value()); + s1.setNumber(t_in.m_strNumber); + + // set these values if a transfer split is needed at the very end. + MyMoneyMoney transfervalue; + + // If the user has chosen to import into an investment account, determine the correct account to use + MyMoneyAccount thisaccount = m_account; + QString brokerageactid; + + if ( thisaccount.accountType() == MyMoneyAccount::Investment ) + { + // determine the brokerage account + brokerageactid = m_account.value("kmm-brokerage-account").utf8(); + if (brokerageactid.isEmpty() ) + { + brokerageactid = file->accountByName(m_account.brokerageName()).id(); + } + + // find the security transacted, UNLESS this transaction didn't + // involve any security. + if ( (t_in.m_eAction != MyMoneyStatement::Transaction::eaNone) + && (t_in.m_eAction != MyMoneyStatement::Transaction::eaInterest) + && (t_in.m_eAction != MyMoneyStatement::Transaction::eaFees)) + { + // the correct account is the stock account which matches two criteria: + // (1) it is a sub-account of the selected investment account, and + // (2a) the symbol of the underlying security matches the security of the + // transaction, or + // (2b) the name of the security matches the name of the security of the transaction. + + // search through each subordinate account + bool found = false; + 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 ); + if((t_in.m_strSymbol.lower() == security.tradingSymbol().lower()) + || (t_in.m_strSecurity.lower() == security.name().lower())) + { + thisaccount = file->account(*it_account); + found = true; + + // Don't update price if there is no price information contained in the transaction + if(t_in.m_eAction != MyMoneyStatement::Transaction::eaCashDividend + && t_in.m_eAction != MyMoneyStatement::Transaction::eaShrsin + && t_in.m_eAction != MyMoneyStatement::Transaction::eaShrsout) + { + // 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() && ((!t_in.m_amount.isZero() && !t_in.m_shares.isZero()) || !t_in.m_price.isZero())) + { + MyMoneyPrice newprice; + if(!t_in.m_price.isZero()) { + newprice = MyMoneyPrice( currencyid, basecurrencyid, t_in.m_datePosted, + t_in.m_price.abs(), i18n("Statement Importer") ); + } else { + newprice = MyMoneyPrice( currencyid, basecurrencyid, t_in.m_datePosted, + (t_in.m_amount / t_in.m_shares).abs(), i18n("Statement Importer") ); + } + file->addPrice(newprice); + } + } + } + + ++it_account; + } + + // If there was no stock account under the m_acccount investment account, + // add one using the security. + if (!found) + { + // The security should always be available, because the statement file + // should separately list all the securities referred to in the file, + // and when we found a security, we added it to the file. + + if ( t_in.m_strSecurity.isEmpty() ) + { + KMessageBox::information(0, i18n("This imported statement contains investment transactions with no security. These transactions will be ignored.").arg(t_in.m_strSecurity),i18n("Security not found"),QString("BlankSecurity")); + return; + } + else + { + MyMoneySecurity security; + QValueList list = MyMoneyFile::instance()->securityList(); + QValueList::ConstIterator it = list.begin(); + while ( it != list.end() && security.id().isEmpty() ) + { + if(t_in.m_strSecurity.lower() == (*it).tradingSymbol().lower() + || t_in.m_strSecurity.lower() == (*it).name().lower()) { + security = *it; + } + ++it; + } + if(!security.id().isEmpty()) + { + thisaccount = MyMoneyAccount(); + thisaccount.setName(security.name()); + thisaccount.setAccountType(MyMoneyAccount::Stock); + thisaccount.setCurrencyId(security.id()); + + file->addAccount(thisaccount, m_account); + kdDebug(0) << __func__ << ": created account " << thisaccount.id() << " for security " << t_in.m_strSecurity << " under account " << m_account.id() << endl; + } + // this security does not exist in the file. + else + { + // This should be rare. A statement should have a security entry for any + // of the securities referred to in the transactions. The only way to get + // here is if that's NOT the case. + KMessageBox::information(0, i18n("This investment account does not contain the \"%1\" security. Transactions involving this security will be ignored.").arg(t_in.m_strSecurity),i18n("Security not found"),QString("MissingSecurity%1").arg(t_in.m_strSecurity.stripWhiteSpace())); + return; + } + } + } + } + + s1.setAccountId(thisaccount.id()); + d->assignUniqueBankID(s1, t_in); + + if (t_in.m_eAction==MyMoneyStatement::Transaction::eaReinvestDividend) + { + s1.setAction(MyMoneySplit::ActionReinvestDividend); + s1.setShares(t_in.m_shares); + + if(!t_in.m_price.isZero()) { + s1.setPrice(t_in.m_price); + } else { + s1.setPrice(((t_in.m_amount - t_in.m_fees) / t_in.m_shares).convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision()))); + } + + + MyMoneySplit s2; + s2.setMemo(t_in.m_strMemo); + if(t_in.m_strInterestCategory.isEmpty()) + s2.setAccountId(d->interestId(thisaccount)); + else + s2.setAccountId(d->interestId(t_in.m_strInterestCategory)); + + s2.setShares(-t_in.m_amount - t_in.m_fees); + s2.setValue(s2.shares()); + t.addSplit(s2); + } + else if (t_in.m_eAction==MyMoneyStatement::Transaction::eaCashDividend) + { + // Cash dividends require setting 2 splits to get all of the information + // in. Split #1 will be the income split, and we'll set it to the first + // income account. This is a hack, but it's needed in order to get the + // amount into the transaction. + + // There are some sign issues. The OFX plugin universally reverses the sign + // for investment transactions. + // + // The way we interpret the sign on 'amount' is the s1 split, which is always + // the thing that's NOT the cash account. For dividends, it's the income + // category, for buy/sell it's the stock account. + // + // For cash account transactions, the s1 split IS the cash account split, + // which explains why they have to be reversed for investment transactions + // + // Ergo, the 'amount' is negative at this point and needs to stay negative. + // The 'fees' is positive. + // + // This should probably change. It would be more consistent to ALWAYS + // interpret the 'amount' as the cash account part. + + if(t_in.m_strInterestCategory.isEmpty()) + s1.setAccountId(d->interestId(thisaccount)); + else + s1.setAccountId(d->interestId(t_in.m_strInterestCategory)); + s1.setShares(t_in.m_amount); + s1.setValue(t_in.m_amount); + + // Split 2 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. + MyMoneySplit s2; + s2.setMemo(t_in.m_strMemo); + s2.setAction(MyMoneySplit::ActionDividend); + s2.setAccountId(thisaccount.id()); + t.addSplit(s2); + + transfervalue = -t_in.m_amount - t_in.m_fees; + } + else if (t_in.m_eAction==MyMoneyStatement::Transaction::eaInterest) + { + if(t_in.m_strInterestCategory.isEmpty()) + s1.setAccountId(d->interestId(thisaccount)); + else + s1.setAccountId(d->interestId(t_in.m_strInterestCategory)); + s1.setShares(t_in.m_amount); + s1.setValue(t_in.m_amount); + + transfervalue = -t_in.m_amount; + + } + else if (t_in.m_eAction==MyMoneyStatement::Transaction::eaFees) + { + if(t_in.m_strInterestCategory.isEmpty()) + s1.setAccountId(d->feeId(thisaccount)); + else + s1.setAccountId(d->feeId(t_in.m_strInterestCategory)); + s1.setShares(t_in.m_amount); + s1.setValue(t_in.m_amount); + + transfervalue = -t_in.m_amount; + + } + else if ((t_in.m_eAction==MyMoneyStatement::Transaction::eaBuy ) || + (t_in.m_eAction==MyMoneyStatement::Transaction::eaSell)) + { + if(!t_in.m_price.isZero()) { + s1.setPrice(t_in.m_price.abs()); + } else { + MyMoneyMoney total; + total = t_in.m_amount - t_in.m_fees; + if(!t_in.m_shares.isZero()) + s1.setPrice((total / t_in.m_shares).abs().convert(MyMoneyMoney::precToDenom(KMyMoneyGlobalSettings::pricePrecision()))); + } + + s1.setAction(MyMoneySplit::ActionBuyShares); + + // Make sure to setup the sign correctly + if(t_in.m_eAction==MyMoneyStatement::Transaction::eaBuy ) { + s1.setShares(t_in.m_shares.abs()); + s1.setValue(s1.value().abs()); + transfervalue = -(t_in.m_amount.abs()); + } else { + s1.setShares(-(t_in.m_shares.abs())); + s1.setValue(-(s1.value().abs())); + transfervalue = t_in.m_amount.abs(); + } + + } + else if ((t_in.m_eAction==MyMoneyStatement::Transaction::eaShrsin) || + (t_in.m_eAction==MyMoneyStatement::Transaction::eaShrsout)) + { + s1.setValue(MyMoneyMoney()); + s1.setShares(t_in.m_shares); + s1.setAction(MyMoneySplit::ActionAddShares); + } + else if (t_in.m_eAction==MyMoneyStatement::Transaction::eaNone) + { + // User is attempting to import a non-investment transaction into this + // investment account. This is not supportable the way KMyMoney is + // written. However, if a user has an associated brokerage account, + // we can stuff the transaction there. + + QString brokerageactid = m_account.value("kmm-brokerage-account").utf8(); + if (brokerageactid.isEmpty() ) + { + brokerageactid = file->accountByName(m_account.brokerageName()).id(); + } + if ( ! brokerageactid.isEmpty() ) + { + s1.setAccountId(brokerageactid); + d->assignUniqueBankID(s1, t_in); + + // Needed to satisfy the bankid check below. + thisaccount = file->account(brokerageactid); + } + else + { + // Warning!! Your transaction is being thrown away. + } + } + if ( !t_in.m_fees.isZero() ) + { + MyMoneySplit s; + s.setMemo(i18n("(Fees) ") + t_in.m_strMemo); + s.setValue(t_in.m_fees); + s.setShares(t_in.m_fees); + s.setAccountId(d->feeId(thisaccount)); + t.addSplit(s); + } + } + else + { + // For non-investment accounts, just use the selected account + // Note that it is perfectly reasonable to import an investment statement into a non-investment account + // if you really want. The investment-specific information, such as number of shares and action will + // be discarded in that case. + s1.setAccountId(m_account.id()); + d->assignUniqueBankID(s1, t_in); + } + + + QString payeename = t_in.m_strPayee; + if(!payeename.isEmpty()) + { + QString payeeid; + try { + QValueList pList = file->payeeList(); + QValueList::const_iterator it_p; + QMap matchMap; + for(it_p = pList.begin(); it_p != pList.end(); ++it_p) { + bool ignoreCase; + QStringList keys; + QStringList::const_iterator it_s; + switch((*it_p).matchData(ignoreCase, keys)) { + case MyMoneyPayee::matchDisabled: + break; + + case MyMoneyPayee::matchName: + keys << QString("%1").arg(QRegExp::escape((*it_p).name())); + // tricky fall through here + + case MyMoneyPayee::matchKey: + for(it_s = keys.begin(); it_s != keys.end(); ++it_s) { + QRegExp exp(*it_s, !ignoreCase); + if(exp.search(payeename) != -1) { + matchMap[exp.matchedLength()] = (*it_p).id(); + } + } + break; + } + } + + // at this point we can have several scenarios: + // a) multiple matches + // b) a single match + // c) no match at all + // + // for c) we just do nothing, for b) we take the one we found + // in case of a) we take the one with the largest matchedLength() + // which happens to be the last one in the map + if(matchMap.count() > 1) { + QMap::const_iterator it_m = matchMap.end(); + --it_m; + payeeid = *it_m; + } else if(matchMap.count() == 1) + payeeid = *(matchMap.begin()); + + // if we did not find a matching payee, we throw an exception and try to create it + if(payeeid.isEmpty()) + throw new MYMONEYEXCEPTION("payee not matched"); + + s1.setPayeeId(payeeid); + } + catch (MyMoneyException *e) + { + MyMoneyPayee payee; + int rc = KMessageBox::Yes; + + if(m_autoCreatePayee == false) { + // Ask the user if that is what he intended to do? + QString msg = i18n("Do you want to add \"%1\" as payee/receiver?\n\n").arg(payeename); + msg += i18n("Selecting \"Yes\" will create the payee, \"No\" will skip " + "creation of a payee record and remove the payee information " + "from this transaction. Selecting \"Cancel\" aborts the import " + "operation.\n\nIf you select \"No\" here and mark the \"Don't ask " + "again\" checkbox, the payee information for all following transactions " + "referencing \"%1\" will be removed.").arg(payeename); + + QString askKey = QString("Statement-Import-Payee-")+payeename; + if(!m_dontAskAgain.contains(askKey)) { + m_dontAskAgain += askKey; + } + rc = KMessageBox::questionYesNoCancel(0, msg, i18n("New payee/receiver"), + KStdGuiItem::yes(), KStdGuiItem::no(), askKey); + } + delete e; + + if(rc == KMessageBox::Yes) { + // for now, we just add the payee to the pool and turn + // on simple name matching, so that future transactions + // with the same name don't get here again. + // + // In the future, we could open a dialog and ask for + // all the other attributes of the payee, but since this + // is called in the context of an automatic procedure it + // might distract the user. + payee.setName(payeename); + payee.setMatchData(MyMoneyPayee::matchName, true, QStringList()); + if (m_askPayeeCategory) { + // We use a QGuardedPtr because the dialog may get deleted + // during exec() if the parent of the dialog gets deleted. + // In that case the guarded ptr will reset to 0. + QGuardedPtr dialog = new KDialogBase( + "Default Category for Payee", + KDialogBase::Yes | KDialogBase::No | KDialogBase::Cancel, + KDialogBase::Yes, KDialogBase::Cancel, + 0, "questionYesNoCancel", true, true, + KGuiItem(i18n("Save Category")), + KGuiItem(i18n("No Category")), + KGuiItem(i18n("Abort"))); + QVBox *topcontents = new QVBox (dialog); + topcontents->setSpacing(KDialog::spacingHint()*2); + topcontents->setMargin(KDialog::marginHint()); + + //add in caption? and account combo here + QLabel *label1 = new QLabel( topcontents); + label1->setText(i18n("Please select a default category for payee '%1':").arg(payee.name().data())); + + QGuardedPtr accountCombo = new KMyMoneyAccountCombo(topcontents); + dialog->setMainWidget(topcontents); + + int result = dialog->exec(); + + QString accountId; + if (accountCombo && !accountCombo->selectedAccounts().isEmpty()) { + accountId = accountCombo->selectedAccounts().front(); + } + if (dialog) { + delete dialog; + } + //if they hit yes instead of no, then grab setting of account combo + if (result == KDialogBase::Yes) { + payee.setDefaultAccountId(accountId); + } + else if (result != KDialogBase::No) { + //add cancel button? and throw exception like below + throw new MYMONEYEXCEPTION("USERABORT"); + } + } + + try { + file->addPayee(payee); + qDebug("Payee '%s' created", payee.name().data()); + d->payees << payee; + payeeid = payee.id(); + s1.setPayeeId(payeeid); + + } catch(MyMoneyException *e) { + KMessageBox::detailedSorry(0, i18n("Unable to add payee/receiver"), + (e->what() + " " + i18n("thrown in") + " " + e->file()+ ":%1").arg(e->line())); + delete e; + + } + + } else if(rc == KMessageBox::No) { + s1.setPayeeId(QString()); + + } else { + throw new MYMONEYEXCEPTION("USERABORT"); + + } + } + + if(thisaccount.accountType() != MyMoneyAccount::Stock ) { + // + // Fill in other side of the transaction (category/etc) based on payee + // + // Note, this logic is lifted from KLedgerView::slotPayeeChanged(), + // however this case is more complicated, because we have an amount and + // a memo. We just don't have the other side of the transaction. + // + // We'll search for the most recent transaction in this account with + // this payee. If this reference transaction is a simple 2-split + // transaction, it's simple. If it's a complex split, and the amounts + // are different, we have a problem. Somehow we have to balance the + // transaction. For now, we'll leave it unbalanced, and let the user + // handle it. + // + const MyMoneyPayee& payeeObj = MyMoneyFile::instance()->payee(payeeid); + if (t_in.m_listSplits.isEmpty() && payeeObj.defaultAccountEnabled()) { + MyMoneySplit s; + s.setReconcileFlag(MyMoneySplit::Cleared); + s.clearId(); + s.setBankID(QString()); + s.setShares(-s1.shares()); + s.setValue(-s1.value()); + s.setAccountId(payeeObj.defaultAccountId()); + t.addSplit(s); + } + else if (t_in.m_listSplits.isEmpty() && !d->m_skipCategoryMatching) { + MyMoneyTransactionFilter filter(thisaccount.id()); + filter.addPayee(payeeid); + QValueList list = file->transactionList(filter); + if(!list.empty()) + { + // Default to using the most recent transaction as the reference + MyMoneyTransaction t_old = list.last(); + + // if there is more than one matching transaction, try to be a little + // smart about which one we take. for now, we'll see if there's one + // with the same VALUE as our imported transaction, and if so take that one. + if ( list.count() > 1 ) + { + QValueList::ConstIterator it_trans = list.fromLast(); + while ( it_trans != list.end() ) + { + MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id()); + if ( s.value() == s1.value() ) + { + t_old = *it_trans; + break; + } + --it_trans; + } + } + + QValueList::ConstIterator it_split; + for(it_split = t_old.splits().begin(); it_split != t_old.splits().end(); ++it_split) + { + // We don't need the split that covers this account, + // we just need the other ones. + if ( (*it_split).accountId() != thisaccount.id() ) + { + MyMoneySplit s(*it_split); + s.setReconcileFlag(MyMoneySplit::NotReconciled); + s.clearId(); + s.setBankID(QString()); + + if ( t_old.splits().count() == 2 ) + { + s.setShares(-s1.shares()); + s.setValue(-s1.value()); + s.setMemo(s1.memo()); + } + t.addSplit(s); + } + } + } + } + } + } + + s1.setReconcileFlag(t_in.m_reconcile); + t.addSplit(s1); + + // Add the 'account' split if it's needed + if ( ! transfervalue.isZero() ) + { + // in case the transaction has a reference to the brokerage account, we use it + if(!t_in.m_strBrokerageAccount.isEmpty()) { + brokerageactid = file->accountByName(t_in.m_strBrokerageAccount).id(); + } + + if ( !brokerageactid.isEmpty() ) + { + // FIXME This may not deal with foreign currencies properly + MyMoneySplit s; + s.setMemo(t_in.m_strMemo); + s.setValue(transfervalue); + s.setShares(transfervalue); + s.setAccountId(brokerageactid); + s.setReconcileFlag(t_in.m_reconcile); + t.addSplit(s); + } + } + + if ((t_in.m_eAction != MyMoneyStatement::Transaction::eaReinvestDividend) && (t_in.m_eAction!=MyMoneyStatement::Transaction::eaCashDividend) + ) + { + //****************************************** + // process splits + //****************************************** + + QValueList::const_iterator it_s; + for(it_s = t_in.m_listSplits.begin(); it_s != t_in.m_listSplits.end(); ++it_s) { + MyMoneySplit s2; + s2.setAccountId((*it_s).m_accountId); + MyMoneyAccount acc = file->account(s2.accountId()); + if(acc.isAssetLiability()) { + s2.setPayeeId(s1.payeeId()); + } + s2.setMemo((*it_s).m_strMemo); + s2.setShares((*it_s).m_amount); + s2.setValue((*it_s).m_amount); + s2.setReconcileFlag((*it_s).m_reconcile); + t.addSplit(s2); + } + +#if 0 + QString accountId; + int count; + int cnt = 0; + count = t_in.m_listSplits.count(); + + for(cnt = 0; cnt < count; ++cnt ) + { + MyMoneySplit s2 = s1; + s2.setMemo(t_in.m_listSplits[cnt].m_strMemo); + s2.clearId(); + s2.setValue(t_in.m_listSplits[cnt].m_amount); + s2.setShares(t_in.m_listSplits[cnt].m_amount); + s2.setAccountId(QString(t_in.m_listSplits[cnt].m_accountId)); +#if 0 + accountId = file->nameToAccount(t_in.m_listSplits[cnt].m_strCategoryName); + if (accountId.isEmpty()) + accountId = checkCategory(t_in.m_listSplits[cnt].m_strCategoryName, t_in.m_listSplits[0].m_amount, t_in.m_listSplits[cnt].m_amount); + + s2.setAccountId(accountId); +#endif + t.addSplit(s2); + } +#endif + } + + // Add the transaction + try { + + // check for matches already stored in the engine + MyMoneySplit matchedSplit; + TransactionMatcher::autoMatchResultE result; + TransactionMatcher matcher(thisaccount); + matcher.setMatchWindow(KMyMoneyGlobalSettings::matchInterval()); + const MyMoneyObject *o = matcher.findMatch(t, s1, matchedSplit, result); + d->transactionsCount++; + + // if we did not already find this one, we need to process it + if(result != TransactionMatcher::matchedDuplicate) { + d->transactionsAdded++; + file->addTransaction(t); + + if(o) { + if(typeid(*o) == typeid(MyMoneyTransaction)) { + // it matched a simple transaction. that's the easy case + MyMoneyTransaction tm(*(dynamic_cast(o))); + switch(result) { + case TransactionMatcher::notMatched: + case TransactionMatcher::matchedDuplicate: + // no need to do anything here + break; + case TransactionMatcher::matched: + case TransactionMatcher::matchedExact: + qDebug("Detected as match to transaction '%s'", tm.id().data()); + matcher.match(tm, matchedSplit, t, s1, true); + d->transactionsMatched++; + break; + } + + } else if(typeid(*o) == typeid(MyMoneySchedule)) { + // a match has been found in a pending schedule. We'll ask the user if she wants + // to enter the schedule and match it agains the new transaction. Otherwise, we + // just leave the transaction as imported. + MyMoneySchedule schedule(*(dynamic_cast(o))); + if(KMessageBox::questionYesNo(0, QString("%1").arg(i18n("KMyMoney has found a scheduled transaction named %1 which matches an imported transaction. Do you want KMyMoney to enter this schedule now so that the transaction can be matched? ").arg(schedule.name())), i18n("Schedule found")) == KMessageBox::Yes) { + KEnterScheduleDlg dlg(0, schedule); + TransactionEditor* editor = dlg.startEdit(); + if(editor) { + MyMoneyTransaction torig; + // in case the amounts of the scheduled transaction and the + // imported transaction differ, we need to update the amount + // using the transaction editor. + if(matchedSplit.shares() != s1.shares() && !schedule.isFixed()) { + // for now this only works with regular transactions and not + // for investment transactions. As of this, we don't have + // scheduled investment transactions anyway. + StdTransactionEditor* se = dynamic_cast(editor); + if(se) { + // the following call will update the amount field in the + // editor and also adjust a possible VAT assignment. Make + // sure to use only the absolute value of the amount, because + // the editor keeps the sign in a different position (deposit, + // withdrawal tab) + kMyMoneyEdit* amount = dynamic_cast(se->haveWidget("amount")); + if(amount) { + amount->setValue(s1.shares().abs()); + se->slotUpdateAmount(s1.shares().abs().toString()); + + // we also need to update the matchedSplit variable to + // have the modified share/value. + matchedSplit.setShares(s1.shares()); + matchedSplit.setValue(s1.value()); + } + } + } + + editor->createTransaction(torig, dlg.transaction(), dlg.transaction().splits()[0], true); + QString newId; + if(editor->enterTransactions(newId, false, true)) { + if(!newId.isEmpty()) { + torig = MyMoneyFile::instance()->transaction(newId); + schedule.setLastPayment(torig.postDate()); + } + schedule.setNextDueDate(schedule.nextPayment(schedule.nextDueDate())); + MyMoneyFile::instance()->modifySchedule(schedule); + } + + // now match the two transactions + matcher.match(torig, matchedSplit, t, s1); + d->transactionsMatched++; + } + delete editor; + } + } + } + } else { + d->transactionsDuplicate++; + qDebug("Detected as duplicate"); + } + delete o; + } catch (MyMoneyException *e) { + QString message(i18n("Problem adding or matching imported transaction with id '%1': %2").arg(t_in.m_strBankID).arg(e->what())); + qDebug("%s", message.data()); + delete e; + + int result = KMessageBox::warningContinueCancel(0, message); + if ( result == KMessageBox::Cancel ) + throw new MYMONEYEXCEPTION("USERABORT"); + } +} + +bool MyMoneyStatementReader::selectOrCreateAccount(const SelectCreateMode /*mode*/, MyMoneyAccount& account) +{ + bool result = false; + + MyMoneyFile* file = MyMoneyFile::instance(); + + QString accountId; + + // Try to find an existing account in the engine which matches this one. + // There are two ways to be a "matching account". The account number can + // match the statement account OR the "StatementKey" property can match. + // Either way, we'll update the "StatementKey" property for next time. + + QString accountNumber = account.number(); + if ( ! accountNumber.isEmpty() ) + { + // Get a list of all accounts + QValueList accounts; + file->accountList(accounts); + + // Iterate through them + QValueList::const_iterator it_account = accounts.begin(); + while ( it_account != accounts.end() ) + { + if ( + ( (*it_account).value("StatementKey") == accountNumber ) || + ( (*it_account).number() == accountNumber ) + ) + { + MyMoneyAccount newAccount((*it_account).id(), account); + account = newAccount; + accountId = (*it_account).id(); + break; + } + + ++it_account; + } + } + + QString msg = i18n("You have downloaded a statement for the following account:

"); + msg += i18n(" - Account Name: %1").arg(account.name()) + "
"; + msg += i18n(" - Account Type: %1").arg(KMyMoneyUtils::accountTypeToString(account.accountType())) + "
"; + msg += i18n(" - Account Number: %1").arg(account.number()) + "
"; + msg += "
"; + + QString header; + + if(!account.name().isEmpty()) + { + if(!accountId.isEmpty()) + msg += i18n("Do you want to import transactions to this account?"); + else + msg += i18n("KMyMoney cannot determine which of your accounts to use. You can " + "create a new account by pressing the Create button " + "or select another one manually from the selection box below."); + } + else + { + msg += i18n("No account information has been found in the selected statement file. " + "Please select an account using the selection box in the dialog or " + "create a new account by pressing the Create button."); + } + + KMyMoneyUtils::categoryTypeE type = static_cast(KMyMoneyUtils::asset|KMyMoneyUtils::liability); + KAccountSelectDlg accountSelect(type, "StatementImport", kmymoney2); + accountSelect.setHeader(i18n("Import transactions")); + accountSelect.setDescription(msg); + accountSelect.setAccount(account, accountId); + accountSelect.setMode(false); + accountSelect.showAbortButton(true); + accountSelect.m_qifEntry->hide(); + QString accname; + bool done = false; + while ( !done ) + { + if ( accountSelect.exec() == QDialog::Accepted && !accountSelect.selectedAccount().isEmpty() ) + { + result = true; + done = true; + accountId = accountSelect.selectedAccount(); + account = file->account(accountId); + if ( ! accountNumber.isEmpty() && account.value("StatementKey") != accountNumber ) + { + account.setValue("StatementKey", accountNumber); + MyMoneyFileTransaction ft; + try { + MyMoneyFile::instance()->modifyAccount(account); + ft.commit(); + accname = account.name(); + } catch(MyMoneyException* e) { + qDebug("Updating account in MyMoneyStatementReader::selectOrCreateAccount failed"); + delete e; + } + } + } + else + { + if(accountSelect.aborted()) + //throw new MYMONEYEXCEPTION("USERABORT"); + done = true; + else + KMessageBox::error(0, QString("%1").arg(i18n("You must select an account, create a new one, or press the Abort button."))); + } + } + return result; +} + +void MyMoneyStatementReader::setProgressCallback(void(*callback)(int, int, const QString&)) +{ + m_progressCallback = callback; +} + +void MyMoneyStatementReader::signalProgress(int current, int total, const QString& msg) +{ + if(m_progressCallback != 0) + (*m_progressCallback)(current, total, msg); +} + + +#include "mymoneystatementreader.moc" +// vim:cin:si:ai:et:ts=2:sw=2: diff --git a/kmymoney2/converter/mymoneystatementreader.h b/kmymoney2/converter/mymoneystatementreader.h new file mode 100644 index 0000000..46d74d7 --- /dev/null +++ b/kmymoney2/converter/mymoneystatementreader.h @@ -0,0 +1,151 @@ +/*************************************************************************** + mymoneystatementreader + ------------------- + begin : Mon Aug 30 2004 + copyright : (C) 2000-2004 by Michael Edwardes + email : mte@users.sourceforge.net + Javier Campos Morales + Felix Rodriguez + John C + Thomas Baumgart + Kevin Tambascio + Ace Jones + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef MYMONEYSTATEMENTREADER_H +#define MYMONEYSTATEMENTREADER_H + +// ---------------------------------------------------------------------------- +// QT Headers + +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include +#include + +// ---------------------------------------------------------------------------- +// Project Headers + +#include "mymoneyqifprofile.h" +#include "../mymoney/mymoneyaccount.h" +#include "../mymoney/mymoneystatement.h" + +class MyMoneyFileTransaction; +class QStringList; + +/** + * This is a pared-down version of a MyMoneyQifReader object + * + * @author Ace Jones + */ +class MyMoneyStatementReader : public QObject +{ + Q_OBJECT + +public: + MyMoneyStatementReader(); + ~MyMoneyStatementReader(); + + /** + * This method imports data from the MyMoneyStatement object @a s + * into the MyMoney engine. It leaves some statistical information + * in the @a messages string list + * + * @retval true the import was processed successfully + * @retval false the import resulted in a failure. + */ + bool import(const MyMoneyStatement& s, QStringList& messages); + + /** + * This method is used to modify the auto payee creation flag. + * If this flag is set, records for payees that are not currently + * found in the engine will be automatically created with no + * further user interaction required. If this flag is no set, + * the user will be asked if the payee should be created or not. + * If the MyMoneyQifReader object is created auto payee creation + * is turned off. + * + * @param create flag if this feature should be turned on (@p true) + * or turned off (@p false) + */ + void setAutoCreatePayee(bool create); + void setAskPayeeCategory(bool ask); + + const MyMoneyAccount& account() const { return m_account; }; + + void setProgressCallback(void(*callback)(int, int, const QString&)); + + /** + * Returns true in case any transaction has been added to the engine + * during the import of the statement. Only returns useful result + * after import() has been called. + */ + bool anyTransactionAdded(void) const; + +private: + /** + * This method is used to update the progress information. It + * checks if an appropriate function is known and calls it. + * + * For a parameter description see KMyMoneyView::progressCallback(). + */ + void signalProgress(int current, int total, const QString& = ""); + + void processTransactionEntry(const MyMoneyStatement::Transaction& t_in); + void processSecurityEntry(const MyMoneyStatement::Security& s_in); + void processPriceEntry(const MyMoneyStatement::Price& p_in); + + enum SelectCreateMode { + Create = 0, + Select + }; + /** + * This method is used to find an account using the account's name + * stored in @p account in the current MyMoneyFile object. If it does not + * exist, the user has the chance to create it or to skip processing + * of this account. + * + * Please see the documentation for this function in MyMoneyQifReader + * + * @param mode Is either Create or Select depending on the above table + * @param account Reference to MyMoneyAccount object + */ + bool selectOrCreateAccount(const SelectCreateMode mode, MyMoneyAccount& account); + +signals: + /** + * This signal will be emitted when the import is finished. + */ + void importFinished(void); + +private: + /// \internal d-pointer class. + class Private; + /// \internal d-pointer instance. + Private* const d; + MyMoneyAccount m_account; + QStringList m_dontAskAgain; + bool m_skipAccount; + bool m_userAbort; + bool m_autoCreatePayee; + bool m_askPayeeCategory; + MyMoneyFileTransaction* m_ft; + + void (*m_progressCallback)(int, int, const QString&); +}; + +#endif diff --git a/kmymoney2/converter/mymoneytemplate.cpp b/kmymoney2/converter/mymoneytemplate.cpp new file mode 100644 index 0000000..63305c6 --- /dev/null +++ b/kmymoney2/converter/mymoneytemplate.cpp @@ -0,0 +1,420 @@ +/*************************************************************************** + mymoneytemplate.cpp - description + ------------------- + begin : Sat Aug 14 2004 + copyright : (C) 2004 by Thomas Baumgart + email : ipwizard@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 "kdecompat.h" + +// ---------------------------------------------------------------------------- +// QT Includes + +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Includes + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Includes + +#include "mymoneytemplate.h" + +MyMoneyTemplate::MyMoneyTemplate() : + m_progressCallback(0) +{ +} + +MyMoneyTemplate::MyMoneyTemplate(const KURL& url) : + m_progressCallback(0) +{ + loadTemplate(url); +} + +MyMoneyTemplate::~MyMoneyTemplate() +{ +} + +bool MyMoneyTemplate::loadTemplate(const KURL& url) +{ + QString filename; + + if(!url.isValid()) { + qDebug("Invalid template URL '%s'", url.url().latin1()); + return false; + } + + m_source = url; + if(url.isLocalFile()) { + filename = url.path(); + + } else { + bool rc; + rc = KIO::NetAccess::download(url, filename, qApp->mainWidget()); + if(!rc) { + KMessageBox::detailedError(qApp->mainWidget(), + i18n("Error while loading file '%1'!").arg(url.url()), + KIO::NetAccess::lastErrorString(), + i18n("File access error")); + return false; + } + } + + bool rc = true; + QFile file(filename); + QFileInfo info(file); + if(!info.isFile()) { + QString msg=i18n("%1 is not a template file.").arg(filename); + KMessageBox::error(qApp->mainWidget(), QString("

")+msg, i18n("Filetype Error")); + return false; + } + + if(file.open(IO_ReadOnly)) { + QString errMsg; + int errLine, errColumn; + if(!m_doc.setContent(&file, &errMsg, &errLine, &errColumn)) { + QString msg=i18n("Error while reading template file %1 in line %2, column %3").arg(filename).arg(errLine).arg(errColumn); + KMessageBox::detailedError(qApp->mainWidget(), QString("

")+msg, errMsg, i18n("Template Error")); + rc = false; + } else { + rc = loadDescription(); + } + file.close(); + } else { + KMessageBox::sorry(qApp->mainWidget(), i18n("File '%1' not found!").arg(filename)); + rc = false; + } + + // 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(filename); + return rc; +} + +bool MyMoneyTemplate::loadDescription(void) +{ + int validMask = 0x00; + const int validAccount = 0x01; + const int validTitle = 0x02; + const int validShort = 0x04; + const int validLong = 0x08; + const int invalid = 0x10; + const int validHeader = 0x0F; + + QDomElement rootElement = m_doc.documentElement(); + if(!rootElement.isNull() + && rootElement.tagName() == "kmymoney-account-template") { + QDomNode child = rootElement.firstChild(); + while(!child.isNull() && child.isElement()) { + QDomElement childElement = child.toElement(); + // qDebug("MyMoneyTemplate::import: Processing child node %s", childElement.tagName().data()); + if(childElement.tagName() == "accounts") { + m_accounts = childElement.firstChild(); + validMask |= validAccount; + } else if(childElement.tagName() == "title") { + m_title = childElement.text(); + validMask |= validTitle; + } else if(childElement.tagName() == "shortdesc") { + m_shortDesc = childElement.text(); + validMask |= validShort; + } else if(childElement.tagName() == "longdesc") { + m_longDesc = childElement.text(); + validMask |= validLong; + } else { + KMessageBox::error(qApp->mainWidget(), QString("

")+i18n("Invalid tag %1 in template file %2!").arg(childElement.tagName()).arg(m_source.prettyURL())); + validMask |= invalid; + } + child = child.nextSibling(); + } + } + return validMask == validHeader; +} + +bool MyMoneyTemplate::hierarchy(QMap& list, const QString& parent, QDomNode account) +{ + bool rc = true; + while(rc == true && !account.isNull()) { + if(account.isElement()) { + QDomElement accountElement = account.toElement(); + if(accountElement.tagName() == "account") { + QString name = QString("%1:%2").arg(parent).arg(accountElement.attribute("name")); + list[name] = 0; + hierarchy(list, name, account.firstChild()); + } + } + account = account.nextSibling(); + } + return rc; +} + +void MyMoneyTemplate::hierarchy(QMap& list) +{ + bool rc = !m_accounts.isNull(); + QDomNode accounts = m_accounts; + while(rc == true && !accounts.isNull() && accounts.isElement()) { + QDomElement childElement = accounts.toElement(); + if(childElement.tagName() == "account" + && childElement.attribute("name") == "") { + switch(childElement.attribute("type").toUInt()) { + case MyMoneyAccount::Asset: + list[i18n("Asset")] = 0; + rc = hierarchy(list, i18n("Asset"), childElement.firstChild()); + break; + case MyMoneyAccount::Liability: + list[i18n("Liability")] = 0; + rc = hierarchy(list, i18n("Liability"), childElement.firstChild()); + break; + case MyMoneyAccount::Income: + list[i18n("Income")] = 0; + rc = hierarchy(list, i18n("Income"), childElement.firstChild()); + break; + case MyMoneyAccount::Expense: + list[i18n("Expense")] = 0; + rc = hierarchy(list, i18n("Expense"), childElement.firstChild()); + break; + case MyMoneyAccount::Equity: + list[i18n("Equity")] = 0; + rc = hierarchy(list, i18n("Equity"), childElement.firstChild()); + break; + + default: + rc = false; + break; + } + } else { + rc = false; + } + accounts = accounts.nextSibling(); + } +} + +bool MyMoneyTemplate::importTemplate(void(*callback)(int, int, const QString&)) +{ + m_progressCallback = callback; + bool rc = !m_accounts.isNull(); + MyMoneyFile* file = MyMoneyFile::instance(); + signalProgress(0, m_doc.elementsByTagName("account").count(), i18n("Loading template %1").arg(m_source.url())); + m_accountsRead = 0; + + while(rc == true && !m_accounts.isNull() && m_accounts.isElement()) { + QDomElement childElement = m_accounts.toElement(); + if(childElement.tagName() == "account" + && childElement.attribute("name") == "") { + ++m_accountsRead; + MyMoneyAccount parent; + switch(childElement.attribute("type").toUInt()) { + case MyMoneyAccount::Asset: + parent = file->asset(); + break; + case MyMoneyAccount::Liability: + parent = file->liability(); + break; + case MyMoneyAccount::Income: + parent = file->income(); + break; + case MyMoneyAccount::Expense: + parent = file->expense(); + break; + case MyMoneyAccount::Equity: + parent = file->equity(); + break; + + default: + KMessageBox::error(qApp->mainWidget(), QString("

")+i18n("Invalid top-level account type %1 in template file %2!").arg(childElement.attribute("type")).arg(m_source.prettyURL())); + rc = false; + } + + if(rc == true) { + rc = createAccounts(parent, childElement.firstChild()); + } + } else { + rc = false; + } + m_accounts = m_accounts.nextSibling(); + } + signalProgress(-1, -1); + return rc; +} + +bool MyMoneyTemplate::createAccounts(MyMoneyAccount& parent, QDomNode account) +{ + bool rc = true; + while(rc == true && !account.isNull()) { + MyMoneyAccount acc; + if(account.isElement()) { + QDomElement accountElement = account.toElement(); + if(accountElement.tagName() == "account") { + signalProgress(++m_accountsRead, 0); + QValueList subAccountList; + QValueList::ConstIterator it; + it = subAccountList.end(); + if(!parent.accountList().isEmpty()) { + MyMoneyFile::instance()->accountList(subAccountList, parent.accountList()); + for(it = subAccountList.begin(); it != subAccountList.end(); ++it) { + if((*it).name() == accountElement.attribute("name")) { + acc = *it; + break; + } + } + } + if(it == subAccountList.end()) { + // not found, we need to create it + acc.setName(accountElement.attribute("name")); + acc.setAccountType(static_cast(accountElement.attribute("type").toUInt())); + setFlags(acc, account.firstChild()); + try { + MyMoneyFile::instance()->addAccount(acc, parent); + } catch(MyMoneyException *e) { + delete e; + } + } + createAccounts(acc, account.firstChild()); + } + } + account = account.nextSibling(); + } + return rc; +} + +bool MyMoneyTemplate::setFlags(MyMoneyAccount& acc, QDomNode flags) +{ + bool rc = true; + while(rc == true && !flags.isNull()) { + if(flags.isElement()) { + QDomElement flagElement = flags.toElement(); + if(flagElement.tagName() == "flag") { + // make sure, we only store flags we know! + QString value = flagElement.attribute("name"); + if(value == "Tax") { + acc.setValue(value.latin1(), "Yes"); + } else { + KMessageBox::error(qApp->mainWidget(), QString("

")+i18n("Invalid flag type %1 for account %3 in template file %2!").arg(flagElement.attribute("name")).arg(m_source.prettyURL()).arg(acc.name())); + rc = false; + } + } + } + flags = flags.nextSibling(); + } + return rc; +} + +void MyMoneyTemplate::signalProgress(int current, int total, const QString& msg) +{ + if(m_progressCallback != 0) + (*m_progressCallback)(current, total, msg); +} + +bool MyMoneyTemplate::exportTemplate(void(*callback)(int, int, const QString&)) +{ + m_progressCallback = callback; + + m_doc = QDomDocument("KMYMONEY-TEMPLATE"); + + QDomProcessingInstruction instruct = m_doc.createProcessingInstruction(QString("xml"), QString("version=\"1.0\" encoding=\"utf-8\"")); + m_doc.appendChild(instruct); + + QDomElement mainElement = m_doc.createElement("kmymoney-account-template"); + m_doc.appendChild(mainElement); + + QDomElement title = m_doc.createElement("title"); + mainElement.appendChild(title); + + QDomElement shortDesc = m_doc.createElement("shortdesc"); + mainElement.appendChild(shortDesc); + + QDomElement longDesc = m_doc.createElement("longdesc"); + mainElement.appendChild(longDesc); + + QDomElement accounts = m_doc.createElement("accounts"); + mainElement.appendChild(accounts); + + // addAccountStructure(accounts, MyMoneyFile::instance()->asset()); + // addAccountStructure(accounts, MyMoneyFile::instance()->liability()); + addAccountStructure(accounts, MyMoneyFile::instance()->income()); + addAccountStructure(accounts, MyMoneyFile::instance()->expense()); + // addAccountStructure(accounts, MyMoneyFile::instance()->equity()); + + return true; +} + +bool MyMoneyTemplate::addAccountStructure(QDomElement& parent, const MyMoneyAccount& acc) +{ + QDomElement account = m_doc.createElement("account"); + parent.appendChild(account); + + if(MyMoneyFile::instance()->isStandardAccount(acc.id())) + account.setAttribute(QString("name"), QString()); + else + account.setAttribute(QString("name"), acc.name()); + account.setAttribute(QString("type"), acc.accountType()); + + // FIXME: add tax flag stuff + + // any child accounts? + if(acc.accountList().count() > 0) { + QValueList list; + MyMoneyFile::instance()->accountList(list, acc.accountList(), false); + QValueList::Iterator it; + for(it = list.begin(); it != list.end(); ++it) { + addAccountStructure(account, *it); + } + } + return true; +} + +bool MyMoneyTemplate::saveTemplate(const KURL& url) +{ + QString filename; + + if(!url.isValid()) { + qDebug("Invalid template URL '%s'", url.url().latin1()); + return false; + } + + if(url.isLocalFile()) { + filename = url.path(); + KSaveFile qfile(filename, 0600); + if(qfile.status() == 0) { + saveToLocalFile(qfile.file()); + if(!qfile.close()) { + throw new MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'").arg(filename)); + } + } else { + throw new MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'").arg(filename)); + } + } else { + KTempFile tmpfile; + saveToLocalFile(tmpfile.file()); + if(!KIO::NetAccess::upload(tmpfile.name(), url, NULL)) + throw new MYMONEYEXCEPTION(i18n("Unable to upload to '%1'").arg(url.url())); + tmpfile.unlink(); + } + return true; +} + +bool MyMoneyTemplate::saveToLocalFile(QFile* qfile) +{ + QTextStream stream(qfile); + stream.setEncoding(QTextStream::UnicodeUTF8); + stream << m_doc.toString(); + + return true; +} diff --git a/kmymoney2/converter/mymoneytemplate.h b/kmymoney2/converter/mymoneytemplate.h new file mode 100644 index 0000000..5c96b1f --- /dev/null +++ b/kmymoney2/converter/mymoneytemplate.h @@ -0,0 +1,94 @@ +/*************************************************************************** + mymoneytemplate.h - description + ------------------- + begin : Sat Aug 14 2004 + copyright : (C) 2004 by Thomas Baumgart + email : ipwizard@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. * + * * + ***************************************************************************/ + +#ifndef MYMONEYTEMPLATE_H +#define MYMONEYTEMPLATE_H + +// ---------------------------------------------------------------------------- +// QT Includes + +#include +class QFile; +class QListViewItem; + +// ---------------------------------------------------------------------------- +// KDE Includes + +#include + +// ---------------------------------------------------------------------------- +// Project Includes + +#include +#include + +/** + * @author Thomas Baumgart + */ + +/** + * This class represents an account template handler. It is capable + * to read an XML formatted account template file and import it into + * the current engine. Also, it can save the current account structure + * of the engine to an XML formatted template file. + */ +class MyMoneyTemplate +{ +public: + MyMoneyTemplate(); + MyMoneyTemplate(const KURL& url); + ~MyMoneyTemplate(); + + bool loadTemplate(const KURL& url); + bool saveTemplate(const KURL& url); + bool importTemplate(void(*callback)(int, int, const QString&)); + bool exportTemplate(void(*callback)(int, int, const QString&)); + + const QString& title(void) const { return m_title; } + const QString& shortDescription(void) const { return m_shortDesc; } + const QString& longDescription(void) const { return m_longDesc; } + + void hierarchy(QMap& list); + +protected: + bool loadDescription(void); + bool createAccounts(MyMoneyAccount& parent, QDomNode account); + bool setFlags(MyMoneyAccount& acc, QDomNode flags); + bool saveToLocalFile(QFile* qfile); + bool addAccountStructure(QDomElement& parent, const MyMoneyAccount& acc); + bool hierarchy(QMap& list, const QString& parent, QDomNode account); + + /** + * This method is used to update the progress information. It + * checks if an appropriate function is known and calls it. + * + * For a parameter description see KMyMoneyView::progressCallback(). + */ + void signalProgress(int current, int total, const QString& = ""); + +private: + QDomDocument m_doc; + QDomNode m_accounts; + QString m_title; + QString m_shortDesc; + QString m_longDesc; + KURL m_source; + void (*m_progressCallback)(int, int, const QString&); + int m_accountsRead; +}; + +#endif diff --git a/kmymoney2/converter/webpricequote.cpp b/kmymoney2/converter/webpricequote.cpp new file mode 100644 index 0000000..de30963 --- /dev/null +++ b/kmymoney2/converter/webpricequote.cpp @@ -0,0 +1,1050 @@ +/*************************************************************************** + webpricequote.cpp + ------------------- + begin : Thu Dec 30 2004 + copyright : (C) 2004 by Ace Jones + email : Ace Jones + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +// ---------------------------------------------------------------------------- +// QT Headers + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// Project Headers + +#include "../mymoney/mymoneyexception.h" +#include "mymoneyqifprofile.h" +#include "webpricequote.h" + +// define static members +QString WebPriceQuote::m_financeQuoteScriptPath; +QStringList WebPriceQuote::m_financeQuoteSources; + +QString * WebPriceQuote::lastErrorMsg; +int WebPriceQuote::lastErrorCode = 0; + +WebPriceQuote::WebPriceQuote( QObject* _parent, const char* _name ): + QObject( _parent, _name ) +{ + m_financeQuoteScriptPath = + KGlobal::dirs()->findResource("appdata", QString("misc/financequote.pl")); + connect(&m_filter,SIGNAL(processExited(const QString&)),this,SLOT(slotParseQuote(const QString&))); +} + +WebPriceQuote::~WebPriceQuote() +{ +} + +bool WebPriceQuote::launch( const QString& _symbol, const QString& _id, const QString& _sourcename ) +{ + if (_sourcename.contains("Finance::Quote")) + return (launchFinanceQuote (_symbol, _id, _sourcename)); + else + return (launchNative (_symbol, _id, _sourcename)); +} + +bool WebPriceQuote::launchNative( const QString& _symbol, const QString& _id, const QString& _sourcename ) { + bool result = true; + m_symbol = _symbol; + m_id = _id; + +// emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id)); + + // if we're running normally, with a UI, we can just get these the normal way, + // from the config file + if ( kapp ) + { + QString sourcename = _sourcename; + if ( sourcename.isEmpty() ) + sourcename = "Yahoo"; + + if ( quoteSources().contains(sourcename) ) + m_source = WebPriceQuoteSource(sourcename); + else + emit error(QString("Source <%1> does not exist.").arg(sourcename)); + } + // otherwise, if we have no kapp, we have no config. so we just get them from + // the defaults + else + { + if ( _sourcename.isEmpty() ) + m_source = defaultQuoteSources()["Yahoo"]; + else + m_source = defaultQuoteSources()[_sourcename]; + } + + KURL url; + + // if the source has room for TWO symbols.. + if ( m_source.m_url.contains("%2") ) + { + // this is a two-symbol quote. split the symbol into two. valid symbol + // characters are: 0-9, A-Z and the dot. anything else is a separator + QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)",false /*case sensitive*/); + + // if we've truly found 2 symbols delimited this way... + if ( splitrx.search(m_symbol) != -1 ) + url = KURL::fromPathOrURL(m_source.m_url.arg(splitrx.cap(1),splitrx.cap(2))); + else + kdDebug(2) << "WebPriceQuote::launch() did not find 2 symbols" << endl; + } + else + // a regular one-symbol quote + url = KURL::fromPathOrURL(m_source.m_url.arg(m_symbol)); + + // If we're running a non-interactive session (with no UI), we can't + // use KIO::NetAccess, so we have to get our web data the old-fashioned + // way... with 'wget'. + // + // Note that a 'non-interactive' session right now means only the test + // cases. Although in the future if KMM gains a non-UI mode, this would + // still be useful + if ( ! kapp && ! url.isLocalFile() ) + url = KURL::fromPathOrURL("/usr/bin/wget -O - " + url.prettyURL()); + + if ( url.isLocalFile() ) + { + emit status(QString("Executing %1...").arg(url.path())); + + m_filter.clearArguments(); + m_filter << QStringList::split(" ",url.path()); + m_filter.setSymbol(m_symbol); + + // if we're running non-interactive, we'll need to block. + // otherwise, just let us know when it's done. + KProcess::RunMode mode = KProcess::NotifyOnExit; + if ( ! kapp ) + mode = KProcess::Block; + + if(m_filter.start(mode, KProcess::All)) + { + result = true; + m_filter.resume(); + } + else + { + emit error(QString("Unable to launch: %1").arg(url.path())); + slotParseQuote(QString()); + } + } + else + { + emit status(QString("Fetching URL %1...").arg(url.prettyURL())); + + QString tmpFile; + if( download( url, tmpFile, NULL ) ) + { + kdDebug(2) << "Downloaded " << tmpFile << endl; + QFile f(tmpFile); + if ( f.open( IO_ReadOnly ) ) + { + result = true; + QString quote = QTextStream(&f).read(); + f.close(); + slotParseQuote(quote); + } + else + { + slotParseQuote(QString()); + } + removeTempFile( tmpFile ); + } + else + { + emit error(KIO::NetAccess::lastErrorString()); + slotParseQuote(QString()); + } + } + return result; +} + +void WebPriceQuote::removeTempFile(const QString& tmpFile) +{ + if(tmpFile == m_tmpFile) { + unlink(tmpFile); + m_tmpFile = QString(); + } +} + +bool WebPriceQuote::download(const KURL& u, QString & target, QWidget* window) +{ + m_tmpFile = QString(); + + // the following code taken and adapted from KIO::NetAccess::download() + if (target.isEmpty()) + { + KTempFile tmpFile; + target = tmpFile.name(); + m_tmpFile = target; + } + + KURL dest; + dest.setPath( target ); + + + // the following code taken and adapted from KIO::NetAccess::filecopyInternal() + bJobOK = true; // success unless further error occurs + + KIO::Scheduler::checkSlaveOnHold(true); + KIO::Job * job = KIO::file_copy( u, dest, -1, true, false, false ); + job->setWindow (window); + job->addMetaData("cache", "reload"); // bypass cache + connect( job, SIGNAL( result (KIO::Job *) ), + this, SLOT( slotResult (KIO::Job *) ) ); + + enter_loop(); + return bJobOK; + +} + +// The following parts are copied and adjusted from KIO::NetAccess + +// If a troll sees this, he kills me +void qt_enter_modal( QWidget *widget ); +void qt_leave_modal( QWidget *widget ); + +void WebPriceQuote::enter_loop(void) +{ + QWidget dummy(0,0,WType_Dialog | WShowModal); + dummy.setFocusPolicy( QWidget::NoFocus ); + qt_enter_modal(&dummy); + qApp->enter_loop(); + qt_leave_modal(&dummy); +} + +void WebPriceQuote::slotResult( KIO::Job * job ) +{ + lastErrorCode = job->error(); + bJobOK = !job->error(); + if ( !bJobOK ) + { + if ( !lastErrorMsg ) + lastErrorMsg = new QString; + *lastErrorMsg = job->errorString(); + } + + qApp->exit_loop(); +} +// The above parts are copied and adjusted from KIO::NetAccess + +bool WebPriceQuote::launchFinanceQuote ( const QString& _symbol, const QString& _id, + const QString& _sourcename ) { + bool result = true; + m_symbol = _symbol; + m_id = _id; + QString FQSource = _sourcename.section (" ", 1); + m_source = WebPriceQuoteSource (_sourcename, m_financeQuoteScriptPath, + "\"([^,\"]*)\",.*", // symbol regexp + "[^,]*,[^,]*,\"([^\"]*)\"", // price regexp + "[^,]*,([^,]*),.*", // date regexp + "%y-%m-%d"); // date format + + //emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id)); + + + m_filter.clearArguments(); + m_filter << "perl" << m_financeQuoteScriptPath << FQSource << KProcess::quote(_symbol); + m_filter.setUseShell(true); + m_filter.setSymbol(m_symbol); + emit status(QString("Executing %1 %2 %3...").arg(m_financeQuoteScriptPath).arg(FQSource).arg(_symbol)); + + // if we're running non-interactive, we'll need to block. + // otherwise, just let us know when it's done. + KProcess::RunMode mode = KProcess::NotifyOnExit; + if ( ! kapp ) + mode = KProcess::Block; + + if(m_filter.start(mode, KProcess::All)) + { + result = true; + m_filter.resume(); + } + else + { + emit error(QString("Unable to launch: %1").arg(m_financeQuoteScriptPath)); + slotParseQuote(QString()); + } + + return result; +} + +void WebPriceQuote::slotParseQuote(const QString& _quotedata) +{ + QString quotedata = _quotedata; + bool gotprice = false; + bool gotdate = false; + +// kdDebug(2) << "WebPriceQuote::slotParseQuote( " << _quotedata << " ) " << endl; + + if ( ! quotedata.isEmpty() ) + { + if(!m_source.m_skipStripping) { + // + // First, remove extranous non-data elements + // + + // HTML tags + quotedata.remove(QRegExp("<[^>]*>")); + + // &...;'s + quotedata.replace(QRegExp("&\\w+;")," "); + + // Extra white space + quotedata = quotedata.simplifyWhiteSpace(); + } + +#if KMM_DEBUG + // Enable to get a look at the data coming back from the source after it's stripped + QFile file("stripped.txt"); + if ( file.open( IO_WriteOnly ) ) + { + QTextStream( &file ) << quotedata; + file.close(); + } +#endif + + QRegExp symbolRegExp(m_source.m_sym); + QRegExp dateRegExp(m_source.m_date); + QRegExp priceRegExp(m_source.m_price); + + if( symbolRegExp.search(quotedata) > -1) + emit status(i18n("Symbol found: %1").arg(symbolRegExp.cap(1))); + + if(priceRegExp.search(quotedata)> -1) + { + gotprice = true; + + // Deal with european quotes that come back as X.XXX,XX or XX,XXX + // + // We will make the assumption that ALL prices have a decimal separator. + // So "1,000" always means 1.0, not 1000.0. + // + // Remove all non-digits from the price string except the last one, and + // set the last one to a period. + QString pricestr = priceRegExp.cap(1); + + int pos = pricestr.findRev(QRegExp("\\D")); + if ( pos > 0 ) + { + pricestr[pos] = '.'; + pos = pricestr.findRev(QRegExp("\\D"),pos-1); + } + while ( pos > 0 ) + { + pricestr.remove(pos,1); + pos = pricestr.findRev(QRegExp("\\D"),pos); + } + + m_price = pricestr.toDouble(); + emit status(i18n("Price found: %1 (%2)").arg(pricestr).arg(m_price)); + } + + if(dateRegExp.search(quotedata) > -1) + { + QString datestr = dateRegExp.cap(1); + + MyMoneyDateFormat dateparse(m_source.m_dateformat); + try + { + m_date = dateparse.convertString( datestr,false /*strict*/ ); + gotdate = true; + emit status(i18n("Date found: %1").arg(m_date.toString()));; + } + catch (MyMoneyException* e) + { + // emit error(i18n("Unable to parse date %1 using format %2: %3").arg(datestr,dateparse.format(),e->what())); + m_date = QDate::currentDate(); + gotdate = true; + delete e; + } + } + + if ( gotprice && gotdate ) + { + emit quote( m_id, m_symbol, m_date, m_price ); + } + else + { + emit error(i18n("Unable to update price for %1").arg(m_symbol)); + emit failed( m_id, m_symbol ); + } + } + else + { + emit error(i18n("Unable to update price for %1").arg(m_symbol)); + emit failed( m_id, m_symbol ); + } +} + +QMap WebPriceQuote::defaultQuoteSources(void) +{ + QMap result; + + result["Yahoo"] = WebPriceQuoteSource("Yahoo", + "http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1", + "\"([^,\"]*)\",.*", // symbolregexp + "[^,]*,([^,]*),.*", // priceregexp + "[^,]*,[^,]*,\"([^\"]*)\"", // dateregexp + "%m %d %y" // dateformat + ); + + result["Yahoo Currency"] = WebPriceQuoteSource("Yahoo Currency", + "http://finance.yahoo.com/d/quotes.csv?s=%1%2=X&f=sl1d1", + "\"([^,\"]*)\",.*", // symbolregexp + "[^,]*,([^,]*),.*", // priceregexp + "[^,]*,[^,]*,\"([^\"]*)\"", // dateregexp + "%m %d %y" // dateformat + ); + + // 2009-08-20 Yahoo UK has no quotes and has comma separators + // sl1d1 format for Yahoo UK doesn't seem to give a date ever + // sl1d3 gives US locale time (9:99pm) and date (mm/dd/yyyy) + result["Yahoo UK"] = WebPriceQuoteSource("Yahoo UK", + "http://uk.finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d3", + "^([^,]*),.*", // symbolregexp + "^[^,]*,([^,]*),.*", // priceregexp + "^[^,]*,[^,]*,(.*)", // dateregexp + "%m/%d/%y" // dateformat + ); + + // sl1d1 format for Yahoo France doesn't seem to give a date ever + // sl1d3 gives us time (99h99) and date + result["Yahoo France"] = WebPriceQuoteSource("Yahoo France", + "http://fr.finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d3", + "([^;]*).*", // symbolregexp + "[^;]*.([^;]*),*", // priceregexp + "[^;]*.[^;]*...h...([^;]*)", // dateregexp + "%d/%m/%y" // dateformat + ); + + result["Globe & Mail"] = WebPriceQuoteSource("Globe & Mail", + "http://globefunddb.theglobeandmail.com/gishome/plsql/gis.price_history?pi_fund_id=%1", + QString(), // symbolregexp + "Reinvestment Price \\w+ \\d+, \\d+ (\\d+\\.\\d+)", // priceregexp + "Reinvestment Price (\\w+ \\d+, \\d+)", // dateregexp + "%m %d %y" // dateformat + ); + + result["MSN.CA"] = WebPriceQuoteSource("MSN.CA", + "http://ca.moneycentral.msn.com/investor/quotes/quotes.asp?symbol=%1", + QString(), // symbolregexp + "Net Asset Value (\\d+\\.\\d+)", // priceregexp + "NAV update (\\d+\\D+\\d+\\D+\\d+)", // dateregexp + "%d %m %y" // dateformat + ); + // Finanztreff (replaces VWD.DE) and boerseonline supplied by Micahel Zimmerman + result["Finanztreff"] = WebPriceQuoteSource("Finanztreff", + "http://finanztreff.de/kurse_einzelkurs_detail.htn?u=100&i=%1", + QString(), // symbolregexp + "([0-9]+,\\d+).+Gattung:Fonds", // priceregexp + "\\).(\\d+\\D+\\d+\\D+\\d+)", // dateregexp (doesn't work; date in chart + "%d.%m.%y" // dateformat + ); + + result["boerseonline"] = WebPriceQuoteSource("boerseonline", + "http://www.boerse-online.de/tools/boerse/einzelkurs_kurse.htm?&s=%1", + QString(), // symbolregexp + "Akt\\. Kurs.(\\d+,\\d\\d)", // priceregexp + "Datum.(\\d+\\.\\d+\\.\\d+)", // dateregexp (doesn't work; date in chart + "%d.%m.%y" // dateformat + ); + + // The following two price sources were contributed by + // Marc Zahnlecker + + result["Wallstreet-Online.DE (Default)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Default)", + "http://www.wallstreet-online.de/si/?k=%1&spid=ws", + "Symbol:(\\w+)", // symbolregexp + "Letzter Kurs: ([0-9.]+,\\d+)", // priceregexp + ", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp + "%d %m %y" // dateformat + ); + + // This quote source provided by Peter Lord + // The trading symbol will normally be the SEDOL (see wikipedia) but + // the flexibility presently (1/2008) in the code will allow use of + // the ISIN or MEXID (FT specific) codes + result["Financial Times UK Funds"] = WebPriceQuoteSource("Financial Times UK Funds", + "http://funds.ft.com/funds/simpleSearch.do?searchArea=%&search=%1", + "SEDOL[\\ ]*(\\d+.\\d+)", // symbol regexp + "\\(GBX\\)[\\ ]*([0-9,]*.\\d+)[\\ ]*", // price regexp + "Valuation date:[\\ ]*(\\d+/\\d+/\\d+)", // date regexp + "%d/%m/%y" // date format + ); + + // This quote source provided by Danny Scott + result["Yahoo Canada"] = WebPriceQuoteSource("Yahoo Canada", + "http://ca.finance.yahoo.com/q?s=%1", + "%1", // symbol regexp + "Last Trade: (\\d+\\.\\d+)", // price regexp + "day, (.\\D+\\d+\\D+\\d+)", // date regexp + "%m %d %y" // date format + ); + + // (tf2k) The "mpid" is I think the market place id. In this case five + // stands for Hamburg. + // + // Here the id for several market places: 2 Frankfurt, 3 Berlin, 4 + // Düsseldorf, 5 Hamburg, 6 München/Munich, 7 Hannover, 9 Stuttgart, 10 + // Xetra, 32 NASDAQ, 36 NYSE + + result["Wallstreet-Online.DE (Hamburg)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Hamburg)", + "http://fonds.wallstreet-online.de/si/?k=%1&spid=ws&mpid=5", + "Symbol:(\\w+)", // symbolregexp + "Fonds \\(EUR\\) ([0-9.]+,\\d+)", // priceregexp + ", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp + "%d %m %y" // dateformat + ); + + // The following price quote was contributed by + // Piotr Adacha + + // I would like to post new Online Query Settings for KMyMoney. This set is + // suitable to query stooq.com service, providing quotes for stocks, futures, + // mutual funds and other financial instruments from Polish Gielda Papierow + // Wartosciowych (GPW). Unfortunately, none of well-known international + // services provide quotes for this market (biggest one in central and eastern + // Europe), thus, I think it could be helpful for Polish users of KMyMoney (and + // I am one of them for almost a year). + + result["Gielda Papierow Wartosciowych (GPW)"] = WebPriceQuoteSource("Gielda Papierow Wartosciowych (GPW)", + "http://stooq.com/q/?s=%1", + QString(), // symbol regexp + "Kurs.*(\\d+\\.\\d+).*Data", // price regexp + "(\\d{4,4}-\\d{2,2}-\\d{2,2})", // date regexp + "%y %m %d" // date format + ); + + // The following price quote is for getting prices of different funds + // at OMX Baltic market. + result["OMX Baltic funds"] = WebPriceQuoteSource("OMX Baltic funds", + "http://www.baltic.omxgroup.com/market/?pg=nontradeddetails¤cy=0&instrument=%1", + QString(), // symbolregexp + "NAV (\\d+,\\d+)", // priceregexp + "Kpv (\\d+.\\d+.\\d+)", // dateregexp + "%d.%m.%y" // dateformat + ); + + // The following price quote was contributed by + // Peter Hargreaves + // The original posting can be found here: + // http://sourceforge.net/mailarchive/message.php?msg_name=200806060854.11682.pete.h%40pdh-online.info + + // I have PEP and ISA accounts which I invest in Funds with Barclays + // Stockbrokers. They give me Fund data via Financial Express: + // + // https://webfund6.financialexpress.net/Clients/Barclays/default.aspx + // + // A typical Fund Factsheet is: + // + // https://webfund6.financialexpress.net/Clients/Barclays/search_factsheet_summary.aspx?code=0585239 + // + // On the Factsheet to identify the fund you can see ISIN Code GB0005852396. + // In the url, this code is shortened by loosing the first four and last + // characters. + // + // Update: + // + // Nick Elliot has contributed a modified regular expression to cope with values presented + // in pounds as well as those presented in pence. The source can be found here: + // http://forum.kde.org/update-stock-and-currency-prices-t-32049.html + + result["Financial Express"] = WebPriceQuoteSource("Financial Express", + "https://webfund6.financialexpress.net/Clients/Barclays/search_factsheet_summary.aspx?code=%1", + "ISIN Code[^G]*(GB..........).*", // symbolregexp + "Current Market Information[^0-9]*([0-9,\\.]+).*", // priceregexp + "Price Date[^0-9]*(../../....).*", // dateregexp + "%d/%m/%y" // dateformat + ); + + return result; +} + +QStringList WebPriceQuote::quoteSources (const _quoteSystemE _system) { + if (_system == Native) + return (quoteSourcesNative()); + else + return (quoteSourcesFinanceQuote()); +} + +QStringList WebPriceQuote::quoteSourcesNative() +{ + KConfig *kconfig = KGlobal::config(); + QStringList groups = kconfig->groupList(); + + QStringList::Iterator it; + QRegExp onlineQuoteSource(QString("^Online-Quote-Source-(.*)$")); + + // get rid of all 'non online quote source' entries + for(it = groups.begin(); it != groups.end(); it = groups.remove(it)) { + if(onlineQuoteSource.search(*it) >= 0) { + // Insert the name part + groups.insert(it, onlineQuoteSource.cap(1)); + } + } + + // if the user has the OLD quote source defined, now is the + // time to remove that entry and convert it to the new system. + if ( ! groups.count() && kconfig->hasGroup("Online Quotes Options") ) + { + kconfig->setGroup("Online Quotes Options"); + QString url(kconfig->readEntry("URL","http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1")); + QString symbolRegExp(kconfig->readEntry("SymbolRegex","\"([^,\"]*)\",.*")); + QString priceRegExp(kconfig->readEntry("PriceRegex","[^,]*,([^,]*),.*")); + QString dateRegExp(kconfig->readEntry("DateRegex","[^,]*,[^,]*,\"([^\"]*)\"")); + kconfig->deleteGroup("Online Quotes Options"); + + groups += "Old Source"; + kconfig->setGroup(QString("Online-Quote-Source-%1").arg("Old Source")); + kconfig->writeEntry("URL", url); + kconfig->writeEntry("SymbolRegex", symbolRegExp); + kconfig->writeEntry("PriceRegex",priceRegExp); + kconfig->writeEntry("DateRegex", dateRegExp); + kconfig->writeEntry("DateFormatRegex", "%m %d %y"); + kconfig->sync(); + } + + // Set up each of the default sources. These are done piecemeal so that + // when we add a new source, it's automatically picked up. + QMap defaults = defaultQuoteSources(); + QMap::const_iterator it_source = defaults.begin(); + while ( it_source != defaults.end() ) + { + if ( ! groups.contains( (*it_source).m_name ) ) + { + groups += (*it_source).m_name; + (*it_source).write(); + kconfig->sync(); + } + ++it_source; + } + + return groups; +} + +QStringList WebPriceQuote::quoteSourcesFinanceQuote() +{ + if (m_financeQuoteSources.empty()) { // run the process one time only + FinanceQuoteProcess getList; + m_financeQuoteScriptPath = + KGlobal::dirs()->findResource("appdata", QString("misc/financequote.pl")); + getList.launch( m_financeQuoteScriptPath ); + while (!getList.isFinished()) { + qApp->processEvents(); + } + m_financeQuoteSources = getList.getSourceList(); + } + return (m_financeQuoteSources); +} + +// +// Helper class to load/save an individual source +// + +WebPriceQuoteSource::WebPriceQuoteSource(const QString& name, const QString& url, const QString& sym, const QString& price, const QString& date, const QString& dateformat): + m_name(name), + m_url(url), + m_sym(sym), + m_price(price), + m_date(date), + m_dateformat(dateformat) +{ +} + +WebPriceQuoteSource::WebPriceQuoteSource(const QString& name) +{ + m_name = name; + KConfig *kconfig = KGlobal::config(); + kconfig->setGroup(QString("Online-Quote-Source-%1").arg(m_name)); + m_sym = kconfig->readEntry("SymbolRegex"); + m_date = kconfig->readEntry("DateRegex"); + m_dateformat = kconfig->readEntry("DateFormatRegex","%m %d %y"); + m_price = kconfig->readEntry("PriceRegex"); + m_url = kconfig->readEntry("URL"); + m_skipStripping = kconfig->readBoolEntry("SkipStripping", false); +} + +void WebPriceQuoteSource::write(void) const +{ + KConfig *kconfig = KGlobal::config(); + kconfig->setGroup(QString("Online-Quote-Source-%1").arg(m_name)); + kconfig->writeEntry("URL", m_url); + kconfig->writeEntry("PriceRegex", m_price); + kconfig->writeEntry("DateRegex", m_date); + kconfig->writeEntry("DateFormatRegex", m_dateformat); + kconfig->writeEntry("SymbolRegex", m_sym); + if(m_skipStripping) + kconfig->writeEntry("SkipStripping", m_skipStripping); + else + kconfig->deleteEntry("SkipStripping"); +} + +void WebPriceQuoteSource::rename(const QString& name) +{ + remove(); + m_name = name; + write(); +} + +void WebPriceQuoteSource::remove(void) const +{ + KConfig *kconfig = KGlobal::config(); + kconfig->deleteGroup(QString("Online-Quote-Source-%1").arg(m_name)); +} + +// +// Helper class to babysit the KProcess used for running the local script in that case +// + +WebPriceQuoteProcess::WebPriceQuoteProcess(void) +{ + connect(this, SIGNAL(receivedStdout(KProcess*, char*, int)), this, SLOT(slotReceivedDataFromFilter(KProcess*, char*, int))); + connect(this, SIGNAL(processExited(KProcess*)), this, SLOT(slotProcessExited(KProcess*))); +} + +void WebPriceQuoteProcess::slotReceivedDataFromFilter(KProcess* /*_process*/, char* _pcbuffer, int _nbufferlen) +{ + QByteArray data; + data.duplicate(_pcbuffer, _nbufferlen); + +// kdDebug(2) << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << QString(data) << endl; + m_string += QString(data); +} + +void WebPriceQuoteProcess::slotProcessExited(KProcess*) +{ +// kdDebug(2) << "WebPriceQuoteProcess::slotProcessExited()" << endl; + emit processExited(m_string); + m_string.truncate(0); +} + +// +// Helper class to babysit the KProcess used for running the Finance Quote sources script +// + +FinanceQuoteProcess::FinanceQuoteProcess(void) +{ + m_isDone = false; + m_string = ""; + m_fqNames["aex"] = "AEX"; + m_fqNames["aex_futures"] = "AEX Futures"; + m_fqNames["aex_options"] = "AEX Options"; + m_fqNames["amfiindia"] = "AMFI India"; + m_fqNames["asegr"] = "ASE"; + m_fqNames["asia"] = "Asia (Yahoo, ...)"; + m_fqNames["asx"] = "ASX"; + m_fqNames["australia"] = "Australia (ASX, Yahoo, ...)"; + m_fqNames["bmonesbittburns"] = "BMO NesbittBurns"; + m_fqNames["brasil"] = "Brasil (Yahoo, ...)"; + m_fqNames["canada"] = "Canada (Yahoo, ...)"; + m_fqNames["canadamutual"] = "Canada Mutual (Fund Library, ...)"; + m_fqNames["deka"] = "Deka Investments"; + m_fqNames["dutch"] = "Dutch (AEX, ...)"; + m_fqNames["dwsfunds"] = "DWS"; + m_fqNames["europe"] = "Europe (Yahoo, ...)"; + m_fqNames["fidelity"] = "Fidelity (Fidelity, ...)"; + m_fqNames["fidelity_direct"] = "Fidelity Direct"; + m_fqNames["financecanada"] = "Finance Canada"; + m_fqNames["ftportfolios"] = "First Trust (First Trust, ...)"; + m_fqNames["ftportfolios_direct"] = "First Trust Portfolios"; + m_fqNames["fundlibrary"] = "Fund Library"; + m_fqNames["greece"] = "Greece (ASE, ...)"; + m_fqNames["indiamutual"] = "India Mutual (AMFI, ...)"; + m_fqNames["maninv"] = "Man Investments"; + m_fqNames["fool"] = "Motley Fool"; + m_fqNames["nasdaq"] = "Nasdaq (Yahoo, ...)"; + m_fqNames["nz"] = "New Zealand (Yahoo, ...)"; + m_fqNames["nyse"] = "NYSE (Yahoo, ...)"; + m_fqNames["nzx"] = "NZX"; + m_fqNames["platinum"] = "Platinum Asset Management"; + m_fqNames["seb_funds"] = "SEB"; + m_fqNames["sharenet"] = "Sharenet"; + m_fqNames["za"] = "South Africa (Sharenet, ...)"; + m_fqNames["troweprice_direct"] = "T. Rowe Price"; + m_fqNames["troweprice"] = "T. Rowe Price"; + m_fqNames["tdefunds"] = "TD Efunds"; + m_fqNames["tdwaterhouse"] = "TD Waterhouse Canada"; + m_fqNames["tiaacref"] = "TIAA-CREF"; + m_fqNames["trustnet"] = "Trustnet"; + m_fqNames["uk_unit_trusts"] = "U.K. Unit Trusts"; + m_fqNames["unionfunds"] = "Union Investments"; + m_fqNames["tsp"] = "US Govt. Thrift Savings Plan"; + m_fqNames["usfedbonds"] = "US Treasury Bonds"; + m_fqNames["usa"] = "USA (Yahoo, Fool ...)"; + m_fqNames["vanguard"] = "Vanguard"; + m_fqNames["vwd"] = "VWD"; + m_fqNames["yahoo"] = "Yahoo"; + m_fqNames["yahoo_asia"] = "Yahoo Asia"; + m_fqNames["yahoo_australia"] = "Yahoo Australia"; + m_fqNames["yahoo_brasil"] = "Yahoo Brasil"; + m_fqNames["yahoo_europe"] = "Yahoo Europe"; + m_fqNames["yahoo_nz"] = "Yahoo New Zealand"; + m_fqNames["zifunds"] = "Zuerich Investments"; + connect(this, SIGNAL(receivedStdout(KProcess*, char*, int)), this, SLOT(slotReceivedDataFromFilter(KProcess*, char*, int))); + connect(this, SIGNAL(processExited(KProcess*)), this, SLOT(slotProcessExited(KProcess*))); +} + +void FinanceQuoteProcess::slotReceivedDataFromFilter(KProcess* /*_process*/, char* _pcbuffer, int _nbufferlen) +{ + QByteArray data; + data.duplicate(_pcbuffer, _nbufferlen); + +// kdDebug(2) << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << QString(data) << endl; + m_string += QString(data); +} + +void FinanceQuoteProcess::slotProcessExited(KProcess*) +{ +// kdDebug(2) << "WebPriceQuoteProcess::slotProcessExited()" << endl; + m_isDone = true; +} + +void FinanceQuoteProcess::launch (const QString& scriptPath) { + clearArguments(); + arguments.append(QCString("perl")); + arguments.append (QCString(scriptPath)); + arguments.append (QCString("-l")); + if (!start(KProcess::NotifyOnExit, KProcess::Stdout)) qFatal ("Unable to start FQ script"); + return; +} + +QStringList FinanceQuoteProcess::getSourceList() { + QStringList raw = QStringList::split(0x0A, m_string); + QStringList sources; + QStringList::iterator it; + for (it = raw.begin(); it != raw.end(); ++it) { + if (m_fqNames[*it].isEmpty()) sources.append(*it); + else sources.append(m_fqNames[*it]); + } + sources.sort(); + return (sources); +} + +const QString FinanceQuoteProcess::crypticName(const QString& niceName) { + QString ret (niceName); + fqNameMap::iterator it; + for (it = m_fqNames.begin(); it != m_fqNames.end(); ++it) { + if (niceName == it.data()) { + ret = it.key(); + break; + } + } + return (ret); +} + +const QString FinanceQuoteProcess::niceName(const QString& crypticName) { + QString ret (m_fqNames[crypticName]); + if (ret.isEmpty()) ret = crypticName; + return (ret); +} +// +// Universal date converter +// + +// In 'strict' mode, this is designed to be compatable with the QIF profile date +// converter. However, that converter deals with the concept of an apostrophe +// format in a way I don't understand. So for the moment, they are 99% +// compatable, waiting on that issue. (acejones) + +QDate MyMoneyDateFormat::convertString(const QString& _in, bool _strict, unsigned _centurymidpoint) const +{ + // + // Break date format string into component parts + // + + QRegExp formatrex("%([mdy]+)(\\W+)%([mdy]+)(\\W+)%([mdy]+)",false /* case sensitive */); + if ( formatrex.search(m_format) == -1 ) + { + throw new MYMONEYEXCEPTION("Invalid format string"); + } + + QStringList formatParts; + formatParts += formatrex.cap(1); + formatParts += formatrex.cap(3); + formatParts += formatrex.cap(5); + + QStringList formatDelimiters; + formatDelimiters += formatrex.cap(2); + formatDelimiters += formatrex.cap(4); + + // + // Break input string up into component parts, + // using the delimiters found in the format string + // + + QRegExp inputrex; + inputrex.setCaseSensitive(false); + + // strict mode means we must enforce the delimiters as specified in the + // format. non-strict allows any delimiters + if ( _strict ) + inputrex.setPattern(QString("(\\w+)%1(\\w+)%2(\\w+)").arg(formatDelimiters[0],formatDelimiters[1])); + else + inputrex.setPattern("(\\w+)\\W+(\\w+)\\W+(\\w+)"); + + if ( inputrex.search(_in) == -1 ) + { + throw new MYMONEYEXCEPTION("Invalid input string"); + } + + QStringList scannedParts; + scannedParts += inputrex.cap(1).lower(); + scannedParts += inputrex.cap(2).lower(); + scannedParts += inputrex.cap(3).lower(); + + // + // Convert the scanned parts into actual date components + // + + unsigned day = 0, month = 0, year = 0; + bool ok; + QRegExp digitrex("(\\d+)"); + QStringList::const_iterator it_scanned = scannedParts.begin(); + QStringList::const_iterator it_format = formatParts.begin(); + while ( it_scanned != scannedParts.end() ) + { + switch ( (*it_format)[0] ) + { + case 'd': + // remove any extraneous non-digits (e.g. read "3rd" as 3) + ok = false; + if ( digitrex.search(*it_scanned) != -1 ) + day = digitrex.cap(1).toUInt(&ok); + if ( !ok || day > 31 ) + throw new MYMONEYEXCEPTION(QString("Invalid day entry: %1").arg(*it_scanned)); + break; + case 'm': + month = (*it_scanned).toUInt(&ok); + if ( !ok ) + { + // maybe it's a textual date + unsigned i = 1; + while ( i <= 12 ) + { + if(KGlobal::locale()->calendar()->monthName(i, 2000, true).lower() == *it_scanned + || KGlobal::locale()->calendar()->monthName(i, 2000, false).lower() == *it_scanned) + month = i; + ++i; + } + } + + if ( month < 1 || month > 12 ) + throw new MYMONEYEXCEPTION(QString("Invalid month entry: %1").arg(*it_scanned)); + + break; + case 'y': + if ( _strict && (*it_scanned).length() != (*it_format).length()) + throw new MYMONEYEXCEPTION(QString("Length of year (%1) does not match expected length (%2).") + .arg(*it_scanned,*it_format)); + + year = (*it_scanned).toUInt(&ok); + + if (!ok) + throw new MYMONEYEXCEPTION(QString("Invalid year entry: %1").arg(*it_scanned)); + + // + // 2-digit year case + // + // this algorithm will pick a year within +/- 50 years of the + // centurymidpoint parameter. i.e. if the midpoint is 2000, + // then 0-49 will become 2000-2049, and 50-99 will become 1950-1999 + if ( year < 100 ) + { + unsigned centuryend = _centurymidpoint + 50; + unsigned centurybegin = _centurymidpoint - 50; + + if ( year < centuryend % 100 ) + year += 100; + year += centurybegin - centurybegin % 100; + } + + if ( year < 1900 ) + throw new MYMONEYEXCEPTION(QString("Invalid year (%1)").arg(year)); + + break; + default: + throw new MYMONEYEXCEPTION("Invalid format character"); + } + + ++it_scanned; + ++it_format; + } + + QDate result(year,month,day); + if ( ! result.isValid() ) + throw new MYMONEYEXCEPTION(QString("Invalid date (yr%1 mo%2 dy%3)").arg(year).arg(month).arg(day)); + + return result; +} + +// +// Unit test helpers +// + +convertertest::QuoteReceiver::QuoteReceiver(WebPriceQuote* q, QObject* parent, const char *name) : + QObject(parent,name) +{ + connect(q,SIGNAL(quote(const QString&,const QDate&, const double&)), + this,SLOT(slotGetQuote(const QString&,const QDate&, const double&))); + connect(q,SIGNAL(status(const QString&)), + this,SLOT(slotStatus(const QString&))); + connect(q,SIGNAL(error(const QString&)), + this,SLOT(slotError(const QString&))); +} + +convertertest::QuoteReceiver::~QuoteReceiver() +{ +} + +void convertertest::QuoteReceiver::slotGetQuote(const QString&,const QDate& d, const double& m) +{ +// kdDebug(2) << "test::QuoteReceiver::slotGetQuote( , " << d << " , " << m.toString() << " )" << endl; + + m_price = MyMoneyMoney(m); + m_date = d; +} +void convertertest::QuoteReceiver::slotStatus(const QString& msg) +{ +// kdDebug(2) << "test::QuoteReceiver::slotStatus( " << msg << " )" << endl; + + m_statuses += msg; +} +void convertertest::QuoteReceiver::slotError(const QString& msg) +{ +// kdDebug(2) << "test::QuoteReceiver::slotError( " << msg << " )" << endl; + + m_errors += msg; +} + +// vim:cin:si:ai:et:ts=2:sw=2: + +#include "webpricequote.moc" diff --git a/kmymoney2/converter/webpricequote.h b/kmymoney2/converter/webpricequote.h new file mode 100644 index 0000000..e25dfb8 --- /dev/null +++ b/kmymoney2/converter/webpricequote.h @@ -0,0 +1,252 @@ +/*************************************************************************** + webpricequote.h + ------------------- + begin : Thu Dec 30 2004 + copyright : (C) 2004 by Ace Jones + email : Ace Jones + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef WEBPRICEQUOTE_H +#define WEBPRICEQUOTE_H + +// ---------------------------------------------------------------------------- +// QT Headers + +#include +#include +#include +#include + +// ---------------------------------------------------------------------------- +// KDE Headers + +#include +namespace KIO { + class Job; +}; + +// ---------------------------------------------------------------------------- +// Project Headers + +#include "../mymoney/mymoneymoney.h" + +/** +Helper class to attend the process which is running the script, in the case +of a local script being used to fetch the quote. + +@author Thomas Baumgart & Ace Jones +*/ +class WebPriceQuoteProcess: public KProcess +{ + Q_OBJECT +public: + WebPriceQuoteProcess(void); + void setSymbol(const QString& _symbol) { m_symbol = _symbol; m_string.truncate(0); } + +public slots: + void slotReceivedDataFromFilter(KProcess*, char*, int); + void slotProcessExited(KProcess*); + +signals: + void processExited(const QString&); + +private: + QString m_symbol; + QString m_string; +}; + +/** +Helper class to run the Finance::Quote process. This is used only for the purpose of obtaining +a list of valid sources. The actual price quotes are obtained thru WebPriceQuoteProcess. +The class also contains functions to convert between the rather cryptic source names used +by the Finance::Quote package, and more user-friendly names. + +@author Thomas Baumgart & Ace Jones , Tony B + */ +class FinanceQuoteProcess: public KProcess +{ + Q_OBJECT + public: + FinanceQuoteProcess(void); + void launch (const QString& scriptPath); + bool isFinished() { return(m_isDone);}; + QStringList getSourceList(); + const QString crypticName(const QString& niceName); + const QString niceName(const QString& crypticName); + + public slots: + void slotReceivedDataFromFilter(KProcess*, char*, int); + void slotProcessExited(KProcess*); + + private: + bool m_isDone; + QString m_string; + typedef QMap fqNameMap; + fqNameMap m_fqNames; +}; + +/** + * @author Thomas Baumgart & Ace Jones + * + * This is a helper class to store information about an online source + * for stock prices or currency exchange rates. + */ +struct WebPriceQuoteSource +{ + WebPriceQuoteSource() {} + WebPriceQuoteSource(const QString& name); + WebPriceQuoteSource(const QString& name, const QString& url, const QString& sym, const QString& price, const QString& date, const QString& dateformat); + ~WebPriceQuoteSource() {} + + void write(void) const; + void rename(const QString& name); + void remove(void) const; + + QString m_name; + QString m_url; + QString m_sym; + QString m_price; + QString m_date; + QString m_dateformat; + bool m_skipStripping; +}; + +/** +Retrieves a price quote from a web-based quote source + +@author Ace Jones +*/ +class WebPriceQuote: public QObject +{ + Q_OBJECT +public: + WebPriceQuote( QObject* = 0, const char* = 0 ); + ~WebPriceQuote(); + + typedef enum _quoteSystemE { + Native=0, + FinanceQuote + } quoteSystemE; + + /** + * This launches a web-based quote update for the given @p _symbol. + * When the quote is received back from the web source, it will be + * emitted on the 'quote' signal. + * + * @param _symbol the trading symbol of the stock to fetch a price for + * @param _id an arbitrary identifier, which will be emitted in the quote + * signal when a price is sent back. + * @param _source the source of the quote (must be a valid value returned + * by quoteSources(). Send QString() to use the default + * source. + * @return bool Whether the quote fetch process was launched successfully + */ + + bool launch(const QString& _symbol, const QString& _id, const QString& _source=QString()); + + /** + * This returns a list of the names of the quote sources + * currently defined. + * + * @param _system whether to return Native or Finance::Quote source list + * @return QStringList of quote source names + */ + static QStringList quoteSources(const _quoteSystemE _system=Native); + +signals: + void quote(const QString&, const QString&, const QDate&, const double&); + void failed(const QString&, const QString&); + void status(const QString&); + void error(const QString&); + +protected slots: + void slotParseQuote(const QString&); + +protected: + static QMap defaultQuoteSources(void); + +private: + bool download(const KURL& u, QString & target, QWidget* window); + void removeTempFile(const QString& tmpFile); + +private slots: + void slotResult( KIO::Job * job ); + + +private: + bool launchNative(const QString& _symbol, const QString& _id, const QString& _source=QString()); + bool launchFinanceQuote(const QString& _symbol, const QString& _id, const QString& _source=QString()); + void enter_loop(void); + + static QStringList quoteSourcesNative(); + static QStringList quoteSourcesFinanceQuote(); + + WebPriceQuoteProcess m_filter; + QString m_symbol; + QString m_id; + QDate m_date; + double m_price; + WebPriceQuoteSource m_source; + static QString m_financeQuoteScriptPath; + static QStringList m_financeQuoteSources; + + + /** + * Whether the download succeeded or not. Taken from KIO::NetAccess + */ + bool bJobOK; + static QString* lastErrorMsg; + static int lastErrorCode; + QString m_tmpFile; +}; + +class MyMoneyDateFormat +{ +public: + MyMoneyDateFormat(const QString& _format): m_format(_format) {} + QString convertDate(const QDate& _in) const; + QDate convertString(const QString& _in, bool _strict=true, unsigned _centurymidpoint = QDate::currentDate().year() ) const; + const QString& format(void) const { return m_format; } +private: + QString m_format; +}; + +namespace convertertest { + +/** +Simple class to handle signals/slots for unit tests + +@author Ace Jones +*/ +class QuoteReceiver : public QObject +{ +Q_OBJECT +public: + QuoteReceiver(WebPriceQuote* q, QObject *parent = 0, const char *name = 0); + ~QuoteReceiver(); +public slots: + void slotGetQuote(const QString&,const QDate&, const double&); + void slotStatus(const QString&); + void slotError(const QString&); +public: + QStringList m_statuses; + QStringList m_errors; + MyMoneyMoney m_price; + QDate m_date; +}; + +} // end namespace convertertest + + +#endif // WEBPRICEQUOTE_H + +// vim:cin:si:ai:et:ts=2:sw=2: -- cgit v1.2.1