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