diff --git a/contrib/xea2kmt-all b/contrib/xea2kmt-all index 11e074dc5..1802a8e85 100755 --- a/contrib/xea2kmt-all +++ b/contrib/xea2kmt-all @@ -1,34 +1,8 @@ #!/bin/sh # # Update all kmymoney templates from gnucash accounts # # Author: Ralf Habacker # -d=$(dirname $0) -r=$(realpath $d) -s=~/src/kmymoney-build/tools - -if test -z "$1"; then - echo "xea2kmt-all " - exit 1 -fi - -in=$1 -out=$(realpath $r/../kmymoney/templates) -for j in $(cd $in; find -name '*-xea'); do - i=$(echo $j | sed 's,^./,,g') - p=$(dirname $i | sed 's,./,,g') - f=$(basename $i | sed 's,acctchrt_,,g;s,gnucash-xea,kmt,g') - if ! test -d "$out/$p"; then - mkdir -p "$out/$p" - echo "language $p does not exist; creating" - fi - infile="$in$i" - outfile="$out/$p/$f" - if ! test -f "$out/$p/$f"; then - echo "using $infile to create template $outfile" - else - echo "using $infile to update $outfile" - fi - $s/xea2kmt --no-level1-names $infile > $outfile -done +echo "To refresh all kmymoney templates from gnucash compile xea2kmt from kmymoney sources and run" +echo "xea2kmt --no-level1-names --in-dir --out-dir " diff --git a/kmymoney/converter/mymoneyqifreader.cpp b/kmymoney/converter/mymoneyqifreader.cpp index 3ad672327..e5f86b81e 100644 --- a/kmymoney/converter/mymoneyqifreader.cpp +++ b/kmymoney/converter/mymoneyqifreader.cpp @@ -1,2297 +1,2300 @@ /*************************************************************************** mymoneyqifreader.cpp ------------------- begin : Mon Jan 27 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneyqifreader.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include "kjobwidgets.h" #include "kio/job.h" // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyfile.h" #include "kaccountselectdlg.h" #include "kmymoney.h" #include "kmymoneyglobalsettings.h" #include "mymoneystatementreader.h" #include // define this to debug the code. Using external filters // while debugging did not work too good for me, so I added // this code. // #define DEBUG_IMPORT #ifdef DEBUG_IMPORT #ifdef __GNUC__ #warning "DEBUG_IMPORT defined --> external filter not available!!!!!!!" #endif #endif class MyMoneyQifReader::Private { public: Private() : accountType(MyMoneyAccount::Checkings), mapCategories(true) {} const QString accountTypeToQif(MyMoneyAccount::accountTypeE type) const; /** * finalize the current statement and add it to the statement list */ void finishStatement(); bool isTransfer(QString& name, const QString& leftDelim, const QString& rightDelim); /** * Converts the QIF specific N-record of investment transactions into * a category name */ const 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 */ QList statements; /** * a list of already used hashes in this file */ QMap m_hashMap; QString st_AccountName; QString st_AccountId; MyMoneyAccount::accountTypeE accountType; bool firstTransaction; bool mapCategories; MyMoneyQifReader::QifEntryTypeE transactionType; }; void MyMoneyQifReader::Private::fixMultiLineMemo(QString& memo) const { memo.replace("\\n", "\n"); } void MyMoneyQifReader::Private::finishStatement() { // 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()); } MyMoneyStatement::EType type = st.m_eType; //stash type and... // start with a fresh statement st = MyMoneyStatement(); st.m_skipCategoryMatching = !mapCategories; st.m_eType = type; } const 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; } const QString MyMoneyQifReader::Private::typeToAccountName(const QString& type) const { + if (type == "reinvint") + return i18nc("Category name", "Reinvested interest"); + if (type == "reinvdiv") return i18nc("Category name", "Reinvested dividend"); if (type == "reinvlg") return i18nc("Category name", "Reinvested dividend (long term)"); if (type == "reinvsh") return i18nc("Category name", "Reinvested dividend (short term)"); if (type == "div") return i18nc("Category name", "Dividend"); if (type == "intinc") return i18nc("Category name", "Interest"); if (type == "cgshort") return i18nc("Category name", "Capital Gain (short term)"); if (type == "cgmid") return i18nc("Category name", "Capital Gain (mid term)"); if (type == "cglong") return i18nc("Category name", "Capital Gain (long term)"); if (type == "rtrncap") return i18nc("Category name", "Returned capital"); if (type == "miscinc") return i18nc("Category name", "Miscellaneous income"); if (type == "miscexp") return i18nc("Category name", "Miscellaneous expense"); if (type == "sell" || type == "buy") return i18nc("Category name", "Investment fees"); return i18n("Unknown QIF type %1", 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 (The '/' is the Quicken class symbol) // // 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.indexIn(tmp) != -1)) == true) { tmp = exp.cap(1) + exp.cap(2); tmp = tmp.trimmed(); } 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(bytesWritten(qint64)), this, SLOT(slotSendDataToFilter())); connect(&m_filter, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter())); connect(&m_filter, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotImportFinished())); connect(&m_filter, SIGNAL(readyReadStandardError()), this, SLOT(slotReceivedErrorFromFilter())); } MyMoneyQifReader::~MyMoneyQifReader() { delete m_file; delete d; } void MyMoneyQifReader::setCategoryMapping(bool map) { d->mapCategories = map; } void MyMoneyQifReader::setURL(const QUrl &url) { m_url = url; } void MyMoneyQifReader::setProfile(const QString& profile) { m_qifProfile.loadProfile("Profile-" + profile); } void MyMoneyQifReader::slotSendDataToFilter() { long len; if (m_file->atEnd()) { m_filter.closeWriteChannel(); } else { len = m_file->read(m_buffer, sizeof(m_buffer)); if (len == -1) { qWarning("Failed to read block from QIF import file"); m_filter.closeWriteChannel(); m_filter.kill(); } else { m_filter.write(m_buffer, len); } } } void MyMoneyQifReader::slotReceivedErrorFromFilter() { qWarning("%s", qPrintable(QString(m_filter.readAllStandardError()))); } void MyMoneyQifReader::slotReceivedDataFromFilter() { parseReceivedData(m_filter.readAllStandardOutput()); } void MyMoneyQifReader::parseReceivedData(const QByteArray& data) { const char* buff = data.data(); int len = data.length(); 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.trimmed()); } m_lineBuffer = QByteArray(); } else { // collect all others m_lineBuffer += (*buff); } ++buff; --len; } } void MyMoneyQifReader::slotImportFinished() { // check if the last EOL char was missing and add the trailing line if (!m_lineBuffer.isEmpty()) { m_qifLines << QString::fromUtf8(m_lineBuffer.trimmed()); } qDebug("Read %ld bytes", m_pos); QTimer::singleShot(0, this, SLOT(slotProcessData())); } void MyMoneyQifReader::slotProcessData() { 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); QString format; if (dateFormats.count() > 1) { bool ok; format = QInputDialog::getItem(0, i18n("Date format selection"), i18n("Pick the date format that suits your input file"), dateFormats, 05, false, &ok); if (!ok) { m_userAbort = true; } } else format = dateFormats.first(); if (!format.isEmpty()) { m_qifProfile.setInputDateFormat(format); qDebug("Selected date format: '%s'", qPrintable(format)); } else { // cancel the process because there is probably nothing to work with m_userAbort = true; } 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() { 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.clear(); m_data.clear(); if (m_url.isEmpty()) { return rc; } else if (m_url.isLocalFile()) { m_filename = m_url.toLocalFile(); } else { m_filename = QDir::tempPath(); if(!m_filename.endsWith(QDir::separator())) m_filename += QDir::separator(); m_filename += m_url.fileName(); qDebug() << "Source:" << m_url.toDisplayString() << "Destination:" << m_filename; KIO::FileCopyJob *job = KIO::file_copy(m_url, QUrl::fromUserInput(m_filename), -1, KIO::Overwrite); KJobWidgets::setWindow(job, kmymoney); job->exec(); if (job->error()) { KMessageBox::detailedError(0, i18n("Error while loading file '%1'.", m_url.toDisplayString()), job->errorString(), i18n("File access error")); return rc; } } m_file = new QFile(m_filename); if (m_file->open(QIODevice::ReadOnly)) { #ifdef DEBUG_IMPORT Q_LONG len; while (!m_file->atEnd()) { len = m_file->read(m_buffer, sizeof(m_buffer)); if (len == -1) { qWarning("Failed to read block from QIF import file"); } else { parseReceivedData(QByteArray(m_buffer, len)); } } slotImportFinished(); #else QString program; QStringList arguments; program.clear(); arguments.clear(); // start filter process, use 'cat -' as the default filter if (m_qifProfile.filterScriptImport().isEmpty()) { #ifdef Q_OS_WIN32 //krazy:exclude=cpp // this is the Windows equivalent of 'cat -' but since 'type' does not work with stdin // we pass the filename converted to native separators as a parameter program = "cmd.exe"; arguments << "/c"; arguments << "type"; arguments << QDir::toNativeSeparators(m_filename); #else program = "cat"; arguments << "-"; #endif } else { arguments << m_qifProfile.filterScriptImport().split(' ', QString::KeepEmptyParts); } m_entryType = EntryUnknown; m_filter.setProcessChannelMode(QProcess::MergedChannels); m_filter.start(program, arguments); if (m_filter.waitForStarted()) { signalProgress(0, m_file->size(), i18n("Reading QIF...")); slotSendDataToFilter(); rc = true; } else { KMessageBox::detailedError(0, i18n("Error while running the filter '%1'.", m_filter.program()), m_filter.errorString(), i18n("Filter error")); } #endif } return rc; } bool MyMoneyQifReader::finishImport() { bool rc = false; #ifdef DEBUG_IMPORT delete m_file; m_file = 0; // remove the Don't ask again entries KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group(QString::fromLatin1("Notification Messages")); QStringList::ConstIterator it; for (it = m_dontAskAgain.begin(); it != m_dontAskAgain.end(); ++it) { grp.deleteEntry(*it); } grp.sync(); m_dontAskAgain.clear(); m_accountTranslation.clear(); signalProgress(-1, -1); rc = !m_userAbort; #else if (QProcess::Running != m_filter.state()) { delete m_file; m_file = 0; // remove the Don't ask again entries KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group(QString::fromLatin1("Notification Messages")); QStringList::ConstIterator it; for (it = m_dontAskAgain.constBegin(); it != m_dontAskAgain.constEnd(); ++it) { grp.deleteEntry(*it); } grp.sync(); m_dontAskAgain.clear(); m_accountTranslation.clear(); signalProgress(-1, -1); rc = !m_userAbort && QProcess::NormalExit == m_filter.exitStatus(); } 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 if(!m_url.isLocalFile()) KIO::file_delete(QUrl::fromUserInput(m_filename)); // Now to import the statements QList::const_iterator it_st; for (it_st = d->statements.constBegin(); it_st != d->statements.constEnd(); ++it_st) kmymoney->slotStatementImport(*it_st); return rc; } void MyMoneyQifReader::processQifSpecial(const QString& _line) { QString line = _line.mid(1); // get rid of exclamation mark if (line.left(5).toLower() == QString("type:")) { line = line.mid(5); // exportable accounts if (line.toLower() == "ccard" || KMyMoneyGlobalSettings::qifCreditCard().toLower().contains(line.toLower())) { d->accountType = MyMoneyAccount::CreditCard; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "bank" || KMyMoneyGlobalSettings::qifBank().toLower().contains(line.toLower())) { d->accountType = MyMoneyAccount::Checkings; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "cash" || KMyMoneyGlobalSettings::qifCash().toLower().contains(line.toLower())) { d->accountType = MyMoneyAccount::Cash; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "oth a" || KMyMoneyGlobalSettings::qifAsset().toLower().contains(line.toLower())) { d->accountType = MyMoneyAccount::Asset; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "oth l" || line.toLower() == i18nc("QIF tag for liability account", "Oth L").toLower()) { d->accountType = MyMoneyAccount::Liability; d->firstTransaction = true; d->transactionType = m_entryType = EntryTransaction; } else if (line.toLower() == "invst" || line.toLower() == i18nc("QIF tag for investment account", "Invst").toLower()) { d->accountType = MyMoneyAccount::Investment; d->transactionType = m_entryType = EntryInvestmentTransaction; } else if (line.toLower() == "invoice" || KMyMoneyGlobalSettings::qifInvoice().toLower().contains(line.toLower())) { m_entryType = EntrySkip; } else if (line.toLower() == "tax") { m_entryType = EntrySkip; } else if (line.toLower() == "bill") { m_entryType = EntrySkip; // exportable lists } else if (line.toLower() == "cat" || line.toLower() == i18nc("QIF tag for category", "Cat").toLower()) { m_entryType = EntryCategory; } else if (line.toLower() == "security" || line.toLower() == i18nc("QIF tag for security", "Security").toLower()) { m_entryType = EntrySecurity; } else if (line.toLower() == "prices" || line.toLower() == i18nc("QIF tag for prices", "Prices").toLower()) { m_entryType = EntryPrice; } else if (line.toLower() == "payee") { m_entryType = EntryPayee; } else if (line.toLower() == "memorized") { m_entryType = EntryMemorizedTransaction; } else if (line.toLower() == "class" || line.toLower() == i18nc("QIF tag for a class", "Class").toLower()) { m_entryType = EntryClass; } else if (line.toLower() == "budget") { m_entryType = EntrySkip; } else if (line.toLower() == "invitem") { m_entryType = EntrySkip; } else if (line.toLower() == "template") { m_entryType = EntrySkip; } else { qWarning("Unknown type code '%s' in QIF file on line %d", qPrintable(line), m_linenumber); m_entryType = EntrySkip; } // option headers } else if (line.toLower() == "account") { m_entryType = EntryAccount; } else if (line.toLower() == "option:autoswitch") { m_entryType = EntryAccount; } else if (line.toLower() == "clear:autoswitch") { m_entryType = d->transactionType; } } void MyMoneyQifReader::processQifEntry() { // This method processes a 'QIF Entry' which is everything between two caret // signs // try { switch (m_entryType) { case EntryCategory: processCategoryEntry(); break; case EntryUnknown: qDebug() << "Line " << m_linenumber << ": Warning: Found an entry without a type being specified. Checking assumed."; 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: qDebug() << "Line " << m_linenumber << ": Classes are not yet supported!"; break; case EntryMemorizedTransaction: qDebug() << "Line " << m_linenumber << ": Memorized transactions are not yet implemented!"; break; case EntrySkip: break; default: qDebug() << "Line " << m_linenumber << ": EntryType " << m_entryType << " not yet implemented!"; break; } } catch (const MyMoneyException &e) { if (e.what() != "USERABORT") { qDebug() << "Line " << m_linenumber << ": Unhandled error: " << e.what(); } else { m_userAbort = true; } } } const QString MyMoneyQifReader::extractLine(const QChar& id, int cnt) { QStringList::ConstIterator it; m_extractedLine = -1; for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) { ++m_extractedLine; if ((*it)[0] == id) { if (cnt-- == 1) { return (*it).mid(1); } } } m_extractedLine = -1; return QString(); } bool MyMoneyQifReader::extractSplits(QList& listqSplits) const { // *** With apologies to QString MyMoneyQifReader::extractLine *** QStringList::ConstIterator it; bool ret = false; bool memoPresent = false; int neededCount = 0; qSplit q; for (it = m_qifEntry.constBegin(); it != m_qifEntry.constEnd(); ++it) { if (((*it)[0] == 'S') || ((*it)[0] == '$') || ((*it)[0] == 'E')) { memoPresent = false; // in case no memo line in this split if ((*it)[0] == 'E') { q.m_strMemo = (*it).mid(1); // 'E' = Memo d->fixMultiLineMemo(q.m_strMemo); memoPresent = true; // This transaction contains memo } else if ((*it)[0] == 'S') { q.m_strCategoryName = (*it).mid(1); // 'S' = CategoryName neededCount ++; } else if ((*it)[0] == '$') { q.m_amount = (*it).mid(1); // '$' = Amount neededCount ++; } if (neededCount > 1) { // CategoryName & Amount essential listqSplits += q; // Add valid split if (!memoPresent) { // If no memo, clear previous q.m_strMemo.clear(); } qSplit q; // Start new split neededCount = 0; ret = true; } } } return ret; } #if 0 void MyMoneyQifReader::processMSAccountEntry(const MyMoneyAccount::accountTypeE accountType) { if (extractLine('P').toLower() == m_qifProfile.openingBalanceText().toLower()) { 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( KMyMoneyUtils::mainWindow(), 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?", m_account.name(), split.shares().formatMoney(m_account, sec), balance.formatMoney(m_account, sec)), i18n("Overwrite opening balance"), KStandardGuiItem::yes(), KStandardGuiItem::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() { // TODO } void MyMoneyQifReader::processCategoryEntry() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account = MyMoneyAccount(); account.setName(extractLine('N')); account.setDescription(extractLine('D')); MyMoneyAccount parentAccount; //The extractline routine will more than likely return 'empty', // so also have to test that either the 'I' or 'E' was detected //and set up accounts accordingly. if ((!extractLine('I').isEmpty()) || (m_extractedLine != -1)) { account.setAccountType(MyMoneyAccount::Income); parentAccount = file->income(); } else if ((!extractLine('E').isEmpty()) || (m_extractedLine != -1)) { account.setAccountType(MyMoneyAccount::Expense); parentAccount = file->expense(); } // check if we can find the account already in the file MyMoneyAccount acc = kmymoney->findAccount(account, parentAccount); // if not, we just create it if (acc.id().isEmpty()) { MyMoneyAccount brokerage; MyMoneyMoney balance; kmymoney->createAccount(account, parentAccount, brokerage, balance); } } const QString MyMoneyQifReader::transferAccount(const 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)) { m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(acc.brokerageName()); m_qifEntry << QString("Tunknown"); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); accountId = processAccountEntry(false); } m_qifEntry = tmpEntry; // restore local copies m_account = tmpAccount; return accountId; } void MyMoneyQifReader::createOpeningBalance(MyMoneyAccount::_accountTypeE accType) { MyMoneyFile* file = MyMoneyFile::instance(); // if we don't have a name for the current account we need to extract the name from the L-record if (m_account.name().isEmpty()) { QString name = extractLine('L'); if (name.isEmpty()) { name = i18n("QIF imported, no account name supplied"); } d->isTransfer(name, m_qifProfile.accountDelimiter().left(1), m_qifProfile.accountDelimiter().mid(1, 1)); QStringList entry = m_qifEntry; // keep a temp copy m_qifEntry.clear(); // and construct a temp entry to create/search the account m_qifEntry << QString("N%1").arg(name); m_qifEntry << QString("T%1").arg(d->accountTypeToQif(accType)); m_qifEntry << QString("D%1").arg(i18n("Autogenerated by QIF importer")); processAccountEntry(); m_qifEntry = entry; // restore local copy } MyMoneyFileTransaction ft; try { bool needCreate = true; MyMoneyAccount acc = m_account; // in case we're dealing with an investment account, we better use // the accompanying brokerage account for the opening balance acc = file->accountByName(m_account.brokerageName()); // check if we already have an opening balance transaction QString tid = file->openingBalanceTransaction(acc); MyMoneyTransaction ot; if (!tid.isEmpty()) { ot = file->transaction(tid); MyMoneySplit s0 = ot.splitByAccount(acc.id()); // if the value is the same, we can silently skip this transaction if (s0.shares() == m_qifProfile.value('T', extractLine('T'))) { needCreate = false; } if (needCreate) { // in case we create it anyway, we issue a warning to the user to check it manually KMessageBox::sorry(0, QString("%1").arg(i18n("KMyMoney has imported a second opening balance transaction into account %1 which differs from the one found already on file. Please correct this manually once the import is done.", 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 (const 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")); } } void MyMoneyQifReader::processTransactionEntry() { ++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", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.constFind(hash); if (it == d->m_hashMap.constEnd()) { 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.toLower() == "opening balance") || KMyMoneyGlobalSettings::qifOpeningBalance().toLower().contains(payee.toLower()))) { createOpeningBalance(d->accountType); 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; switch (d->accountType) { case MyMoneyAccount::Checkings: d->st.m_eType=MyMoneyStatement::etCheckings; break; case MyMoneyAccount::Savings: d->st.m_eType=MyMoneyStatement::etSavings; break; case MyMoneyAccount::Investment: d->st.m_eType=MyMoneyStatement::etInvestment; break; case MyMoneyAccount::CreditCard: d->st.m_eType=MyMoneyStatement::etCreditCard; break; default: d->st.m_eType=MyMoneyStatement::etNone; break; } 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.", extractLine('D'), m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch (rc) { case KMessageBox::Continue: tr.m_datePosted = (QDate::currentDate()); break; case KMessageBox::Cancel: throw MYMONEYEXCEPTION("USERABORT"); break; } } tmp = extractLine('L'); pos = tmp.lastIndexOf("--"); 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); // split transaction // ****** ensure each field is ****** // * attached to correct split * QList listqSplits; if (! extractSplits(listqSplits)) { 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.trimmed(); 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) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to/from an investment account. Transaction ignored."; return; } if (account.id() == m_account.id()) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored."; accountId.clear(); } } catch (const MyMoneyException &) { qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found"; accountId.clear(); } } if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = tmp; tr.m_listSplits.append(s2); } } else { int count; for (count = 1; count <= listqSplits.count(); ++count) { // Use true splits 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.lastIndexOf("--"); if (pos != -1) { tmp = tmp.left(pos); } tmp = tmp.trimmed(); 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) { qDebug() << "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()) { qDebug() << "Line " << m_linenumber << ": Cannot transfer to the same account. Transfer ignored."; accountId.clear(); } } catch (const MyMoneyException &) { qDebug() << "Line " << m_linenumber << ": Account with id " << accountId.data() << " not found"; accountId.clear(); } } 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() { // qDebug() << "Investment Transaction:" << m_qifEntry.count() << " lines"; /* 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. */ 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.", extractLine('D'), m_qifProfile.inputDateFormat()), i18n("Invalid date format")); switch (rc) { case KMessageBox::Continue: tr.m_datePosted = QDate::currentDate(); break; case KMessageBox::Cancel: throw 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", qPrintable(m_qifProfile.date(extractLine('D')).toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = d->m_hashMap.constFind(hash); if (it == d->m_hashMap.constEnd()) { 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').toLower(); if (securityname.isEmpty()) { qDebug() << "Line " << m_linenumber << ": Investment transaction without a security is not supported."; 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().toLower(); QString name = security.name().toLower(); 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) { qDebug() << "Line " << m_linenumber << ": Security " << securityname << " not found in this account. Transaction ignored."; // 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.", securityname), i18n("Security not found"), QString("MissingSecurity%1").arg(securityname.trimmed())); 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').toLower(); // remove trailing X, which seems to have no purpose (?!) bool xAction = false; if (action.endsWith('x')) { action = action.left(action.length() - 1); xAction = true; } 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)) { tmp = tmp.remove(QRegExp("[\\[\\]]")); // xAction != true so ignore any'[ and ]' if (!tmp.isEmpty()) { // use 'L' record name tr.m_strBrokerageAccount = tmp; transferAccount(tmp); // make sure the account exists } else { tr.m_strBrokerageAccount = m_account.brokerageName();// use brokerage account transferAccount(m_account.brokerageName()); // make sure the account exists } } else { tmp = tmp.remove(QRegExp("[\\[\\]]")); // xAction != true so ignore any'[ and ]' tr.m_strInterestCategory = tmp; tr.m_strBrokerageAccount = m_account.brokerageName(); } // Whether to create a cash split for the other side of the value QString accountname; //= extractLine('L'); - if (action == "reinvdiv" || action == "reinvlg" || action == "reinvsh") { + if (action == "reinvint" || 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); // 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 -= 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); // 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); } if (action == "intinc") { MyMoneyMoney price = m_qifProfile.value('I', extractLine('I')); tr.m_amount -= tr.m_fees; if ((!quantity.isZero()) && (!price.isZero())) tr.m_amount = -(quantity * price); } else // 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))).trimmed(); } else if (action == "xin" || action == "xout") { QString payee = extractLine('P'); if (!payee.isEmpty() && ((payee.toLower() == "opening balance") || KMyMoneyGlobalSettings::qifOpeningBalance().toLower().contains(payee.toLower()))) { 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") { d->st.m_listPrices += price; tr.m_price = m_qifProfile.value('I', extractLine('I')); tr.m_shares = quantity; tr.m_amount = -amount; tr.m_eAction = (MyMoneyStatement::Transaction::eaBuy); } else if (action == "sell") { d->st.m_listPrices += price; tr.m_price = m_qifProfile.value('I', extractLine('I')); 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 // qDebug() << "Line " << m_linenumber << ": Stock split not supported (date=" << date << " security=" << securityname << " factor=" << splitfactor.toString() << ")"; // s1.setShares(splitfactor); // s1.setValue(0); // s1.setAction(MyMoneySplit::ActionSplitShares); // return; } else { // Unsupported action type qDebug() << "Line " << m_linenumber << ": Unsupported transaction action (" << action << ")"; return; } d->st.m_strAccountName = accountname; // accountname appears not to get set 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.constBegin(); while (it_accid != list.constEnd()) { 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.constBegin(); while (it_accid != list.constEnd()) { 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; } const 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()); kmymoney->createAccount(account, parent, brokerage, MyMoneyMoney()); accountId = account.id(); } } return accountId; } const 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').toLower().remove(QRegExp("\\s+")); if (type == m_qifProfile.profileType().toLower().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); qDebug() << "Line " << m_linenumber << ": Unknown account type '" << type << "', checkings assumed"; } // check if we can find the account already in the file MyMoneyAccount acc = kmymoney->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)", 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(); kmymoney->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 // but only create it (not here, but later) if it is needed if (account.accountType() == MyMoneyAccount::Investment) { brokerage.setName(QString()); // brokerage name empty so account not created yet brokerage.setAccountType(MyMoneyAccount::Checkings); brokerage.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); } kmymoney->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(); // needed here for account selection d->transactionType = transactionType; } return acc.id(); } void MyMoneyQifReader::selectOrCreateAccount(const SelectCreateMode mode, MyMoneyAccount& account, const MyMoneyMoney& balance) { MyMoneyFile* file = MyMoneyFile::instance(); QString accountId; QString msg; QString typeStr; QString leadIn; KMyMoneyUtils::categoryTypeE type; QMap::ConstIterator it; type = KMyMoneyUtils::none; switch (account.accountGroup()) { default: type = KMyMoneyUtils::asset; type = (KMyMoneyUtils::categoryTypeE)(type | KMyMoneyUtils::liability); typeStr = i18n("account"); leadIn = i18n("al"); break; case MyMoneyAccount::Income: case MyMoneyAccount::Expense: type = KMyMoneyUtils::income; type = (KMyMoneyUtils::categoryTypeE)(type | KMyMoneyUtils::expense); typeStr = i18n("category"); leadIn = i18n("ei"); msg = i18n("Category selection"); break; } QPointer accountSelect = new KAccountSelectDlg(type, "QifImport", kmymoney); if (!msg.isEmpty()) accountSelect->setWindowTitle(msg); it = m_accountTranslation.constFind(QString(leadIn + MyMoneyFile::AccountSeperator + account.name()).toLower()); if (it != m_accountTranslation.constEnd()) { try { account = file->account(*it); delete accountSelect; return; } catch (const MyMoneyException &e) { const QString message(i18n("Account \"%1\" disappeared: %2", account.name(), e.what())); KMessageBox::error(0, message); } } // This is so the QPointer to the dialog gets properly destroyed if something throws. try { 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); delete accountSelect; return; } else { switch (KMessageBox::questionYesNo(0, i18nc("The 'type of object' 'x' does not exist", "The %1 '%2' does not exist. Do you " "want to create it?", typeStr, account.name()))) { case KMessageBox::Yes: break; case KMessageBox::No: default: delete accountSelect; return; } } } else { accountSelect->setHeader(i18nc("To select account", "Select %1", typeStr)); if (!accountId.isEmpty()) { msg = i18n("The %1 %2 currently exists. Do you want " "to import transactions to this account?", typeStr, account.name()); } else { msg = i18n("The %1 %2 currently does not exist. You can " "create a new %3 by pressing the Create button " "or select another %4 manually from the selection box.", typeStr, account.name(), typeStr, 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->setTextColor(QColor("red")); accountSelect->m_qifEntry->append(*it_e); accountSelect->m_qifEntry->setTextColor(QColor("black")); ++i; } for (;;) { if (accountSelect->exec() == QDialog::Accepted) { if (!accountSelect->selectedAccount().isEmpty()) { accountId = accountSelect->selectedAccount(); m_accountTranslation[QString(leadIn + MyMoneyFile::AccountSeperator + account.name()).toLower()] = 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( KMyMoneyUtils::mainWindow(), 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?", account.name(), MyMoneyUtils::formatMoney(split.shares(), account, sec), MyMoneyUtils::formatMoney(balance, account, sec)), i18n("Overwrite opening balance"), KStandardGuiItem::yes(), KStandardGuiItem::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 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.")); } } } } catch (...) { // cleanup the dialog pointer. delete accountSelect; throw; } delete accountSelect; } 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() { /* !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.constBegin(); // Make a price for each line QRegExp priceExp("\"(.*)\",(.*),\"(.*)\""); while (it_line != m_qifEntry.constEnd()) { if (priceExp.indexIn(*it_line) != -1) { MyMoneyStatement::Price price; price.m_strSecurity = priceExp.cap(1); QString pricestr = priceExp.cap(2); QString datestr = priceExp.cap(3); qDebug() << "Price:" << price.m_strSecurity << " / " << pricestr << " / " << datestr; // 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() { /* !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; } diff --git a/kmymoney/converter/mymoneystatementreader.cpp b/kmymoney/converter/mymoneystatementreader.cpp index c8c70b056..b04bcba56 100644 --- a/kmymoney/converter/mymoneystatementreader.cpp +++ b/kmymoney/converter/mymoneystatementreader.cpp @@ -1,1535 +1,1537 @@ /*************************************************************************** mymoneystatementreader.cpp ------------------- begin : Mon Aug 30 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneystatementreader.h" #include // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include #include #include #include #include #include #include "kaccountselectdlg.h" #include "transactionmatcher.h" #include "kenterscheduledlg.h" #include "kmymoney.h" #include "kmymoneyaccountcombo.h" #include "models.h" #include "existingtransactionmatchfinder.h" #include "scheduledtransactionmatchfinder.h" bool matchNotEmpty(const QString &l, const QString &r) { return !l.isEmpty() && QString::compare(l, r, Qt::CaseInsensitive) == 0; } 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 expenseId(const QString& name); QString feeId(const QString& name); void assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in); void setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate); MyMoneyAccount lastAccount; QList transactions; QList payees; int transactionsCount; int transactionsAdded; int transactionsMatched; int transactionsDuplicate; QMap uniqIds; QMap securitiesBySymbol; QMap securitiesByName; bool m_skipCategoryMatching; private: void scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName); /** * This method tries to figure out the category to be used for fees and interest * from previous transactions in the given @a investmentAccount and returns the * ids of those categories in @a feesId and @a interestId. The last used category * will be returned. */ void previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId); 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) { // Adapted from KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) // Needed to find/create category:sub-categories MyMoneyFile* file = MyMoneyFile::instance(); QString id = file->categoryToAccount(name, MyMoneyAccount::UnknownAccountType); // if it does not exist, we have to create it if (id.isEmpty()) { MyMoneyAccount newAccount; MyMoneyAccount parentAccount = parent; newAccount.setName(name) ; int pos; // check for ':' in the name and use it as separator for a hierarchy while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeperator)) != -1) { QString part = newAccount.name().left(pos); QString remainder = newAccount.name().mid(pos + 1); const MyMoneyAccount& existingAccount = file->subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { newAccount.setName(part); newAccount.setAccountType(parentAccount.accountType()); file->addAccount(newAccount, parentAccount); parentAccount = newAccount; } else { parentAccount = existingAccount; } newAccount.setParentAccountId(QString()); // make sure, there's no parent newAccount.clearId(); // and no id set for adding newAccount.removeAccountIds(); // and no sub-account ids newAccount.setName(remainder); }//end while newAccount.setAccountType(parentAccount.accountType()); // make sure we have a currency. If none is assigned, we assume base currency if (newAccount.currencyId().isEmpty()) newAccount.setCurrencyId(file->baseCurrency().id()); file->addAccount(newAccount, parentAccount); id = newAccount.id(); } return id; } QString MyMoneyStatementReader::Private::expenseId(const QString& name) { MyMoneyAccount parent = MyMoneyFile::instance()->expense(); return nameToId(name, parent); } 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::previouslyUsedCategories(const QString& investmentAccount, QString& feesId, QString& interestId) { feesId.clear(); interestId.clear(); MyMoneyFile* file = MyMoneyFile::instance(); try { MyMoneyAccount acc = file->account(investmentAccount); MyMoneyTransactionFilter filter(investmentAccount); filter.setReportAllSplits(false); // since we assume an investment account here, we need to collect the stock accounts as well filter.addAccount(acc.accountList()); QList< QPair > list; file->transactionList(list, filter); QList< QPair >::const_iterator it_t; for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { const MyMoneyTransaction& t = (*it_t).first; MyMoneySplit s = (*it_t).second; MyMoneyAccount acc = file->account(s.accountId()); // stock split shouldn't be fee or interest bacause it won't play nice with dissectTransaction // it was caused by processTransactionEntry adding splits in wrong order != with manual transaction entering if (acc.accountGroup() == MyMoneyAccount::Expense || acc.accountGroup() == MyMoneyAccount::Income) { foreach (auto sNew , t.splits()) { acc = file->account(sNew.accountId()); if (acc.accountGroup() != MyMoneyAccount::Expense && // shouldn't be fee acc.accountGroup() != MyMoneyAccount::Income && // shouldn't be interest (sNew.value() != sNew.shares() || // shouldn't be checking account... (sNew.value() == sNew.shares() && sNew.price() != MyMoneyMoney::ONE))) { // ...but sometimes it may look like checking account s = sNew; break; } } } MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency; MyMoneySplit::investTransactionTypeE transactionType; KMyMoneyUtils::dissectTransaction(t, s, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); if (!feeSplits.isEmpty()) { feesId = feeSplits.first().accountId(); if (!interestId.isEmpty()) break; } if (!interestSplits.isEmpty()) { interestId = interestSplits.first().accountId(); if (!feesId.isEmpty()) break; } } } catch (const MyMoneyException &) { } } void MyMoneyStatementReader::Private::scanCategories(QString& id, const MyMoneyAccount& invAcc, const MyMoneyAccount& parentAccount, const QString& defaultName) { if (!scannedCategories) { previouslyUsedCategories(invAcc.id(), m_feeId, m_interestId); scannedCategories = true; } if (id.isEmpty()) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount acc = file->accountByName(defaultName); // if it does not exist, we have to create it if (acc.id().isEmpty()) { MyMoneyAccount parent = parentAccount; acc.setName(defaultName); acc.setAccountType(parent.accountType()); acc.setCurrencyId(parent.currencyId()); file->addAccount(acc, parent); } id = acc.id(); } } void MyMoneyStatementReader::Private::assignUniqueBankID(MyMoneySplit& s, const MyMoneyStatement::Transaction& t_in) { if (! t_in.m_strBankID.isEmpty()) { // make sure that id's are unique from this point on by appending a -# // postfix if needed QString base(t_in.m_strBankID); QString hash(base); int idx = 1; for (;;) { QMap::const_iterator it; it = uniqIds.constFind(hash); if (it == uniqIds.constEnd()) { uniqIds[hash] = true; break; } hash = QString("%1-%2").arg(base).arg(idx); ++idx; } s.setBankID(hash); } } void MyMoneyStatementReader::Private::setupPrice(MyMoneySplit &s, const MyMoneyAccount &splitAccount, const MyMoneyAccount &transactionAccount, const QDate &postDate) { if (transactionAccount.currencyId() != splitAccount.currencyId()) { // a currency converstion is needed assume that split has already a proper value MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity toCurrency = file->security(splitAccount.currencyId()); MyMoneySecurity fromCurrency = file->security(transactionAccount.currencyId()); // get the price for the transaction's date const MyMoneyPrice &price = file->price(fromCurrency.id(), toCurrency.id(), postDate); // if the price is valid calculate the shares if (price.isValid()) { const int fract = splitAccount.fraction(toCurrency); const MyMoneyMoney &shares = s.value() * price.rate(toCurrency.id()); s.setShares(shares.convert(fract)); qDebug("Setting second split shares to %s", qPrintable(s.shares().formatMoney(toCurrency.id(), 2))); } else { qDebug("No price entry was found to convert from '%s' to '%s' on '%s'", qPrintable(fromCurrency.tradingSymbol()), qPrintable(toCurrency.tradingSymbol()), qPrintable(postDate.toString(Qt::ISODate))); } } } 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() 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_brokerageAccount = 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 (const MyMoneyException &) { qDebug("Received reference '%s' to unknown account in statement", qPrintable(s.m_accountId)); } } 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(kmymoney, 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::UnknownAccountType); 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 (const MyMoneyException &) { qDebug("Updating account in MyMoneyStatementReader::startImport failed"); } } if (!m_account.name().isEmpty()) messages += i18n("Importing statement for account %1", m_account.name()); else if (s.m_listTransactions.count() == 0) messages += i18n("Importing statement without transactions"); qDebug("Importing statement for '%s'", qPrintable(m_account.name())); // // Process the securities // signalProgress(0, s.m_listSecurities.count(), "Importing Statement ..."); int progress = 0; QList::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)", qPrintable(m_account.name())); signalProgress(0, s.m_listTransactions.count(), "Importing Statement ..."); int progress = 0; QList::const_iterator it_t = s.m_listTransactions.begin(); while (it_t != s.m_listTransactions.end() && !m_userAbort) { processTransactionEntry(*it_t); signalProgress(++progress, 0); ++it_t; } qDebug("Processing transactions done (%s)", qPrintable(m_account.name())); } catch (const MyMoneyException &e) { if (e.what() == "USERABORT") m_userAbort = true; else qDebug("Caught exception from processTransactionEntry() not caused by USERABORT: %s", qPrintable(e.what())); } signalProgress(-1, -1); } // // process price entries // if (!m_userAbort) { try { signalProgress(0, s.m_listPrices.count(), "Importing Statement ..."); QList slist = MyMoneyFile::instance()->securityList(); QList::const_iterator it_s; for (it_s = slist.constBegin(); it_s != slist.constEnd(); ++it_s) { d->securitiesBySymbol[(*it_s).tradingSymbol()] = *it_s; d->securitiesByName[(*it_s).name()] = *it_s; } int progress = 0; QList::const_iterator it_p = s.m_listPrices.begin(); while (it_p != s.m_listPrices.end()) { processPriceEntry(*it_p); signalProgress(++progress, 0); ++it_p; } } catch (const MyMoneyException &e) { if (e.what() == "USERABORT") m_userAbort = true; else qDebug("Caught exception from processPriceEntry() not caused by USERABORT: %s", qPrintable(e.what())); } signalProgress(-1, -1); } bool rc = false; // delete all payees created in vain int payeeCount = d->payees.count(); QList::const_iterator it_p; for (it_p = d->payees.constBegin(); it_p != d->payees.constEnd(); ++it_p) { try { MyMoneyFile::instance()->removePayee(*it_p); --payeeCount; } catch (const MyMoneyException &) { // if we can't delete it, it must be in use which is ok for us } } 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", s.m_dateEnd.toString(Qt::ISODate), s.m_closingBalance.formatMoney("", 2)); } messages += i18n(" Transactions"); messages += i18np(" %1 processed", " %1 processed", d->transactionsCount); messages += i18ncp("x transactions have been added", " %1 added", " %1 added", d->transactionsAdded); messages += i18np(" %1 matched", " %1 matched", d->transactionsMatched); messages += i18np(" %1 duplicate", " %1 duplicates", d->transactionsDuplicate); messages += i18n(" Payees"); messages += i18ncp("x transactions have been created", " %1 created", " %1 created", payeeCount); messages += QString(); // remove the Don't ask again entries KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group(QString::fromLatin1("Notification Messages")); QStringList::ConstIterator it; for (it = m_dontAskAgain.constBegin(); it != m_dontAskAgain.constEnd(); ++it) { grp.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", qPrintable(m_account.name())); return rc; } void MyMoneyStatementReader::processPriceEntry(const MyMoneyStatement::Price& p_in) { MyMoneyFile* file = MyMoneyFile::instance(); QString currency = file->baseCurrency().id(); QString security; if (!p_in.m_strCurrency.isEmpty()) { security = p_in.m_strSecurity; currency = p_in.m_strCurrency; } else if (d->securitiesBySymbol.contains(p_in.m_strSecurity)) { security = d->securitiesBySymbol[p_in.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else if (d->securitiesByName.contains(p_in.m_strSecurity)) { security = d->securitiesByName[p_in.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else return; MyMoneyPrice price(security, currency, p_in.m_date, p_in.m_amount, p_in.m_sourceName.isEmpty() ? i18n("Prices Importer") : p_in.m_sourceName); 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; QList list = file->securityList(); QList::ConstIterator it = list.constBegin(); while (it != list.constEnd() && security.id().isEmpty()) { if (matchNotEmpty(sec_in.m_strSymbol, (*it).tradingSymbol()) || matchNotEmpty(sec_in.m_strName, (*it).name())) { 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.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(); qDebug() << "Created " << security.name() << " with id " << security.id(); } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Error creating security record: %1", e.what()), i18n("Error")); } } else { qDebug() << "Found " << security.name() << " with id " << security.id(); } } void MyMoneyStatementReader::processTransactionEntry(const MyMoneyStatement::Transaction& statementTransactionUnderImport) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyTransaction transactionUnderImport; QString dbgMsg; dbgMsg = QString("Process on: '%1', id: '%3', amount: '%2', fees: '%4'") .arg(statementTransactionUnderImport.m_datePosted.toString(Qt::ISODate)) .arg(statementTransactionUnderImport.m_amount.formatMoney("", 2)) .arg(statementTransactionUnderImport.m_strBankID) .arg(statementTransactionUnderImport.m_fees.formatMoney("", 2)); qDebug("%s", qPrintable(dbgMsg)); // mark it imported for the view transactionUnderImport.setImported(); // TODO (Ace) We can get the commodity from the statement!! // Although then we would need UI to verify transactionUnderImport.setCommodity(m_account.currencyId()); transactionUnderImport.setPostDate(statementTransactionUnderImport.m_datePosted); transactionUnderImport.setMemo(statementTransactionUnderImport.m_strMemo); MyMoneySplit s1; MyMoneySplit s2; MyMoneySplit sFees; MyMoneySplit sBrokerage; s1.setMemo(statementTransactionUnderImport.m_strMemo); s1.setValue(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees); s1.setShares(s1.value()); s1.setNumber(statementTransactionUnderImport.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").toUtf8(); if (brokerageactid.isEmpty()) { brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id(); } if (brokerageactid.isEmpty()) { brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount); } if (brokerageactid.isEmpty()) { brokerageactid = file->nameToAccount(thisaccount.brokerageName()); } if (brokerageactid.isEmpty()) { brokerageactid = SelectBrokerageAccount(); } // find the security transacted, UNLESS this transaction didn't // involve any security. if ((statementTransactionUnderImport.m_eAction != MyMoneyStatement::Transaction::eaNone) // eaInterest transactions MAY have a security. // && (t_in.m_eAction != MyMoneyStatement::Transaction::eaInterest) && (statementTransactionUnderImport.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.constBegin(); QString currencyid; while (!found && it_account != accounts.constEnd()) { currencyid = file->account(*it_account).currencyId(); MyMoneySecurity security = file->security(currencyid); if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, security.tradingSymbol()) || matchNotEmpty(statementTransactionUnderImport.m_strSecurity, security.name())) { thisaccount = file->account(*it_account); found = true; } ++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 (statementTransactionUnderImport.m_strSecurity.isEmpty()) { KMessageBox::information(0, i18n("This imported statement contains investment transactions with no security. These transactions will be ignored."), i18n("Security not found"), QString("BlankSecurity")); return; } else { MyMoneySecurity security; QList list = MyMoneyFile::instance()->securityList(); QList::ConstIterator it = list.constBegin(); while (it != list.constEnd() && security.id().isEmpty()) { if (matchNotEmpty(statementTransactionUnderImport.m_strSymbol, (*it).tradingSymbol()) || matchNotEmpty(statementTransactionUnderImport.m_strSecurity, (*it).name())) { security = *it; } ++it; } if (!security.id().isEmpty()) { thisaccount = MyMoneyAccount(); thisaccount.setName(security.name()); thisaccount.setAccountType(MyMoneyAccount::Stock); thisaccount.setCurrencyId(security.id()); currencyid = thisaccount.currencyId(); file->addAccount(thisaccount, m_account); qDebug() << Q_FUNC_INFO << ": created account " << thisaccount.id() << " for security " << statementTransactionUnderImport.m_strSecurity << " under account " << m_account.id(); } // 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. int ret = KMessageBox::warningContinueCancel(0, i18n("
This investment account does not contain the \"%1\" security.
" "
Transactions involving this security will be ignored.
", statementTransactionUnderImport.m_strSecurity), i18n("Security not found"), KStandardGuiItem::cont(), KStandardGuiItem::cancel()); if (ret == KMessageBox::Cancel) { m_userAbort = true; } return; } } } // Don't update price if there is no price information contained in the transaction if (statementTransactionUnderImport.m_eAction != MyMoneyStatement::Transaction::eaCashDividend && statementTransactionUnderImport.m_eAction != MyMoneyStatement::Transaction::eaShrsin && statementTransactionUnderImport.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(); const MyMoneyPrice &price = file->price(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, true); if (!price.isValid() && ((!statementTransactionUnderImport.m_amount.isZero() && !statementTransactionUnderImport.m_shares.isZero()) || !statementTransactionUnderImport.m_price.isZero())) { MyMoneyPrice newprice; if (!statementTransactionUnderImport.m_price.isZero()) { newprice = MyMoneyPrice(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, statementTransactionUnderImport.m_price.abs(), i18n("Statement Importer")); } else { newprice = MyMoneyPrice(currencyid, basecurrencyid, statementTransactionUnderImport.m_datePosted, (statementTransactionUnderImport.m_amount / statementTransactionUnderImport.m_shares).abs(), i18n("Statement Importer")); } file->addPrice(newprice); } } } s1.setAccountId(thisaccount.id()); d->assignUniqueBankID(s1, statementTransactionUnderImport); if (statementTransactionUnderImport.m_eAction == MyMoneyStatement::Transaction::eaReinvestDividend) { s1.setAction(MyMoneySplit::ActionReinvestDividend); s1.setShares(statementTransactionUnderImport.m_shares); if (!statementTransactionUnderImport.m_price.isZero()) { s1.setPrice(statementTransactionUnderImport.m_price); } else { if (statementTransactionUnderImport.m_shares.isZero()) { KMessageBox::information(0, i18n("This imported statement contains investment transactions with no share amount. These transactions will be ignored."), i18n("No share amount provided"), QString("BlankAmount")); return; } MyMoneyMoney total = -statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees; s1.setPrice((total / statementTransactionUnderImport.m_shares).convertPrecision(file->security(thisaccount.currencyId()).pricePrecision())); } s2.setMemo(statementTransactionUnderImport.m_strMemo); if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s2.setAccountId(d->interestId(thisaccount)); else s2.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); s2.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); s2.setValue(s2.shares()); } else if (statementTransactionUnderImport.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. if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s1.setAccountId(d->interestId(thisaccount)); else {// Ensure category sub-accounts are dealt with properly s1.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); } s1.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); s1.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); // 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. s2.setMemo(statementTransactionUnderImport.m_strMemo); s2.setAction(MyMoneySplit::ActionDividend); s2.setAccountId(thisaccount.id()); /* at this point any fees have been taken into account already * so don't deduct them again. * BUG 322381 */ transfervalue = statementTransactionUnderImport.m_amount; } else if (statementTransactionUnderImport.m_eAction == MyMoneyStatement::Transaction::eaInterest) { if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s1.setAccountId(d->interestId(thisaccount)); else {// Ensure category sub-accounts are dealt with properly if (statementTransactionUnderImport.m_amount.isPositive()) s1.setAccountId(d->interestId(statementTransactionUnderImport.m_strInterestCategory)); else s1.setAccountId(d->expenseId(statementTransactionUnderImport.m_strInterestCategory)); } s1.setShares(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); s1.setValue(-statementTransactionUnderImport.m_amount - statementTransactionUnderImport.m_fees); /// *********** Add split as per Div ********** // 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. s2.setMemo(statementTransactionUnderImport.m_strMemo); s2.setAction(MyMoneySplit::ActionInterestIncome); s2.setAccountId(thisaccount.id()); transfervalue = statementTransactionUnderImport.m_amount; } else if (statementTransactionUnderImport.m_eAction == MyMoneyStatement::Transaction::eaFees) { if (statementTransactionUnderImport.m_strInterestCategory.isEmpty()) s1.setAccountId(d->feeId(thisaccount)); else// Ensure category sub-accounts are dealt with properly s1.setAccountId(d->feeId(statementTransactionUnderImport.m_strInterestCategory)); s1.setShares(statementTransactionUnderImport.m_amount); s1.setValue(statementTransactionUnderImport.m_amount); transfervalue = statementTransactionUnderImport.m_amount; } else if ((statementTransactionUnderImport.m_eAction == MyMoneyStatement::Transaction::eaBuy) || (statementTransactionUnderImport.m_eAction == MyMoneyStatement::Transaction::eaSell)) { s1.setAction(MyMoneySplit::ActionBuyShares); if (!statementTransactionUnderImport.m_price.isZero()) { s1.setPrice(statementTransactionUnderImport.m_price.abs()); } else if (!statementTransactionUnderImport.m_shares.isZero()) { MyMoneyMoney total = statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs(); s1.setPrice((total / statementTransactionUnderImport.m_shares).abs().convertPrecision(file->security(thisaccount.currencyId()).pricePrecision())); } if (statementTransactionUnderImport.m_eAction == MyMoneyStatement::Transaction::eaBuy) s1.setShares(statementTransactionUnderImport.m_shares.abs()); else s1.setShares(-statementTransactionUnderImport.m_shares.abs()); s1.setValue(-(statementTransactionUnderImport.m_amount + statementTransactionUnderImport.m_fees.abs())); transfervalue = statementTransactionUnderImport.m_amount; } else if ((statementTransactionUnderImport.m_eAction == MyMoneyStatement::Transaction::eaShrsin) || (statementTransactionUnderImport.m_eAction == MyMoneyStatement::Transaction::eaShrsout)) { s1.setValue(MyMoneyMoney()); s1.setShares(statementTransactionUnderImport.m_shares); s1.setAction(MyMoneySplit::ActionAddShares); } else if (statementTransactionUnderImport.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").toUtf8(); if (brokerageactid.isEmpty()) { brokerageactid = file->accountByName(m_account.brokerageName()).id(); } if (! brokerageactid.isEmpty()) { s1.setAccountId(brokerageactid); d->assignUniqueBankID(s1, statementTransactionUnderImport); // Needed to satisfy the bankid check below. thisaccount = file->account(brokerageactid); } else { // Warning!! Your transaction is being thrown away. } } if (!statementTransactionUnderImport.m_fees.isZero()) { sFees.setMemo(i18n("(Fees) %1", statementTransactionUnderImport.m_strMemo)); sFees.setValue(statementTransactionUnderImport.m_fees); sFees.setShares(statementTransactionUnderImport.m_fees); sFees.setAccountId(d->feeId(thisaccount)); } } 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, statementTransactionUnderImport); } QString payeename = statementTransactionUnderImport.m_strPayee; if (!payeename.isEmpty()) { qDebug() << QLatin1String("Start matching payee") << payeename; QString payeeid; try { QList pList = file->payeeList(); QList::const_iterator it_p; QMap matchMap; for (it_p = pList.constBegin(); it_p != pList.constEnd(); ++it_p) { bool ignoreCase; QStringList keys; QStringList::const_iterator it_s; const MyMoneyPayee::payeeMatchType matchType = (*it_p).matchData(ignoreCase, keys); switch (matchType) { case MyMoneyPayee::matchDisabled: break; case MyMoneyPayee::matchName: case MyMoneyPayee::matchNameExact: keys << QString("%1").arg(QRegExp::escape((*it_p).name())); if(matchType == MyMoneyPayee::matchNameExact) { keys.clear(); keys << QString("^%1$").arg(QRegExp::escape((*it_p).name())); } // intentional fall through case MyMoneyPayee::matchKey: for (it_s = keys.constBegin(); it_s != keys.constEnd(); ++it_s) { QRegExp exp(*it_s, ignoreCase ? Qt::CaseInsensitive : Qt::CaseSensitive); if (exp.indexIn(payeename) != -1) { qDebug("Found match with '%s' on '%s'", qPrintable(payeename), qPrintable((*it_p).name())); 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) { qDebug("Multiple matches"); QMap::const_iterator it_m = matchMap.constEnd(); --it_m; payeeid = *it_m; } else if (matchMap.count() == 1) { qDebug("Single matches"); payeeid = *(matchMap.constBegin()); } // if we did not find a matching payee, we throw an exception and try to create it if (payeeid.isEmpty()) throw MYMONEYEXCEPTION("payee not matched"); s1.setPayeeId(payeeid); } catch (const MyMoneyException &) { 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", 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 \"Do not ask " "again\" checkbox, the payee information for all following transactions " "referencing \"%1\" will be removed.", 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"), KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(), askKey); } 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::matchKey, true, QStringList() << QString("^%1$").arg(QRegExp::escape(payeename))); if (m_askPayeeCategory) { // We use a QPointer 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. QPointer dialog = new QDialog(0); dialog->setWindowTitle(i18n("Default Category for Payee")); dialog->setModal(true); QWidget *mainWidget = new QWidget; QVBoxLayout *topcontents = new QVBoxLayout(mainWidget); //add in caption? and account combo here QLabel *label1 = new QLabel(i18n("Please select a default category for payee '%1'", payeename)); topcontents->addWidget(label1); AccountNamesFilterProxyModel *filterProxyModel = new AccountNamesFilterProxyModel(this); + filterProxyModel->setHideEquityAccounts(!KMyMoneyGlobalSettings::expertMode()); filterProxyModel->addAccountGroup(MyMoneyAccount::Asset); filterProxyModel->addAccountGroup(MyMoneyAccount::Liability); + filterProxyModel->addAccountGroup(MyMoneyAccount::Equity); filterProxyModel->addAccountGroup(MyMoneyAccount::Income); filterProxyModel->addAccountGroup(MyMoneyAccount::Expense); filterProxyModel->setSourceModel(Models::instance()->accountsModel()); filterProxyModel->sort(0); QPointer accountCombo = new KMyMoneyAccountCombo(filterProxyModel, dialog); topcontents->addWidget(accountCombo); mainWidget->setLayout(topcontents); QVBoxLayout *mainLayout = new QVBoxLayout; QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel|QDialogButtonBox::No|QDialogButtonBox::Yes); dialog->setLayout(mainLayout); mainLayout->addWidget(mainWidget); dialog->connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept())); dialog->connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject())); mainLayout->addWidget(buttonBox); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Save Category"))); KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KGuiItem(i18n("No Category"))); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KGuiItem(i18n("Abort"))); int result = dialog->exec(); QString accountId; if (accountCombo && !accountCombo->getSelected().isEmpty()) { accountId = accountCombo->getSelected(); } delete dialog; //if they hit yes instead of no, then grab setting of account combo if (result == QDialog::Accepted) { payee.setDefaultAccountId(accountId); } else if (result != QDialog::Rejected) { //add cancel button? and throw exception like below throw MYMONEYEXCEPTION("USERABORT"); } } try { file->addPayee(payee); qDebug("Payee '%s' created", qPrintable(payee.name())); d->payees << payee; payeeid = payee.id(); s1.setPayeeId(payeeid); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to add payee/receiver"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } else if (rc == KMessageBox::No) { s1.setPayeeId(QString()); } else { throw 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 (statementTransactionUnderImport.m_listSplits.isEmpty() && payeeObj.defaultAccountEnabled()) { MyMoneyAccount splitAccount = file->account(payeeObj.defaultAccountId()); MyMoneySplit s; s.setReconcileFlag(MyMoneySplit::Cleared); s.clearId(); s.setBankID(QString()); s.setShares(-s1.shares()); s.setValue(-s1.value()); s.setAccountId(payeeObj.defaultAccountId()); d->setupPrice(s, splitAccount, m_account, statementTransactionUnderImport.m_datePosted); transactionUnderImport.addSplit(s); file->addVATSplit(transactionUnderImport, m_account, splitAccount, statementTransactionUnderImport.m_amount); } else if (statementTransactionUnderImport.m_listSplits.isEmpty() && !d->m_skipCategoryMatching) { MyMoneyTransactionFilter filter(thisaccount.id()); filter.addPayee(payeeid); QList 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) { QList::ConstIterator it_trans = list.constEnd(); if (it_trans != list.constBegin()) --it_trans; while (it_trans != list.constBegin()) { MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id()); if (s.value() == s1.value()) { // keep searching if this transaction references a closed account if (!MyMoneyFile::instance()->referencesClosedAccount(*it_trans)) { t_old = *it_trans; break; } } --it_trans; } // check constBegin, just in case if (it_trans == list.constBegin()) { MyMoneySplit s = (*it_trans).splitByAccount(thisaccount.id()); if (s.value() == s1.value()) { t_old = *it_trans; } } } // Only copy the splits if the transaction found does not reference a closed account if (!MyMoneyFile::instance()->referencesClosedAccount(t_old)) { QList::ConstIterator it_split; for (it_split = t_old.splits().constBegin(); it_split != t_old.splits().constEnd(); ++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()); s.removeMatch(); if (t_old.splits().count() == 2) { s.setShares(-s1.shares()); s.setValue(-s1.value()); s.setMemo(s1.memo()); } MyMoneyAccount splitAccount = file->account(s.accountId()); qDebug("Adding second split to %s(%s)", qPrintable(splitAccount.name()), qPrintable(s.accountId())); d->setupPrice(s, splitAccount, m_account, statementTransactionUnderImport.m_datePosted); transactionUnderImport.addSplit(s); } } } } } } } s1.setReconcileFlag(statementTransactionUnderImport.m_reconcile); // 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 // but if brokerageactid has already been set, keep that. if (!statementTransactionUnderImport.m_strBrokerageAccount.isEmpty() && brokerageactid.isEmpty()) { brokerageactid = file->nameToAccount(statementTransactionUnderImport.m_strBrokerageAccount); } if (brokerageactid.isEmpty()) { brokerageactid = file->accountByName(statementTransactionUnderImport.m_strBrokerageAccount).id(); } // There is no BrokerageAccount so have to nowhere to put this split. if (!brokerageactid.isEmpty()) { sBrokerage.setMemo(statementTransactionUnderImport.m_strMemo); sBrokerage.setValue(transfervalue); sBrokerage.setShares(transfervalue); sBrokerage.setAccountId(brokerageactid); sBrokerage.setReconcileFlag(statementTransactionUnderImport.m_reconcile); MyMoneyAccount splitAccount = file->account(sBrokerage.accountId()); d->setupPrice(sBrokerage, splitAccount, m_account, statementTransactionUnderImport.m_datePosted); } } if (!(sBrokerage == MyMoneySplit())) transactionUnderImport.addSplit(sBrokerage); if (!(sFees == MyMoneySplit())) transactionUnderImport.addSplit(sFees); if (!(s2 == MyMoneySplit())) transactionUnderImport.addSplit(s2); transactionUnderImport.addSplit(s1); if ((statementTransactionUnderImport.m_eAction != MyMoneyStatement::Transaction::eaReinvestDividend) && (statementTransactionUnderImport.m_eAction != MyMoneyStatement::Transaction::eaCashDividend) && (statementTransactionUnderImport.m_eAction != MyMoneyStatement::Transaction::eaInterest) ) { //****************************************** // process splits //****************************************** QList::const_iterator it_s; for (it_s = statementTransactionUnderImport.m_listSplits.begin(); it_s != statementTransactionUnderImport.m_listSplits.end(); ++it_s) { MyMoneySplit s3; s3.setAccountId((*it_s).m_accountId); MyMoneyAccount acc = file->account(s3.accountId()); s3.setPayeeId(s1.payeeId()); s3.setMemo((*it_s).m_strMemo); s3.setShares((*it_s).m_amount); s3.setValue((*it_s).m_amount); s3.setReconcileFlag((*it_s).m_reconcile); d->setupPrice(s3, acc, m_account, statementTransactionUnderImport.m_datePosted); transactionUnderImport.addSplit(s3); } } // Add the transaction try { // check for matches already stored in the engine TransactionMatchFinder::MatchResult result; TransactionMatcher matcher(thisaccount); d->transactionsCount++; ExistingTransactionMatchFinder existingTrMatchFinder(KMyMoneyGlobalSettings::matchInterval()); result = existingTrMatchFinder.findMatch(transactionUnderImport, s1); if (result != TransactionMatchFinder::MatchNotFound) { MyMoneyTransaction matchedTransaction = existingTrMatchFinder.getMatchedTransaction(); if (!matchedTransaction.isImported() || result == TransactionMatchFinder::MatchPrecise) { // don't match with just imported transaction MyMoneySplit matchedSplit = existingTrMatchFinder.getMatchedSplit(); handleMatchingOfExistingTransaction(matcher, matchedTransaction, matchedSplit, transactionUnderImport, s1, result); return; } } addTransaction(transactionUnderImport); ScheduledTransactionMatchFinder scheduledTrMatchFinder(thisaccount, KMyMoneyGlobalSettings::matchInterval()); result = scheduledTrMatchFinder.findMatch(transactionUnderImport, s1); if (result != TransactionMatchFinder::MatchNotFound) { MyMoneySplit matchedSplit = scheduledTrMatchFinder.getMatchedSplit(); MyMoneySchedule matchedSchedule = scheduledTrMatchFinder.getMatchedSchedule(); handleMatchingOfScheduledTransaction(matcher, matchedSchedule, matchedSplit, transactionUnderImport, s1); return; } } catch (const MyMoneyException &e) { QString message(i18n("Problem adding or matching imported transaction with id '%1': %2", statementTransactionUnderImport.m_strBankID, e.what())); qDebug("%s", qPrintable(message)); int result = KMessageBox::warningContinueCancel(0, message); if (result == KMessageBox::Cancel) throw MYMONEYEXCEPTION("USERABORT"); } } QString MyMoneyStatementReader::SelectBrokerageAccount() { if (m_brokerageAccount.id().isEmpty()) { m_brokerageAccount.setAccountType(MyMoneyAccount::Checkings); if (!m_userAbort) m_userAbort = ! selectOrCreateAccount(Select, m_brokerageAccount); } return m_brokerageAccount.id(); } 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 QList accounts; file->accountList(accounts); // Iterate through them QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { if ( ((*it_account).value("StatementKey") == accountNumber) || ((*it_account).number() == accountNumber) ) { MyMoneyAccount newAccount((*it_account).id(), account); account = newAccount; accountId = (*it_account).id(); break; } ++it_account; } } QString msg = i18n("You have downloaded a statement for the following account:

"); msg += i18n(" - Account Name: %1", account.name()) + "
"; msg += i18n(" - Account Type: %1", KMyMoneyUtils::accountTypeToString(account.accountType())) + "
"; msg += i18n(" - Account Number: %1", account.number()) + "
"; msg += "
"; QString header; if (!account.name().isEmpty()) { if (!accountId.isEmpty()) msg += i18n("Do you want to import transactions to this account?"); else msg += i18n("KMyMoney cannot determine which of your accounts to use. You can " "create a new account by pressing the Create button " "or select another one manually from the selection box below."); } else { msg += i18n("No account information has been found in the selected statement file. " "Please select an account using the selection box in the dialog or " "create a new account by pressing the Create button."); } KMyMoneyUtils::categoryTypeE type; if (account.accountType() == MyMoneyAccount::Checkings) { type = static_cast(KMyMoneyUtils::checking); } else if (account.accountType() == MyMoneyAccount::Savings) { type = static_cast(KMyMoneyUtils::savings); } else if (account.accountType() == MyMoneyAccount::Investment) { type = static_cast(KMyMoneyUtils::investment); } else if (account.accountType() == MyMoneyAccount::CreditCard) { type = static_cast(KMyMoneyUtils::creditCard); } else { type = static_cast(KMyMoneyUtils::asset | KMyMoneyUtils::liability); } QPointer accountSelect = new KAccountSelectDlg(type, "StatementImport", kmymoney); 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 (const MyMoneyException &) { qDebug("Updating account in MyMoneyStatementReader::selectOrCreateAccount failed"); } } } else { if (accountSelect->aborted()) //throw MYMONEYEXCEPTION("USERABORT"); done = true; else KMessageBox::error(0, QLatin1String("") + i18n("You must select an account, create a new one, or press the Abort button.") + QLatin1String("")); } } delete accountSelect; 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); } void MyMoneyStatementReader::handleMatchingOfExistingTransaction(TransactionMatcher & matcher, MyMoneyTransaction matchedTransaction, MyMoneySplit matchedSplit, MyMoneyTransaction & importedTransaction, const MyMoneySplit & importedSplit, const TransactionMatchFinder::MatchResult & matchResult) { switch (matchResult) { case TransactionMatchFinder::MatchNotFound: break; case TransactionMatchFinder::MatchDuplicate: d->transactionsDuplicate++; qDebug("Detected transaction duplicate"); break; case TransactionMatchFinder::MatchImprecise: case TransactionMatchFinder::MatchPrecise: addTransaction(importedTransaction); qDebug("Detected as match to transaction '%s'", qPrintable(matchedTransaction.id())); matcher.match(matchedTransaction, matchedSplit, importedTransaction, importedSplit, true); d->transactionsMatched++; break; } } void MyMoneyStatementReader::handleMatchingOfScheduledTransaction(TransactionMatcher & matcher, MyMoneySchedule matchedSchedule, MyMoneySplit matchedSplit, const MyMoneyTransaction & importedTransaction, const MyMoneySplit & importedSplit) { QPointer editor; if (askUserToEnterScheduleForMatching(matchedSchedule, importedSplit, importedTransaction)) { KEnterScheduleDlg dlg(0, matchedSchedule); editor = dlg.startEdit(); if (editor) { MyMoneyTransaction torig; try { // 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() != importedSplit.shares() && !matchedSchedule.isFixed()) { // for now this only works with regular transactions and not // for investment transactions. As of this, we don't have // scheduled investment transactions anyway. StdTransactionEditor* se = dynamic_cast(editor.data()); if (se) { // the following call will update the amount field in the // editor and also adjust a possible VAT assignment. Make // sure to use only the absolute value of the amount, because // the editor keeps the sign in a different position (deposit, // withdrawal tab) kMyMoneyEdit* amount = dynamic_cast(se->haveWidget("amount")); if (amount) { amount->setValue(importedSplit.shares().abs()); se->slotUpdateAmount(importedSplit.shares().abs().toString()); // we also need to update the matchedSplit variable to // have the modified share/value. matchedSplit.setShares(importedSplit.shares()); matchedSplit.setValue(importedSplit.value()); } } } editor->createTransaction(torig, dlg.transaction(), dlg.transaction().splits().isEmpty() ? MyMoneySplit() : dlg.transaction().splits().front(), true); QString newId; if (editor->enterTransactions(newId, false, true)) { if (!newId.isEmpty()) { torig = MyMoneyFile::instance()->transaction(newId); matchedSchedule.setLastPayment(torig.postDate()); } matchedSchedule.setNextDueDate(matchedSchedule.nextPayment(matchedSchedule.nextDueDate())); MyMoneyFile::instance()->modifySchedule(matchedSchedule); } // now match the two transactions matcher.match(torig, matchedSplit, importedTransaction, importedSplit); d->transactionsMatched++; } catch (const MyMoneyException &e) { // make sure we get rid of the editor before // the KEnterScheduleDlg is destroyed delete editor; throw e; // rethrow } } // delete the editor delete editor; } } void MyMoneyStatementReader::addTransaction(MyMoneyTransaction& transaction) { MyMoneyFile* file = MyMoneyFile::instance(); file->addTransaction(transaction); d->transactionsAdded++; } bool MyMoneyStatementReader::askUserToEnterScheduleForMatching(const MyMoneySchedule& matchedSchedule, const MyMoneySplit& importedSplit, const MyMoneyTransaction & importedTransaction) const { QString scheduleName = matchedSchedule.name(); int currencyDenom = m_account.fraction(MyMoneyFile::instance()->currency(m_account.currencyId())); QString splitValue = importedSplit.value().formatMoney(currencyDenom); QString payeeName = MyMoneyFile::instance()->payee(importedSplit.payeeId()).name(); QString questionMsg = i18n("KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %1
" "Transaction: %2 %3
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", scheduleName, splitValue, payeeName); // check that dates are within user's setting const int gap = abs(matchedSchedule.transaction().postDate().toJulianDay() - importedTransaction.postDate().toJulianDay()); if (gap > KMyMoneyGlobalSettings::matchInterval()) questionMsg = i18np("KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %2
" "Transaction: %3 %4
" "The transaction dates are one day apart.
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", "KMyMoney has found a scheduled transaction which matches an imported transaction.
" "Schedule name: %2
" "Transaction: %3 %4
" "The transaction dates are %1 days apart.
" "Do you want KMyMoney to enter this schedule now so that the transaction can be matched?", gap ,scheduleName, splitValue, payeeName); const int userAnswer = KMessageBox::questionYesNo(0, QLatin1String("") + questionMsg + QLatin1String(""), i18n("Schedule found")); return (userAnswer == KMessageBox::Yes); } diff --git a/kmymoney/converter/mymoneytemplate.cpp b/kmymoney/converter/mymoneytemplate.cpp index 189726cb6..591906f1e 100644 --- a/kmymoney/converter/mymoneytemplate.cpp +++ b/kmymoney/converter/mymoneytemplate.cpp @@ -1,457 +1,486 @@ /*************************************************************************** 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 "mymoneytemplate.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyutils.h" MyMoneyTemplate::MyMoneyTemplate() : m_progressCallback(0), m_accountsRead(0) { } MyMoneyTemplate::MyMoneyTemplate(const QUrl& url) : m_progressCallback(0), m_accountsRead(0) { loadTemplate(url); } MyMoneyTemplate::~MyMoneyTemplate() { } bool MyMoneyTemplate::loadTemplate(const QUrl &url) { QString filename; if (!url.isValid()) { qDebug("Invalid template URL '%s'", qPrintable(url.url())); return false; } m_source = url; if (url.isLocalFile()) { filename = url.toLocalFile(); } else { bool rc = false; // TODO: port to kf5 //rc = KIO::NetAccess::download(url, filename, KMyMoneyUtils::mainWindow()); if (!rc) { KMessageBox::detailedError(KMyMoneyUtils::mainWindow(), i18n("Error while loading file '%1'.", url.url()), // TODO: port to kf5 QString(),//KIO::NetAccess::lastErrorString(), i18n("File access error")); return false; } } bool rc = true; QFile file(filename); QFileInfo info(file); if (!info.isFile()) { QString msg = i18n("

%1 is not a template file.

", filename); KMessageBox::error(KMyMoneyUtils::mainWindow(), msg, i18n("Filetype Error")); return false; } if (file.open(QIODevice::ReadOnly)) { QString errMsg; int errLine, errColumn; if (!m_doc.setContent(&file, &errMsg, &errLine, &errColumn)) { QString msg = i18n("

Error while reading template file %1 in line %2, column %3

", filename, errLine, errColumn); KMessageBox::detailedError(KMyMoneyUtils::mainWindow(), msg, errMsg, i18n("Template Error")); rc = false; } else { rc = loadDescription(); } file.close(); } else { KMessageBox::sorry(KMyMoneyUtils::mainWindow(), i18n("File '%1' not found.", 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 // TODO: port to kf5 //KIO::NetAccess::removeTempFile(filename); return rc; } bool MyMoneyTemplate::loadDescription() { 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(KMyMoneyUtils::mainWindow(), i18n("

Invalid tag %1 in template file %2

", childElement.tagName(), m_source.toDisplayString())); validMask |= invalid; } child = child.nextSibling(); } } return validMask == validHeader; } bool MyMoneyTemplate::hierarchy(QMap& list, const QString& parent, QDomNode account) { bool rc = true; while (rc == true && !account.isNull()) { if (account.isElement()) { QDomElement accountElement = account.toElement(); if (accountElement.tagName() == "account") { QString name = QString("%1:%2").arg(parent).arg(accountElement.attribute("name")); list[name] = 0; hierarchy(list, name, account.firstChild()); } } account = account.nextSibling(); } return rc; } void MyMoneyTemplate::hierarchy(QMap& list) { bool rc = !m_accounts.isNull(); QDomNode accounts = m_accounts; while (rc == true && !accounts.isNull() && accounts.isElement()) { - QDomElement childElement = accounts.toElement(); - if (childElement.tagName() == "account" - && childElement.attribute("name").isEmpty()) { - switch (childElement.attribute("type").toUInt()) { + QDomElement rootNode = accounts.toElement(); + QString name = rootNode.attribute("name"); + if (rootNode.tagName() == "account") { + rootNode = rootNode.firstChild().toElement(); + MyMoneyAccount::accountTypeE type = static_cast(accounts.toElement().attribute("type").toUInt()); + switch (type) { 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()); + if (name.isEmpty()) + name = MyMoneyAccount::accountTypeToString(type); + list[name] = 0; + rc = hierarchy(list, name, rootNode); 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", 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").isEmpty()) { + if (childElement.tagName() == "account") { ++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(KMyMoneyUtils::mainWindow(), i18n("

Invalid top-level account type %1 in template file %2

", childElement.attribute("type"), m_source.toDisplayString())); rc = false; } if (rc == true) { - rc = createAccounts(parent, childElement.firstChild()); + if (childElement.attribute("name").isEmpty()) + rc = createAccounts(parent, childElement.firstChild()); + else + rc = createAccounts(parent, childElement); } } 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); QList subAccountList; QList::ConstIterator it; it = subAccountList.constEnd(); if (!parent.accountList().isEmpty()) { MyMoneyFile::instance()->accountList(subAccountList, parent.accountList()); for (it = subAccountList.constBegin(); it != subAccountList.constEnd(); ++it) { if ((*it).name() == accountElement.attribute("name")) { acc = *it; break; } } } if (it == subAccountList.constEnd()) { // not found, we need to create it acc.setName(accountElement.attribute("name")); acc.setAccountType(static_cast(accountElement.attribute("type").toUInt())); setFlags(acc, account.firstChild()); try { MyMoneyFile::instance()->addAccount(acc, parent); } catch (const MyMoneyException &) { } } 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.toLatin1(), "Yes"); + } else if (value == "OpeningBalanceAccount") { + acc.setValue("OpeningBalanceAccount", "Yes"); } else { KMessageBox::error(KMyMoneyUtils::mainWindow(), i18n("

Invalid flag type %1 for account %3 in template file %2

", flagElement.attribute("name"), m_source.toDisplayString(), acc.name())); rc = false; } + QString currency = flagElement.attribute("currency"); + if (!currency.isEmpty()) + acc.setCurrencyId(currency); } } 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"); + QDomText t = m_doc.createTextNode(m_title); + title.appendChild(t); mainElement.appendChild(title); QDomElement shortDesc = m_doc.createElement("shortdesc"); + t = m_doc.createTextNode(m_shortDesc); + shortDesc.appendChild(t); mainElement.appendChild(shortDesc); QDomElement longDesc = m_doc.createElement("longdesc"); + t = m_doc.createTextNode(m_longDesc); + longDesc.appendChild(t); mainElement.appendChild(longDesc); QDomElement accounts = m_doc.createElement("accounts"); mainElement.appendChild(accounts); addAccountStructure(accounts, MyMoneyFile::instance()->asset()); addAccountStructure(accounts, MyMoneyFile::instance()->expense()); addAccountStructure(accounts, MyMoneyFile::instance()->income()); addAccountStructure(accounts, MyMoneyFile::instance()->liability()); addAccountStructure(accounts, MyMoneyFile::instance()->equity()); return true; } const QString& MyMoneyTemplate::title() const { return m_title; } const QString& MyMoneyTemplate::shortDescription() const { return m_shortDesc; } const QString& MyMoneyTemplate::longDescription() const { return m_longDesc; } +void MyMoneyTemplate::setTitle(const QString &s) +{ + m_title = s; +} + +void MyMoneyTemplate::setShortDescription(const QString &s) +{ + m_shortDesc = s; +} + +void MyMoneyTemplate::setLongDescription(const QString &s) +{ + m_longDesc = s; +} + static bool nameLessThan(MyMoneyAccount &a1, MyMoneyAccount &a2) { return a1.name() < a2.name(); } 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 + if (acc.pairs().contains("OpeningBalanceAccount")) { + QString openingBalanceAccount = acc.value("OpeningBalanceAccount"); + if (openingBalanceAccount == "Yes") { + QDomElement flag = m_doc.createElement("flag"); + flag.setAttribute(QString("name"), "OpeningBalanceAccount"); + flag.setAttribute(QString("currency"), acc.currencyId()); + account.appendChild(flag); + } + } // any child accounts? if (acc.accountList().count() > 0) { QList list; MyMoneyFile::instance()->accountList(list, acc.accountList(), false); qSort(list.begin(), list.end(), nameLessThan); QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { addAccountStructure(account, *it); } } return true; } bool MyMoneyTemplate::saveTemplate(const QUrl &url) { QString filename; if (!url.isValid()) { qDebug("Invalid template URL '%s'", qPrintable(url.url())); return false; } if (url.isLocalFile()) { filename = url.toLocalFile(); QSaveFile qfile(filename/*, 0600*/); if (qfile.open(QIODevice::WriteOnly)) { saveToLocalFile(&qfile); if (!qfile.commit()) { throw MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'", filename)); } } else { throw MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'", filename)); } } else { QTemporaryFile tmpfile; QSaveFile qfile(tmpfile.fileName()); if (qfile.open(QIODevice::WriteOnly)) { saveToLocalFile(&qfile); if (!qfile.commit()) { throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'", url.url())); } } else { throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'", url.url())); } // TODO: port to kf5 //if (!KIO::NetAccess::upload(tmpfile.fileName(), url, 0)) // throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'", url.url())); } return true; } bool MyMoneyTemplate::saveToLocalFile(QSaveFile* qfile) { QTextStream stream(qfile); stream.setCodec("UTF-8"); stream << m_doc.toString(); stream.flush(); return true; } diff --git a/kmymoney/converter/mymoneytemplate.h b/kmymoney/converter/mymoneytemplate.h index 5617513e1..38a5830e7 100644 --- a/kmymoney/converter/mymoneytemplate.h +++ b/kmymoney/converter/mymoneytemplate.h @@ -1,96 +1,100 @@ /*************************************************************************** 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 #include #include #include class QFile; class QTreeWidgetItem; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" #include "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(); explicit MyMoneyTemplate(const QUrl &url); ~MyMoneyTemplate(); bool loadTemplate(const QUrl &url); bool saveTemplate(const QUrl &url); bool importTemplate(void(*callback)(int, int, const QString&)); bool exportTemplate(void(*callback)(int, int, const QString&)); const QString& title() const; const QString& shortDescription() const; const QString& longDescription() const; + void setTitle(const QString &s); + void setShortDescription(const QString &s); + void setLongDescription(const QString &s); + void hierarchy(QMap& list); protected: bool loadDescription(); bool createAccounts(MyMoneyAccount& parent, QDomNode account); bool setFlags(MyMoneyAccount& acc, QDomNode flags); bool saveToLocalFile(QSaveFile* qfile); bool addAccountStructure(QDomElement& parent, const MyMoneyAccount& acc); bool hierarchy(QMap& list, const QString& parent, QDomNode account); /** * This method is used to update the progress information. It * checks if an appropriate function is known and calls it. * * For a parameter description see KMyMoneyView::progressCallback(). */ void signalProgress(int current, int total, const QString& = ""); private: QDomDocument m_doc; QDomNode m_accounts; QString m_title; QString m_shortDesc; QString m_longDesc; QUrl m_source; void (*m_progressCallback)(int, int, const QString&); int m_accountsRead; }; #endif diff --git a/kmymoney/converter/webpricequote.cpp b/kmymoney/converter/webpricequote.cpp index 15cb385ae..d9a3a1351 100644 --- a/kmymoney/converter/webpricequote.cpp +++ b/kmymoney/converter/webpricequote.cpp @@ -1,1243 +1,1243 @@ /*************************************************************************** webpricequote.cpp ------------------- begin : Thu Dec 30 2004 copyright : (C) 2004 by Ace Jones email : Ace Jones copyright : (C) 2017 by Łukasz Wojniłowicz email : Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * 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 "webpricequote.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyexception.h" #include "mymoneyqifprofile.h" #include "mymoneyfile.h" Q_DECLARE_LOGGING_CATEGORY(WEBPRICEQUOTE) Q_LOGGING_CATEGORY(WEBPRICEQUOTE, "kmymoney_webpricequote") // define static members QString WebPriceQuote::m_financeQuoteScriptPath; QStringList WebPriceQuote::m_financeQuoteSources; class WebPriceQuote::Private { public: WebPriceQuoteProcess m_filter; QString m_quoteData; QString m_symbol; QString m_id; QDate m_date; QDate m_fromDate; QDate m_toDate; double m_price; WebPriceQuoteSource m_source; PricesProfile m_CSVSource; }; WebPriceQuote::WebPriceQuote(QObject* _parent): QObject(_parent), d(new Private) { // only do this once (I know, it is not thread safe, but it should // always yield the same result so we don't do any semaphore foo here) if (m_financeQuoteScriptPath.isEmpty()) { m_financeQuoteScriptPath = QStandardPaths::locate(QStandardPaths::DataLocation, QString("misc/financequote.pl")); } connect(&d->m_filter, SIGNAL(processExited(QString)), this, SLOT(slotParseQuote(QString))); } WebPriceQuote::~WebPriceQuote() { delete d; } void WebPriceQuote::setDate(const QDate& _from, const QDate& _to) { d->m_fromDate = _from; d->m_toDate = _to; } bool WebPriceQuote::launch(const QString& _symbol, const QString& _id, const QString& _sourcename) { if (_sourcename.contains("Finance::Quote")) return (launchFinanceQuote(_symbol, _id, _sourcename)); else if ((!d->m_fromDate.isValid() || !d->m_toDate.isValid()) || (d->m_fromDate == d->m_toDate && d->m_toDate == QDate::currentDate())) return (launchNative(_symbol, _id, _sourcename)); else return launchCSV(_symbol, _id, _sourcename); } bool WebPriceQuote::launchCSV(const QString& _symbol, const QString& _id, const QString& _sourcename) { d->m_symbol = _symbol; d->m_id = _id; // emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id)); // Get sources from the config file QString sourcename = _sourcename; if (sourcename.isEmpty()) return false; if (sourcename == QLatin1String("Yahoo Currency")) sourcename = QLatin1String("Stooq Currency"); if (quoteSources().contains(sourcename)) d->m_source = WebPriceQuoteSource(sourcename); else emit error(i18n("Source %1 does not exist.", sourcename)); int monthOffset = 0; if (sourcename.contains(QLatin1String("Yahoo"), Qt::CaseInsensitive)) monthOffset = -1; QUrl url; QString urlStr = d->m_source.m_csvUrl; int i = urlStr.indexOf(QLatin1String("%y")); if (i != -1) urlStr.replace(i, 2, QString().setNum(d->m_fromDate.year())); i = urlStr.indexOf(QLatin1String("%y")); if (i != -1) urlStr.replace(i, 2, QString().setNum(d->m_toDate.year())); i = urlStr.indexOf(QLatin1String("%m")); if (i != -1) urlStr.replace(i, 2, QString().setNum(d->m_fromDate.month() + monthOffset).rightJustified(2, QLatin1Char('0'))); i = urlStr.indexOf(QLatin1String("%m")); if (i != -1) urlStr.replace(i, 2, QString().setNum(d->m_toDate.month() + monthOffset).rightJustified(2, QLatin1Char('0'))); i = urlStr.indexOf(QLatin1String("%d")); if (i != -1) urlStr.replace(i, 2, QString().setNum(d->m_fromDate.day()).rightJustified(2, QLatin1Char('0'))); i = urlStr.indexOf(QLatin1String("%d")); if (i != -1) urlStr.replace(i, 2, QString().setNum(d->m_toDate.day()).rightJustified(2, QLatin1Char('0'))); if (urlStr.contains(QLatin1String("%y")) || urlStr.contains(QLatin1String("%m")) || urlStr.contains(QLatin1String("%d"))) { emit error(i18n("Cannot resolve input date.")); emit failed(d->m_id, d->m_symbol); return false; } bool isCurrency = false; if (urlStr.contains(QLatin1String("%2"))) { d->m_CSVSource.m_profileType = ProfileCurrencyPrices; isCurrency = true; } else d->m_CSVSource.m_profileType = ProfileStockPrices; d->m_CSVSource.m_profileName = sourcename; if (!d->m_CSVSource.readSettings(CSVImporter::configFile())) { QMap result = defaultCSVQuoteSources(); d->m_CSVSource = result.value(sourcename); if (d->m_CSVSource.m_profileName.isEmpty()) { emit error(i18n("CSV source %1 does not exist.", sourcename)); emit failed(d->m_id, d->m_symbol); return false; } } if (isCurrency) { // 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\\.]+)", Qt::CaseInsensitive); // if we've truly found 2 symbols delimited this way... if (splitrx.indexIn(d->m_symbol) != -1) { url = QUrl(urlStr.arg(splitrx.cap(1), splitrx.cap(2))); d->m_CSVSource.m_currencySymbol = splitrx.cap(2); d->m_CSVSource.m_securitySymbol = splitrx.cap(1); } else { qCDebug(WEBPRICEQUOTE) << "WebPriceQuote::launch() did not find 2 symbols"; emit error(i18n("Cannot find from and to currency.")); emit failed(d->m_id, d->m_symbol); return false; } } else { // a regular one-symbol quote url = QUrl(urlStr.arg(d->m_symbol)); d->m_CSVSource.m_securityName = MyMoneyFile::instance()->security(d->m_id).name(); d->m_CSVSource.m_securitySymbol = d->m_symbol; } if (url.isLocalFile()) { emit error(i18n("Local quote sources aren't supported.")); emit failed(d->m_id, d->m_symbol); return false; } else { //silent download emit status(i18n("Fetching URL %1...", url.toDisplayString())); QString tmpFile; { QTemporaryFile tmpFileFile; tmpFileFile.setAutoRemove(false); if (tmpFileFile.open()) qDebug() << "created tmpfile"; tmpFile = tmpFileFile.fileName(); } QFile::remove(tmpFile); const QUrl dest = QUrl::fromLocalFile(tmpFile); KIO::Scheduler::checkSlaveOnHold(true); KIO::Job *job = KIO::file_copy(url, dest, -1, KIO::HideProgressInfo); connect(job, SIGNAL(result(KJob*)), this, SLOT(downloadCSV(KJob*))); } return true; } bool WebPriceQuote::launchNative(const QString& _symbol, const QString& _id, const QString& _sourcename) { d->m_symbol = _symbol; d->m_id = _id; // emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id)); // Get sources from the config file QString sourcename = _sourcename; if (sourcename.isEmpty()) sourcename = "Yahoo"; if (quoteSources().contains(sourcename)) d->m_source = WebPriceQuoteSource(sourcename); else emit error(i18n("Source %1 does not exist.", sourcename)); QUrl url; // if the source has room for TWO symbols.. if (d->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\\.]+)", Qt::CaseInsensitive); // if we've truly found 2 symbols delimited this way... if (splitrx.indexIn(d->m_symbol) != -1) { url = QUrl(d->m_source.m_url.arg(splitrx.cap(1), splitrx.cap(2))); } else { qCDebug(WEBPRICEQUOTE) << "WebPriceQuote::launch() did not find 2 symbols"; } } else { // a regular one-symbol quote url = QUrl(d->m_source.m_url.arg(d->m_symbol)); } if (url.isLocalFile()) { emit status(i18nc("The process x is executing", "Executing %1...", url.toLocalFile())); QString program; QStringList arguments = url.toLocalFile().split(' ', QString::SkipEmptyParts); if (!arguments.isEmpty()) { program = arguments.first(); arguments.removeFirst(); } d->m_filter.setSymbol(d->m_symbol); d->m_filter.setProcessChannelMode(QProcess::MergedChannels); d->m_filter.start(program, arguments); if (!d->m_filter.waitForStarted()) { emit error(i18n("Unable to launch: %1", url.toLocalFile())); slotParseQuote(QString()); } } else { //silent download emit status(i18n("Fetching URL %1...", url.toDisplayString())); QString tmpFile; { QTemporaryFile tmpFileFile; tmpFileFile.setAutoRemove(false); if (tmpFileFile.open()) qDebug() << "created tmpfile"; tmpFile = tmpFileFile.fileName(); } QFile::remove(tmpFile); const QUrl dest = QUrl::fromLocalFile(tmpFile); KIO::Scheduler::checkSlaveOnHold(true); KIO::Job *job = KIO::file_copy(url, dest, -1, KIO::HideProgressInfo); connect(job, SIGNAL(result(KJob*)), this, SLOT(downloadResult(KJob*))); } return true; } void WebPriceQuote::downloadCSV(KJob* job) { QString tmpFile = dynamic_cast(job)->destUrl().toLocalFile(); QUrl url = dynamic_cast(job)->srcUrl(); if (!job->error()) { qDebug() << "Downloaded" << tmpFile << "from" << url; QFile f(tmpFile); if (f.open(QIODevice::ReadOnly)) { f.close(); slotParseCSVQuote(tmpFile); } else { emit error(i18n("Failed to open downloaded file")); slotParseCSVQuote(QString()); } } else { emit error(job->errorString()); slotParseCSVQuote(QString()); } } void WebPriceQuote::downloadResult(KJob* job) { QString tmpFile = dynamic_cast(job)->destUrl().toLocalFile(); QUrl url = dynamic_cast(job)->srcUrl(); if (!job->error()) { qDebug() << "Downloaded" << tmpFile << "from" << url; QFile f(tmpFile); if (f.open(QIODevice::ReadOnly)) { // Find out the page encoding and convert it to unicode QByteArray page = f.readAll(); KEncodingProber prober(KEncodingProber::Universal); prober.feed(page); QTextCodec* codec = QTextCodec::codecForName(prober.encoding()); if (!codec) codec = QTextCodec::codecForLocale(); QString quote = codec->toUnicode(page); f.close(); slotParseQuote(quote); } else { emit error(i18n("Failed to open downloaded file")); slotParseQuote(QString()); } QFile::remove(tmpFile); } else { emit error(job->errorString()); slotParseQuote(QString()); } } bool WebPriceQuote::launchFinanceQuote(const QString& _symbol, const QString& _id, const QString& _sourcename) { bool result = true; d->m_symbol = _symbol; d->m_id = _id; QString FQSource = _sourcename.section(' ', 1); d->m_source = WebPriceQuoteSource(_sourcename, m_financeQuoteScriptPath, m_financeQuoteScriptPath, "\"([^,\"]*)\",.*", // symbol regexp "[^,]*,[^,]*,\"([^\"]*)\"", // price regexp "[^,]*,([^,]*),.*", // date regexp "%y-%m-%d"); // date format //emit status(QString("(Debug) symbol=%1 id=%2...").arg(_symbol,_id)); QStringList arguments; arguments << m_financeQuoteScriptPath << FQSource << KShell::quoteArg(_symbol); d->m_filter.setSymbol(d->m_symbol); emit status(i18nc("Executing 'script' 'online source' 'investment symbol' ", "Executing %1 %2 %3...", m_financeQuoteScriptPath, FQSource, _symbol)); d->m_filter.setProcessChannelMode(QProcess::MergedChannels); d->m_filter.start(QLatin1Literal("perl"), arguments); // This seems to work best if we just block until done. if (d->m_filter.waitForFinished()) { result = true; } else { emit error(i18n("Unable to launch: %1", m_financeQuoteScriptPath)); slotParseQuote(QString()); } return result; } void WebPriceQuote::slotParseCSVQuote(const QString& filename) { bool isOK = true; if (filename.isEmpty()) isOK = false; else { MyMoneyStatement st; CSVImporter* csvImporter = new CSVImporter; st = csvImporter->unattendedPricesImport(filename, &d->m_CSVSource); if (!st.m_listPrices.isEmpty()) emit csvquote(d->m_id, d->m_symbol, st); else isOK = false; delete csvImporter; QFile::remove(filename); } if (!isOK) { emit error(i18n("Unable to update price for %1", d->m_symbol)); emit failed(d->m_id, d->m_symbol); } } void WebPriceQuote::slotParseQuote(const QString& _quotedata) { QString quotedata = _quotedata; d->m_quoteData = quotedata; bool gotprice = false; bool gotdate = false; qCDebug(WEBPRICEQUOTE) << "quotedata" << _quotedata; if (! quotedata.isEmpty()) { if (!d->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.simplified(); qCDebug(WEBPRICEQUOTE) << "stripped text" << quotedata; } QRegExp symbolRegExp(d->m_source.m_sym); QRegExp dateRegExp(d->m_source.m_date); QRegExp priceRegExp(d->m_source.m_price); if (symbolRegExp.indexIn(quotedata) > -1) { qCDebug(WEBPRICEQUOTE) << "Symbol" << symbolRegExp.cap(1); - emit status(i18n("Symbol found: %1", symbolRegExp.cap(1))); + emit status(i18n("Symbol found: '%1'", symbolRegExp.cap(1))); } if (priceRegExp.indexIn(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.lastIndexOf(QRegExp("\\D")); if (pos > 0) { pricestr[pos] = '.'; pos = pricestr.lastIndexOf(QRegExp("\\D"), pos - 1); } while (pos > 0) { pricestr.remove(pos, 1); pos = pricestr.lastIndexOf(QRegExp("\\D"), pos); } d->m_price = pricestr.toDouble(); qCDebug(WEBPRICEQUOTE) << "Price" << pricestr; - emit status(i18n("Price found: %1 (%2)", pricestr, d->m_price)); + emit status(i18n("Price found: '%1' (%2)", pricestr, d->m_price)); } if (dateRegExp.indexIn(quotedata) > -1) { QString datestr = dateRegExp.cap(1); MyMoneyDateFormat dateparse(d->m_source.m_dateformat); try { d->m_date = dateparse.convertString(datestr, false /*strict*/); gotdate = true; qCDebug(WEBPRICEQUOTE) << "Date" << datestr; - emit status(i18n("Date found: %1", d->m_date.toString()));; + emit status(i18n("Date found: '%1'", d->m_date.toString()));; } catch (const MyMoneyException &) { // emit error(i18n("Unable to parse date %1 using format %2: %3").arg(datestr,dateparse.format(),e.what())); d->m_date = QDate::currentDate(); gotdate = true; } } if (gotprice && gotdate) { emit quote(d->m_id, d->m_symbol, d->m_date, d->m_price); } else { emit error(i18n("Unable to update price for %1 (no price or no date)", d->m_symbol)); emit failed(d->m_id, d->m_symbol); } } else { emit error(i18n("Unable to update price for %1 (empty quote data)", d->m_symbol)); emit failed(d->m_id, d->m_symbol); } } const QMap WebPriceQuote::defaultCSVQuoteSources() { QMap result; result[QLatin1String("Stooq")] = PricesProfile(QLatin1String("Stooq"), 106, 1, 0, 0, 1, 0, 0, QMap{{ColumnDate, 0}, {ColumnPrice, 4}}, 2, ProfileStockPrices); result[QLatin1String("Stooq Currency")] = PricesProfile(QLatin1String("Stooq Currency"), 106, 1, 0, 0, 1, 0, 0, QMap{{ColumnDate, 0}, {ColumnPrice, 4}}, 2, ProfileCurrencyPrices); result[QLatin1String("Yahoo")] = PricesProfile(QLatin1String("Yahoo"), 106, 1, 0, 0, 0, 0, 0, QMap{{ColumnDate, 0}, {ColumnPrice, 4}}, 2, ProfileStockPrices); return result; } const QMap WebPriceQuote::defaultQuoteSources() { QMap result; result["Yahoo"] = WebPriceQuoteSource("Yahoo", "http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1", "http://ichart.finance.yahoo.com/table.csv?s=%1&a=%m&b=%d&c=%y&d=%m&e=%d&f=%y&g=d&ignore=.csv", "\"([^,\"]*)\",.*", // 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 "(\\d+\\.\\d+) [+-]\\d+.\\d+", // priceregexp "Time of last trade (\\d+/\\d+/\\d+)", //dateregexp "%d %m %y" // dateformat ); // Finanztreff (replaces VWD.DE) and boerseonline supplied by Micahel Zimmerman result["Finanztreff"] = WebPriceQuoteSource("Finanztreff", "http://finanztreff.de/kurse_einzelkurs_detail.htn?u=100&i=%1", "", QString(), // symbolregexp "([0-9]+,\\d+).+Gattung:Fonds", // priceregexp "\\).(\\d+\\D+\\d+\\D+\\d+)", // dateregexp (doesn't work; date in chart "%d.%m.%y" // dateformat ); result["boerseonline"] = WebPriceQuoteSource("boerseonline", "http://www.boerse-online.de/tools/boerse/einzelkurs_kurse.htm?&s=%1", "", QString(), // symbolregexp "Akt\\. Kurs.([\\d\\.]+,\\d\\d)", // priceregexp "Datum.(\\d+\\.\\d+\\.\\d+)", // dateregexp (doesn't work; date in chart "%d.%m.%y" // dateformat ); // The following two price sources were contributed by // Marc Zahnlecker result["Wallstreet-Online.DE (Default)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Default)", "http://www.wallstreet-online.de/si/?k=%1&spid=ws", "", "Symbol:(\\w+)", // symbolregexp "Letzter Kurs: ([0-9.]+,\\d+)", // priceregexp ", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp "%d %m %y" // dateformat ); // This quote source provided by e-mail and should replace the previous one: // From: David Houlden // To: kmymoney@kde.org // Date: Sat, 6 Apr 2013 13:22:45 +0100 result["Financial Times UK Funds"] = WebPriceQuoteSource("Financial Times UK Funds", "http://funds.ft.com/uk/Tearsheet/Summary?s=%1", "", "data-display-symbol=\"(.*):", // symbol regexp "class=\"text first\">([\\d,]*\\d+\\.\\d+)", // price regexp "As of market close\\ (.*)\\.", // date regexp "%m %d %y", // date format true // skip HTML stripping ); // This quote source provided by Danny Scott result["Yahoo Canada"] = WebPriceQuoteSource("Yahoo Canada", "http://ca.finance.yahoo.com/q?s=%1", "", "%1", // symbol regexp "Last Trade: (\\d+\\.\\d+)", // price regexp "day, (.\\D+\\d+\\D+\\d+)", // date regexp "%m %d %y" // date format ); // (tf2k) The "mpid" is I think the market place id. In this case five // stands for Hamburg. // // Here the id for several market places: 2 Frankfurt, 3 Berlin, 4 // Düsseldorf, 5 Hamburg, 6 München/Munich, 7 Hannover, 9 Stuttgart, 10 // Xetra, 32 NASDAQ, 36 NYSE result["Wallstreet-Online.DE (Hamburg)"] = WebPriceQuoteSource("Wallstreet-Online.DE (Hamburg)", "http://fonds.wallstreet-online.de/si/?k=%1&spid=ws&mpid=5", "", "Symbol:(\\w+)", // symbolregexp "Fonds \\(EUR\\) ([0-9.]+,\\d+)", // priceregexp ", (\\d+\\D+\\d+\\D+\\d+)", // dateregexp "%d %m %y" // dateformat ); // The following price quote was contributed by // Piotr Adacha // I would like to post new Online Query Settings for KMyMoney. This set is // suitable to query stooq.com service, providing quotes for stocks, futures, // mutual funds and other financial instruments from Polish Gielda Papierow // Wartosciowych (GPW). Unfortunately, none of well-known international // services provide quotes for this market (biggest one in central and eastern // Europe), thus, I think it could be helpful for Polish users of KMyMoney (and // I am one of them for almost a year). result["Stooq"] = WebPriceQuoteSource("Stooq", "http://stooq.com/q/?s=%1", "http://stooq.pl/q/d/l/?s=%1&d1=%y%m%d&d2=%y%m%d&i=d&c=1", QString(), // symbol regexp "Last.*(\\d+\\.\\d+).*Date", // price regexp "(\\d{4,4}-\\d{2,2}-\\d{2,2})", // date regexp "%y %m %d" // date format ); result[QLatin1String("Stooq Currency")] = WebPriceQuoteSource("Stooq Currency", "http://stooq.com/q/?s=%1%2", "http://stooq.pl/q/d/l/?s=%1%2&d1=%y%m%d&d2=%y%m%d&i=d&c=1", QString(), // symbol regexp "Last.*(\\d+\\.\\d+).*Date", // price regexp "(\\d{4,4}-\\d{2,2}-\\d{2,2})", // date regexp "%y %m %d" // date format ); // The following price quote is for getting prices of different funds // at OMX Baltic market. result["OMX Baltic funds"] = WebPriceQuoteSource("OMX Baltic funds", "http://www.baltic.omxgroup.com/market/?pg=nontradeddetails¤cy=0&instrument=%1", "", QString(), // symbolregexp "NAV (\\d+,\\d+)", // priceregexp "Kpv (\\d+.\\d+.\\d+)", // dateregexp "%d.%m.%y" // dateformat ); // The following price quote was contributed by // Peter Hargreaves // The original posting can be found here: // http://sourceforge.net/mailarchive/message.php?msg_name=200806060854.11682.pete.h%40pdh-online.info // I have PEP and ISA accounts which I invest in Funds with Barclays // Stockbrokers. They give me Fund data via Financial Express: // // https://webfund6.financialexpress.net/Clients/Barclays/default.aspx // // A typical Fund Factsheet is: // // https://webfund6.financialexpress.net/Clients/Barclays/search_factsheet_summary.aspx?code=0585239 // // On the Factsheet to identify the fund you can see ISIN Code GB0005852396. // In the url, this code is shortened by loosing the first four and last // characters. // // Update: // // Nick Elliot has contributed a modified regular expression to cope with values presented // in pounds as well as those presented in pence. The source can be found here: // http://forum.kde.org/update-stock-and-currency-prices-t-32049.html result["Financial Express"] = WebPriceQuoteSource("Financial Express", "https://webfund6.financialexpress.net/Clients/Barclays/search_factsheet_summary.aspx?code=%1", "", "ISIN Code[^G]*(GB..........).*", // symbolregexp "Current Market Information[^0-9]*([0-9,\\.]+).*", // priceregexp "Price Date[^0-9]*(../../....).*", // dateregexp "%d/%m/%y" // dateformat ); // I suggest to include www.cash.ch as one of the pre-populated online // quote sources. // Rationale // It features Swiss funds that are otherwise hard to find. A typical // example: Swiss private pension accounts (in German termed 'Säule 3a') // may usually only invest in dedicated funds that are otherwise (almost) // not traded; the UBS Vitainvest series // (http://www.ubs.com/1/e/ubs_ch/private/insurance/fisca/securities/part_wealth.html) // is such a series of funds. result["Cash CH"] = WebPriceQuoteSource("Cash CH", "http://www.cash.ch/boerse/fonds/fondsguide/kursinfo/fullquote/%1", "", "", // symbolregexp "([1-9][0-9]*\\.[0-9][0-9])", // priceregexp "([0-3][0-9]\\.[0-1][0-9]\\.[1-2][0-9][0-9][0-9])", // dateregexp "%d.%m.%y", // dateformat true // skip stripping ); return result; } const QStringList WebPriceQuote::quoteSources(const _quoteSystemE _system) { if (_system == Native) return (quoteSourcesNative()); else return (quoteSourcesFinanceQuote()); } const QStringList WebPriceQuote::quoteSourcesNative() { KSharedConfigPtr kconfig = KSharedConfig::openConfig(); 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.erase(it)) { if (onlineQuoteSource.indexIn(*it) >= 0) { // Insert the name part it = groups.insert(it, onlineQuoteSource.cap(1)); ++it; } } // 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")) { KConfigGroup grp = kconfig->group("Online Quotes Options"); QString url(grp.readEntry("URL", "http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1")); QString symbolRegExp(grp.readEntry("SymbolRegex", "\"([^,\"]*)\",.*")); QString priceRegExp(grp.readEntry("PriceRegex", "[^,]*,([^,]*),.*")); QString dateRegExp(grp.readEntry("DateRegex", "[^,]*,[^,]*,\"([^\"]*)\"")); kconfig->deleteGroup("Online Quotes Options"); groups += "Old Source"; grp = kconfig->group(QString("Online-Quote-Source-%1").arg("Old Source")); grp.writeEntry("URL", url); grp.writeEntry("CSVURL", "http://finance.yahoo.com/d/quotes.csv?s=%1&f=sl1d1"); grp.writeEntry("SymbolRegex", symbolRegExp); grp.writeEntry("PriceRegex", priceRegExp); grp.writeEntry("DateRegex", dateRegExp); grp.writeEntry("DateFormatRegex", "%m %d %y"); grp.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. And any changes // are also picked up. QMap defaults = defaultQuoteSources(); QMap::const_iterator it_source = defaults.constBegin(); while (it_source != defaults.constEnd()) { if (! groups.contains((*it_source).m_name)) { groups += (*it_source).m_name; (*it_source).write(); kconfig->sync(); } ++it_source; } return groups; } const QStringList WebPriceQuote::quoteSourcesFinanceQuote() { if (m_financeQuoteSources.empty()) { // run the process one time only // since this is a static function it can be called without constructing an object // so we need to make sure that m_financeQuoteScriptPath is properly initialized if (m_financeQuoteScriptPath.isEmpty()) { m_financeQuoteScriptPath = QStandardPaths::locate(QStandardPaths::DataLocation, QString("misc/financequote.pl")); } FinanceQuoteProcess getList; getList.launch(m_financeQuoteScriptPath); while (!getList.isFinished()) { QCoreApplication::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 &csvUrl, const QString& sym, const QString& price, const QString& date, const QString& dateformat, bool skipStripping): m_name(name), m_url(url), m_csvUrl(csvUrl), m_sym(sym), m_price(price), m_date(date), m_dateformat(dateformat), m_skipStripping(skipStripping) { } WebPriceQuoteSource::WebPriceQuoteSource(const QString& name) { m_name = name; KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp = kconfig->group(QString("Online-Quote-Source-%1").arg(m_name)); m_sym = grp.readEntry("SymbolRegex"); m_date = grp.readEntry("DateRegex"); m_dateformat = grp.readEntry("DateFormatRegex", "%m %d %y"); m_price = grp.readEntry("PriceRegex"); m_url = grp.readEntry("URL"); m_csvUrl = grp.readEntry("CSVURL"); m_skipStripping = grp.readEntry("SkipStripping", false); } void WebPriceQuoteSource::write() const { KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp = kconfig->group(QString("Online-Quote-Source-%1").arg(m_name)); grp.writeEntry("URL", m_url); grp.writeEntry("CSVURL", m_csvUrl); grp.writeEntry("PriceRegex", m_price); grp.writeEntry("DateRegex", m_date); grp.writeEntry("DateFormatRegex", m_dateformat); grp.writeEntry("SymbolRegex", m_sym); if (m_skipStripping) grp.writeEntry("SkipStripping", m_skipStripping); else grp.deleteEntry("SkipStripping"); } void WebPriceQuoteSource::rename(const QString& name) { remove(); m_name = name; write(); } void WebPriceQuoteSource::remove() const { KSharedConfigPtr kconfig = KSharedConfig::openConfig(); 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() { connect(this, SIGNAL(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter())); connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotProcessExited(int,QProcess::ExitStatus))); } void WebPriceQuoteProcess::slotReceivedDataFromFilter() { // qDebug() << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << QString(data); m_string += QString(readAllStandardOutput()); } void WebPriceQuoteProcess::slotProcessExited(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { // qDebug() << "WebPriceQuoteProcess::slotProcessExited()"; emit processExited(m_string); m_string.truncate(0); } // // Helper class to babysit the KProcess used for running the Finance Quote sources script // FinanceQuoteProcess::FinanceQuoteProcess() { 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(readyReadStandardOutput()), this, SLOT(slotReceivedDataFromFilter())); connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotProcessExited())); connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(slotProcessExited())); } void FinanceQuoteProcess::slotReceivedDataFromFilter() { QByteArray data(readAllStandardOutput()); // qDebug() << "WebPriceQuoteProcess::slotReceivedDataFromFilter(): " << QString(data); m_string += QString(data); } void FinanceQuoteProcess::slotProcessExited() { // qDebug() << "WebPriceQuoteProcess::slotProcessExited()"; m_isDone = true; } void FinanceQuoteProcess::launch(const QString& scriptPath) { QStringList arguments; arguments << scriptPath << QLatin1Literal("-l"); setProcessChannelMode(QProcess::ForwardedOutputChannel); start(QLatin1Literal("perl"), arguments); if (! waitForStarted()) qWarning("Unable to start FQ script"); return; } const QStringList FinanceQuoteProcess::getSourceList() const { QStringList raw = m_string.split(0x0A, QString::SkipEmptyParts); 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) const { QString ret(niceName); fqNameMap::const_iterator it; for (it = m_fqNames.begin(); it != m_fqNames.end(); ++it) { if (niceName == it.value()) { ret = it.key(); break; } } return (ret); } const QString FinanceQuoteProcess::niceName(const QString& crypticName) const { 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) const 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]+)", Qt::CaseInsensitive); if (formatrex.indexIn(m_format) == -1) { throw 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.setCaseSensitivity(Qt::CaseInsensitive); // 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.indexIn(_in) == -1) { throw MYMONEYEXCEPTION("Invalid input string"); } QStringList scannedParts; scannedParts += inputrex.cap(1).toLower(); scannedParts += inputrex.cap(2).toLower(); scannedParts += inputrex.cap(3).toLower(); // // 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.constBegin(); QStringList::const_iterator it_format = formatParts.constBegin(); while (it_scanned != scannedParts.constEnd()) { // decide upon the first character of the part switch ((*it_format).at(0).cell()) { case 'd': // remove any extraneous non-digits (e.g. read "3rd" as 3) ok = false; if (digitrex.indexIn(*it_scanned) != -1) day = digitrex.cap(1).toUInt(&ok); if (!ok || day > 31) throw MYMONEYEXCEPTION(QString("Invalid day entry: %1").arg(*it_scanned)); break; case 'm': month = (*it_scanned).toUInt(&ok); if (!ok) { month = 0; // maybe it's a textual date unsigned i = 1; // search the name in the current selected locale QLocale locale; while (i <= 12) { if (locale.standaloneMonthName(i).toLower() == *it_scanned || locale.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) { month = i; break; } ++i; } // in case we did not find the month in the current locale, // we look for it in the C locale if(month == 0) { QLocale localeC(QLocale::C); if( !(locale == localeC)) { i = 1; while (i <= 12) { if (localeC.standaloneMonthName(i).toLower() == *it_scanned || localeC.standaloneMonthName(i, QLocale::ShortFormat).toLower() == *it_scanned) { month = i; break; } ++i; } } } } if (month < 1 || month > 12) throw MYMONEYEXCEPTION(QString("Invalid month entry: %1").arg(*it_scanned)); break; case 'y': if (_strict && (*it_scanned).length() != (*it_format).length()) throw 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 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 MYMONEYEXCEPTION(QString("Invalid year (%1)").arg(year)); break; default: throw MYMONEYEXCEPTION("Invalid format character"); } ++it_scanned; ++it_format; } QDate result(year, month, day); if (! result.isValid()) throw 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) : QObject(parent) { connect(q, SIGNAL(quote(QString,QString,QDate,double)), this, SLOT(slotGetQuote(QString,QString,QDate,double))); connect(q, SIGNAL(status(QString)), this, SLOT(slotStatus(QString))); connect(q, SIGNAL(error(QString)), this, SLOT(slotError(QString))); } convertertest::QuoteReceiver::~QuoteReceiver() { } void convertertest::QuoteReceiver::slotGetQuote(const QString&, const QString&, const QDate& d, const double& m) { // qDebug() << "test::QuoteReceiver::slotGetQuote( , " << d << " , " << m.toString() << " )"; m_price = MyMoneyMoney(m); m_date = d; } void convertertest::QuoteReceiver::slotStatus(const QString& msg) { // qDebug() << "test::QuoteReceiver::slotStatus( " << msg << " )"; m_statuses += msg; } void convertertest::QuoteReceiver::slotError(const QString& msg) { // qDebug() << "test::QuoteReceiver::slotError( " << msg << " )"; m_errors += msg; } // leave this moc until we will have resolved our dependency issues // now 'converter' depends on 'kmymoney' a pointer to the application // defined in main.cpp, which makes this static library unusable without // the --as-needed linker flag; // otherwise the 'moc' methods of this object will be linked into the automoc // object file which contains methods from all the other objects from this // library, thus even if the --as-needed option is used all objects will be // pulled in while linking 'convertertest' which only needs the WebPriceQuote // object - spent a whole day investigating this #include "moc_webpricequote.cpp" diff --git a/kmymoney/dialogs/CMakeLists.txt b/kmymoney/dialogs/CMakeLists.txt index 997c7d1a6..7a2f18208 100644 --- a/kmymoney/dialogs/CMakeLists.txt +++ b/kmymoney/dialogs/CMakeLists.txt @@ -1,110 +1,112 @@ add_subdirectory( settings ) ########### next target ############### set(libdialogs_a_SOURCES splitadjustdialog.cpp investactivities.cpp investtransactioneditor.cpp kaccountselectdlg.cpp kbackupdlg.cpp kbalancechartdlg.cpp kbalancewarning.cpp kcategoryreassigndlg.cpp kchooseimportexportdlg.cpp kconfirmmanualenterdlg.cpp kcurrencycalculator.cpp kcurrencyeditdlg.cpp kavailablecurrencydlg.cpp kcurrencyeditordlg.cpp keditscheduledlg.cpp kenterscheduledlg.cpp kequitypriceupdatedlg.cpp kequitypriceupdateconfdlg.cpp kexportdlg.cpp kfindtransactiondlg.cpp kgeneratesqldlg.cpp kgncimportoptionsdlg.cpp kgncpricesourcedlg.cpp kgpgkeyselectiondlg.cpp kimportdlg.cpp kloadtemplatedlg.cpp kmergetransactionsdlg.cpp kmymoneyfileinfodlg.cpp kmymoneypricedlg.cpp kmymoneysplittable.cpp knewaccountdlg.cpp knewbankdlg.cpp knewbudgetdlg.cpp knewequityentrydlg.cpp editpersonaldatadlg.cpp kpayeereassigndlg.cpp ktagreassigndlg.cpp kreportconfigurationfilterdlg.cpp kselectdatabasedlg.cpp kselecttransactionsdlg.cpp ksplittransactiondlg.cpp + ktemplateexportdlg.cpp kupdatestockpricedlg.cpp mymoneyqifprofileeditor.cpp transactioneditor.cpp transactionmatcher.cpp konlinetransferform.cpp ) set(dialogs_HEADERS splitadjustdialog.h investtransactioneditor.h kcurrencycalculator.h transactioneditor.h ) set(dialogs_UI splitadjustdialog.ui kaccountselectdlgdecl.ui kbackupdlgdecl.ui kcategoryreassigndlgdecl.ui kchooseimportexportdlgdecl.ui kconfirmmanualenterdlgdecl.ui kcurrencycalculatordecl.ui kcurrencyeditdlg.ui kavailablecurrencydlg.ui kcurrencyeditordlg.ui keditscheduledlgdecl.ui kenterscheduledlgdecl.ui kequitypriceupdatedlgdecl.ui kequitypriceupdateconfdlg.ui kexportdlgdecl.ui kfindtransactiondlgdecl.ui kgeneratesqldlgdecl.ui kgncimportoptionsdlgdecl.ui kgncpricesourcedlgdecl.ui kimportdlgdecl.ui kloadtemplatedlgdecl.ui kmymoneyfileinfodlgdecl.ui kmymoneypricedlgdecl.ui knewaccountdlgdecl.ui knewbankdlgdecl.ui knewbudgetdlgdecl.ui knewequityentrydecl.ui editpersonaldatadlgdecl.ui kpayeereassigndlgdecl.ui ktagreassigndlgdecl.ui kselectdatabasedlgdecl.ui kselecttransactionsdlgdecl.ui ksortoptiondlg.ui ksplitcorrectiondlg.ui ksplittransactiondlgdecl.ui + ktemplateexportdlg.ui kupdatestockpricedlgdecl.ui mymoneyqifprofileeditordecl.ui ../widgets/kaccounttemplateselectordecl.ui ../widgets/transactionsortoptiondecl.ui konlinetransferformdecl.ui ) ki18n_wrap_ui(libdialogs_a_SOURCES ${dialogs_UI} ) add_library(dialogs STATIC ${libdialogs_a_SOURCES}) target_link_libraries(dialogs PUBLIC KChart KF5::ItemViews KF5::I18n KF5::TextWidgets KF5::Completion Qt5::Widgets Qt5::Sql Alkimia::alkimia kmm_mymoney onlinetask_interfaces kmm_widgets ) target_link_libraries(dialogs LINK_PUBLIC kmm_widgets kmm_mymoney onlinetask_interfaces ) ########### install files ############### install(FILES ${dialogs_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR}/kmymoney COMPONENT Devel) diff --git a/kmymoney/dialogs/kfindtransactiondlg.cpp b/kmymoney/dialogs/kfindtransactiondlg.cpp index 41eaf74dd..90833b428 100644 --- a/kmymoney/dialogs/kfindtransactiondlg.cpp +++ b/kmymoney/dialogs/kfindtransactiondlg.cpp @@ -1,934 +1,937 @@ /*************************************************************************** kfindtransactiondlg.cpp ------------------- copyright : (C) 2003, 2007 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 "config-kmymoney.h" #include "kfindtransactiondlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include #include #include #include #include #include #include #include #include "ui_kfindtransactiondlgdecl.h" #include "ui_ksortoptiondlg.h" #include enum ItemRoles { ItemIdRole = Qt::UserRole }; struct KSortOptionDlg::Private { Ui::KSortOptionDlg ui; }; KSortOptionDlg::KSortOptionDlg(QWidget *parent) : QDialog(parent), d(new Private) { d->ui.setupUi(this); init(); } KSortOptionDlg::~KSortOptionDlg() { delete d; } void KSortOptionDlg::init() { } void KSortOptionDlg::setSortOption(const QString& option, const QString& def) { if (option.isEmpty()) { d->ui.m_sortOption->setSettings(def); d->ui.m_useDefault->setChecked(true); } else { d->ui.m_sortOption->setSettings(option); d->ui.m_useDefault->setChecked(false); } } QString KSortOptionDlg::sortOption() const { QString rc; if (!d->ui.m_useDefault->isChecked()) { rc = d->ui.m_sortOption->settings(); } return rc; } void KSortOptionDlg::hideDefaultButton() { d->ui.m_useDefault->hide(); } KFindTransactionDlg::KFindTransactionDlg(QWidget *parent) : QDialog(parent), m_needReload(false), m_ui(new Ui::KFindTransactionDlgDecl) { m_ui->setupUi(this); m_dateRange = new DateRangeDlg(m_ui->m_dateTab); m_ui->dateRangeLayout->insertWidget(0, m_dateRange); m_ui->ButtonGroup1->setId(m_ui->m_amountButton, 0); m_ui->ButtonGroup1->setId(m_ui->m_amountRangeButton, 1); m_ui->m_register->installEventFilter(this); m_ui->m_tabWidget->setTabEnabled(m_ui->m_tabWidget->indexOf(m_ui->m_resultPage), false); // 'cause we don't have a separate setupTextPage connect(m_ui->m_textEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateSelections())); // if return is pressed trigger a search (slotSearch checks if it's possible to perform the search) connect(m_ui->m_textEdit, SIGNAL(returnPressed()), this, SLOT(slotSearch())); setupAccountsPage(); setupCategoriesPage(); setupAmountPage(); setupPayeesPage(); setupTagsPage(); setupDetailsPage(); // We don't need to add the default into the list (see ::slotShowHelp() why) // m_helpAnchor[m_ui->m_textTab] = QString("details.search"); m_helpAnchor[m_ui->m_accountTab] = QString("details.search.account"); m_helpAnchor[m_ui->m_dateTab] = QString("details.search.date"); m_helpAnchor[m_ui->m_amountTab] = QString("details.search.amount"); m_helpAnchor[m_ui->m_categoryTab] = QString("details.search.category"); m_helpAnchor[m_ui->m_payeeTab] = QString("details.search.payee"); m_helpAnchor[m_ui->m_tagTab] = QString("details.search.tag"); //FIXME-ALEX update Help m_helpAnchor[m_ui->m_detailsTab] = QString("details.search.details"); // setup the register QList cols; cols << KMyMoneyRegister::DateColumn; cols << KMyMoneyRegister::AccountColumn; cols << KMyMoneyRegister::DetailColumn; cols << KMyMoneyRegister::ReconcileFlagColumn; cols << KMyMoneyRegister::PaymentColumn; cols << KMyMoneyRegister::DepositColumn; m_ui->m_register->setupRegister(MyMoneyAccount(), cols); m_ui->m_register->setSelectionMode(QTableWidget::SingleSelection); connect(m_ui->m_register, SIGNAL(editTransaction()), this, SLOT(slotSelectTransaction())); connect(m_ui->m_register->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotSortOptions())); slotUpdateSelections(); // setup the connections connect(m_ui->buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(slotSearch())); connect(m_ui->buttonBox->button(QDialogButtonBox::Reset), SIGNAL(clicked()), this, SLOT(slotReset())); connect(m_ui->buttonBox->button(QDialogButtonBox::Reset), SIGNAL(clicked()), m_ui->m_accountsView, SLOT(slotSelectAllAccounts())); connect(m_ui->buttonBox->button(QDialogButtonBox::Reset), SIGNAL(clicked()), m_ui->m_categoriesView, SLOT(slotSelectAllAccounts())); connect(m_ui->buttonBox->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(deleteLater())); connect(m_ui->buttonBox->button(QDialogButtonBox::Help), SIGNAL(clicked()), this, SLOT(slotShowHelp())); // only allow searches when a selection has been made m_ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); KGuiItem::assign(m_ui->buttonBox->button(QDialogButtonBox::Apply), KStandardGuiItem::find()); m_ui->buttonBox->button(QDialogButtonBox::Apply)->setToolTip(i18nc("@info:tooltip for find transaction apply button", "Search transactions")); connect(this, SIGNAL(selectionNotEmpty(bool)), m_ui->buttonBox->button(QDialogButtonBox::Apply), SLOT(setEnabled(bool))); // get signal about engine changes connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotRefreshView())); slotUpdateSelections(); m_ui->m_textEdit->setFocus(); } KFindTransactionDlg::~KFindTransactionDlg() { delete m_ui; } void KFindTransactionDlg::slotReset() { m_ui->m_textEdit->setText(QString()); m_ui->m_regExp->setChecked(false); m_ui->m_caseSensitive->setChecked(false); m_ui->m_textNegate->setCurrentItem(0); m_ui->m_amountEdit->setEnabled(true); m_ui->m_amountFromEdit->setEnabled(false); m_ui->m_amountToEdit->setEnabled(false); m_ui->m_amountEdit->loadText(QString()); m_ui->m_amountFromEdit->loadText(QString()); m_ui->m_amountToEdit->loadText(QString()); m_ui->m_amountButton->setChecked(true); m_ui->m_amountRangeButton->setChecked(false); m_ui->m_emptyPayeesButton->setChecked(false); selectAllItems(m_ui->m_payeesView, true); m_ui->m_emptyTagsButton->setChecked(false); selectAllItems(m_ui->m_tagsView, true); m_ui->m_typeBox->setCurrentIndex(MyMoneyTransactionFilter::allTypes); m_ui->m_stateBox->setCurrentIndex(MyMoneyTransactionFilter::allStates); m_ui->m_validityBox->setCurrentIndex(MyMoneyTransactionFilter::anyValidity); m_ui->m_nrEdit->setEnabled(true); m_ui->m_nrFromEdit->setEnabled(false); m_ui->m_nrToEdit->setEnabled(false); m_ui->m_nrEdit->setText(QString()); m_ui->m_nrFromEdit->setText(QString()); m_ui->m_nrToEdit->setText(QString()); m_ui->m_nrButton->setChecked(true); m_ui->m_nrRangeButton->setChecked(false); m_ui->m_tabWidget->setTabEnabled(m_ui->m_tabWidget->indexOf(m_ui->m_resultPage), false); m_ui->m_tabWidget->setCurrentIndex(m_ui->m_tabWidget->indexOf(m_ui->m_criteriaTab)); // the following call implies a call to slotUpdateSelections, // that's why we call it last m_dateRange->slotReset(); slotUpdateSelections(); } void KFindTransactionDlg::slotUpdateSelections() { QString txt; // Text tab if (!m_ui->m_textEdit->text().isEmpty()) { if (!txt.isEmpty()) txt += ", "; txt += i18n("Text"); m_ui->m_regExp->setEnabled(QRegExp(m_ui->m_textEdit->text()).isValid()); } else m_ui->m_regExp->setEnabled(false); m_ui->m_caseSensitive->setEnabled(!m_ui->m_textEdit->text().isEmpty()); m_ui->m_textNegate->setEnabled(!m_ui->m_textEdit->text().isEmpty()); // Account tab if (!m_ui->m_accountsView->allItemsSelected()) { if (!txt.isEmpty()) txt += ", "; txt += i18n("Account"); } m_dateRange->slotUpdateSelections(txt); // Amount tab if ((m_ui->m_amountButton->isChecked() && m_ui->m_amountEdit->isValid()) || (m_ui->m_amountRangeButton->isChecked() && (m_ui->m_amountFromEdit->isValid() || m_ui->m_amountToEdit->isValid()))) { if (!txt.isEmpty()) txt += ", "; txt += i18n("Amount"); } // Categories tab if (!m_ui->m_categoriesView->allItemsSelected()) { if (!txt.isEmpty()) txt += ", "; txt += i18n("Category"); } // Tags tab if (!allItemsSelected(m_ui->m_tagsView) || m_ui->m_emptyTagsButton->isChecked()) { if (!txt.isEmpty()) txt += ", "; txt += i18n("Tags"); } m_ui->m_tagsView->setEnabled(!m_ui->m_emptyTagsButton->isChecked()); // Payees tab if (!allItemsSelected(m_ui->m_payeesView) || m_ui->m_emptyPayeesButton->isChecked()) { if (!txt.isEmpty()) txt += ", "; txt += i18n("Payees"); } m_ui->m_payeesView->setEnabled(!m_ui->m_emptyPayeesButton->isChecked()); // Details tab if (m_ui->m_typeBox->currentIndex() != 0 || m_ui->m_stateBox->currentIndex() != 0 || m_ui->m_validityBox->currentIndex() != 0 || (m_ui->m_nrButton->isChecked() && m_ui->m_nrEdit->text().length() != 0) || (m_ui->m_nrRangeButton->isChecked() && (m_ui->m_nrFromEdit->text().length() != 0 || m_ui->m_nrToEdit->text().length() != 0))) { if (!txt.isEmpty()) txt += ", "; txt += i18n("Details"); } //Show a warning about transfers if Categories are filtered - bug #1523508 if (!m_ui->m_categoriesView->allItemsSelected()) { m_ui->m_transferWarning->setText(i18n("Warning: Filtering by Category will exclude all transfers from the results.")); } else { m_ui->m_transferWarning->setText(""); } // disable the search button if no selection is made emit selectionNotEmpty(!txt.isEmpty()); if (txt.isEmpty()) { txt = i18nc("No selection", "(None)"); } m_ui->m_selectedCriteria->setText(i18n("Current selections: %1", txt)); } bool KFindTransactionDlg::allItemsSelected(const QTreeWidgetItem *item) const { QTreeWidgetItem* it_v; for (int i = 0; i < item->childCount(); ++i) { it_v = item->child(i); if (!(it_v->checkState(0) == Qt::Checked && allItemsSelected(it_v))) { return false; } } return true; } bool KFindTransactionDlg::allItemsSelected(const QTreeWidget* view) const { QTreeWidgetItem* it_v; for (int i = 0; i < view->invisibleRootItem()->childCount(); ++i) { it_v = view->invisibleRootItem()->child(i); if (it_v->flags() & Qt::ItemIsUserCheckable) { if (!(it_v->checkState(0) == Qt::Checked && allItemsSelected(it_v))) { return false; } else { if (!allItemsSelected(it_v)) return false; } } } return true; } void KFindTransactionDlg::setupAccountsPage() { m_ui->m_accountsView->setSelectionMode(QTreeWidget::MultiSelection); AccountSet accountSet; accountSet.addAccountGroup(MyMoneyAccount::Asset); accountSet.addAccountGroup(MyMoneyAccount::Liability); + accountSet.addAccountGroup(MyMoneyAccount::Equity); + accountSet.addAccountGroup(MyMoneyAccount::Income); + accountSet.addAccountGroup(MyMoneyAccount::Expense); //set the accountset to show closed account if the settings say so accountSet.setHideClosedAccounts(KMyMoneyGlobalSettings::hideClosedAccounts()); accountSet.load(m_ui->m_accountsView); connect(m_ui->m_accountsView, SIGNAL(stateChanged()), this, SLOT(slotUpdateSelections())); } void KFindTransactionDlg::selectAllItems(QTreeWidget* view, const bool state) { QTreeWidgetItem* it_v; for (int i = 0; i < view->invisibleRootItem()->childCount(); ++i) { it_v = view->invisibleRootItem()->child(i); if (it_v->flags() & Qt::ItemIsUserCheckable) { it_v->setCheckState(0, state ? Qt::Checked : Qt::Unchecked); } selectAllSubItems(it_v, state); } slotUpdateSelections(); } void KFindTransactionDlg::selectItems(QTreeWidget* view, const QStringList& list, const bool state) { QTreeWidgetItem* it_v; for (int i = 0; i < view->invisibleRootItem()->childCount(); ++i) { it_v = view->invisibleRootItem()->child(i); QVariant idData = it_v->data(0, ItemIdRole); if (it_v->flags() & Qt::ItemIsUserCheckable && list.contains(idData.toString())) { it_v->setCheckState(0, state ? Qt::Checked : Qt::Unchecked); } selectSubItems(it_v, list, state); } slotUpdateSelections(); } void KFindTransactionDlg::setupCategoriesPage() { m_ui->m_categoriesView->setSelectionMode(QTreeWidget::MultiSelection); AccountSet categorySet; categorySet.addAccountGroup(MyMoneyAccount::Income); categorySet.addAccountGroup(MyMoneyAccount::Expense); categorySet.load(m_ui->m_categoriesView); connect(m_ui->m_categoriesView, SIGNAL(stateChanged()), this, SLOT(slotUpdateSelections())); } void KFindTransactionDlg::selectAllSubItems(QTreeWidgetItem* item, const bool state) { QTreeWidgetItem* it_v; for (int i = 0; i < item->childCount(); ++i) { it_v = item->child(i); it_v->setCheckState(0, state ? Qt::Checked : Qt::Unchecked); selectAllSubItems(it_v, state); } } void KFindTransactionDlg::selectSubItems(QTreeWidgetItem* item, const QStringList& list, const bool state) { QTreeWidgetItem* it_v; for (int i = 0; i < item->childCount(); ++i) { it_v = item->child(i); QVariant idData = it_v->data(0, ItemIdRole); if (list.contains(idData.toString())) it_v->setCheckState(0, state ? Qt::Checked : Qt::Unchecked); selectSubItems(it_v, list, state); } } void KFindTransactionDlg::setupAmountPage() { connect(m_ui->m_amountButton, SIGNAL(clicked()), this, SLOT(slotAmountSelected())); connect(m_ui->m_amountRangeButton, SIGNAL(clicked()), this, SLOT(slotAmountRangeSelected())); connect(m_ui->m_amountEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateSelections())); connect(m_ui->m_amountFromEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateSelections())); connect(m_ui->m_amountToEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateSelections())); m_ui->m_amountButton->setChecked(true); slotAmountSelected(); } void KFindTransactionDlg::slotAmountSelected() { m_ui->m_amountEdit->setEnabled(true); m_ui->m_amountFromEdit->setEnabled(false); m_ui->m_amountToEdit->setEnabled(false); slotUpdateSelections(); } void KFindTransactionDlg::slotAmountRangeSelected() { m_ui->m_amountEdit->setEnabled(false); m_ui->m_amountFromEdit->setEnabled(true); m_ui->m_amountToEdit->setEnabled(true); slotUpdateSelections(); } void KFindTransactionDlg::setupPayeesPage() { m_ui->m_payeesView->setSelectionMode(QAbstractItemView::SingleSelection); m_ui->m_payeesView->header()->hide(); m_ui->m_payeesView->setAlternatingRowColors(true); loadPayees(); m_ui->m_payeesView->sortItems(0, Qt::AscendingOrder); m_ui->m_emptyPayeesButton->setCheckState(Qt::Unchecked); connect(m_ui->m_allPayeesButton, SIGNAL(clicked()), this, SLOT(slotSelectAllPayees())); connect(m_ui->m_clearPayeesButton, SIGNAL(clicked()), this, SLOT(slotDeselectAllPayees())); connect(m_ui->m_emptyPayeesButton, SIGNAL(stateChanged(int)), this, SLOT(slotUpdateSelections())); connect(m_ui->m_payeesView, SIGNAL(itemChanged(QTreeWidgetItem*,int)), this, SLOT(slotUpdateSelections())); } void KFindTransactionDlg::loadPayees() { MyMoneyFile* file = MyMoneyFile::instance(); QList list; QList::Iterator it_l; list = file->payeeList(); // load view for (it_l = list.begin(); it_l != list.end(); ++it_l) { QTreeWidgetItem* item = new QTreeWidgetItem(m_ui->m_payeesView); item->setText(0, (*it_l).name()); item->setData(0, ItemIdRole, (*it_l).id()); item->setCheckState(0, Qt::Checked); } } void KFindTransactionDlg::slotSelectAllPayees() { selectAllItems(m_ui->m_payeesView, true); } void KFindTransactionDlg::slotDeselectAllPayees() { selectAllItems(m_ui->m_payeesView, false); } void KFindTransactionDlg::setupTagsPage() { m_ui->m_tagsView->setSelectionMode(QAbstractItemView::SingleSelection); m_ui->m_tagsView->header()->hide(); m_ui->m_tagsView->setAlternatingRowColors(true); loadTags(); m_ui->m_tagsView->sortItems(0, Qt::AscendingOrder); m_ui->m_emptyTagsButton->setCheckState(Qt::Unchecked); connect(m_ui->m_allTagsButton, SIGNAL(clicked()), this, SLOT(slotSelectAllTags())); connect(m_ui->m_clearTagsButton, SIGNAL(clicked()), this, SLOT(slotDeselectAllTags())); connect(m_ui->m_emptyTagsButton, SIGNAL(stateChanged(int)), this, SLOT(slotUpdateSelections())); connect(m_ui->m_tagsView, SIGNAL(itemChanged(QTreeWidgetItem*,int)), this, SLOT(slotUpdateSelections())); } void KFindTransactionDlg::loadTags() { MyMoneyFile* file = MyMoneyFile::instance(); QList list; QList::Iterator it_l; list = file->tagList(); // load view for (it_l = list.begin(); it_l != list.end(); ++it_l) { QTreeWidgetItem* item = new QTreeWidgetItem(m_ui->m_tagsView); item->setText(0, (*it_l).name()); item->setData(0, ItemIdRole, (*it_l).id()); item->setCheckState(0, Qt::Checked); } } void KFindTransactionDlg::slotSelectAllTags() { selectAllItems(m_ui->m_tagsView, true); } void KFindTransactionDlg::slotDeselectAllTags() { selectAllItems(m_ui->m_tagsView, false); } void KFindTransactionDlg::setupDetailsPage() { connect(m_ui->m_typeBox, SIGNAL(activated(int)), this, SLOT(slotUpdateSelections())); connect(m_ui->m_stateBox, SIGNAL(activated(int)), this, SLOT(slotUpdateSelections())); connect(m_ui->m_validityBox, SIGNAL(activated(int)), this, SLOT(slotUpdateSelections())); connect(m_ui->m_nrButton, SIGNAL(clicked()), this, SLOT(slotNrSelected())); connect(m_ui->m_nrRangeButton, SIGNAL(clicked()), this, SLOT(slotNrRangeSelected())); connect(m_ui->m_nrEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateSelections())); connect(m_ui->m_nrFromEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateSelections())); connect(m_ui->m_nrToEdit, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateSelections())); m_ui->m_nrButton->setChecked(true); slotNrSelected(); } void KFindTransactionDlg::slotNrSelected() { m_ui->m_nrEdit->setEnabled(true); m_ui->m_nrFromEdit->setEnabled(false); m_ui->m_nrToEdit->setEnabled(false); slotUpdateSelections(); } void KFindTransactionDlg::slotNrRangeSelected() { m_ui->m_nrEdit->setEnabled(false); m_ui->m_nrFromEdit->setEnabled(true); m_ui->m_nrToEdit->setEnabled(true); slotUpdateSelections(); } void KFindTransactionDlg::addItemToFilter(const opTypeE op, const QString& id) { switch (op) { case addAccountToFilter: m_filter.addAccount(id); break; case addCategoryToFilter: m_filter.addCategory(id); break; case addPayeeToFilter: m_filter.addPayee(id); break; case addTagToFilter: m_filter.addTag(id); break; } } void KFindTransactionDlg::scanCheckListItems(const QTreeWidgetItem* item, const opTypeE op) { QTreeWidgetItem* it_v; for (int i = 0; i < item->childCount(); ++i) { it_v = item->child(i); QVariant idData = it_v->data(0, ItemIdRole); if (it_v->flags() & Qt::ItemIsUserCheckable) { if (it_v->checkState(0) == Qt::Checked) addItemToFilter(op, idData.toString()); } scanCheckListItems(it_v, op); } } void KFindTransactionDlg::scanCheckListItems(const QTreeWidget* view, const opTypeE op) { QTreeWidgetItem* it_v; for (int i = 0; i < view->invisibleRootItem()->childCount(); ++i) { it_v = view->invisibleRootItem()->child(i); QVariant idData = it_v->data(0, ItemIdRole); if (it_v->flags() & Qt::ItemIsUserCheckable) { if (it_v->checkState(0) == Qt::Checked) { addItemToFilter(op, idData.toString()); } } scanCheckListItems(it_v, op); } } void KFindTransactionDlg::setupFilter() { m_filter.clear(); // Text tab if (!m_ui->m_textEdit->text().isEmpty()) { QRegExp exp(m_ui->m_textEdit->text(), m_ui->m_caseSensitive->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive, !m_ui->m_regExp->isChecked() ? QRegExp::Wildcard : QRegExp::RegExp); m_filter.setTextFilter(exp, m_ui->m_textNegate->currentIndex() != 0); } // Account tab if (!m_ui->m_accountsView->allItemsSelected()) { // retrieve a list of selected accounts QStringList list; m_ui->m_accountsView->selectedItems(list); // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected if (!KMyMoneyGlobalSettings::expertMode()) { QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { MyMoneyAccount acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == MyMoneyAccount::Investment) { for (it_b = acc.accountList().constBegin(); it_b != acc.accountList().constEnd(); ++it_b) { if (!list.contains(*it_b)) { missing.append(*it_b); } } } } list += missing; } m_filter.addAccount(list); } // Date tab if (m_dateRange->m_ui->m_dateRange->currentItem() != 0) { m_filter.setDateFilter(m_dateRange->m_ui->m_fromDate->date(), m_dateRange->m_ui->m_toDate->date()); } // Amount tab if ((m_ui->m_amountButton->isChecked() && m_ui->m_amountEdit->isValid())) { m_filter.setAmountFilter(m_ui->m_amountEdit->value(), m_ui->m_amountEdit->value()); } else if ((m_ui->m_amountRangeButton->isChecked() && (m_ui->m_amountFromEdit->isValid() || m_ui->m_amountToEdit->isValid()))) { MyMoneyMoney from(MyMoneyMoney::minValue), to(MyMoneyMoney::maxValue); if (m_ui->m_amountFromEdit->isValid()) from = m_ui->m_amountFromEdit->value(); if (m_ui->m_amountToEdit->isValid()) to = m_ui->m_amountToEdit->value(); m_filter.setAmountFilter(from, to); } // Categories tab if (!m_ui->m_categoriesView->allItemsSelected()) { m_filter.addCategory(m_ui->m_categoriesView->selectedItems()); } // Tags tab if (m_ui->m_emptyTagsButton->isChecked()) { m_filter.addTag(QString()); } else if (!allItemsSelected(m_ui->m_tagsView)) { scanCheckListItems(m_ui->m_tagsView, addTagToFilter); } // Payees tab if (m_ui->m_emptyPayeesButton->isChecked()) { m_filter.addPayee(QString()); } else if (!allItemsSelected(m_ui->m_payeesView)) { scanCheckListItems(m_ui->m_payeesView, addPayeeToFilter); } // Details tab if (m_ui->m_typeBox->currentIndex() != 0) m_filter.addType(m_ui->m_typeBox->currentIndex()); if (m_ui->m_stateBox->currentIndex() != 0) m_filter.addState(m_ui->m_stateBox->currentIndex()); if (m_ui->m_validityBox->currentIndex() != 0) m_filter.addValidity(m_ui->m_validityBox->currentIndex()); if (m_ui->m_nrButton->isChecked() && !m_ui->m_nrEdit->text().isEmpty()) m_filter.setNumberFilter(m_ui->m_nrEdit->text(), m_ui->m_nrEdit->text()); if (m_ui->m_nrRangeButton->isChecked() && (!m_ui->m_nrFromEdit->text().isEmpty() || !m_ui->m_nrToEdit->text().isEmpty())) { m_filter.setNumberFilter(m_ui->m_nrFromEdit->text(), m_ui->m_nrToEdit->text()); } } void KFindTransactionDlg::slotSearch() { // perform the search only if the button is enabled if (!m_ui->buttonBox->button(QDialogButtonBox::Apply)->isEnabled()) return; // setup the filter from the dialog widgets setupFilter(); // filter is setup, now fill the register slotRefreshView(); m_ui->m_register->setFocus(); } void KFindTransactionDlg::slotRefreshView() { m_needReload = true; if (isVisible()) { loadView(); m_needReload = false; } } void KFindTransactionDlg::showEvent(QShowEvent* event) { if (m_needReload) { loadView(); m_needReload = false; } QDialog::showEvent(event); } void KFindTransactionDlg::loadView() { // setup sort order m_ui->m_register->setSortOrder(KMyMoneyGlobalSettings::sortSearchView()); // clear out old data m_ui->m_register->clear(); // retrieve the list from the engine MyMoneyFile::instance()->transactionList(m_transactionList, m_filter); // create the elements for the register QList >::const_iterator it; QMapuniqueMap; MyMoneyMoney deposit, payment; int splitCount = 0; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { const MyMoneySplit& split = (*it).second; MyMoneyAccount acc = MyMoneyFile::instance()->account(split.accountId()); ++splitCount; uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Register::transactionFactory(m_ui->m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); { // debug stuff if (split.shares().isNegative()) { payment += split.shares().abs(); } else { deposit += split.shares().abs(); } } } // add the group markers m_ui->m_register->addGroupMarkers(); // sort the transactions according to the sort setting m_ui->m_register->sortItems(); // remove trailing and adjacent markers m_ui->m_register->removeUnwantedGroupMarkers(); // turn on the ledger lens for the register m_ui->m_register->setLedgerLensForced(); m_ui->m_register->updateRegister(true); m_ui->m_register->setFocusToTop(); m_ui->m_register->selectItem(m_ui->m_register->focusItem()); #ifdef KMM_DEBUG m_ui->m_foundText->setText(i18np("Found %1 matching transaction (D %2 / P %3 = %4)", "Found %1 matching transactions (D %2 / P %3 = %4)", splitCount, deposit.formatMoney("", 2), payment.formatMoney("", 2), (deposit - payment).formatMoney("", 2))); #else m_ui->m_foundText->setText(i18np("Found %1 matching transaction", "Found %1 matching transactions", splitCount)); #endif m_ui->m_tabWidget->setTabEnabled(m_ui->m_tabWidget->indexOf(m_ui->m_resultPage), true); m_ui->m_tabWidget->setCurrentIndex(m_ui->m_tabWidget->indexOf(m_ui->m_resultPage)); QTimer::singleShot(10, this, SLOT(slotRightSize())); } void KFindTransactionDlg::slotRightSize() { m_ui->m_register->update(); } void KFindTransactionDlg::resizeEvent(QResizeEvent* ev) { // Columns // 1 = Date // 2 = Account // 4 = Detail // 5 = C // 6 = Payment // 7 = Deposit // don't forget the resizer QDialog::resizeEvent(ev); if (!m_ui->m_register->isVisible()) return; // resize the register int w = m_ui->m_register->contentsRect().width(); int m_debitWidth = 80; int m_creditWidth = 80; m_ui->m_register->adjustColumn(1); m_ui->m_register->adjustColumn(2); m_ui->m_register->adjustColumn(5); m_ui->m_register->setColumnWidth(6, m_debitWidth); m_ui->m_register->setColumnWidth(7, m_creditWidth); for (int i = 0; i < m_ui->m_register->columnCount(); ++i) { switch (i) { case 4: // skip the one, we want to set break; default: w -= m_ui->m_register->columnWidth(i); break; } } m_ui->m_register->setColumnWidth(4, w); } void KFindTransactionDlg::slotSelectTransaction() { QList list = m_ui->m_register->selectedItems(); if (!list.isEmpty()) { KMyMoneyRegister::Transaction* t = dynamic_cast(list[0]); if (t) { emit transactionSelected(t->split().accountId(), t->transaction().id()); hide(); } } } bool KFindTransactionDlg::eventFilter(QObject* o, QEvent* e) { bool rc = false; if (o->isWidgetType()) { if (e->type() == QEvent::KeyPress) { const QWidget* w = dynamic_cast(o); QKeyEvent *k = static_cast(e); if (w == m_ui->m_register) { switch (k->key()) { default: break; case Qt::Key_Return: case Qt::Key_Enter: rc = true; slotSelectTransaction(); break; } } } } return rc; } void KFindTransactionDlg::slotShowHelp() { QString anchor = m_helpAnchor[m_ui->m_criteriaTab->currentWidget()]; if (anchor.isEmpty()) anchor = QString("details.search"); KHelpClient::invokeHelp(anchor); } void KFindTransactionDlg::slotSortOptions() { QPointer dlg = new KSortOptionDlg(this); dlg->setSortOption(KMyMoneyGlobalSettings::sortSearchView(), QString()); dlg->hideDefaultButton(); if (dlg->exec() == QDialog::Accepted) { QString sortOrder = dlg->sortOption(); if (sortOrder != KMyMoneyGlobalSettings::sortSearchView()) { KMyMoneyGlobalSettings::setSortSearchView(sortOrder); slotRefreshView(); } } delete dlg; } diff --git a/kmymoney/dialogs/knewaccountdlg.cpp b/kmymoney/dialogs/knewaccountdlg.cpp index aded1b965..0a6343513 100644 --- a/kmymoney/dialogs/knewaccountdlg.cpp +++ b/kmymoney/dialogs/knewaccountdlg.cpp @@ -1,885 +1,933 @@ /*************************************************************************** knewaccountdlg.cpp ------------------- copyright : (C) 2000 by Michael Edwardes 2004 by Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 "knewaccountdlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include #include #include #include #include #include "kmymoneycurrencyselector.h" #include "mymoneykeyvaluecontainer.h" #include "knewbankdlg.h" #include "kmymoneyfile.h" #include "kmymoneyutils.h" #include "models.h" HierarchyFilterProxyModel::HierarchyFilterProxyModel(QObject *parent) : AccountsFilterProxyModel(parent) { } /** * The current account and all it's children are not selectable because the view is used to select a possible parent account. */ Qt::ItemFlags HierarchyFilterProxyModel::flags(const QModelIndex &index) const { Qt::ItemFlags flags = AccountsFilterProxyModel::flags(index); QModelIndex currentIndex = index; while (currentIndex.isValid()) { QVariant accountId = data(currentIndex, AccountsModel::AccountIdRole); if (accountId.isValid() && accountId.toString() == m_currentAccountId) { flags = flags & ~Qt::ItemIsSelectable & ~Qt::ItemIsEnabled; } currentIndex = currentIndex.parent(); } return flags; } /** * Set the account for which to select a parent. * * @param currentAccountId The current account. */ void HierarchyFilterProxyModel::setCurrentAccountId(const QString ¤tAccountId) { m_currentAccountId = currentAccountId; } /** * Get the index of the selected parent account. * * @return The model index of the selected parent account. */ QModelIndex HierarchyFilterProxyModel::getSelectedParentAccountIndex() const { QModelIndexList list = match(index(0, 0), AccountsModel::AccountIdRole, m_currentAccountId, -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (!list.empty()) { return list.front().parent(); } return QModelIndex(); } /** * Filter the favorites accounts group. */ bool HierarchyFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { if (!source_parent.isValid()) { QVariant data = sourceModel()->index(source_row, 0, source_parent).data(AccountsModel::AccountIdRole); if (data.isValid() && data.toString() == AccountsModel::favoritesAccountId) return false; } return AccountsFilterProxyModel::filterAcceptsRow(source_row, source_parent); } /** * Filter all but the first column. */ bool HierarchyFilterProxyModel::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const { Q_UNUSED(source_parent) if (source_column == 0) return true; return false; } KNewAccountDlg::KNewAccountDlg(const MyMoneyAccount& account, bool isEditing, bool categoryEditor, QWidget *parent, const QString& title) : KNewAccountDlgDecl(parent), m_account(account), m_categoryEditor(categoryEditor), m_isEditing(isEditing) { QString columnName = ((categoryEditor) ? i18n("Categories") : i18n("Accounts")); MyMoneyFile *file = MyMoneyFile::instance(); // initialize the m_parentAccount member MyMoneyAccount::accountTypeE filterAccountGroup = m_account.accountGroup(); switch (m_account.accountGroup()) { case MyMoneyAccount::Asset: m_parentAccount = file->asset(); break; case MyMoneyAccount::Liability: m_parentAccount = file->liability(); break; case MyMoneyAccount::Income: m_parentAccount = file->income(); break; case MyMoneyAccount::Expense: m_parentAccount = file->expense(); break; case MyMoneyAccount::Equity: m_parentAccount = file->equity(); break; default: qDebug("Seems we have an account that hasn't been mapped to the top five"); if (m_categoryEditor) { m_parentAccount = file->income(); filterAccountGroup = MyMoneyAccount::Income; } else { m_parentAccount = file->asset(); filterAccountGroup = MyMoneyAccount::Asset; } } m_amountGroup->setId(m_grossAmount, 0); m_amountGroup->setId(m_netAmount, 1); // the proxy filter model m_filterProxyModel = new HierarchyFilterProxyModel(this); m_filterProxyModel->setHideClosedAccounts(true); m_filterProxyModel->setHideEquityAccounts(!KMyMoneyGlobalSettings::expertMode()); m_filterProxyModel->addAccountGroup(filterAccountGroup); m_filterProxyModel->setCurrentAccountId(m_account.id()); m_filterProxyModel->setSourceModel(Models::instance()->accountsModel()); m_filterProxyModel->setDynamicSortFilter(true); m_parentAccounts->setAlternatingRowColors(true); m_parentAccounts->setIconSize(QSize(22, 22)); m_parentAccounts->setSortingEnabled(true); m_parentAccounts->setAllColumnsShowFocus(true); m_parentAccounts->setModel(m_filterProxyModel); m_parentAccounts->sortByColumn(0, Qt::AscendingOrder); m_subAccountLabel->setText(i18n("Is a sub account")); accountNameEdit->setText(account.name()); descriptionEdit->setText(account.description()); typeCombo->setEnabled(true); // load the price mode combo m_priceMode->insertItem(i18nc("default price mode", "(default)"), 0); m_priceMode->insertItem(i18n("Price per share"), 1); m_priceMode->insertItem(i18n("Total for all shares"), 2); int priceMode = 0; if (m_account.accountType() == MyMoneyAccount::Investment) { m_priceMode->setEnabled(true); if (!m_account.value("priceMode").isEmpty()) priceMode = m_account.value("priceMode").toInt(); } m_priceMode->setCurrentItem(priceMode); bool haveMinBalance = false; bool haveMaxCredit = false; if (!m_account.openingDate().isValid()) { m_account.setOpeningDate(KMyMoneyGlobalSettings::firstFiscalDate()); } m_openingDateEdit->setDate(m_account.openingDate()); + handleOpeningBalanceCheckbox(m_account.currencyId()); + if (categoryEditor) { // get rid of the tabs that are not used for categories int tab = m_tab->indexOf(m_institutionTab); if (tab != -1) m_tab->removeTab(tab); tab = m_tab->indexOf(m_limitsTab); if (tab != -1) m_tab->removeTab(tab); //m_qlistviewParentAccounts->setEnabled(true); accountNoEdit->setEnabled(false); m_institutionBox->hide(); m_qcheckboxNoVat->hide(); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Income)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Expense)); // Hardcoded but acceptable - if above we set the default to income do the same here switch (account.accountType()) { case MyMoneyAccount::Expense: typeCombo->setCurrentItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Expense), false); break; case MyMoneyAccount::Income: default: typeCombo->setCurrentItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Income), false); break; } m_currency->setEnabled(true); if (m_isEditing) { typeCombo->setEnabled(false); m_currency->setDisabled(MyMoneyFile::instance()->isReferenced(m_account)); } m_qcheckboxPreferred->hide(); m_qcheckboxTax->setChecked(account.value("Tax").toLower() == "yes"); m_costCenterRequiredCheckBox->setChecked(account.isCostCenterRequired()); loadVatAccounts(); } else { // get rid of the tabs that are not used for accounts int taxtab = m_tab->indexOf(m_taxTab); if (taxtab != -1) { - if (m_account.isAssetLiability()) { m_vatCategory->setText(i18n("VAT account")); - m_vatAssignmentFrame->hide(); m_qcheckboxTax->setChecked(account.value("Tax") == "Yes"); - } else { + loadVatAccounts(); + } else { m_tab->removeTab(taxtab); - } } m_costCenterRequiredCheckBox->hide(); switch (m_account.accountType()) { case MyMoneyAccount::Savings: case MyMoneyAccount::Cash: haveMinBalance = true; break; case MyMoneyAccount::Checkings: haveMinBalance = true; haveMaxCredit = true; break; case MyMoneyAccount::CreditCard: haveMaxCredit = true; break; default: // no limit available, so we might get rid of the tab int tab = m_tab->indexOf(m_limitsTab); if (tab != -1) m_tab->removeTab(tab); // don't try to hide the widgets we just wiped // in the next step haveMaxCredit = haveMinBalance = true; break; } if (!haveMaxCredit) { m_maxCreditLabel->setEnabled(false); m_maxCreditLabel->hide(); m_maxCreditEarlyEdit->hide(); m_maxCreditAbsoluteEdit->hide(); } if (!haveMinBalance) { m_minBalanceLabel->setEnabled(false); m_minBalanceLabel->hide(); m_minBalanceEarlyEdit->hide(); m_minBalanceAbsoluteEdit->hide(); } QString typeString = KMyMoneyUtils::accountTypeToString(account.accountType()); if (m_isEditing) { if (account.isLiquidAsset()) { typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Checkings)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Savings)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Cash)); } else { typeCombo->addItem(typeString); // Once created, accounts of other account types are not // allowed to be changed. typeCombo->setEnabled(false); } // Once created, a currency cannot be changed if it is referenced. m_currency->setDisabled(MyMoneyFile::instance()->isReferenced(m_account)); } else { typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Checkings)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Savings)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Cash)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::CreditCard)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Loan)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Investment)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Asset)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Liability)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Stock)); /* typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::CertificateDep)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::MoneyMarket)); typeCombo->addItem(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Currency)); */ // Do not create account types that are not supported // by the current engine. if (account.accountType() == MyMoneyAccount::UnknownAccountType || account.accountType() == MyMoneyAccount::CertificateDep || account.accountType() == MyMoneyAccount::MoneyMarket || account.accountType() == MyMoneyAccount::Currency) typeString = KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Checkings); } typeCombo->setCurrentItem(typeString, false); if (account.isInvest()) m_institutionBox->hide(); accountNoEdit->setText(account.number()); m_qcheckboxPreferred->setChecked(account.value("PreferredAccount") == "Yes"); m_qcheckboxNoVat->setChecked(account.value("NoVat") == "Yes"); loadKVP("iban", ibanEdit); loadKVP("minBalanceAbsolute", m_minBalanceAbsoluteEdit); loadKVP("minBalanceEarly", m_minBalanceEarlyEdit); loadKVP("maxCreditAbsolute", m_maxCreditAbsoluteEdit); loadKVP("maxCreditEarly", m_maxCreditEarlyEdit); // reverse the sign for display purposes if (!m_maxCreditAbsoluteEdit->lineedit()->text().isEmpty()) m_maxCreditAbsoluteEdit->setValue(m_maxCreditAbsoluteEdit->value()*MyMoneyMoney::MINUS_ONE); if (!m_maxCreditEarlyEdit->lineedit()->text().isEmpty()) m_maxCreditEarlyEdit->setValue(m_maxCreditEarlyEdit->value()*MyMoneyMoney::MINUS_ONE); loadKVP("lastNumberUsed", m_lastCheckNumberUsed); if (m_account.isInvest()) { typeCombo->setEnabled(false); m_qcheckboxPreferred->hide(); m_currencyText->hide(); m_currency->hide(); } else { // use the old field and override a possible new value if (!MyMoneyMoney(account.value("minimumBalance")).isZero()) { m_minBalanceAbsoluteEdit->setValue(MyMoneyMoney(account.value("minimumBalance"))); } } // m_qcheckboxTax->hide(); TODO should only be visible for VAT category/account } m_currency->setSecurity(file->currency(account.currencyId())); // Load the institutions // then the accounts QString institutionName; try { if (m_isEditing && !account.institutionId().isEmpty()) institutionName = file->institution(account.institutionId()).name(); else institutionName.clear(); } catch (const MyMoneyException &e) { qDebug("exception in init for account dialog: %s", qPrintable(e.what())); } if (m_account.isInvest()) m_parentAccounts->setEnabled(false); if (!categoryEditor) slotLoadInstitutions(institutionName); accountNameEdit->setFocus(); if (!title.isEmpty()) setWindowTitle(title); connect(buttonBox, SIGNAL(rejected()), SLOT(reject())); connect(buttonBox, SIGNAL(accepted()), this, SLOT(okClicked())); connect(m_parentAccounts->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(slotSelectionChanged(QItemSelection,QItemSelection))); connect(m_qbuttonNew, SIGNAL(clicked()), this, SLOT(slotNewClicked())); connect(typeCombo, SIGNAL(activated(QString)), this, SLOT(slotAccountTypeChanged(QString))); connect(accountNameEdit, SIGNAL(textChanged(QString)), this, SLOT(slotCheckFinished())); connect(m_vatCategory, SIGNAL(toggled(bool)), this, SLOT(slotVatChanged(bool))); connect(m_vatAssignment, SIGNAL(toggled(bool)), this, SLOT(slotVatAssignmentChanged(bool))); connect(m_vatCategory, SIGNAL(toggled(bool)), this, SLOT(slotCheckFinished())); connect(m_vatAssignment, SIGNAL(toggled(bool)), this, SLOT(slotCheckFinished())); connect(m_vatRate, SIGNAL(textChanged(QString)), this, SLOT(slotCheckFinished())); connect(m_vatAccount, SIGNAL(stateChanged()), this, SLOT(slotCheckFinished())); + connect(m_currency, SIGNAL(activated(int)), this, SLOT(slotCheckCurrency())); connect(m_minBalanceEarlyEdit, SIGNAL(valueChanged(QString)), this, SLOT(slotAdjustMinBalanceAbsoluteEdit(QString))); connect(m_minBalanceAbsoluteEdit, SIGNAL(valueChanged(QString)), this, SLOT(slotAdjustMinBalanceEarlyEdit(QString))); connect(m_maxCreditEarlyEdit, SIGNAL(valueChanged(QString)), this, SLOT(slotAdjustMaxCreditAbsoluteEdit(QString))); connect(m_maxCreditAbsoluteEdit, SIGNAL(valueChanged(QString)), this, SLOT(slotAdjustMaxCreditEarlyEdit(QString))); connect(m_qcomboboxInstitutions, SIGNAL(activated(QString)), this, SLOT(slotLoadInstitutions(QString))); QModelIndex parentIndex = m_filterProxyModel->getSelectedParentAccountIndex(); m_parentAccounts->expand(parentIndex); m_parentAccounts->selectionModel()->select(parentIndex, QItemSelectionModel::SelectCurrent); m_parentAccounts->scrollTo(parentIndex, QAbstractItemView::PositionAtTop); m_vatCategory->setChecked(false); m_vatAssignment->setChecked(false); // make sure our account does not have an id and no parent assigned // and certainly no children in case we create a new account if (!m_isEditing) { m_account.clearId(); m_account.setParentAccountId(QString()); QStringList::ConstIterator it; while ((it = m_account.accountList().begin()) != m_account.accountList().end()) m_account.removeAccountId(*it); } else { if (!m_account.value("VatRate").isEmpty()) { m_vatCategory->setChecked(true); m_vatRate->setValue(MyMoneyMoney(m_account.value("VatRate"))*MyMoneyMoney(100, 1)); } else { if (!m_account.value("VatAccount").isEmpty()) { QString accId = m_account.value("VatAccount").toLatin1(); try { // make sure account exists MyMoneyFile::instance()->account(accId); m_vatAssignment->setChecked(true); m_vatAccount->setSelected(accId); m_grossAmount->setChecked(true); if (m_account.value("VatAmount") == "Net") m_netAmount->setChecked(true); } catch (const MyMoneyException &) { } } } } slotVatChanged(m_vatCategory->isChecked()); slotVatAssignmentChanged(m_vatAssignment->isChecked()); slotCheckFinished(); kMandatoryFieldGroup* requiredFields = new kMandatoryFieldGroup(this); requiredFields->setOkButton(buttonBox->button(QDialogButtonBox::Ok)); // button to be enabled when all fields present requiredFields->add(accountNameEdit); } void KNewAccountDlg::setOpeningBalance(const MyMoneyMoney& balance) { m_openingBalanceEdit->setValue(balance); } void KNewAccountDlg::setOpeningBalanceShown(bool shown) { m_openingBalanceLabel->setVisible(shown); m_openingBalanceEdit->setVisible(shown); } void KNewAccountDlg::setOpeningDateShown(bool shown) { m_openingDateLabel->setVisible(shown); m_openingDateEdit->setVisible(shown); } void KNewAccountDlg::okClicked() { MyMoneyFile* file = MyMoneyFile::instance(); QString accountNameText = accountNameEdit->text(); if (accountNameText.isEmpty()) { KMessageBox::error(this, i18n("You have not specified a name.\nPlease fill in this field.")); accountNameEdit->setFocus(); return; } MyMoneyAccount parent = parentAccount(); if (parent.name().length() == 0) { KMessageBox::error(this, i18n("Please select a parent account.")); return; } if (!m_categoryEditor) { QString institutionNameText = m_qcomboboxInstitutions->currentText(); if (institutionNameText != i18n("(No Institution)")) { try { MyMoneyFile *file = MyMoneyFile::instance(); QList list = file->institutionList(); QList::ConstIterator institutionIterator; for (institutionIterator = list.constBegin(); institutionIterator != list.constEnd(); ++institutionIterator) { if ((*institutionIterator).name() == institutionNameText) m_account.setInstitutionId((*institutionIterator).id()); } } catch (const MyMoneyException &e) { qDebug("Exception in account institution set: %s", qPrintable(e.what())); } } else { m_account.setInstitutionId(QString()); } } m_account.setName(accountNameText); m_account.setNumber(accountNoEdit->text()); storeKVP("iban", ibanEdit); storeKVP("minBalanceAbsolute", m_minBalanceAbsoluteEdit); storeKVP("minBalanceEarly", m_minBalanceEarlyEdit); // the figures for credit line with reversed sign if (!m_maxCreditAbsoluteEdit->lineedit()->text().isEmpty()) m_maxCreditAbsoluteEdit->setValue(m_maxCreditAbsoluteEdit->value()*MyMoneyMoney::MINUS_ONE); if (!m_maxCreditEarlyEdit->lineedit()->text().isEmpty()) m_maxCreditEarlyEdit->setValue(m_maxCreditEarlyEdit->value()*MyMoneyMoney::MINUS_ONE); storeKVP("maxCreditAbsolute", m_maxCreditAbsoluteEdit); storeKVP("maxCreditEarly", m_maxCreditEarlyEdit); if (!m_maxCreditAbsoluteEdit->lineedit()->text().isEmpty()) m_maxCreditAbsoluteEdit->setValue(m_maxCreditAbsoluteEdit->value()*MyMoneyMoney::MINUS_ONE); if (!m_maxCreditEarlyEdit->lineedit()->text().isEmpty()) m_maxCreditEarlyEdit->setValue(m_maxCreditEarlyEdit->value()*MyMoneyMoney::MINUS_ONE); storeKVP("lastNumberUsed", m_lastCheckNumberUsed); // delete a previous version of the minimumbalance information storeKVP("minimumBalance", QString(), QString()); MyMoneyAccount::accountTypeE acctype; if (!m_categoryEditor) { acctype = KMyMoneyUtils::stringToAccountType(typeCombo->currentText()); // If it's a loan, check if the parent is asset or liability. In // case of asset, we change the account type to be AssetLoan if (acctype == MyMoneyAccount::Loan && parent.accountGroup() == MyMoneyAccount::Asset) acctype = MyMoneyAccount::AssetLoan; } else { acctype = parent.accountGroup(); QString newName; if (!MyMoneyFile::instance()->isStandardAccount(parent.id())) { newName = MyMoneyFile::instance()->accountToCategory(parent.id()) + MyMoneyFile::AccountSeperator; } newName += accountNameText; if (!file->categoryToAccount(newName, acctype).isEmpty() && (file->categoryToAccount(newName, acctype) != m_account.id())) { KMessageBox::error(this, QString("") + i18n("A category named %1 already exists. You cannot create a second category with the same name.", newName) + QString("")); return; } } m_account.setAccountType(acctype); m_account.setDescription(descriptionEdit->toPlainText()); m_account.setOpeningDate(m_openingDateEdit->date()); if (!m_categoryEditor) { m_account.setCurrencyId(m_currency->security().id()); storeKVP("PreferredAccount", m_qcheckboxPreferred); storeKVP("NoVat", m_qcheckboxNoVat); if (m_minBalanceAbsoluteEdit->isVisible()) { m_account.setValue("minimumBalance", m_minBalanceAbsoluteEdit->value().toString()); } } else { if (KMyMoneyGlobalSettings::hideUnusedCategory() && !m_isEditing) { KMessageBox::information(this, i18n("You have selected to suppress the display of unused categories in the KMyMoney configuration dialog. The category you just created will therefore only be shown if it is used. Otherwise, it will be hidden in the accounts/categories view."), i18n("Hidden categories"), "NewHiddenCategory"); } m_account.setCostCenterRequired(m_costCenterRequiredCheckBox->isChecked()); } storeKVP("Tax", m_qcheckboxTax); + if (m_qcheckboxOpeningBalance->isChecked()) + m_account.setValue("OpeningBalanceAccount", "Yes"); + else + m_account.deletePair("OpeningBalanceAccount"); + m_account.deletePair("VatAccount"); m_account.deletePair("VatAmount"); m_account.deletePair("VatRate"); if (m_vatCategory->isChecked()) { m_account.setValue("VatRate", (m_vatRate->value().abs() / MyMoneyMoney(100, 1)).toString()); } else { if (m_vatAssignment->isChecked() && !m_vatAccount->selectedItems().isEmpty()) { m_account.setValue("VatAccount", m_vatAccount->selectedItems().first()); if (m_netAmount->isChecked()) m_account.setValue("VatAmount", "Net"); } } accept(); } void KNewAccountDlg::loadKVP(const QString& key, kMyMoneyEdit* widget) { if (!widget) return; if (m_account.value(key).isEmpty()) { widget->clearText(); } else { widget->setValue(MyMoneyMoney(m_account.value(key))); } } void KNewAccountDlg::loadKVP(const QString& key, KLineEdit* widget) { if (!widget) return; widget->setText(m_account.value(key)); } void KNewAccountDlg::storeKVP(const QString& key, const QString& text, const QString& value) { if (text.isEmpty()) m_account.deletePair(key); else m_account.setValue(key, value); } void KNewAccountDlg::storeKVP(const QString& key, QCheckBox* widget) { if (widget) { if(widget->isChecked()) { m_account.setValue(key, "Yes");; } else { m_account.deletePair(key); } } } void KNewAccountDlg::storeKVP(const QString& key, kMyMoneyEdit* widget) { storeKVP(key, widget->lineedit()->text(), widget->text()); } void KNewAccountDlg::storeKVP(const QString& key, KLineEdit* widget) { storeKVP(key, widget->text(), widget->text()); } const MyMoneyAccount& KNewAccountDlg::account() { // assign the right currency to the account m_account.setCurrencyId(m_currency->security().id()); // and the price mode switch (m_priceMode->currentItem()) { case 0: m_account.deletePair("priceMode"); break; case 1: case 2: m_account.setValue("priceMode", QString("%1").arg(m_priceMode->currentItem())); break; } return m_account; } const MyMoneyAccount& KNewAccountDlg::parentAccount() { return m_parentAccount; } void KNewAccountDlg::slotSelectionChanged(const QItemSelection ¤t, const QItemSelection &previous) { Q_UNUSED(previous) if (!current.indexes().empty()) { QVariant account = m_parentAccounts->model()->data(current.indexes().front(), AccountsModel::AccountRole); if (account.isValid()) { m_parentAccount = account.value(); m_subAccountLabel->setText(i18n("Is a sub account of %1", m_parentAccount.name())); } } } void KNewAccountDlg::loadVatAccounts() { QList list; MyMoneyFile::instance()->accountList(list); QList::Iterator it; QStringList loadListExpense; QStringList loadListIncome; QStringList loadListAsset; QStringList loadListLiability; for (it = list.begin(); it != list.end(); ++it) { if (!(*it).value("VatRate").isEmpty()) { if ((*it).accountType() == MyMoneyAccount::Expense) loadListExpense += (*it).id(); else if ((*it).accountType() == MyMoneyAccount::Income) loadListIncome += (*it).id(); else if ((*it).accountType() == MyMoneyAccount::Asset) loadListAsset += (*it).id(); else if ((*it).accountType() == MyMoneyAccount::Liability) loadListLiability += (*it).id(); } } AccountSet vatSet; if (!loadListAsset.isEmpty()) vatSet.load(m_vatAccount, i18n("Asset"), loadListAsset, true); if (!loadListLiability.isEmpty()) vatSet.load(m_vatAccount, i18n("Liability"), loadListLiability, false); if (!loadListIncome.isEmpty()) vatSet.load(m_vatAccount, i18n("Income"), loadListIncome, false); if (!loadListExpense.isEmpty()) vatSet.load(m_vatAccount, i18n("Expense"), loadListExpense, false); } void KNewAccountDlg::slotLoadInstitutions(const QString& name) { m_qcomboboxInstitutions->clear(); QString bic; // Are we forcing the user to use institutions? m_qcomboboxInstitutions->addItem(i18n("(No Institution)")); m_bicValue->setText(" "); ibanEdit->setEnabled(false); accountNoEdit->setEnabled(false); try { MyMoneyFile *file = MyMoneyFile::instance(); QList list = file->institutionList(); QList::ConstIterator institutionIterator; for (institutionIterator = list.constBegin(); institutionIterator != list.constEnd(); ++institutionIterator) { if ((*institutionIterator).name() == name) { ibanEdit->setEnabled(true); accountNoEdit->setEnabled(true); m_bicValue->setText((*institutionIterator).value("bic")); } m_qcomboboxInstitutions->addItem((*institutionIterator).name()); } m_qcomboboxInstitutions->setCurrentItem(name, false); } catch (const MyMoneyException &e) { qDebug("Exception in institution load: %s", qPrintable(e.what())); } } void KNewAccountDlg::slotNewClicked() { MyMoneyInstitution institution; QPointer dlg = new KNewBankDlg(institution, this); if (dlg->exec()) { MyMoneyFileTransaction ft; try { MyMoneyFile *file = MyMoneyFile::instance(); institution = dlg->institution(); file->addInstitution(institution); ft.commit(); slotLoadInstitutions(institution.name()); } catch (const MyMoneyException &) { KMessageBox::information(this, i18n("Cannot add institution")); } } delete dlg; } void KNewAccountDlg::slotAccountTypeChanged(const QString& typeStr) { MyMoneyAccount::accountTypeE type; MyMoneyAccount::accountTypeE oldType; type = KMyMoneyUtils::stringToAccountType(typeStr); try { oldType = m_account.accountType(); if (oldType != type) { m_account.setAccountType(type); // update the account group displayed in the accounts hierarchy m_filterProxyModel->clear(); m_filterProxyModel->addAccountGroup(m_account.accountGroup()); } } catch (const MyMoneyException &) { qWarning("Unexpected exception in KNewAccountDlg::slotAccountTypeChanged()"); } } void KNewAccountDlg::slotCheckFinished() { bool showButton = true; if (accountNameEdit->text().length() == 0) { showButton = false; } if (m_vatCategory->isChecked() && m_vatRate->value() <= MyMoneyMoney()) { showButton = false; } else { if (m_vatAssignment->isChecked() && m_vatAccount->selectedItems().isEmpty()) showButton = false; } buttonBox->button(QDialogButtonBox::Ok)->setEnabled(showButton); } void KNewAccountDlg::slotVatChanged(bool state) { if (state) { m_vatCategoryFrame->show(); m_vatAssignmentFrame->hide(); } else { m_vatCategoryFrame->hide(); if (!m_account.isAssetLiability()) { m_vatAssignmentFrame->show(); } } } void KNewAccountDlg::slotVatAssignmentChanged(bool state) { m_vatAccount->setEnabled(state); m_amountGroupBox->setEnabled(state); } void KNewAccountDlg::adjustEditWidgets(kMyMoneyEdit* dst, kMyMoneyEdit* src, char mode, int corr) { MyMoneyMoney factor(corr, 1); if (m_account.accountGroup() == MyMoneyAccount::Asset) factor = -factor; switch (mode) { case '<': if (src->value()*factor < dst->value()*factor) dst->setValue(src->value()); break; case '>': if (src->value()*factor > dst->value()*factor) dst->setValue(src->value()); break; } } +void KNewAccountDlg::handleOpeningBalanceCheckbox(const QString ¤cyId) +{ + if (m_account.accountType() == MyMoneyAccount::Equity) { + // check if there is another opening balance account with the same currency + bool isOtherOpenBalancingAccount = false; + QList list; + MyMoneyFile::instance()->accountList(list); + QList::Iterator it; + for (it = list.begin(); it != list.end(); ++it) { + if (it->id() == m_account.id() || currencyId != it->currencyId() + || it->accountType() != MyMoneyAccount::Equity) + continue; + if (it->value("OpeningBalanceAccount") == "Yes") { + isOtherOpenBalancingAccount = true; + break; + } + } + if (!isOtherOpenBalancingAccount) { + bool isOpenBalancingAccount = m_account.value("OpeningBalanceAccount") == "Yes"; + m_qcheckboxOpeningBalance->setChecked(isOpenBalancingAccount); + if (isOpenBalancingAccount) { + // let only allow state change if no transactions are assigned to this account + bool hasTransactions = MyMoneyFile::instance()->transactionCount(m_account.id()) != 0; + m_qcheckboxOpeningBalance->setEnabled(!hasTransactions); + if (hasTransactions) + m_qcheckboxOpeningBalance->setToolTip(i18n("Option has been disabled because there are transactions assigned to this account")); + } + } else { + m_qcheckboxOpeningBalance->setChecked(false); + m_qcheckboxOpeningBalance->setEnabled(false); + m_qcheckboxOpeningBalance->setToolTip(i18n("Option has been disabled because there is another account flagged to be an opening balance account for this currency")); + } + } else { + m_qcheckboxOpeningBalance->setVisible(false); + } +} + void KNewAccountDlg::slotAdjustMinBalanceAbsoluteEdit(const QString&) { adjustEditWidgets(m_minBalanceAbsoluteEdit, m_minBalanceEarlyEdit, '<', -1); } void KNewAccountDlg::slotAdjustMinBalanceEarlyEdit(const QString&) { adjustEditWidgets(m_minBalanceEarlyEdit, m_minBalanceAbsoluteEdit, '>', -1); } void KNewAccountDlg::slotAdjustMaxCreditAbsoluteEdit(const QString&) { adjustEditWidgets(m_maxCreditAbsoluteEdit, m_maxCreditEarlyEdit, '>', 1); } void KNewAccountDlg::slotAdjustMaxCreditEarlyEdit(const QString&) { adjustEditWidgets(m_maxCreditEarlyEdit, m_maxCreditAbsoluteEdit, '<', 1); } +void KNewAccountDlg::slotCheckCurrency() +{ + handleOpeningBalanceCheckbox(m_currency->security().id()); +} + void KNewAccountDlg::addTab(QWidget* w, const QString& name) { if (w) { w->setParent(m_tab); m_tab->addTab(w, name); } } diff --git a/kmymoney/dialogs/knewaccountdlg.h b/kmymoney/dialogs/knewaccountdlg.h index 66bb0b8b1..581cd0aa3 100644 --- a/kmymoney/dialogs/knewaccountdlg.h +++ b/kmymoney/dialogs/knewaccountdlg.h @@ -1,157 +1,159 @@ /*************************************************************************** knewaccountdlg.h ------------------- copyright : (C) 2000 by Michael Edwardes ***************************************************************************/ /*************************************************************************** * * * 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 KNEWACCOUNTDLG_H #define KNEWACCOUNTDLG_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Headers // ---------------------------------------------------------------------------- // Project Includes #include #include #include #include "ui_knewaccountdlgdecl.h" namespace reports { } class HierarchyFilterProxyModel : public AccountsFilterProxyModel { Q_OBJECT public: HierarchyFilterProxyModel(QObject *parent = 0); virtual Qt::ItemFlags flags(const QModelIndex &index) const; void setCurrentAccountId(const QString &selectedAccountId); QModelIndex getSelectedParentAccountIndex() const; protected: bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const; bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const; private: QString m_currentAccountId; }; /** * This dialog lets you create/edit an account. */ class KNewAccountDlgDecl : public QDialog, public Ui::kNewAccountDlgDecl { public: KNewAccountDlgDecl(QWidget *parent) : QDialog(parent) { setupUi(this); } }; class KNewAccountDlg : public KNewAccountDlgDecl { Q_OBJECT private: MyMoneyAccount m_account; MyMoneyAccount m_parentAccount; HierarchyFilterProxyModel *m_filterProxyModel; bool m_categoryEditor; bool m_isEditing; void loadVatAccounts(); void storeKVP(const QString& key, kMyMoneyEdit* widget); void storeKVP(const QString& key, KLineEdit* widget); void storeKVP(const QString& key, const QString& text, const QString& value); void storeKVP(const QString& key, QCheckBox* widget); void loadKVP(const QString& key, kMyMoneyEdit* widget); void loadKVP(const QString& key, KLineEdit* widget); public: /** * This is the constructor of the dialog. The parameters define the environment * in which the dialog will be used. Depending on the environment, certain rules * apply and will be handled by the dialog. * * @param account The original data to be used to create the account. In case * of @p isEditing is false, the account id, the parent account id * and the list of all child accounts will be cleared. * @param isEditing If @p false, rules for new account creation apply. * If @p true, rules for account editing apply * @param categoryEditor If @p false, rules for asset/liability accounts apply. * If @p true, rules for income/expense account apply. * @param parent Pointer to parent object (passed to QDialog). Default is 0. * @param title Caption of the object (passed to QDialog). Default is empty string. */ KNewAccountDlg(const MyMoneyAccount& account, bool isEditing, bool categoryEditor, QWidget *parent = 0, const QString& title = QString()); /** * This method returns the edited account object. */ const MyMoneyAccount& account(); /** * This method returns the parent account of the edited account object. */ const MyMoneyAccount& parentAccount(); void setOpeningBalance(const MyMoneyMoney& balance); const MyMoneyMoney openingBalance() const { return m_openingBalanceEdit->value(); }; void setOpeningBalanceShown(bool shown); void setOpeningDateShown(bool shown); /** * This method adds an additional tab pointed to with @a w to the tab widget. * This tab is usually defined by a plugin (eg. online banking). If @a w is * zero, this is a NOP. @a name is used as the text to be placed on the tab. */ void addTab(QWidget* w, const QString& name); protected: void displayOnlineBankingStatus(); void adjustEditWidgets(kMyMoneyEdit* dst, kMyMoneyEdit* src, char mode, int corr); + void handleOpeningBalanceCheckbox(const QString ¤cyId); protected slots: void okClicked(); void slotSelectionChanged(const QItemSelection ¤t, const QItemSelection &previous); void slotAccountTypeChanged(const QString& type); void slotVatChanged(bool); void slotVatAssignmentChanged(bool); void slotNewClicked(); void slotCheckFinished(); void slotLoadInstitutions(const QString&); void slotAdjustMinBalanceAbsoluteEdit(const QString&); void slotAdjustMinBalanceEarlyEdit(const QString&); void slotAdjustMaxCreditAbsoluteEdit(const QString&); void slotAdjustMaxCreditEarlyEdit(const QString&); + void slotCheckCurrency(); }; #endif diff --git a/kmymoney/dialogs/knewaccountdlgdecl.ui b/kmymoney/dialogs/knewaccountdlgdecl.ui index b088d6b40..716f6dac9 100644 --- a/kmymoney/dialogs/knewaccountdlgdecl.ui +++ b/kmymoney/dialogs/knewaccountdlgdecl.ui @@ -1,893 +1,906 @@ kNewAccountDlgDecl 0 0 670 518 New Account Dialog true 0 General 60 0 60 32767 Name: false Opening information 6 6 6 6 6 Currency: false true 1 0 Type: false 0 0 Date: false Balance: false Options Qt::Horizontal QSizePolicy::Expanding 140 20 Last check number: false 0 0 32767 32767 Notes: Qt::AlignTop false No auto-VAT-assignment Price entry false false 1 0 Preferred Account Cost Center assignment required + + + + true + + + <html><head/><body><p>Flag this account to be an opening balance account</p></body></html> + + + Opening Balance Account + + + Qt::Vertical QSizePolicy::Expanding 20 16 Institution Institution 6 0 0 0 0 0 0 New... 60 0 60 32767 Number: false BIC false IBAN false false Qt::Vertical QSizePolicy::Expanding 21 30 Hierarchy Subaccount false Limits This page allows to setup certain limits. KMyMoney will warn you when the account balance reaches a limit. true Type false false Absolute limit false Maximum credit limit false false false false Minimum balance false QFrame::HLine QFrame::Sunken Early warning false Qt::Vertical QSizePolicy::Expanding 20 30 Tax VAT details VAT category QFrame::NoFrame QFrame::Plain 0 0 0 0 VAT percentage false Qt::Horizontal QSizePolicy::Expanding 21 20 Qt::Vertical QSizePolicy::Expanding 20 40 QFrame::NoFrame QFrame::Plain 0 0 0 0 Enable auto VAT assignment 0 0 32767 32767 Amount entered QFrame::StyledPanel QFrame::Sunken Gross amount true m_amountGroup - Net amount + Net a&mount m_amountGroup Qt::Vertical QSizePolicy::Expanding 20 21 Include on Tax Reports QFrame::HLine QFrame::Sunken QDialogButtonBox::Cancel|QDialogButtonBox::Ok KLineEdit QLineEdit
klineedit.h
KComboBox QComboBox
kcombobox.h
KTextEdit QTextEdit
ktextedit.h
kMyMoneyEdit QWidget
../widgets/kmymoneyedit.h
1
KMyMoneyGeneralCombo QWidget
kmymoneymvccombo.h
1
kMyMoneyDateInput QWidget
kmymoneydateinput.h
1
KMyMoneySecuritySelector KComboBox
kmymoneycurrencyselector.h
1
KMyMoneyAccountTreeView QTreeView
kmymoneyaccounttreeview.h
kMyMoneyAccountSelector QWidget
../widgets/kmymoneyaccountselector.h
m_tab accountNameEdit typeCombo m_currency m_openingDateEdit m_openingBalanceEdit m_lastCheckNumberUsed descriptionEdit m_qcheckboxPreferred m_qcheckboxNoVat m_qcomboboxInstitutions m_qbuttonNew ibanEdit accountNoEdit m_minBalanceEarlyEdit m_minBalanceAbsoluteEdit m_maxCreditEarlyEdit m_maxCreditAbsoluteEdit m_vatCategory m_vatRate m_vatAssignment m_grossAmount m_qcheckboxTax
diff --git a/kmymoney/dialogs/ktemplateexportdlg.cpp b/kmymoney/dialogs/ktemplateexportdlg.cpp new file mode 100644 index 000000000..611a65425 --- /dev/null +++ b/kmymoney/dialogs/ktemplateexportdlg.cpp @@ -0,0 +1,45 @@ +/*************************************************************************** + ktemplateexportlg.cpp + --------------------- + copyright : (C) 2016 by Ralf Habacker + +***************************************************************************/ + +/*************************************************************************** + * * + * 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 "ktemplateexportdlg.h" +#include "ui_ktemplateexportdlg.h" + +KTemplateExportDlg::KTemplateExportDlg(QWidget *parent) : + QDialog(parent), + ui(new Ui::KTemplateExportDlg) +{ + ui->setupUi(this); +} + +KTemplateExportDlg::~KTemplateExportDlg() +{ + delete ui; +} + +QString KTemplateExportDlg::title() const +{ + return ui->m_title->text(); +} + +QString KTemplateExportDlg::shortDescription() const +{ + return ui->m_shortDescription->text(); +} + +QString KTemplateExportDlg::longDescription() const +{ + return ui->m_longDescription->document()->toPlainText(); +} diff --git a/kmymoney/dialogs/ktemplateexportdlg.h b/kmymoney/dialogs/ktemplateexportdlg.h new file mode 100644 index 000000000..1b55c74f3 --- /dev/null +++ b/kmymoney/dialogs/ktemplateexportdlg.h @@ -0,0 +1,42 @@ +/*************************************************************************** + ktemplateexportlg.cpp + --------------------- + copyright : (C) 2016 by Ralf Habacker + +***************************************************************************/ + +/*************************************************************************** + * * + * 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 KTEMPLATEEXPORTDLG_H +#define KTEMPLATEEXPORTDLG_H + +#include + +namespace Ui { +class KTemplateExportDlg; +} + +class KTemplateExportDlg : public QDialog +{ + Q_OBJECT + +public: + explicit KTemplateExportDlg(QWidget *parent = 0); + ~KTemplateExportDlg(); + + QString title() const; + QString shortDescription() const; + QString longDescription() const; + +private: + Ui::KTemplateExportDlg *ui; +}; + +#endif // KTEMPLATEEXPORTDLG_H diff --git a/kmymoney/dialogs/ktemplateexportdlg.ui b/kmymoney/dialogs/ktemplateexportdlg.ui new file mode 100644 index 000000000..916d724f4 --- /dev/null +++ b/kmymoney/dialogs/ktemplateexportdlg.ui @@ -0,0 +1,100 @@ + + + KTemplateExportDlg + + + + 0 + 0 + 397 + 282 + + + + Template Export Attributes + + + + + + + + + Short description + + + + + + + Long Description + + + + + + + + + + Title + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + m_title + m_shortDescription + m_longDescription + buttonBox + + + + + buttonBox + accepted() + KTemplateExportDlg + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + KTemplateExportDlg + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/kmymoney/dialogs/settings/ksettingsgeneral.cpp b/kmymoney/dialogs/settings/ksettingsgeneral.cpp index 2fa4c35de..2814eb3e2 100644 --- a/kmymoney/dialogs/settings/ksettingsgeneral.cpp +++ b/kmymoney/dialogs/settings/ksettingsgeneral.cpp @@ -1,83 +1,110 @@ /*************************************************************************** ksettingsgeneral.cpp -------------------- copyright : (C) 2005 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 "ksettingsgeneral.h" // ---------------------------------------------------------------------------- // QT Includes #include +#include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include #include "models.h" #include "accountsmodel.h" KSettingsGeneral::KSettingsGeneral(QWidget* parent) : KSettingsGeneralDecl(parent) { // hide the internally used date field kcfg_StartDate->hide(); // setup connections, so that the sort optios get loaded once the edit fields are filled connect(kcfg_StartDate, SIGNAL(dateChanged(QDate)), this, SLOT(slotLoadStartDate(QDate))); // setup connections, so that changes by the user are forwarded to the (hidden) edit fields connect(m_startDateEdit, SIGNAL(dateChanged(QDate)), kcfg_StartDate, SLOT(setDate(QDate))); + connect(choosePath, SIGNAL(pressed()), this, SLOT(slotChooseLogPath())); initialHideZeroBalanceEquities = kcfg_HideZeroBalanceEquities->isChecked(); } KSettingsGeneral::~KSettingsGeneral() { } +void KSettingsGeneral::slotChooseLogPath() +{ + QString filePath = QFileDialog::getExistingDirectory(this, i18n("Choose file path"), QDir::homePath()); + kcfg_logPath->setText(filePath); + slotUpdateLogTypes(); +} + void KSettingsGeneral::slotLoadStartDate(const QDate&) { // only need this once disconnect(kcfg_StartDate, SIGNAL(dateChanged(QDate)), this, SLOT(slotLoadStartDate(QDate))); m_startDateEdit->setDate(kcfg_StartDate->date()); } +void KSettingsGeneral::slotUpdateLogTypes() +{ + bool enable = kcfg_logPath->text().isEmpty() ? false : true; + kcfg_logImportedStatements->setEnabled(enable); + kcfg_logOfxTransactions->setEnabled(enable); + if (!enable) + { + kcfg_logImportedStatements->setChecked(enable); + kcfg_logOfxTransactions->setChecked(enable); + } +} + +void KSettingsGeneral::showEvent(QShowEvent *event) +{ + KSettingsGeneralDecl::showEvent(event); + slotUpdateLogTypes(); +} + void KSettingsGeneral::slotUpdateEquitiesVisibility() { if (initialHideZeroBalanceEquities == kcfg_HideZeroBalanceEquities->isChecked()) // setting hasn't been changed, so return return; initialHideZeroBalanceEquities = kcfg_HideZeroBalanceEquities->isChecked(); AccountsModel* accountsModel = Models::instance()->accountsModel(); // items' model for accounts' page InstitutionsModel* institutionsModel = Models::instance()->institutionsModel(); // items' model for institutions' page MyMoneyFile *file = MyMoneyFile::instance(); QList accountsList; file->accountList(accountsList); foreach (const auto account, accountsList) { if (account.isInvest() && account.balance().isZero()) { // search only for zero balance stocks if (initialHideZeroBalanceEquities) { accountsModel->slotObjectRemoved(MyMoneyFile::notifyAccount, account.id()); // remove item from accounts' page institutionsModel->slotObjectRemoved(MyMoneyFile::notifyAccount, account.id()); // remove item from institutions' page } else { accountsModel->slotObjectAdded(MyMoneyFile::notifyAccount, dynamic_cast(&account)); // add item to accounts' page institutionsModel->slotObjectAdded(MyMoneyFile::notifyAccount, dynamic_cast(&account)); // add item to institutions' page } } } } diff --git a/kmymoney/dialogs/settings/ksettingsgeneral.h b/kmymoney/dialogs/settings/ksettingsgeneral.h index 8b5f16160..3dd741243 100644 --- a/kmymoney/dialogs/settings/ksettingsgeneral.h +++ b/kmymoney/dialogs/settings/ksettingsgeneral.h @@ -1,55 +1,60 @@ /*************************************************************************** ksettingsgeneral.h ------------------- copyright : (C) 2005 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 KSETTINGSGENERAL_H #define KSETTINGSGENERAL_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "ui_ksettingsgeneraldecl.h" class KSettingsGeneralDecl : public QWidget, public Ui::KSettingsGeneralDecl { public: KSettingsGeneralDecl(QWidget *parent) : QWidget(parent) { setupUi(this); } }; class KSettingsGeneral : public KSettingsGeneralDecl { Q_OBJECT private: bool initialHideZeroBalanceEquities; public: KSettingsGeneral(QWidget* parent = 0); ~KSettingsGeneral(); protected slots: + void slotChooseLogPath(); void slotLoadStartDate(const QDate&); + void slotUpdateLogTypes(); + +protected: + void showEvent(QShowEvent* event); public slots: void slotUpdateEquitiesVisibility(); }; #endif diff --git a/kmymoney/dialogs/settings/ksettingsgeneraldecl.ui b/kmymoney/dialogs/settings/ksettingsgeneraldecl.ui index e679115ec..b1502e408 100644 --- a/kmymoney/dialogs/settings/ksettingsgeneraldecl.ui +++ b/kmymoney/dialogs/settings/ksettingsgeneraldecl.ui @@ -1,577 +1,672 @@ KSettingsGeneralDecl 0 0 600 499 General Settings 0 Global Autosave options Autosave periodically false 60 minutes false Qt::Horizontal QSizePolicy::Expanding 308 22 Autosave when file is modified upon close Setup number of backups to keep (local file only) Whenever the current data is saved into a local file, KMyMoney keeps the selected number of previous states of the file. Set it to 0 to turn the feature off. true Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 20 1 Number of backups to keep (0=off) Qt::Horizontal 40 20 Fiscal Year Your fiscal year starts on false 366 January February March April May June July August September October November December Qt::Horizontal QSizePolicy::Expanding 282 17 External programs Calculator Qt::Vertical QSizePolicy::Expanding 20 20 Views Startup page options Start with ho&mepage true Start with last selected view Type of the KMyMoney view List true Tree Tabbed Show title bar on each page Synchronize account selection of ledger and investment view Qt::Vertical QSizePolicy::Expanding 20 319 Filter Accounts / Categories This option hides all categories in the categories view that are not used in at least a single transaction. They are still shown in the category selection lists. Do not show unused categories This option hides all accounts that have been closed by the user in views and selection lists. You can use <b>View/Show all accounts</b> to temporarily show hidden accounts in the views. Do not show closed accounts Show equity accounts This option will display the categories in the accounts view also. Show categories in the accounts list view Do not show zero balance equities Schedules This option hides all finished schedules in the schedules view. Do not show finished schedules Transactions Do not show transactions prior to false Qt::Horizontal QSizePolicy::Expanding 63 20 This option hides all reconciled transactions in the ledger view. Do not show reconciled transactions Qt::Vertical QSizePolicy::Expanding 20 30 + + + Support + + + + + + Logging + + + + + 10 + 100 + 151 + 21 + + + + Log imported statements + + + + + + 60 + 30 + 341 + 20 + + + + true + + + + + + 10 + 30 + 41 + 20 + + + + Log path + + + + + + 10 + 130 + 131 + 21 + + + + Log OFX transactions + + + + + + 10 + 50 + 551 + 50 + + + + On choosing a log file path please keep in mind that log files may contain sensitive data (e.g. passwords in clear-text etc). + + + true + + + + + + 406 + 30 + 31 + 21 + + + + ... + + + + + + KLineEdit QLineEdit
klineedit.h
KComboBox QComboBox
kcombobox.h
kMyMoneyDateInput QFrame
kmymoneydateinput.h
1
kcfg_AutoSaveFile toggled(bool) m_periodFrame setEnabled(bool) 103 274 188 263
diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index a694c679f..c09f05b33 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,7925 +1,7945 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2007 by Thomas Baumgart ****************************************************************************/ /*************************************************************************** * * * 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 "config-kmymoney.h" #include "kmymoney.h" // for _getpid #ifdef Q_OS_WIN32 //krazy:exclude=cpp #include #else #if HAVE_SYS_TYPES_H #include #endif #if HAVE_UNISTD_H #include #endif #endif // ---------------------------------------------------------------------------- // Std C++ / STL Includes #include #include #include #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // only for performance tests #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef KF5Holidays_FOUND #include #include #endif // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyglobalsettings.h" #include "kmymoneyadaptor.h" #include "dialogs/settings/ksettingskmymoney.h" #include "dialogs/kbackupdlg.h" #include "dialogs/kexportdlg.h" #include "dialogs/kimportdlg.h" #include "dialogs/mymoneyqifprofileeditor.h" #include "dialogs/kenterscheduledlg.h" #include "dialogs/kconfirmmanualenterdlg.h" #include "dialogs/kmymoneypricedlg.h" #include "dialogs/kcurrencyeditdlg.h" #include "dialogs/kequitypriceupdatedlg.h" #include "dialogs/kmymoneyfileinfodlg.h" #include "dialogs/kfindtransactiondlg.h" #include "dialogs/knewbankdlg.h" #include "wizards/newinvestmentwizard/knewinvestmentwizard.h" #include "dialogs/knewaccountdlg.h" #include "dialogs/editpersonaldatadlg.h" #include "dialogs/kselectdatabasedlg.h" #include "dialogs/kcurrencycalculator.h" #include "dialogs/keditscheduledlg.h" #include "wizards/newloanwizard/knewloanwizard.h" #include "wizards/newloanwizard/keditloanwizard.h" #include "dialogs/kpayeereassigndlg.h" #include "dialogs/ktagreassigndlg.h" #include "dialogs/kcategoryreassigndlg.h" #include "dialogs/kmergetransactionsdlg.h" #include "wizards/endingbalancedlg/kendingbalancedlg.h" #include "dialogs/kbalancechartdlg.h" #include "dialogs/kgeneratesqldlg.h" #include "dialogs/kloadtemplatedlg.h" #include "dialogs/kgpgkeyselectiondlg.h" +#include "dialogs/ktemplateexportdlg.h" #include "dialogs/transactionmatcher.h" #include "wizards/newuserwizard/knewuserwizard.h" #include "wizards/newaccountwizard/knewaccountwizard.h" #include "dialogs/kbalancewarning.h" #include "widgets/onlinejobmessagesview.h" #include "widgets/kmymoneymvccombo.h" #include "widgets/kmymoneycompletion.h" #include "views/kmymoneyview.h" #include "views/konlinejoboutbox.h" #include "models/onlinejobmessagesmodel.h" #include "mymoney/mymoneyutils.h" #include "mymoney/mymoneystatement.h" #include "mymoney/storage/mymoneystoragedump.h" #include "mymoney/mymoneyforecast.h" #include "mymoney/onlinejob.h" #include "mymoney/onlinetransfer.h" #include "mymoney/onlinejobadministration.h" #include "converter/mymoneyqifwriter.h" #include "converter/mymoneyqifreader.h" #include "converter/mymoneystatementreader.h" #include "converter/mymoneytemplate.h" #include "plugins/interfaces/kmmviewinterface.h" #include "plugins/interfaces/kmmstatementinterface.h" #include "plugins/interfaces/kmmimportinterface.h" #include "plugins/interfaceloader.h" #include "plugins/onlinepluginextended.h" #include "pluginloader.h" #include #include #include "konlinetransferform.h" #include #include #include "kmymoneyutils.h" static constexpr char recoveryKeyId[] = "59B0F826D2B08440"; // define the default period to warn about an expiring recoverkey to 30 days // but allows to override this setting during build time #ifndef RECOVER_KEY_EXPIRATION_WARNING #define RECOVER_KEY_EXPIRATION_WARNING 30 #endif enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: Private(KMyMoneyApp *app) : q(app), m_ft(0), m_moveToAccountSelector(0), m_statementXMLindex(0), m_balanceWarning(0), m_collectingStatements(false), m_pluginLoader(0), m_myMoneyView(0), m_progressBar(0), m_qifReader(0), m_smtReader(0), m_searchDlg(0), m_autoSaveTimer(0), m_progressTimer(0), m_inAutoSaving(false), m_transactionEditor(0), m_endingBalanceDlg(0), m_saveEncrypted(0), m_additionalKeyLabel(0), m_additionalKeyButton(0), m_recentFiles(0), #ifdef KF5Holidays_FOUND m_holidayRegion(0), #endif m_applicationIsReady(true) { // since the days of the week are from 1 to 7, // and a day of the week is used to index this bit array, // resize the array to 8 elements (element 0 is left unused) m_processingDays.resize(8); } void closeFile(); void unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount); /** * The public interface. */ KMyMoneyApp * const q; MyMoneyFileTransaction* m_ft; kMyMoneyAccountSelector* m_moveToAccountSelector; int m_statementXMLindex; KBalanceWarning* m_balanceWarning; bool m_collectingStatements; QStringList m_statementResults; KMyMoneyPlugin::PluginLoader* m_pluginLoader; QString m_lastPayeeEnteredId; /** the configuration object of the application */ KSharedConfigPtr m_config; /** * @brief List of all plugged plugins * * The key is the file name of the plugin. */ QMap m_plugins; /** * @brief List of plugged importer plugins * * The key is the objectName of the plugin. */ QMap m_importerPlugins; /** * @brief List of plugged online plugins * * The key is the objectName of the plugin. */ QMap m_onlinePlugins; /** * The following variable represents the state while crafting a backup. * It can have the following values * * - IDLE: the default value if not performing a backup * - MOUNTING: when a mount command has been issued * - COPYING: when a copy command has been issued * - UNMOUNTING: when an unmount command has been issued */ backupStateE m_backupState; /** * This variable keeps the result of the backup operation. */ int m_backupResult; /** * This variable is set, when the user selected to mount/unmount * the backup volume. */ bool m_backupMount; QProcess m_proc; /// A pointer to the view holding the tabs. KMyMoneyView *m_myMoneyView; /// The URL of the file currently being edited when open. QUrl m_fileName; bool m_startDialog; QString m_mountpoint; QProgressBar* m_progressBar; QTime m_lastUpdate; QLabel* m_statusLabel; MyMoneyQifReader* m_qifReader; MyMoneyStatementReader* m_smtReader; // allows multiple imports to be launched trough web connect and to be executed sequentially QQueue m_importUrlsQueue; KFindTransactionDlg* m_searchDlg; QObject* m_pluginInterface; MyMoneyAccount m_selectedAccount; MyMoneyAccount m_reconciliationAccount; MyMoneyAccount m_selectedInvestment; MyMoneyInstitution m_selectedInstitution; MyMoneySchedule m_selectedSchedule; MyMoneySecurity m_selectedCurrency; MyMoneyPrice m_selectedPrice; QList m_selectedPayees; QList m_selectedTags; QList m_selectedBudgets; KMyMoneyRegister::SelectedTransactions m_selectedTransactions; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // pointer to the current transaction editor TransactionEditor* m_transactionEditor; // Reconciliation dialog KEndingBalanceDlg* m_endingBalanceDlg; // Pointer to the combo box used for key selection during // File/Save as KComboBox* m_saveEncrypted; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QStringList m_additionalGpgKeys; QLabel* m_additionalKeyLabel; QPushButton* m_additionalKeyButton; KRecentFilesAction* m_recentFiles; #ifdef KF5Holidays_FOUND // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif QBitArray m_processingDays; QMap m_holidayMap; QStringList m_consistencyCheckResult; bool m_applicationIsReady; // methods void consistencyCheck(bool alwaysDisplayResults); void setCustomColors(); void copyConsistencyCheckResults(); void saveConsistencyCheckResults(); }; KMyMoneyApp::KMyMoneyApp(QWidget* parent) : KXmlGuiWindow(parent), d(new Private(this)) { #ifdef KMM_DBUS new KmymoneyAdaptor(this); QDBusConnection::sessionBus().registerObject("/KMymoney", this); QDBusConnection::sessionBus().interface()->registerService( "org.kde.kmymoney", QDBusConnectionInterface::DontQueueService); #endif // Register the main engine types used as meta-objects qRegisterMetaType("MyMoneyMoney"); qRegisterMetaType("MyMoneySecurity"); // preset the pointer because we need it during the course of this constructor kmymoney = this; d->m_config = KSharedConfig::openConfig(); d->setCustomColors(); MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneyGlobalSettings::firstFiscalMonth(), KMyMoneyGlobalSettings::firstFiscalDay()); updateCaption(true); QFrame* frame = new QFrame(this); frame->setFrameStyle(QFrame::NoFrame); // values for margin (11) and spacing(6) taken from KDialog implementation QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, frame); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(6); if (KMyMoneySettings::iconsTheme().compare(QLatin1Literal("system")) != 0) QIcon::setThemeName(KMyMoneySettings::iconsTheme()); initStatusBar(); initActions(); initDynamicMenus(); d->m_myMoneyView = new KMyMoneyView(frame); layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, SIGNAL(aboutToChangeView()), this, SLOT(slotResetSelections())); connect(d->m_myMoneyView, SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)), this, SLOT(slotUpdateActions())); connectActionsAndViews(); /////////////////////////////////////////////////////////////////// // call inits to invoke all other construction parts readOptions(); // now initialize the plugin structure createInterfaces(); loadPlugins(); setCentralWidget(frame); connect(&d->m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotProcessExited())); // force to show the home page if the file is closed connect(action("view_show_transaction_detail"), SIGNAL(toggled(bool)), d->m_myMoneyView, SLOT(slotShowTransactionDetail(bool))); d->m_backupState = BACKUP_IDLE; // TODO: port kf5 int weekStart = 1;//KLocale::global()->workingWeekStartDay(); int weekEnd = 7;//KLocale::global()->workingWeekEndDay(); bool startFirst = (weekStart < weekEnd); for (int i = 0; i < 8; i++) { if (startFirst) d->m_processingDays.setBit(i, (i >= weekStart && i <= weekEnd)); else d->m_processingDays.setBit(i, (i >= weekStart || i <= weekEnd)); } d->m_autoSaveTimer = new QTimer(this); d->m_progressTimer = new QTimer(this); connect(d->m_autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); connect(d->m_progressTimer, SIGNAL(timeout()), this, SLOT(slotStatusProgressDone())); // make sure, we get a note when the engine changes state connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotDataChanged())); // make sure we have a balance warning object d->m_balanceWarning = new KBalanceWarning(this); // setup the initial configuration slotUpdateConfiguration(); // kickstart date change timer slotDateChanged(); connect(this, SIGNAL(fileLoaded(QUrl)), onlineJobAdministration::instance(), SLOT(updateOnlineTaskProperties())); } KMyMoneyApp::~KMyMoneyApp() { delete d->m_searchDlg; delete d->m_qifReader; delete d->m_transactionEditor; delete d->m_endingBalanceDlg; delete d->m_moveToAccountSelector; #ifdef KF5Holidays_FOUND delete d->m_holidayRegion; #endif delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_fileName; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } void KMyMoneyApp::slotObjectDestroyed(QObject* o) { if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = 0; } } void KMyMoneyApp::slotInstallConsistencyCheckContextMenu() { // this code relies on the implementation of KMessageBox::informationList to add a context menu to that list, // please adjust it if it's necessary or rewrite the way the consistency check results are displayed if (QWidget* dialog = QApplication::activeModalWidget()) { if (QListWidget* widget = dialog->findChild()) { // give the user a hint that the data can be saved widget->setToolTip(i18n("This is the consistency check log, use the context menu to copy or save it.")); widget->setWhatsThis(widget->toolTip()); widget->setContextMenuPolicy(Qt::CustomContextMenu); connect(widget, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotShowContextMenuForConsistencyCheck(QPoint))); } } } void KMyMoneyApp::slotShowContextMenuForConsistencyCheck(const QPoint &pos) { // allow the user to save the consistency check results if (QWidget* widget = qobject_cast< QWidget* >(sender())) { QMenu contextMenu(widget); QAction* copy = new QAction(i18n("Copy to clipboard"), widget); QAction* save = new QAction(i18n("Save to file"), widget); contextMenu.addAction(copy); contextMenu.addAction(save); QAction *result = contextMenu.exec(widget->mapToGlobal(pos)); if (result == copy) { // copy the consistency check results to the clipboard d->copyConsistencyCheckResults(); } else if (result == save) { // save the consistency check results to a file d->saveConsistencyCheckResults(); } } } void KMyMoneyApp::createTransactionMoveMenu() { if (!d->m_moveToAccountSelector) { QWidget* w = factory()->container("transaction_move_menu", this); QMenu *menu = dynamic_cast(w); if (menu) { QWidgetAction *accountSelectorAction = new QWidgetAction(menu); d->m_moveToAccountSelector = new kMyMoneyAccountSelector(menu, 0, false); d->m_moveToAccountSelector->setObjectName("transaction_move_menu_selector"); accountSelectorAction->setDefaultWidget(d->m_moveToAccountSelector); menu->addAction(accountSelectorAction); connect(d->m_moveToAccountSelector, SIGNAL(destroyed(QObject*)), this, SLOT(slotObjectDestroyed(QObject*))); connect(d->m_moveToAccountSelector, SIGNAL(itemSelected(QString)), this, SLOT(slotMoveToAccount(QString))); } } } void KMyMoneyApp::initDynamicMenus() { connect(this, SIGNAL(accountSelected(MyMoneyAccount)), this, SLOT(slotUpdateMoveToAccountMenu())); connect(this, SIGNAL(transactionsSelected(KMyMoneyRegister::SelectedTransactions)), this, SLOT(slotUpdateMoveToAccountMenu())); connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotUpdateMoveToAccountMenu())); } void KMyMoneyApp::initActions() { // ************* // The File menu // ************* actionCollection()->addAction(KStandardAction::New, this, SLOT(slotFileNew())); actionCollection()->addAction(KStandardAction::Open, this, SLOT(slotFileOpen())); d->m_recentFiles = KStandardAction::openRecent(this, SLOT(slotFileOpenRecent(QUrl)), actionCollection()); actionCollection()->addAction(KStandardAction::Save, this, SLOT(slotFileSave())); actionCollection()->addAction(KStandardAction::SaveAs, this, SLOT(slotFileSaveAs())); actionCollection()->addAction(KStandardAction::Close, this, SLOT(slotFileClose())); actionCollection()->addAction(KStandardAction::Quit, this, SLOT(slotFileQuit())); actionCollection()->addAction(KStandardAction::Print, this, SLOT(slotPrintView())); QAction *open_database = actionCollection()->addAction("open_database"); open_database->setText(i18n("Open database...")); connect(open_database, SIGNAL(triggered()), this, SLOT(slotOpenDatabase())); QAction *saveas_database = actionCollection()->addAction("saveas_database"); saveas_database->setText(i18n("Save as database...")); saveas_database->setIcon(QIcon::fromTheme("svn-update")); connect(saveas_database, SIGNAL(triggered()), this, SLOT(slotSaveAsDatabase())); QAction *file_backup = actionCollection()->addAction("file_backup"); file_backup->setText(i18n("Backup...")); file_backup->setIcon(QIcon::fromTheme(QStringLiteral("utilities-file-archiver"), QIcon::fromTheme(QStringLiteral("package")))); connect(file_backup, SIGNAL(triggered()), this, SLOT(slotFileBackup())); QAction *file_import_qif = actionCollection()->addAction("file_import_qif"); file_import_qif->setText(i18n("QIF...")); connect(file_import_qif, SIGNAL(triggered()), this, SLOT(slotQifImport())); QAction *file_import_gnc = actionCollection()->addAction("file_import_gnc"); file_import_gnc->setText(i18n("GnuCash...")); connect(file_import_gnc, SIGNAL(triggered()), this, SLOT(slotGncImport())); QAction *file_import_statement = actionCollection()->addAction("file_import_statement"); file_import_statement->setText(i18n("Statement file...")); connect(file_import_statement, SIGNAL(triggered()), this, SLOT(slotStatementImport())); QAction *file_import_template = actionCollection()->addAction("file_import_template"); file_import_template->setText(i18n("Account Template...")); connect(file_import_template, SIGNAL(triggered()), this, SLOT(slotLoadAccountTemplates())); QAction *file_export_template = actionCollection()->addAction("file_export_template"); file_export_template->setText(i18n("Account Template...")); connect(file_export_template, SIGNAL(triggered()), this, SLOT(slotSaveAccountTemplates())); QAction *file_export_qif = actionCollection()->addAction("file_export_qif"); file_export_qif->setText(i18n("QIF...")); connect(file_export_qif, SIGNAL(triggered()), this, SLOT(slotQifExport())); QAction *view_personal_data = actionCollection()->addAction("view_personal_data"); view_personal_data->setText(i18n("Personal Data...")); view_personal_data->setIcon(QIcon::fromTheme(QStringLiteral("user-properties"), QIcon::fromTheme(QStringLiteral("system-users")))); connect(view_personal_data, SIGNAL(triggered()), this, SLOT(slotFileViewPersonal())); #ifdef KMM_DEBUG QAction *file_dump = actionCollection()->addAction("file_dump"); file_dump->setText(i18n("Dump Memory")); connect(file_dump, SIGNAL(triggered()), this, SLOT(slotFileFileInfo())); #endif QAction *view_file_info = actionCollection()->addAction("view_file_info"); view_file_info->setText(i18n("File-Information...")); view_file_info->setIcon(QIcon::fromTheme("document-properties")); connect(view_file_info, SIGNAL(triggered()), this, SLOT(slotFileInfoDialog())); // ************* // The Edit menu // ************* QAction *edit_find_transaction = actionCollection()->addAction("edit_find_transaction"); edit_find_transaction->setText(i18n("Find transaction...")); edit_find_transaction->setIcon(KMyMoneyUtils::overlayIcon("view-financial-transfer", "edit-find")); actionCollection()->setDefaultShortcut(edit_find_transaction, QKeySequence("Ctrl+F")); connect(edit_find_transaction, SIGNAL(triggered()), this, SLOT(slotFindTransaction())); // ************* // The View menu // ************* KToggleAction *view_show_transaction_detail = actionCollection()->add("view_show_transaction_detail"); view_show_transaction_detail->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"), QIcon::fromTheme(QStringLiteral("edit-find")))); view_show_transaction_detail->setText(i18n("Show Transaction Detail")); actionCollection()->setDefaultShortcut(view_show_transaction_detail, QKeySequence("Ctrl+T")); KToggleAction *view_hide_reconciled_transactions = actionCollection()->add("view_hide_reconciled_transactions"); view_hide_reconciled_transactions->setText(i18n("Hide reconciled transactions")); if (QIcon::hasThemeIcon(QStringLiteral("hide-reconciled"))) view_hide_reconciled_transactions->setIcon(QIcon::fromTheme(QStringLiteral("hide-reconciled"))); else view_hide_reconciled_transactions->setIcon(KMyMoneyUtils::overlayIcon("merge", "view-close")); actionCollection()->setDefaultShortcut(view_hide_reconciled_transactions, QKeySequence("Ctrl+R")); connect(view_hide_reconciled_transactions, SIGNAL(triggered()), this, SLOT(slotHideReconciledTransactions())); KToggleAction *view_hide_unused_categories = actionCollection()->add("view_hide_unused_categories"); view_hide_unused_categories->setText(i18n("Hide unused categories")); if (QIcon::hasThemeIcon(QStringLiteral("hide-categories"))) view_hide_unused_categories->setIcon(QIcon::fromTheme(QStringLiteral("hide-categories"))); else view_hide_unused_categories->setIcon(KMyMoneyUtils::overlayIcon("view-financial-categories", "view-close")); actionCollection()->setDefaultShortcut(view_hide_unused_categories, QKeySequence("Ctrl+U")); connect(view_hide_unused_categories, SIGNAL(triggered()), this, SLOT(slotHideUnusedCategories())); KToggleAction *view_show_all_accounts = actionCollection()->add("view_show_all_accounts"); view_show_all_accounts->setText(i18n("Show all accounts")); actionCollection()->setDefaultShortcut(view_show_all_accounts, QKeySequence("Ctrl+Shift+A")); connect(view_show_all_accounts, SIGNAL(triggered()), this, SLOT(slotShowAllAccounts())); // ********************* // The institutions menu // ********************* QAction *institution_new = actionCollection()->addAction("institution_new"); institution_new->setText(i18n("New institution...")); institution_new->setIcon(KMyMoneyUtils::overlayIcon("view-bank", "list-add", Qt::TopRightCorner)); connect(institution_new, SIGNAL(triggered()), this, SLOT(slotInstitutionNew())); QAction *institution_edit = actionCollection()->addAction("institution_edit"); institution_edit->setText(i18n("Edit institution...")); institution_edit->setIcon(KMyMoneyUtils::overlayIcon("view-bank", "document-edit")); connect(institution_edit, SIGNAL(triggered()), this, SLOT(slotInstitutionEdit())); QAction *institution_delete = actionCollection()->addAction("institution_delete"); institution_delete->setText(i18n("Delete institution...")); institution_delete->setIcon(KMyMoneyUtils::overlayIcon("view-bank", "edit-delete")); connect(institution_delete, SIGNAL(triggered()), this, SLOT(slotInstitutionDelete())); // ***************** // The accounts menu // ***************** QAction *account_new = actionCollection()->addAction("account_new"); account_new->setText(i18n("New account...")); account_new->setIcon(KMyMoneyUtils::overlayIcon("view-bank-account", "list-add", Qt::TopRightCorner)); connect(account_new, SIGNAL(triggered()), this, SLOT(slotAccountNew())); // note : action "category_new" is included in this menu but defined below QAction *account_open = actionCollection()->addAction("account_open"); account_open->setText(i18n("Open ledger")); account_open->setIcon(QIcon::fromTheme("view-financial-list", QIcon::fromTheme(QStringLiteral("ledger")))); connect(account_open, SIGNAL(triggered()), this, SLOT(slotAccountOpen())); QAction *account_reconcile = actionCollection()->addAction("account_reconcile"); account_reconcile->setText(i18n("Reconcile...")); account_reconcile->setIcon(QIcon::fromTheme(QStringLiteral("merge"), QIcon::fromTheme(QStringLiteral("reconcile")))); actionCollection()->setDefaultShortcut(account_reconcile, QKeySequence("Ctrl+Shift+R")); connect(account_reconcile, SIGNAL(triggered()), this, SLOT(slotAccountReconcileStart())); QAction *account_reconcile_finish = actionCollection()->addAction("account_reconcile_finish"); account_reconcile_finish->setText(i18nc("Finish reconciliation", "Finish")); account_reconcile_finish->setIcon(KMyMoneyUtils::overlayIcon("merge", "dialog-ok")); connect(account_reconcile_finish, SIGNAL(triggered()), this, SLOT(slotAccountReconcileFinish())); QAction *account_reconcile_postpone = actionCollection()->addAction("account_reconcile_postpone"); account_reconcile_postpone->setText(i18n("Postpone reconciliation")); account_reconcile_postpone->setIcon(QIcon::fromTheme("media-playback-pause")); connect(account_reconcile_postpone, SIGNAL(triggered()), this, SLOT(slotAccountReconcilePostpone())); QAction *account_edit = actionCollection()->addAction("account_edit"); account_edit->setText(i18n("Edit account...")); account_edit->setIcon(KMyMoneyUtils::overlayIcon("view-bank-account", "document-edit")); connect(account_edit, SIGNAL(triggered()), this, SLOT(slotAccountEdit())); QAction *account_delete = actionCollection()->addAction("account_delete"); account_delete->setText(i18n("Delete account...")); account_delete->setIcon(KMyMoneyUtils::overlayIcon("view-bank-account", "edit-delete")); connect(account_delete, SIGNAL(triggered()), this, SLOT(slotAccountDelete())); QAction *account_close = actionCollection()->addAction("account_close"); account_close->setText(i18n("Close account")); account_close->setIcon(KMyMoneyUtils::overlayIcon("view-bank-account", "dialog-close")); connect(account_close, SIGNAL(triggered()), this, SLOT(slotAccountClose())); QAction *account_reopen = actionCollection()->addAction("account_reopen"); account_reopen->setText(i18n("Reopen account")); account_reopen->setIcon(KMyMoneyUtils::overlayIcon("view-bank-account", "dialog-ok")); connect(account_reopen, SIGNAL(triggered()), this, SLOT(slotAccountReopen())); QAction *account_transaction_report = actionCollection()->addAction("account_transaction_report"); account_transaction_report->setText(i18n("Transaction report")); account_transaction_report->setIcon(QIcon::fromTheme(QStringLiteral("view-financial-list"), QIcon::fromTheme(QStringLiteral("ledger")))); connect(account_transaction_report, SIGNAL(triggered()), this, SLOT(slotAccountTransactionReport())); QAction *account_chart = actionCollection()->addAction("account_chart"); account_chart->setText(i18n("Show balance chart...")); account_chart->setIcon(QIcon::fromTheme(QStringLiteral("office-chart-line"), QIcon::fromTheme(QStringLiteral("report-line"), QIcon::fromTheme(QStringLiteral("account-types-investments"))))); connect(account_chart, SIGNAL(triggered()), this, SLOT(slotAccountChart())); QAction *account_online_map = actionCollection()->addAction("account_online_map"); account_online_map->setText(i18n("Map to online account...")); account_online_map->setIcon(QIcon::fromTheme("news-subscribe")); connect(account_online_map, SIGNAL(triggered()), this, SLOT(slotAccountMapOnline())); QAction *account_online_unmap = actionCollection()->addAction("account_online_unmap"); account_online_unmap->setText(i18n("Unmap account...")); account_online_unmap->setIcon(QIcon::fromTheme("news-unsubscribe")); connect(account_online_unmap, SIGNAL(triggered()), this, SLOT(slotAccountUnmapOnline())); KActionMenu* menu = new KActionMenu(KMyMoneyUtils::overlayIcon("view-bank-account", "download"), i18nc("Update online accounts menu", "Update"), this); actionCollection()->addAction("account_online_update_menu", menu); // activating the menu button is the same as selecting the current account connect(menu, SIGNAL(triggered()), this, SLOT(slotAccountUpdateOnline())); QAction *account_online_update = actionCollection()->addAction("account_online_update"); account_online_update->setText(i18n("Update account...")); account_online_update->setIcon(KMyMoneyUtils::overlayIcon("view-bank-account", "download")); connect(account_online_update, SIGNAL(triggered()), this, SLOT(slotAccountUpdateOnline())); menu->addAction(account_online_update); QAction *account_online_update_all = actionCollection()->addAction("account_online_update_all"); account_online_update_all->setText(i18n("Update all accounts...")); account_online_update_all->setIcon(KMyMoneyUtils::overlayIcon("view-bank-account", "download")); connect(account_online_update_all, SIGNAL(triggered()), this, SLOT(slotAccountUpdateOnlineAll())); menu->addAction(account_online_update_all); QAction *account_online_transfer = actionCollection()->addAction("account_online_new_credit_transfer"); account_online_transfer->setText(i18n("New credit transfer")); account_online_transfer->setIcon(KMyMoneyUtils::overlayIcon("view-bank-account", "mail-message-new")); connect(account_online_transfer, SIGNAL(triggered()), this, SLOT(slotNewOnlineTransfer())); connect(onlineJobAdministration::instance(), SIGNAL(canSendCreditTransferChanged(bool)), account_online_transfer, SLOT(setEnabled(bool))); // ******************* // The categories menu // ******************* QAction *category_new = actionCollection()->addAction("category_new"); category_new->setText(i18n("New category...")); category_new->setIcon(KMyMoneyUtils::overlayIcon("view-financial-categories", "list-add", Qt::TopRightCorner)); connect(category_new, SIGNAL(triggered()), this, SLOT(slotCategoryNew())); QAction *category_edit = actionCollection()->addAction("category_edit"); category_edit->setText(i18n("Edit category...")); category_edit->setIcon(KMyMoneyUtils::overlayIcon("view-financial-categories", "document-edit")); connect(category_edit, SIGNAL(triggered()), this, SLOT(slotAccountEdit())); QAction *category_delete = actionCollection()->addAction("category_delete"); category_delete->setText(i18n("Delete category...")); category_delete->setIcon(KMyMoneyUtils::overlayIcon("view-financial-categories", "edit-delete")); connect(category_delete, SIGNAL(triggered()), this, SLOT(slotAccountDelete())); // ************** // The tools menu // ************** QAction *tools_qif_editor = actionCollection()->addAction("tools_qif_editor"); tools_qif_editor->setText(i18n("QIF Profile Editor...")); tools_qif_editor->setIcon(QIcon::fromTheme("document-properties")); connect(tools_qif_editor, SIGNAL(triggered()), this, SLOT(slotQifProfileEditor())); QAction *tools_currency_editor = actionCollection()->addAction("tools_currency_editor"); tools_currency_editor->setText(i18n("Currencies...")); tools_currency_editor->setIcon(QIcon::fromTheme("view-currency-list")); connect(tools_currency_editor, SIGNAL(triggered()), this, SLOT(slotCurrencyDialog())); QAction *tools_price_editor = actionCollection()->addAction("tools_price_editor"); tools_price_editor->setText(i18n("Prices...")); connect(tools_price_editor, SIGNAL(triggered()), this, SLOT(slotPriceDialog())); QAction *tools_update_prices = actionCollection()->addAction("tools_update_prices"); tools_update_prices->setText(i18n("Update Stock and Currency Prices...")); tools_update_prices->setIcon(KMyMoneyUtils::overlayIcon("view-investment", "download")); connect(tools_update_prices, SIGNAL(triggered()), this, SLOT(slotEquityPriceUpdate())); QAction *tools_consistency_check = actionCollection()->addAction("tools_consistency_check"); tools_consistency_check->setText(i18n("Consistency Check")); connect(tools_consistency_check, SIGNAL(triggered()), this, SLOT(slotFileConsistencyCheck())); QAction *tools_performancetest = actionCollection()->addAction("tools_performancetest"); tools_performancetest->setText(i18n("Performance-Test")); tools_performancetest->setIcon(QIcon::fromTheme("fork")); connect(tools_performancetest, SIGNAL(triggered()), this, SLOT(slotPerformanceTest())); QAction *tools_generate_sql = actionCollection()->addAction("tools_generate_sql"); tools_generate_sql->setText(i18n("Generate Database SQL")); connect(tools_generate_sql, SIGNAL(triggered()), this, SLOT(slotGenerateSql())); QAction *tools_kcalc = actionCollection()->addAction("tools_kcalc"); tools_kcalc->setText(i18n("Calculator...")); tools_kcalc->setIcon(QIcon::fromTheme("accessories-calculator")); connect(tools_kcalc, SIGNAL(triggered()), this, SLOT(slotToolsStartKCalc())); // ***************** // The settings menu // ***************** actionCollection()->addAction(KStandardAction::Preferences, this, SLOT(slotSettings())); QAction *settings_enable_messages = actionCollection()->addAction("settings_enable_messages"); settings_enable_messages->setText(i18n("Enable all messages")); connect(settings_enable_messages, SIGNAL(triggered()), this, SLOT(slotEnableMessages())); QAction *settings_language = actionCollection()->addAction("settings_language"); settings_language->setText(i18n("KDE language settings...")); connect(settings_language, SIGNAL(triggered()), this, SLOT(slotKDELanguageSettings())); // ************* // The help menu // ************* QAction *help_show_tip = actionCollection()->addAction("help_show_tip"); help_show_tip->setText(i18n("&Show tip of the day")); help_show_tip->setIcon(QIcon::fromTheme(QStringLiteral("ktip"), QIcon::fromTheme(QStringLiteral("info")))); connect(help_show_tip, SIGNAL(triggered()), this, SLOT(slotShowTipOfTheDay())); // *************************** // Actions w/o main menu entry // *************************** QAction *transaction_new = actionCollection()->addAction("transaction_new"); transaction_new->setText(i18nc("New transaction button", "New")); transaction_new->setIcon(KMyMoneyUtils::overlayIcon("view-financial-transfer", "list-add", Qt::TopRightCorner)); actionCollection()->setDefaultShortcut(transaction_new, QKeySequence(Qt::CTRL | Qt::Key_Insert)); connect(transaction_new, SIGNAL(triggered()), this, SLOT(slotTransactionsNew())); // we use Return as the same shortcut for Edit and Enter. Therefore, we don't allow // to change them (The standard KDE dialog complains anyway if you want to assign // the same shortcut to two actions) QAction *transaction_edit = actionCollection()->addAction("transaction_edit"); transaction_edit->setText(i18nc("Edit transaction button", "Edit")); transaction_edit->setIcon(KMyMoneyUtils::overlayIcon("view-financial-transfer", "document-edit")); connect(transaction_edit, SIGNAL(triggered()), this, SLOT(slotTransactionsEdit())); QAction *transaction_enter = actionCollection()->addAction("transaction_enter"); transaction_enter->setText(i18nc("Enter transaction", "Enter")); transaction_enter->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"), QIcon::fromTheme(QStringLiteral("finish")))); connect(transaction_enter, SIGNAL(triggered()), this, SLOT(slotTransactionsEnter())); QAction *transaction_editsplits = actionCollection()->addAction("transaction_editsplits"); transaction_editsplits->setText(i18nc("Edit split button", "Edit splits")); transaction_editsplits->setIcon(QIcon::fromTheme(QStringLiteral("split"), QIcon::fromTheme(QStringLiteral("transaction-split")))); connect(transaction_editsplits, SIGNAL(triggered()), this, SLOT(slotTransactionsEditSplits())); QAction *transaction_cancel = actionCollection()->addAction("transaction_cancel"); transaction_cancel->setText(i18nc("Cancel transaction edit", "Cancel")); transaction_cancel->setIcon(QIcon::fromTheme(QStringLiteral("dialog-cancel"), QIcon::fromTheme(QStringLiteral("stop")))); connect(transaction_cancel, SIGNAL(triggered()), this, SLOT(slotTransactionsCancel())); QAction *transaction_delete = actionCollection()->addAction("transaction_delete"); transaction_delete->setText(i18nc("Delete transaction", "Delete")); transaction_delete->setIcon(QIcon::fromTheme("edit-delete")); connect(transaction_delete, SIGNAL(triggered()), this, SLOT(slotTransactionsDelete())); QAction *transaction_duplicate = actionCollection()->addAction("transaction_duplicate"); transaction_duplicate->setText(i18nc("Duplicate transaction", "Duplicate")); transaction_duplicate->setIcon(QIcon::fromTheme("edit-copy")); connect(transaction_duplicate, SIGNAL(triggered()), this, SLOT(slotTransactionDuplicate())); QAction *transaction_match = actionCollection()->addAction("transaction_match"); transaction_match->setText(i18nc("Button text for match transaction", "Match")); transaction_match->setIcon(KMyMoneyUtils::overlayIcon("view-financial-transfer", "document-import")); connect(transaction_match, SIGNAL(triggered()), this, SLOT(slotTransactionMatch())); QAction *transaction_accept = actionCollection()->addAction("transaction_accept"); transaction_accept->setText(i18nc("Accept 'imported' and 'matched' transaction", "Accept")); transaction_accept->setIcon(KMyMoneyUtils::overlayIcon("view-financial-transfer", "dialog-ok-apply")); connect(transaction_accept, SIGNAL(triggered()), this, SLOT(slotTransactionsAccept())); QAction *transaction_mark_toggle = actionCollection()->addAction("transaction_mark_toggle"); transaction_mark_toggle->setText(i18nc("Toggle reconciliation flag", "Toggle")); actionCollection()->setDefaultShortcut(transaction_mark_toggle, QKeySequence("Ctrl+Space")); connect(transaction_mark_toggle, SIGNAL(triggered()), this, SLOT(slotToggleReconciliationFlag())); QAction *transaction_mark_cleared = actionCollection()->addAction("transaction_mark_cleared"); transaction_mark_cleared->setText(i18nc("Mark transaction cleared", "Cleared")); actionCollection()->setDefaultShortcut(transaction_mark_cleared, QKeySequence("Ctrl+Alt+Space")); connect(transaction_mark_cleared, SIGNAL(triggered()), this, SLOT(slotMarkTransactionCleared())); QAction *transaction_mark_reconciled = actionCollection()->addAction("transaction_mark_reconciled"); transaction_mark_reconciled->setText(i18nc("Mark transaction reconciled", "Reconciled")); actionCollection()->setDefaultShortcut(transaction_mark_reconciled, QKeySequence("Ctrl+Shift+Space")); connect(transaction_mark_reconciled, SIGNAL(triggered()), this, SLOT(slotMarkTransactionReconciled())); QAction *transaction_mark_notreconciled = actionCollection()->addAction("transaction_mark_notreconciled"); transaction_mark_notreconciled->setText(i18nc("Mark transaction not reconciled", "Not reconciled")); connect(transaction_mark_notreconciled, SIGNAL(triggered()), this, SLOT(slotMarkTransactionNotReconciled())); QAction *transaction_select_all = actionCollection()->addAction("transaction_select_all"); transaction_select_all->setText(i18nc("Select all transactions", "Select all")); actionCollection()->setDefaultShortcut(transaction_select_all, QKeySequence("Ctrl+A")); connect(transaction_select_all, SIGNAL(triggered()), this, SIGNAL(selectAllTransactions())); QAction *transaction_goto_account = actionCollection()->addAction("transaction_goto_account"); transaction_goto_account->setText(i18n("Go to account")); transaction_goto_account->setIcon(QIcon::fromTheme("go-jump")); connect(transaction_goto_account, SIGNAL(triggered()), this, SLOT(slotTransactionGotoAccount())); QAction *transaction_goto_payee = actionCollection()->addAction("transaction_goto_payee"); transaction_goto_payee->setText(i18n("Go to payee")); transaction_goto_payee->setIcon(QIcon::fromTheme("go-jump")); connect(transaction_goto_payee, SIGNAL(triggered()), this, SLOT(slotTransactionGotoPayee())); QAction *transaction_create_schedule = actionCollection()->addAction("transaction_create_schedule"); transaction_create_schedule->setText(i18n("Create scheduled transaction...")); transaction_create_schedule->setIcon(QIcon::fromTheme("appointment-new")); connect(transaction_create_schedule, SIGNAL(triggered()), this, SLOT(slotTransactionCreateSchedule())); QAction *transaction_assign_number = actionCollection()->addAction("transaction_assign_number"); transaction_assign_number->setText(i18n("Assign next number")); actionCollection()->setDefaultShortcut(transaction_assign_number, QKeySequence("Ctrl+Shift+N")); connect(transaction_assign_number, SIGNAL(triggered()), this, SLOT(slotTransactionAssignNumber())); QAction *transaction_combine = actionCollection()->addAction("transaction_combine"); transaction_combine->setText(i18nc("Combine transactions", "Combine")); connect(transaction_combine, SIGNAL(triggered()), this, SLOT(slotTransactionCombine())); QAction *transaction_copy_splits = actionCollection()->addAction("transaction_copy_splits"); transaction_copy_splits->setText(i18n("Copy splits")); connect(transaction_copy_splits, SIGNAL(triggered()), this, SLOT(slotTransactionCopySplits())); //Investment QAction *investment_new = actionCollection()->addAction("investment_new"); investment_new->setText(i18n("New investment...")); investment_new->setIcon(KMyMoneyUtils::overlayIcon("view-investment", "list-add", Qt::TopRightCorner)); connect(investment_new, SIGNAL(triggered()), this, SLOT(slotInvestmentNew())); QAction *investment_edit = actionCollection()->addAction("investment_edit"); investment_edit->setText(i18n("Edit investment...")); investment_edit->setIcon(KMyMoneyUtils::overlayIcon("view-investment", "document-edit")); connect(investment_edit, SIGNAL(triggered()), this, SLOT(slotInvestmentEdit())); QAction *investment_delete = actionCollection()->addAction("investment_delete"); investment_delete->setText(i18n("Delete investment...")); investment_delete->setIcon(KMyMoneyUtils::overlayIcon("view-investment", "edit-delete")); connect(investment_delete, SIGNAL(triggered()), this, SLOT(slotInvestmentDelete())); QAction *investment_online_price_update = actionCollection()->addAction("investment_online_price_update"); investment_online_price_update->setText(i18n("Online price update...")); investment_online_price_update->setIcon(KMyMoneyUtils::overlayIcon("view-investment", "download")); connect(investment_online_price_update, SIGNAL(triggered()), this, SLOT(slotOnlinePriceUpdate())); QAction *investment_manual_price_update = actionCollection()->addAction("investment_manual_price_update"); investment_manual_price_update->setText(i18n("Manual price update...")); connect(investment_manual_price_update, SIGNAL(triggered()), this, SLOT(slotManualPriceUpdate())); //Schedule QAction *schedule_new = actionCollection()->addAction("schedule_new"); schedule_new->setText(i18n("New scheduled transaction")); schedule_new->setIcon(QIcon::fromTheme("appointment-new")); connect(schedule_new, SIGNAL(triggered()), this, SLOT(slotScheduleNew())); QAction *schedule_edit = actionCollection()->addAction("schedule_edit"); schedule_edit->setText(i18n("Edit scheduled transaction")); schedule_edit->setIcon(QIcon::fromTheme("document-edit")); connect(schedule_edit, SIGNAL(triggered()), this, SLOT(slotScheduleEdit())); QAction *schedule_delete = actionCollection()->addAction("schedule_delete"); schedule_delete->setText(i18n("Delete scheduled transaction")); schedule_delete->setIcon(QIcon::fromTheme("edit-delete")); connect(schedule_delete, SIGNAL(triggered()), this, SLOT(slotScheduleDelete())); QAction *schedule_duplicate = actionCollection()->addAction("schedule_duplicate"); schedule_duplicate->setText(i18n("Duplicate scheduled transaction")); schedule_duplicate->setIcon(QIcon::fromTheme("edit-copy")); connect(schedule_duplicate, SIGNAL(triggered()), this, SLOT(slotScheduleDuplicate())); QAction *schedule_enter = actionCollection()->addAction("schedule_enter"); schedule_enter->setText(i18n("Enter next transaction...")); schedule_enter->setIcon(QIcon::fromTheme("key-enter")); connect(schedule_enter, SIGNAL(triggered()), this, SLOT(slotScheduleEnter())); QAction *schedule_skip = actionCollection()->addAction("schedule_skip"); schedule_skip->setText(i18n("Skip next transaction...")); schedule_skip->setIcon(QIcon::fromTheme("media-seek-forward")); connect(schedule_skip, SIGNAL(triggered()), this, SLOT(slotScheduleSkip())); //Payees QAction *payee_new = actionCollection()->addAction("payee_new"); payee_new->setText(i18n("New payee")); payee_new->setIcon(QIcon::fromTheme("list-add-user")); connect(payee_new, SIGNAL(triggered()), this, SLOT(slotPayeeNew())); QAction *payee_rename = actionCollection()->addAction("payee_rename"); payee_rename->setText(i18n("Rename payee")); payee_rename->setIcon(QIcon::fromTheme("user-properties")); connect(payee_rename, SIGNAL(triggered()), this, SIGNAL(payeeRename())); QAction *payee_delete = actionCollection()->addAction("payee_delete"); payee_delete->setText(i18n("Delete payee")); payee_delete->setIcon(QIcon::fromTheme("list-remove-user")); connect(payee_delete, SIGNAL(triggered()), this, SLOT(slotPayeeDelete())); QAction *payee_merge = actionCollection()->addAction("payee_merge"); payee_merge->setText(i18n("Merge payees")); payee_merge->setIcon(QIcon::fromTheme("merge")); connect(payee_merge, SIGNAL(triggered()), this, SLOT(slotPayeeMerge())); //Tags QAction *tag_new = actionCollection()->addAction("tag_new"); tag_new->setText(i18n("New tag")); tag_new->setIcon(QIcon::fromTheme("list-add-tag")); connect(tag_new, SIGNAL(triggered()), this, SLOT(slotTagNew())); QAction *tag_rename = actionCollection()->addAction("tag_rename"); tag_rename->setText(i18n("Rename tag")); tag_rename->setIcon(QIcon::fromTheme("tag-rename")); connect(tag_rename, SIGNAL(triggered()), this, SIGNAL(tagRename())); QAction *tag_delete = actionCollection()->addAction("tag_delete"); tag_delete->setText(i18n("Delete tag")); tag_delete->setIcon(QIcon::fromTheme("list-remove-tag")); connect(tag_delete, SIGNAL(triggered()), this, SLOT(slotTagDelete())); //Budget QAction *budget_new = actionCollection()->addAction("budget_new"); budget_new->setText(i18n("New budget")); budget_new->setIcon(KMyMoneyUtils::overlayIcon("view-time-schedule-calculus", "list-add", Qt::TopRightCorner)); connect(budget_new, SIGNAL(triggered()), this, SLOT(slotBudgetNew())); QAction *budget_rename = actionCollection()->addAction("budget_rename"); budget_rename->setText(i18n("Rename budget")); budget_rename->setIcon(KMyMoneyUtils::overlayIcon("view-time-schedule-calculus", "document-edit")); connect(budget_rename, SIGNAL(triggered()), this, SIGNAL(budgetRename())); QAction *budget_delete = actionCollection()->addAction("budget_delete"); budget_delete->setText(i18n("Delete budget")); budget_delete->setIcon(KMyMoneyUtils::overlayIcon("view-time-schedule-calculus", "edit-delete")); connect(budget_delete, SIGNAL(triggered()), this, SLOT(slotBudgetDelete())); QAction *budget_copy = actionCollection()->addAction("budget_copy"); budget_copy->setText(i18n("Copy budget")); budget_copy->setIcon(KMyMoneyUtils::overlayIcon("view-time-schedule-calculus", "edit-copy")); connect(budget_copy, SIGNAL(triggered()), this, SLOT(slotBudgetCopy())); QAction *budget_change_year = actionCollection()->addAction("budget_change_year"); budget_change_year->setText(i18n("Change budget year")); budget_change_year->setIcon(QIcon::fromTheme("view-calendar")); connect(budget_change_year, SIGNAL(triggered()), this, SLOT(slotBudgetChangeYear())); QAction *budget_forecast = actionCollection()->addAction("budget_forecast"); budget_forecast->setText(i18n("Budget based on forecast")); budget_forecast->setIcon(QIcon::fromTheme("view-financial-forecast")); connect(budget_forecast, SIGNAL(triggered()), this, SLOT(slotBudgetForecast())); // Currency actions QAction *currency_new = actionCollection()->addAction("currency_new"); currency_new->setText(i18n("New currency")); currency_new->setIcon(QIcon::fromTheme("document-new")); connect(currency_new, SIGNAL(triggered()), this, SLOT(slotCurrencyNew())); QAction *currency_rename = actionCollection()->addAction("currency_rename"); currency_rename->setText(i18n("Rename currency")); currency_rename->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"), QIcon::fromTheme(QStringLiteral("text-editor")))); connect(currency_rename, SIGNAL(triggered()), this, SIGNAL(currencyRename())); QAction *currency_delete = actionCollection()->addAction("currency_delete"); currency_delete->setText(i18n("Delete currency")); currency_delete->setIcon(QIcon::fromTheme("edit-delete")); connect(currency_delete, SIGNAL(triggered()), this, SLOT(slotCurrencyDelete())); QAction *currency_setbase = actionCollection()->addAction("currency_setbase"); currency_setbase->setText(i18n("Select as base currency")); currency_setbase->setIcon(QIcon::fromTheme("kmymoney")); connect(currency_setbase, SIGNAL(triggered()), this, SLOT(slotCurrencySetBase())); //price actions QAction *price_new = actionCollection()->addAction("price_new"); price_new->setText(i18n("New price...")); price_new->setIcon(QIcon::fromTheme("document-new")); connect(price_new, SIGNAL(triggered()), this, SIGNAL(priceNew())); QAction *price_edit = actionCollection()->addAction("price_edit"); price_edit->setText(i18n("Edit price...")); price_edit->setIcon(QIcon::fromTheme("document-edit")); connect(price_edit, SIGNAL(triggered()), this, SIGNAL(priceEdit())); QAction *price_update = actionCollection()->addAction("price_update"); price_update->setText(i18n("Online Price Update...")); price_update->setIcon(KMyMoneyUtils::overlayIcon("view-currency-list", "download")); connect(price_update, SIGNAL(triggered()), this, SIGNAL(priceOnlineUpdate())); QAction *price_delete = actionCollection()->addAction("price_delete"); price_delete->setText(i18n("Delete price...")); price_delete->setIcon(QIcon::fromTheme("edit-delete")); connect(price_delete, SIGNAL(triggered()), this, SIGNAL(priceDelete())); //debug actions #ifdef KMM_DEBUG QAction *new_user_wizard = actionCollection()->addAction("new_user_wizard"); new_user_wizard->setText(i18n("Test new feature")); actionCollection()->setDefaultShortcut(new_user_wizard, QKeySequence("Ctrl+G")); connect(new_user_wizard, SIGNAL(triggered()), this, SLOT(slotNewFeature())); KToggleAction *debug_traces = actionCollection()->add("debug_traces"); debug_traces->setText(i18n("Debug Traces")); connect(debug_traces, SIGNAL(triggered()), this, SLOT(slotToggleTraces())); #endif KToggleAction *debug_timers = actionCollection()->add("debug_timers"); debug_timers->setText(i18n("Debug Timers")); connect(debug_timers, SIGNAL(triggered()), this, SLOT(slotToggleTimers())); // onlineJob actions QAction* onlineJob_delete = actionCollection()->addAction("onlinejob_delete"); onlineJob_delete->setText(i18n("Remove credit transfer")); onlineJob_delete->setIcon(QIcon::fromTheme("edit-delete")); QAction* onlineJob_edit = actionCollection()->addAction("onlinejob_edit"); onlineJob_edit->setText(i18n("Edit credit transfer")); onlineJob_edit->setIcon(QIcon::fromTheme("document-edit")); QAction* onlineJob_log = actionCollection()->addAction("onlinejob_log"); onlineJob_log->setText(i18n("Show log")); connect(onlineJob_log, SIGNAL(triggered()), this, SLOT(slotOnlineJobLog())); // ************************ // Currently unused actions // ************************ #if 0 new KToolBarPopupAction(i18n("View back"), "go-previous", 0, this, SLOT(slotShowPreviousView()), actionCollection(), "go_back"); new KToolBarPopupAction(i18n("View forward"), "go-next", 0, this, SLOT(slotShowNextView()), actionCollection(), "go_forward"); action("go_back")->setEnabled(false); action("go_forward")->setEnabled(false); #endif // Setup transaction detail switch toggleAction("view_show_transaction_detail")->setChecked(KMyMoneyGlobalSettings::showRegisterDetailed()); toggleAction("view_hide_reconciled_transactions")->setChecked(KMyMoneyGlobalSettings::hideReconciledTransactions()); toggleAction("view_hide_unused_categories")->setChecked(KMyMoneyGlobalSettings::hideUnusedCategory()); toggleAction("view_show_all_accounts")->setChecked(false); // use the absolute path to your kmymoneyui.rc file for testing purpose in createGUI(); setupGUI(); QMenu *menuContainer; menuContainer = static_cast(factory()->container("import", this)); if (QIcon::hasThemeIcon(QStringLiteral("document-import"))) menuContainer->setIcon(QIcon::fromTheme(QStringLiteral("document-import"))); else menuContainer->setIcon(QIcon::fromTheme(QStringLiteral("format-indent-less"))); menuContainer = static_cast(factory()->container("export", this)); if (QIcon::hasThemeIcon(QStringLiteral("document-export"))) menuContainer->setIcon(QIcon::fromTheme(QStringLiteral("document-export"))); else menuContainer->setIcon(QIcon::fromTheme(QStringLiteral("format-indent-more"))); } void KMyMoneyApp::connectActionsAndViews() { KOnlineJobOutbox *const outbox = d->m_myMoneyView->getOnlineJobOutbox(); Q_CHECK_PTR(outbox); QAction *const onlineJob_delete = actionCollection()->action("onlinejob_delete"); Q_CHECK_PTR(onlineJob_delete); connect(onlineJob_delete, SIGNAL(triggered()), outbox, SLOT(slotRemoveJob())); QAction *const onlineJob_edit = actionCollection()->action("onlinejob_edit"); Q_CHECK_PTR(onlineJob_edit); connect(onlineJob_edit, SIGNAL(triggered()), outbox, SLOT(slotEditJob())); } void KMyMoneyApp::dumpActions() const { const QList list = actionCollection()->actions(); QList::const_iterator it; for (it = list.begin(); it != list.end(); ++it) { std::cout << qPrintable((*it)->objectName()) << ": " << qPrintable((*it)->text()) << std::endl; } } KToggleAction* KMyMoneyApp::toggleAction(const QString& actionName) const { static KToggleAction dummyAction(QString("Dummy"), 0); KToggleAction* p = dynamic_cast(actionCollection()->action(QString(actionName.toLatin1()))); if (!p) { qWarning("ToggleAction with name '%s' not found!", qPrintable(actionName)); p = &dummyAction; } return p; } void KMyMoneyApp::initStatusBar() { /////////////////////////////////////////////////////////////////// // STATUSBAR d->m_statusLabel = new QLabel(statusBar()); statusBar()->addWidget(d->m_statusLabel); ready(); // Initialization of progress bar taken from KDevelop ;-) d->m_progressBar = new QProgressBar(statusBar()); statusBar()->addWidget(d->m_progressBar); d->m_progressBar->setFixedHeight(d->m_progressBar->sizeHint().height() - 8); // hide the progress bar for now slotStatusProgressBar(-1, -1); } void KMyMoneyApp::saveOptions() { KConfigGroup grp = d->m_config->group("General Options"); grp.writeEntry("Geometry", size()); grp.writeEntry("Show Statusbar", toggleAction("options_show_statusbar")->isChecked()); KConfigGroup toolbarGrp = d->m_config->group("mainToolBar"); toolBar("mainToolBar")->saveSettings(toolbarGrp); d->m_recentFiles->saveEntries(d->m_config->group("Recent Files")); } void KMyMoneyApp::readOptions() { KConfigGroup grp = d->m_config->group("General Options"); toggleAction("view_hide_reconciled_transactions")->setChecked(KMyMoneyGlobalSettings::hideReconciledTransactions()); toggleAction("view_hide_unused_categories")->setChecked(KMyMoneyGlobalSettings::hideUnusedCategory()); d->m_recentFiles->loadEntries(d->m_config->group("Recent Files")); // Startdialog is written in the settings dialog d->m_startDialog = grp.readEntry("StartDialog", true); } void KMyMoneyApp::resizeEvent(QResizeEvent* ev) { KMainWindow::resizeEvent(ev); updateCaption(true); } int KMyMoneyApp::askSaveOnClose() { int ans; if (KMyMoneyGlobalSettings::autoSaveOnClose()) { ans = KMessageBox::Yes; } else { ans = KMessageBox::warningYesNoCancel(this, i18n("The file has been changed, save it?")); } return ans; } bool KMyMoneyApp::queryClose() { if (!isReady()) return false; if (d->m_myMoneyView->dirty()) { int ans = askSaveOnClose(); if (ans == KMessageBox::Cancel) return false; else if (ans == KMessageBox::Yes) { bool saved = slotFileSave(); saveOptions(); return saved; } } if (d->m_myMoneyView->isDatabase()) slotFileClose(); // close off the database saveOptions(); return true; } ///////////////////////////////////////////////////////////////////// // SLOT IMPLEMENTATION ///////////////////////////////////////////////////////////////////// void KMyMoneyApp::slotFileInfoDialog() { QPointer dlg = new KMyMoneyFileInfoDlg(0); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPerformanceTest() { // dump performance report to stderr int measurement[2]; QTime timer; MyMoneyAccount acc; qDebug("--- Starting performance tests ---"); // AccountList MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; timer.start(); for (int i = 0; i < 1000; ++i) { QList list; MyMoneyFile::instance()->accountList(list); measurement[i != 0] = timer.elapsed(); } std::cerr << "accountList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of asset account(s) MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of asset account MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "totalBalance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of expense account(s) MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of expense account MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); timer.start(); for (int i = 0; i < 1000; ++i) { MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] = timer.elapsed(); } std::cerr << "totalBalance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // transaction list MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { list = MyMoneyFile::instance()->transactionList(filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // transaction list MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { MyMoneyFile::instance()->transactionList(list, filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList(list)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } MyMoneyFile::instance()->preloadCache(); } void KMyMoneyApp::slotFileNew() { KMSTATUS(i18n("Creating new document...")); slotFileClose(); if (!d->m_myMoneyView->fileOpen()) { // next line required until we move all file handling out of KMyMoneyView d->m_myMoneyView->newFile(); d->m_fileName = QUrl(); updateCaption(); NewUserWizard::Wizard *wizard = new NewUserWizard::Wizard(); if (wizard->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); try { // store the user info file->setUser(wizard->user()); // create and setup base currency file->addCurrency(wizard->baseCurrency()); file->setBaseCurrency(wizard->baseCurrency()); // create a possible institution MyMoneyInstitution inst = wizard->institution(); if (inst.name().length()) { file->addInstitution(inst); } // create a possible checking account MyMoneyAccount acc = wizard->account(); if (acc.name().length()) { acc.setInstitutionId(inst.id()); MyMoneyAccount asset = file->asset(); file->addAccount(acc, asset); // create possible opening balance transaction if (!wizard->openingBalance().isZero()) { file->createOpeningBalanceTransaction(acc, wizard->openingBalance()); } } // import the account templates QList templates = wizard->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(&progressCallback); } d->m_fileName = wizard->url(); ft.commit(); KMyMoneyGlobalSettings::setFirstTimeRun(false); // FIXME This is a bit clumsy. We re-read the freshly // created file to be able to run through all the // fixup logic and then save it to keep the modified // flag off. slotFileSave(); d->m_myMoneyView->readFile(d->m_fileName); slotFileSave(); // now keep the filename in the recent files used list //KRecentFilesAction *p = dynamic_cast(action(KStandardAction::name(KStandardAction::OpenRecent))); //if(p) d->m_recentFiles->addUrl(d->m_fileName); writeLastUsedFile(d->m_fileName.url()); } catch (const MyMoneyException &) { // next line required until we move all file handling out of KMyMoneyView d->m_myMoneyView->closeFile(); } if (wizard->startSettingsAfterFinished()) slotSettings(); } else { // next line required until we move all file handling out of KMyMoneyView d->m_myMoneyView->closeFile(); } delete wizard; updateCaption(); emit fileLoaded(d->m_fileName); } } QUrl KMyMoneyApp::selectFile(const QString& /*title*/, const QString& _path, const QString& mask, QFileDialog::FileMode mode, QWidget* widget) { Q_UNUSED(mask) Q_UNUSED(mode) QString path(_path); // if the path is not specified open the file dialog in the last used directory // 'kmymoney' is the keyword that identifies the last used directory in KFileDialog if (path.isEmpty()) path = "kfiledialog:///kmymoney-import"; // TODO: port KF5 QPointer dialog = new QFileDialog(this, QString(), path); //dialog->setMode(mode); QUrl url; if (dialog->exec() == QDialog::Accepted && dialog != 0) { // TODO: port KF5 //url = dialog->selectedUrl(); } // in case we have an additional widget, we remove it from the // dialog, so that the caller can still access it. Therefore, it is // the callers responsibility to delete the object if (widget) widget->setParent(0); delete dialog; return url; } // General open void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); QString prevDir = readLastUsedDir(); QPointer dialog = new QFileDialog(this, QString(), prevDir, i18n("KMyMoney files (*.kmy *.xml);;All files")); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { slotFileOpenRecent(dialog->selectedUrls().first()); } delete dialog; } void KMyMoneyApp::slotOpenDatabase() { KMSTATUS(i18n("Open a file.")); QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite); if (!dialog->checkDrivers()) { delete dialog; return; } if (dialog->exec() == QDialog::Accepted && dialog != 0) { slotFileOpenRecent(dialog->selectedURL()); } delete dialog; } bool KMyMoneyApp::isImportableFile(const QUrl &url) { bool result = false; // Iterate through the plugins and see if there's a loaded plugin who can handle it QMap::const_iterator it_plugin = d->m_importerPlugins.constBegin(); while (it_plugin != d->m_importerPlugins.constEnd()) { if ((*it_plugin)->isMyFormat(url.path())) { result = true; break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == d->m_importerPlugins.constEnd()) if (MyMoneyStatement::isStatementFile(url.path())) result = true; // Place code here to test for QIF and other locally-supported formats // (i.e. not a plugin). If you add them here, be sure to add it to // the webConnect function. return result; } void KMyMoneyApp::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); QUrl lastFile = d->m_fileName; // check if there are other instances which might have this file open QList list = instanceList(); QList::ConstIterator it; bool duplicate = false; #ifdef KMM_DBUS for (it = list.constBegin(); duplicate == false && it != list.constEnd(); ++it) { QDBusInterface remoteApp(*it, "/KMymoney", "org.kde.kmymoney"); QDBusReply reply = remoteApp.call("filename"); if (!reply.isValid()) { qDebug("D-Bus error while calling app->filename()"); } else { if (reply.value() == url.url()) { duplicate = true; } } } #endif if (!duplicate) { QUrl newurl = url; if ((newurl.scheme() == "sql")) { if (QUrlQuery(newurl).queryItemValue("driver") == "QMYSQL3") { // fix any old urls // TODO: port KF5 //newurl.removeQueryItem("driver"); //newurl.addQueryItem("driver", "QMYSQL"); } if (QUrlQuery(newurl).queryItemValue("driver") == "QSQLITE3") { // TODO: port KF5 //newurl.removeQueryItem("driver"); //newurl.addQueryItem("driver", "QSQLITE"); } // check if a password is needed. it may be if the URL came from the last/recent file list QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite, newurl); if (!dialog->checkDrivers()) { delete dialog; return; } // if we need to supply a password, then show the dialog // otherwise it isn't needed if ((QUrlQuery(newurl).queryItemValue("secure").toLower() == "yes") && newurl.password().isEmpty()) { if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { newurl = dialog->selectedURL(); } else { delete dialog; return; } } delete dialog; } // TODO: port KF5 if ((newurl.scheme() == "sql") || (newurl.isValid() /*&& KIO::NetAccess::exists(newurl, KIO::NetAccess::SourceSide, this)*/)) { slotFileClose(); if (!d->m_myMoneyView->fileOpen()) { try { if (d->m_myMoneyView->readFile(newurl)) { if ((d->m_myMoneyView->isNativeFile())) { d->m_fileName = newurl; updateCaption(); d->m_recentFiles->addUrl(newurl); writeLastUsedFile(newurl.toDisplayString(QUrl::PreferLocalFile)); } else { d->m_fileName = QUrl(); // imported files have no filename } // Check the schedules slotCheckSchedules(); } } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot open file as requested. Error was: %1", e.what())); } updateCaption(); emit fileLoaded(d->m_fileName); } else { /*fileOpen failed - should we do something or maybe fileOpen puts out the message... - it does for database*/ } } else { // newurl invalid slotFileClose(); KMessageBox::sorry(this, i18n("

%1 is either an invalid filename or the file does not exist. You can open another file or create a new one.

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("File not found")); } } else { // isDuplicate KMessageBox::sorry(this, i18n("

File %1 is already opened in another instance of KMyMoney

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Duplicate open")); } } bool KMyMoneyApp::slotFileSave() { // if there's nothing changed, there's no need to save anything if (!d->m_myMoneyView->dirty()) return true; bool rc = false; KMSTATUS(i18n("Saving file...")); if (d->m_fileName.isEmpty()) return slotFileSaveAs(); d->consistencyCheck(false); /*if (myMoneyView->isDatabase()) { rc = myMoneyView->saveDatabase(m_fileName); // the 'save' function is no longer relevant for a database*/ setEnabled(false); rc = d->m_myMoneyView->saveFile(d->m_fileName, MyMoneyFile::instance()->value("kmm-encryption-key")); setEnabled(true); d->m_autoSaveTimer->stop(); updateCaption(); return rc; } void KMyMoneyApp::slotFileSaveAsFilterChanged(const QString& filter) { if (!d->m_saveEncrypted) return; if (filter.compare(QLatin1String("*.kmy"), Qt::CaseInsensitive) != 0) { d->m_saveEncrypted->setCurrentItem(0); d->m_saveEncrypted->setEnabled(false); } else { d->m_saveEncrypted->setEnabled(true); } } void KMyMoneyApp::slotManageGpgKeys() { QPointer dlg = new KGpgKeySelectionDlg(this); dlg->setKeys(d->m_additionalGpgKeys); if (dlg->exec() == QDialog::Accepted && dlg != 0) { d->m_additionalGpgKeys = dlg->keys(); d->m_additionalKeyLabel->setText(i18n("Additional encryption keys to be used: %1", d->m_additionalGpgKeys.count())); } delete dlg; } void KMyMoneyApp::slotKeySelected(int idx) { int cnt = 0; if (idx != 0) { cnt = d->m_additionalGpgKeys.count(); } d->m_additionalKeyLabel->setEnabled(idx != 0); d->m_additionalKeyButton->setEnabled(idx != 0); d->m_additionalKeyLabel->setText(i18n("Additional encryption keys to be used: %1", cnt)); } bool KMyMoneyApp::slotFileSaveAs() { bool rc = false; // in event of it being a database, ensure that all data is read into storage for saveas if (d->m_myMoneyView->isDatabase()) dynamic_cast(MyMoneyFile::instance()->storage())->fillStorage(); KMSTATUS(i18n("Saving file with a new filename...")); // fill the additional key list with the default d->m_additionalGpgKeys = KMyMoneyGlobalSettings::gpgRecipientList(); QWidget* vbox = new QWidget(); QVBoxLayout *vboxVBoxLayout = new QVBoxLayout(vbox); vboxVBoxLayout->setMargin(0); if (KGPGFile::GPGAvailable()) { QWidget* keyBox = new QWidget(vbox); QHBoxLayout *keyBoxHBoxLayout = new QHBoxLayout(keyBox); keyBoxHBoxLayout->setMargin(0); vboxVBoxLayout->addWidget(keyBox); QLabel *keyLabel = new QLabel(i18n("Encryption key to be used"), keyBox); keyBoxHBoxLayout->addWidget(keyLabel); d->m_saveEncrypted = new KComboBox(keyBox); keyBoxHBoxLayout->addWidget(d->m_saveEncrypted); QWidget* labelBox = new QWidget(vbox); QHBoxLayout *labelBoxHBoxLayout = new QHBoxLayout(labelBox); labelBoxHBoxLayout->setMargin(0); vboxVBoxLayout->addWidget(labelBox); d->m_additionalKeyLabel = new QLabel(i18n("Additional encryption keys to be used: %1", d->m_additionalGpgKeys.count()), labelBox); labelBoxHBoxLayout->addWidget(d->m_additionalKeyLabel); d->m_additionalKeyButton = new QPushButton(i18n("Manage additional keys"), labelBox); labelBoxHBoxLayout->addWidget(d->m_additionalKeyButton); connect(d->m_additionalKeyButton, SIGNAL(clicked()), this, SLOT(slotManageGpgKeys())); connect(d->m_saveEncrypted, SIGNAL(activated(int)), this, SLOT(slotKeySelected(int))); // fill the secret key list and combo box QStringList keyList; KGPGFile::secretKeyList(keyList); d->m_saveEncrypted->addItem(i18n("No encryption")); for (QStringList::iterator it = keyList.begin(); it != keyList.end(); ++it) { QStringList fields = (*it).split(':', QString::SkipEmptyParts); if (fields[0] != recoveryKeyId) { // replace parenthesis in name field with brackets QString name = fields[1]; name.replace('(', "["); name.replace(')', "]"); name = QString("%1 (0x%2)").arg(name).arg(fields[0]); d->m_saveEncrypted->addItem(name); if (name.contains(KMyMoneyGlobalSettings::gpgRecipient())) { d->m_saveEncrypted->setCurrentItem(name); } } } } QString prevDir; // don't prompt file name if not a native file if (d->m_myMoneyView->isNativeFile()) prevDir = readLastUsedDir(); QPointer dlg = new QFileDialog(this, i18n("Save As"), prevDir, QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.kmy")).arg(i18nc("KMyMoney (Filefilter)", "KMyMoney files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.xml")).arg(i18nc("XML (Filefilter)", "XML files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.anon.xml")).arg(i18nc("Anonymous (Filefilter)", "Anonymous files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*")).arg(i18nc("All files (Filefilter)", "All files"))); dlg->setAcceptMode(QFileDialog::AcceptSave); connect(dlg, SIGNAL(filterChanged(QString)), this, SLOT(slotFileSaveAsFilterChanged(QString))); if (dlg->exec() == QDialog::Accepted && dlg != 0) { QUrl newURL = dlg->selectedUrls().first(); if (!newURL.fileName().isEmpty()) { d->consistencyCheck(false); // deleting the dialog will delete the combobox pointed to by d->m_saveEncrypted so get the key name here QString selectedKeyName; if (d->m_saveEncrypted && d->m_saveEncrypted->currentIndex() != 0) selectedKeyName = d->m_saveEncrypted->currentText(); d->m_saveEncrypted = 0; QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); // append extension if not present if (!newName.endsWith(QLatin1String(".kmy"), Qt::CaseInsensitive) && !newName.endsWith(QLatin1String(".xml"), Qt::CaseInsensitive)) newName.append(QLatin1String(".kmy")); newURL = QUrl::fromUserInput(newName); d->m_recentFiles->addUrl(newURL); setEnabled(false); // If this is the anonymous file export, just save it, don't actually take the // name, or remember it! Don't even try to encrypt it if (newName.endsWith(QLatin1String(".anon.xml"), Qt::CaseInsensitive)) rc = d->m_myMoneyView->saveFile(newURL); else { d->m_fileName = newURL; QString encryptionKeys; QRegExp keyExp(".* \\((.*)\\)"); if (keyExp.indexIn(selectedKeyName) != -1) { encryptionKeys = keyExp.cap(1); } if (!d->m_additionalGpgKeys.isEmpty()) { if (!encryptionKeys.isEmpty()) encryptionKeys.append(QLatin1Char(',')); encryptionKeys.append(d->m_additionalGpgKeys.join(QLatin1Char(','))); } rc = d->m_myMoneyView->saveFile(d->m_fileName, encryptionKeys); //write the directory used for this file as the default one for next time. writeLastUsedDir(newURL.toDisplayString(QUrl::RemoveFilename | QUrl::PreferLocalFile | QUrl::StripTrailingSlash)); writeLastUsedFile(newName); } d->m_autoSaveTimer->stop(); setEnabled(true); } } delete dlg; updateCaption(); return rc; } bool KMyMoneyApp::slotSaveAsDatabase() { bool rc = false; QUrl oldUrl; // in event of it being a database, ensure that all data is read into storage for saveas if (d->m_myMoneyView->isDatabase()) { dynamic_cast(MyMoneyFile::instance()->storage())->fillStorage(); oldUrl = d->m_fileName.isEmpty() ? lastOpenedURL() : d->m_fileName; } KMSTATUS(i18n("Saving file to database...")); QPointer dialog = new KSelectDatabaseDlg(QIODevice::WriteOnly); QUrl url = oldUrl; if (!dialog->checkDrivers()) { delete dialog; return (false); } while (oldUrl == url && dialog->exec() == QDialog::Accepted && dialog != 0) { url = dialog->selectedURL(); // If the protocol is SQL for the old and new, and the hostname and database names match // Let the user know that the current database cannot be saved on top of itself. if (url.scheme() == "sql" && oldUrl.scheme() == "sql" && oldUrl.host() == url.host() && QUrlQuery(oldUrl).queryItemValue("driver") == QUrlQuery(url).queryItemValue("driver") && oldUrl.path().right(oldUrl.path().length() - 1) == url.path().right(url.path().length() - 1)) { KMessageBox::sorry(this, i18n("Cannot save to current database.")); } else { try { rc = d->m_myMoneyView->saveAsDatabase(url); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot save to current database: %1", e.what())); } } } delete dialog; if (rc) { //KRecentFilesAction *p = dynamic_cast(action("file_open_recent")); //if(p) d->m_recentFiles->addUrl(url); writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); } d->m_autoSaveTimer->stop(); updateCaption(); return rc; } void KMyMoneyApp::slotFileCloseWindow() { KMSTATUS(i18n("Closing window...")); if (d->m_myMoneyView->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } close(); } void KMyMoneyApp::slotFileClose() { bool okToSelect = true; // check if transaction editor is open and ask user what he wants to do slotTransactionsCancelOrEnter(okToSelect); if (!okToSelect) return; // no update status here, as we might delete the status too early. if (d->m_myMoneyView->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } d->closeFile(); } void KMyMoneyApp::slotFileQuit() { // don't modify the status message here as this will prevent quit from working!! // See the beginning of queryClose() and isReady() why. Thomas Baumgart 2005-10-17 bool quitApplication = true; QList memberList = KMainWindow::memberList(); if (!memberList.isEmpty()) { QList::const_iterator w_it = memberList.constBegin(); for (; w_it != memberList.constEnd(); ++w_it) { // only close the window if the closeEvent is accepted. If the user presses Cancel on the saveModified() dialog, // the window and the application stay open. if (!(*w_it)->close()) { quitApplication = false; break; } } } // We will only quit if all windows were processed and not cancelled if (quitApplication) QCoreApplication::quit(); } void KMyMoneyApp::slotHideReconciledTransactions() { KMyMoneyGlobalSettings::setHideReconciledTransactions(toggleAction("view_hide_reconciled_transactions")->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotHideUnusedCategories() { KMyMoneyGlobalSettings::setHideUnusedCategory(toggleAction("view_hide_unused_categories")->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotShowAllAccounts() { d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotToggleTraces() { MyMoneyTracer::onOff(toggleAction("debug_traces")->isChecked() ? 1 : 0); } void KMyMoneyApp::slotToggleTimers() { extern bool timersOn; // main.cpp timersOn = toggleAction("debug_timers")->isChecked(); } QString KMyMoneyApp::slotStatusMsg(const QString &text) { /////////////////////////////////////////////////////////////////// // change status message permanently QString previousMessage = d->m_statusLabel->text(); d->m_applicationIsReady = false; QString currentMessage = text; if (currentMessage.isEmpty() || currentMessage == i18nc("Application is ready to use", "Ready.")) { d->m_applicationIsReady = true; currentMessage = i18nc("Application is ready to use", "Ready."); } statusBar()->clearMessage(); d->m_statusLabel->setText(currentMessage); return previousMessage; } void KMyMoneyApp::ready() { slotStatusMsg(QString()); } bool KMyMoneyApp::isReady() { return d->m_applicationIsReady; } void KMyMoneyApp::slotStatusProgressBar(int current, int total) { if (total == -1 && current == -1) { // reset if (d->m_progressTimer) { d->m_progressTimer->start(500); // remove from screen in 500 msec d->m_progressBar->setValue(d->m_progressBar->maximum()); } } else if (total != 0) { // init d->m_progressTimer->stop(); d->m_progressBar->setMaximum(total); d->m_progressBar->setValue(0); d->m_progressBar->show(); } else { // update QTime currentTime = QTime::currentTime(); // only process painting if last update is at least 250 ms ago if (abs(d->m_lastUpdate.msecsTo(currentTime)) > 250) { d->m_progressBar->setValue(current); d->m_lastUpdate = currentTime; } } } void KMyMoneyApp::slotStatusProgressDone() { d->m_progressTimer->stop(); d->m_progressBar->reset(); d->m_progressBar->hide(); d->m_progressBar->setValue(0); } void KMyMoneyApp::progressCallback(int current, int total, const QString& msg) { if (!msg.isEmpty()) kmymoney->slotStatusMsg(msg); kmymoney->slotStatusProgressBar(current, total); } void KMyMoneyApp::slotFileViewPersonal() { if (!d->m_myMoneyView->fileOpen()) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } KMSTATUS(i18n("Viewing personal data...")); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPayee user = file->user(); QPointer editPersonalDataDlg = new EditPersonalDataDlg(user.name(), user.address(), user.city(), user.state(), user.postcode(), user.telephone(), user.email(), this, i18n("Edit Personal Data")); if (editPersonalDataDlg->exec() == QDialog::Accepted && editPersonalDataDlg != 0) { user.setName(editPersonalDataDlg->userNameText); user.setAddress(editPersonalDataDlg->userStreetText); user.setCity(editPersonalDataDlg->userTownText); user.setState(editPersonalDataDlg->userCountyText); user.setPostcode(editPersonalDataDlg->userPostcodeText); user.setTelephone(editPersonalDataDlg->userTelephoneText); user.setEmail(editPersonalDataDlg->userEmailText); MyMoneyFileTransaction ft; try { file->setUser(user); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to store user information: %1", e.what())); } } delete editPersonalDataDlg; } void KMyMoneyApp::slotFileFileInfo() { if (!d->m_myMoneyView->fileOpen()) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } QFile g("kmymoney.dump"); g.open(QIODevice::WriteOnly); QDataStream st(&g); MyMoneyStorageDump dumper; dumper.writeStream(st, dynamic_cast(MyMoneyFile::instance()->storage())); g.close(); } void KMyMoneyApp::slotLoadAccountTemplates() { KMSTATUS(i18n("Importing account templates.")); int rc; QPointer dlg = new KLoadTemplateDlg(); if ((rc = dlg->exec()) == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { // import the account templates QList templates = dlg->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(&progressCallback); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to import template(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } delete dlg; } void KMyMoneyApp::slotSaveAccountTemplates() { KMSTATUS(i18n("Exporting account templates.")); QString savePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/templates/" + QLocale().name(); QDir d(savePath); if (!d.exists()) d.mkpath(savePath); QString newName = QFileDialog::getSaveFileName(this, i18n("Save as..."), savePath, i18n("KMyMoney template files (*.kmt);;All files (*)")); // // If there is no file extension, then append a .kmt at the end of the file name. // If there is a file extension, make sure it is .kmt, delete any others. // if (!newName.isEmpty()) { // find last . delimiter int nLoc = newName.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = newName.left(nLoc + 1); strExt = newName.right(newName.length() - (nLoc + 1)); if ((strExt.indexOf("kmt", 0, Qt::CaseInsensitive) == -1)) { strTemp.append("kmt"); //append to make complete file name newName = strTemp; } } else { newName.append(".kmt"); } if (okToWriteFile(QUrl::fromLocalFile(newName))) { - MyMoneyTemplate templ; - templ.exportTemplate(&progressCallback); - templ.saveTemplate(QUrl::fromLocalFile(newName)); + QPointer dlg = new KTemplateExportDlg(this); + if (dlg->exec() == QDialog::Accepted && dlg) { + MyMoneyTemplate templ; + templ.setTitle(dlg->title()); + templ.setShortDescription(dlg->shortDescription()); + templ.setLongDescription(dlg->longDescription()); + templ.exportTemplate(&progressCallback); + templ.saveTemplate(QUrl::fromLocalFile(newName)); + } + delete dlg; } } } void KMyMoneyApp::slotQifImport() { if (d->m_qifReader == 0) { // FIXME: the menu entry for qif import should be disabled here QPointer dlg = new KImportDlg(0); if (dlg->exec() == QDialog::Accepted && dlg != 0) { KMSTATUS(i18n("Importing file...")); d->m_qifReader = new MyMoneyQifReader; // remove all kmm-statement-#.txt files d->unlinkStatementXML(); connect(d->m_qifReader, SIGNAL(importFinished()), this, SLOT(slotQifImportFinished())); d->m_qifReader->setURL(dlg->file()); d->m_qifReader->setProfile(dlg->profile()); d->m_qifReader->setCategoryMapping(dlg->m_typeComboBox->currentIndex() == 0); d->m_qifReader->setProgressCallback(&progressCallback); // disable all standard widgets during the import setEnabled(false); d->m_ft = new MyMoneyFileTransaction(); d->m_collectingStatements = true; d->m_statementResults.clear(); if (!d->m_qifReader->startImport()) { // if the import failed to start make sure that slotQifImportFinished is called otherwise the application will be left disabled QTimer::singleShot(0, this, SLOT(slotQifImportFinished())); } } delete dlg; slotUpdateActions(); } } void KMyMoneyApp::slotQifImportFinished() { if (d->m_qifReader != 0) { d->m_qifReader->finishImport(); d->m_ft->commit(); d->m_collectingStatements = false; KMessageBox::informationList(this, i18n("The statements have been processed with the following results:"), d->m_statementResults, i18n("Statement stats")); #if 0 // fixme: re-enable the QIF import menu options if (d->m_qifReader->finishImport()) { if (verifyImportedData(d->m_qifReader->account())) { // keep the new data set, destroy the backup copy delete d->m_engineBackup; d->m_engineBackup = 0; } } if (d->m_engineBackup != 0) { // user cancelled, destroy the updated set and keep the backup copy IMyMoneyStorage* data = file->storage(); if (data != 0) { file->detachStorage(data); delete data; } file->attachStorage(d->m_engineBackup); d->m_engineBackup = 0; } #endif // update the views as they might still contain invalid data // from the import session. The same applies for the window caption d->m_myMoneyView->slotRefreshViews(); updateCaption(); delete d->m_qifReader; d->m_qifReader = 0; } delete d->m_ft; d->m_ft = 0; slotStatusProgressBar(-1, -1); ready(); // re-enable all standard widgets setEnabled(true); slotUpdateActions(); } void KMyMoneyApp::slotGncImport() { if (d->m_myMoneyView->fileOpen()) { switch (KMessageBox::questionYesNoCancel(0, i18n("You cannot import GnuCash data into an existing file. Do you wish to save this file?"), PACKAGE)) { case KMessageBox::Yes: slotFileSave(); break; case KMessageBox::No: d->closeFile(); break; default: return; } } KMSTATUS(i18n("Importing a GnuCash file.")); QUrl fileToRead = QFileDialog::getOpenFileUrl(this, QString(), QUrl(), i18n("GnuCash files (*.gnucash *.xac *.gnc);;All files (*)")); if (!fileToRead.isEmpty()) { // call the importer d->m_myMoneyView->readFile(fileToRead); // imported files don't have a name d->m_fileName = QUrl(); updateCaption(); emit fileLoaded(d->m_fileName); } } void KMyMoneyApp::slotAccountChart() { if (!d->m_selectedAccount.id().isEmpty()) { QPointer dlg = new KBalanceChartDlg(d->m_selectedAccount, this); dlg->exec(); delete dlg; } } // // KMyMoneyApp::slotStatementImport() is for testing only. The MyMoneyStatement // is not intended to be exposed to users in XML form. // void KMyMoneyApp::slotStatementImport() { bool result = false; KMSTATUS(i18n("Importing an XML Statement.")); QList files{QFileDialog::getOpenFileUrls(this, QString(), QUrl(), i18n("XML files (*.xml);;All files (*)"))}; if (!files.isEmpty()) { d->m_collectingStatements = (files.count() > 1); foreach (const QUrl &url, files) { qDebug("Processing '%s'", qPrintable(url.path())); result |= slotStatementImport(url.path()); } /* QFile f( dialog->selectedURL().path() ); f.open( QIODevice::ReadOnly ); QString error = "Unable to parse file"; QDomDocument* doc = new QDomDocument; if(doc->setContent(&f, FALSE)) { if ( doc->doctype().name() == "KMYMONEY-STATEMENT" ) { QDomElement rootElement = doc->documentElement(); if(!rootElement.isNull()) { QDomNode child = rootElement.firstChild(); if(!child.isNull() && child.isElement()) { MyMoneyStatement s; if ( s.read(child.toElement()) ) result = slotStatementImport(s); else error = "File does not contain any statements"; } } } else error = "File is not a KMyMoney Statement"; } delete doc; if ( !result ) { QMessageBox::critical( this, i18n("Critical Error"), i18n("Unable to read file %1: %2").arg( dialog->selectedURL().path()).arg(error), QMessageBox::Ok, 0 ); }*/ } if (!result) { // re-enable all standard widgets setEnabled(true); } } bool KMyMoneyApp::slotStatementImport(const QString& url) { bool result = false; MyMoneyStatement s; if (MyMoneyStatement::readXMLFile(s, url)) result = slotStatementImport(s); else KMessageBox::error(this, i18n("Error importing %1: This file is not a valid KMM statement file.", url), i18n("Invalid Statement")); return result; } bool KMyMoneyApp::slotStatementImport(const MyMoneyStatement& s, bool silent) { bool result = false; // keep a copy of the statement - MyMoneyStatement::writeXMLFile(s, QString("/home/thb/kmm-statement-%1.txt").arg(d->m_statementXMLindex++)); + if (KMyMoneySettings::logImportedStatements()) { + QString logFile = QString("%1/kmm-statement-%2.txt").arg(KMyMoneySettings::logPath()).arg(d->m_statementXMLindex++); + MyMoneyStatement::writeXMLFile(s, logFile); + } // we use an object on the heap here, so that we can check the presence // of it during slotUpdateActions() by looking at the pointer. d->m_smtReader = new MyMoneyStatementReader; d->m_smtReader->setAutoCreatePayee(true); d->m_smtReader->setProgressCallback(&progressCallback); // disable all standard widgets during the import setEnabled(false); QStringList messages; result = d->m_smtReader->import(s, messages); bool transactionAdded = d->m_smtReader->anyTransactionAdded(); // get rid of the statement reader and tell everyone else // about the destruction by setting the pointer to zero delete d->m_smtReader; d->m_smtReader = 0; slotStatusProgressBar(-1, -1); ready(); // re-enable all standard widgets setEnabled(true); if (!d->m_collectingStatements && !silent) KMessageBox::informationList(this, i18n("The statement has been processed with the following results:"), messages, i18n("Statement stats")); else if (transactionAdded) d->m_statementResults += messages; slotUpdateActions();// Re-enable menu items after import via plugin. return result; } void KMyMoneyApp::slotQifExport() { KMSTATUS(i18n("Exporting file...")); QPointer dlg = new KExportDlg(0); if (dlg->exec() == QDialog::Accepted && dlg != 0) { if (okToWriteFile(QUrl::fromLocalFile(dlg->filename()))) { MyMoneyQifWriter writer; connect(&writer, SIGNAL(signalProgress(int,int)), this, SLOT(slotStatusProgressBar(int,int))); writer.write(dlg->filename(), dlg->profile(), dlg->accountId(), dlg->accountSelected(), dlg->categorySelected(), dlg->startDate(), dlg->endDate()); } } delete dlg; } bool KMyMoneyApp::okToWriteFile(const QUrl &url) { Q_UNUSED(url) // check if the file exists and warn the user bool reallySaveFile = true; // TODO: port KF5 //if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, this)) { // if (KMessageBox::warningYesNo(this, QLatin1String("") + i18n("The file %1 already exists. Do you really want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)) + QLatin1String(""), i18n("File already exists")) != KMessageBox::Yes) // reallySaveFile = false; //} return reallySaveFile; } void KMyMoneyApp::slotSettings() { // if we already have an instance of the settings dialog, then use it if (KConfigDialog::showDialog("KMyMoney-Settings")) return; // otherwise, we have to create it KConfigDialog* dlg = new KSettingsKMyMoney(this, "KMyMoney-Settings", KMyMoneyGlobalSettings::self()); connect(dlg, &KConfigDialog::settingsChanged, this, &KMyMoneyApp::slotUpdateConfiguration); dlg->show(); } void KMyMoneyApp::slotUpdateConfiguration() { MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneyGlobalSettings::firstFiscalMonth(), KMyMoneyGlobalSettings::firstFiscalDay()); d->m_myMoneyView->updateViewType(); // update the sql storage module settings MyMoneyStorageSql::setStartDate(KMyMoneyGlobalSettings::startDate().date()); // update the report module settings MyMoneyReport::setLineWidth(KMyMoneyGlobalSettings::lineWidth()); // update the holiday region configuration setHolidayRegion(KMyMoneyGlobalSettings::holidayRegion()); d->m_myMoneyView->slotRefreshViews(); // re-read autosave configuration d->m_autoSaveEnabled = KMyMoneyGlobalSettings::autoSaveFile(); d->m_autoSavePeriod = KMyMoneyGlobalSettings::autoSavePeriod(); // stop timer if turned off but running if (d->m_autoSaveTimer->isActive() && !d->m_autoSaveEnabled) { d->m_autoSaveTimer->stop(); } // start timer if turned on and needed but not running if (!d->m_autoSaveTimer->isActive() && d->m_autoSaveEnabled && d->m_myMoneyView->dirty()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } d->setCustomColors(); // check if the recovery key is still valid or expires soon if (KMyMoneySettings::writeDataEncrypted() && KMyMoneySettings::encryptRecover()) { if (KGPGFile::GPGAvailable()) { KGPGFile file; QDateTime expirationDate = file.keyExpires(QLatin1String(recoveryKeyId)); if (expirationDate.isValid() && QDateTime::currentDateTime().daysTo(expirationDate) <= RECOVER_KEY_EXPIRATION_WARNING) { bool skipMessage = false; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp; QDate lastWarned; if (kconfig) { grp = d->m_config->group("General Options"); lastWarned = grp.readEntry("LastRecoverKeyExpirationWarning", QDate()); if (QDate::currentDate() == lastWarned) { skipMessage = true; } } if (!skipMessage) { if (kconfig) { grp.writeEntry("LastRecoverKeyExpirationWarning", QDate::currentDate()); } KMessageBox::information(this, i18np("You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 day. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", "You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 days. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", QDateTime::currentDateTime().daysTo(expirationDate)), i18n("Recover key expires soon")); } } } } } /** No descriptions */ void KMyMoneyApp::slotFileBackup() { // Save the file first so isLocalFile() works if (d->m_myMoneyView && d->m_myMoneyView->dirty()) { if (KMessageBox::questionYesNo(this, i18n("The file must be saved first " "before it can be backed up. Do you want to continue?")) == KMessageBox::No) { return; } slotFileSave(); } if (d->m_fileName.isEmpty()) return; if (!d->m_fileName.isLocalFile()) { KMessageBox::sorry(this, i18n("The current implementation of the backup functionality only supports local files as source files. Your current source file is '%1'.", d->m_fileName.url()), i18n("Local files only")); return; } // TODO: port KF5 #if 0 QPointer backupDlg = new KBackupDlg(this); int returncode = backupDlg->exec(); if (returncode == QDialog::Accepted && backupDlg != 0) { d->m_backupMount = backupDlg->mountCheckBox->isChecked(); d->m_proc.clearProgram(); d->m_backupState = BACKUP_MOUNTING; d->m_mountpoint = backupDlg->txtMountPoint->text(); if (d->m_backupMount) { progressCallback(0, 300, i18n("Mounting %1", d->m_mountpoint)); d->m_proc.setProgram("mount"); d->m_proc << d->m_mountpoint; d->m_proc.start(); } else { // If we don't have to mount a device, we just issue // a dummy command to start the copy operation progressCallback(0, 300, ""); d->m_proc.setProgram("true"); d->m_proc.start(); } } delete backupDlg; #endif } /** No descriptions */ void KMyMoneyApp::slotProcessExited() { // TODO: port KF5 #if 0 switch (d->m_backupState) { case BACKUP_MOUNTING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { d->m_proc.clearProgram(); QString today; today.sprintf("-%04d-%02d-%02d.kmy", QDate::currentDate().year(), QDate::currentDate().month(), QDate::currentDate().day()); QString backupfile = d->m_mountpoint + '/' + d->m_fileName.fileName(); KMyMoneyUtils::appendCorrectFileExt(backupfile, today); // check if file already exists and ask what to do d->m_backupResult = 0; QFile f(backupfile); if (f.exists()) { int answer = KMessageBox::warningContinueCancel(this, i18n("Backup file for today exists on that device. Replace?"), i18n("Backup"), KGuiItem(i18n("&Replace"))); if (answer == KMessageBox::Cancel) { d->m_backupResult = 1; if (d->m_backupMount) { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } else { d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } } } if (d->m_backupResult == 0) { progressCallback(50, 0, i18n("Writing %1", backupfile)); //FIXME: FIX on windows d->m_proc << "cp" << "-f" << d->m_fileName.path() << backupfile; d->m_backupState = BACKUP_COPYING; d->m_proc.start(); } } else { KMessageBox::information(this, i18n("Error mounting device"), i18n("Backup")); d->m_backupResult = 1; if (d->m_backupMount) { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } else { d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } } break; case BACKUP_COPYING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { if (d->m_backupMount) { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } else { progressCallback(300, 0, i18nc("Backup done", "Done")); KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } } else { qDebug("cp exit code is %d", d->m_proc.exitCode()); d->m_backupResult = 1; KMessageBox::information(this, i18n("Error copying file to device"), i18n("Backup")); if (d->m_backupMount) { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } else { d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } } break; case BACKUP_UNMOUNTING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { progressCallback(300, 0, i18nc("Backup done", "Done")); if (d->m_backupResult == 0) KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); } else { KMessageBox::information(this, i18n("Error unmounting device"), i18n("Backup")); } d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); break; default: qWarning("Unknown state for backup operation!"); progressCallback(-1, -1, QString()); ready(); break; } #endif } void KMyMoneyApp::slotShowTipOfTheDay() { KTipDialog::showTip(d->m_myMoneyView, "", true); } void KMyMoneyApp::slotShowPreviousView() { } void KMyMoneyApp::slotShowNextView() { } void KMyMoneyApp::slotGenerateSql() { QPointer editor = new KGenerateSqlDlg(this); editor->setObjectName("Generate Database SQL"); editor->exec(); delete editor; } void KMyMoneyApp::slotQifProfileEditor() { QPointer editor = new MyMoneyQifProfileEditor(true, this); editor->setObjectName("QIF Profile Editor"); editor->exec(); delete editor; } void KMyMoneyApp::slotToolsStartKCalc() { QString cmd = KMyMoneyGlobalSettings::externalCalculator(); // if none is present, we fall back to the default if (cmd.isEmpty()) { #if defined(Q_OS_WIN32) cmd = QLatin1String("calc"); #elif defined(Q_OS_MAC) cmd = QLatin1String("open -a Calculator"); #else cmd = QLatin1String("kcalc"); #endif } KRun::runCommand(cmd, this); } void KMyMoneyApp::slotFindTransaction() { if (d->m_searchDlg == 0) { d->m_searchDlg = new KFindTransactionDlg(); connect(d->m_searchDlg, SIGNAL(destroyed()), this, SLOT(slotCloseSearchDialog())); connect(d->m_searchDlg, SIGNAL(transactionSelected(QString,QString)), d->m_myMoneyView, SLOT(slotLedgerSelected(QString,QString))); } d->m_searchDlg->show(); d->m_searchDlg->raise(); d->m_searchDlg->activateWindow(); } void KMyMoneyApp::slotCloseSearchDialog() { if (d->m_searchDlg) d->m_searchDlg->deleteLater(); d->m_searchDlg = 0; } void KMyMoneyApp::createInstitution(MyMoneyInstitution& institution) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { file->addInstitution(institution); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Cannot add institution: %1", e.what())); } } void KMyMoneyApp::slotInstitutionNew() { MyMoneyInstitution institution; slotInstitutionNew(institution); } void KMyMoneyApp::slotInstitutionNew(MyMoneyInstitution& institution) { institution.clearId(); QPointer dlg = new KNewBankDlg(institution); if (dlg->exec() == QDialog::Accepted && dlg != 0) { institution = dlg->institution(); createInstitution(institution); } delete dlg; } void KMyMoneyApp::slotInstitutionEdit(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyInstitution)) return; // make sure the selected object has an id if (d->m_selectedInstitution.id().isEmpty()) return; try { MyMoneyFile* file = MyMoneyFile::instance(); //grab a pointer to the view, regardless of it being a account or institution view. MyMoneyInstitution institution = file->institution(d->m_selectedInstitution.id()); // bankSuccess is not checked anymore because d->m_file->institution will throw anyway QPointer dlg = new KNewBankDlg(institution); if (dlg->exec() == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { file->modifyInstitution(dlg->institution()); ft.commit(); slotSelectInstitution(file->institution(dlg->institution().id())); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to store institution: %1", e.what())); } } delete dlg; } catch (const MyMoneyException &e) { if (!obj.id().isEmpty()) KMessageBox::information(this, i18n("Unable to edit institution: %1", e.what())); } } void KMyMoneyApp::slotInstitutionDelete() { MyMoneyFile *file = MyMoneyFile::instance(); try { MyMoneyInstitution institution = file->institution(d->m_selectedInstitution.id()); if ((KMessageBox::questionYesNo(this, i18n("

Do you really want to delete the institution %1?

", institution.name()))) == KMessageBox::No) return; MyMoneyFileTransaction ft; try { file->removeInstitution(institution); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to delete institution: %1", e.what())); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to delete institution: %1", e.what())); } } const MyMoneyAccount& KMyMoneyApp::findAccount(const MyMoneyAccount& acc, const MyMoneyAccount& parent) const { static MyMoneyAccount nullAccount; MyMoneyFile* file = MyMoneyFile::instance(); QList parents; try { // search by id if (!acc.id().isEmpty()) { return file->account(acc.id()); } // collect the parents. in case parent does not have an id, we scan the all top-level accounts if (parent.id().isEmpty()) { parents << file->asset(); parents << file->liability(); parents << file->income(); parents << file->expense(); parents << file->equity(); } else { parents << parent; } QList::const_iterator it_p; for (it_p = parents.constBegin(); it_p != parents.constEnd(); ++it_p) { MyMoneyAccount parentAccount = *it_p; // search by name (allow hierarchy) int pos; // check for ':' in the name and use it as separator for a hierarchy QString name = acc.name(); while ((pos = name.indexOf(MyMoneyFile::AccountSeperator)) != -1) { QString part = name.left(pos); QString remainder = name.mid(pos + 1); const MyMoneyAccount& existingAccount = file->subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { return existingAccount; } parentAccount = existingAccount; name = remainder; } const MyMoneyAccount& existingAccount = file->subAccountByName(parentAccount, name); if (!existingAccount.id().isEmpty()) { if (acc.accountType() != MyMoneyAccount::UnknownAccountType) { if (acc.accountType() != existingAccount.accountType()) continue; } return existingAccount; } } } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Unable to find account: %1", e.what())); } return nullAccount; } void KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { MyMoneyFile* file = MyMoneyFile::instance(); // make sure we have a currency. If none is assigned, we assume base currency if (newAccount.currencyId().isEmpty()) newAccount.setCurrencyId(file->baseCurrency().id()); MyMoneyFileTransaction ft; try { int pos; // check for ':' in the name and use it as separator for a hierarchy while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeperator)) != -1) { QString part = newAccount.name().left(pos); QString remainder = newAccount.name().mid(pos + 1); const MyMoneyAccount& existingAccount = file->subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { newAccount.setName(part); file->addAccount(newAccount, parentAccount); parentAccount = newAccount; } else { parentAccount = existingAccount; } newAccount.setParentAccountId(QString()); // make sure, there's no parent newAccount.clearId(); // and no id set for adding newAccount.removeAccountIds(); // and no sub-account ids newAccount.setName(remainder); } const MyMoneySecurity& sec = file->security(newAccount.currencyId()); // Check the opening balance if (openingBal.isPositive() && newAccount.accountGroup() == MyMoneyAccount::Liability) { QString message = i18n("This account is a liability and if the " "opening balance represents money owed, then it should be negative. " "Negate the amount?\n\n" "Please click Yes to change the opening balance to %1,\n" "Please click No to leave the amount as %2,\n" "Please click Cancel to abort the account creation." , MyMoneyUtils::formatMoney(-openingBal, newAccount, sec) , MyMoneyUtils::formatMoney(openingBal, newAccount, sec)); int ans = KMessageBox::questionYesNoCancel(this, message); if (ans == KMessageBox::Yes) { openingBal = -openingBal; } else if (ans == KMessageBox::Cancel) return; } file->addAccount(newAccount, parentAccount); // in case of a loan account, we add the initial payment if ((newAccount.accountType() == MyMoneyAccount::Loan || newAccount.accountType() == MyMoneyAccount::AssetLoan) && !newAccount.value("kmm-loan-payment-acc").isEmpty() && !newAccount.value("kmm-loan-payment-date").isEmpty()) { MyMoneyAccountLoan acc(newAccount); MyMoneyTransaction t; MyMoneySplit a, b; a.setAccountId(acc.id()); b.setAccountId(acc.value("kmm-loan-payment-acc").toLatin1()); a.setValue(acc.loanAmount()); if (acc.accountType() == MyMoneyAccount::Loan) a.setValue(-a.value()); a.setShares(a.value()); b.setValue(-a.value()); b.setShares(b.value()); a.setMemo(i18n("Loan payout")); b.setMemo(i18n("Loan payout")); t.setPostDate(QDate::fromString(acc.value("kmm-loan-payment-date"), Qt::ISODate)); newAccount.deletePair("kmm-loan-payment-acc"); newAccount.deletePair("kmm-loan-payment-date"); MyMoneyFile::instance()->modifyAccount(newAccount); t.addSplit(a); t.addSplit(b); file->addTransaction(t); file->createOpeningBalanceTransaction(newAccount, openingBal); // in case of an investment account we check if we should create // a brokerage account } else if (newAccount.accountType() == MyMoneyAccount::Investment && !brokerageAccount.name().isEmpty()) { file->addAccount(brokerageAccount, parentAccount); // set a link from the investment account to the brokerage account file->modifyAccount(newAccount); file->createOpeningBalanceTransaction(brokerageAccount, openingBal); } else file->createOpeningBalanceTransaction(newAccount, openingBal); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add account: %1", e.what())); } } void KMyMoneyApp::slotCategoryNew(const QString& name, QString& id) { MyMoneyAccount account; account.setName(name); slotCategoryNew(account, MyMoneyFile::instance()->expense()); id = account.id(); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { if (KMessageBox::questionYesNo(this, QString("%1").arg(i18n("

The category %1 currently does not exist. Do you want to create it?

The parent account will default to %2 but can be changed in the following dialog.

", account.name(), parent.name())), i18n("Create category"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "CreateNewCategories") == KMessageBox::Yes) { createCategory(account, parent); } else { // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("CreateNewCategories")); } } } void KMyMoneyApp::slotCategoryNew() { MyMoneyAccount parent; MyMoneyAccount account; // Preselect the parent account by looking at the current selected account/category if (!d->m_selectedAccount.id().isEmpty() && d->m_selectedAccount.isIncomeExpense()) { MyMoneyFile* file = MyMoneyFile::instance(); try { parent = file->account(d->m_selectedAccount.id()); } catch (const MyMoneyException &) { } } createCategory(account, parent); } void KMyMoneyApp::createCategory(MyMoneyAccount& account, const MyMoneyAccount& parent) { if (!parent.id().isEmpty()) { try { // make sure parent account exists MyMoneyFile::instance()->account(parent.id()); account.setParentAccountId(parent.id()); account.setAccountType(parent.accountType()); } catch (const MyMoneyException &) { } } QPointer dialog = new KNewAccountDlg(account, false, true, 0, i18n("Create a new Category")); dialog->setOpeningBalanceShown(false); dialog->setOpeningDateShown(false); if (dialog->exec() == QDialog::Accepted && dialog != 0) { MyMoneyAccount parentAccount, brokerageAccount; account = dialog->account(); parentAccount = dialog->parentAccount(); createAccount(account, parentAccount, brokerageAccount, MyMoneyMoney()); } delete dialog; } void KMyMoneyApp::slotAccountNew() { MyMoneyAccount acc; acc.setInstitutionId(d->m_selectedInstitution.id()); acc.setOpeningDate(KMyMoneyGlobalSettings::firstFiscalDate()); slotAccountNew(acc); } void KMyMoneyApp::slotAccountNew(MyMoneyAccount& account) { NewAccountWizard::Wizard* wizard = new NewAccountWizard::Wizard(); connect(wizard, SIGNAL(createInstitution(MyMoneyInstitution&)), this, SLOT(slotInstitutionNew(MyMoneyInstitution&))); connect(wizard, SIGNAL(createAccount(MyMoneyAccount&)), this, SLOT(slotAccountNew(MyMoneyAccount&))); connect(wizard, SIGNAL(createPayee(QString,QString&)), this, SLOT(slotPayeeNew(QString,QString&))); connect(wizard, SIGNAL(createCategory(MyMoneyAccount&,MyMoneyAccount)), this, SLOT(slotCategoryNew(MyMoneyAccount&,MyMoneyAccount))); wizard->setAccount(account); if (wizard->exec() == QDialog::Accepted) { MyMoneyAccount acc = wizard->account(); if (!(acc == MyMoneyAccount())) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); try { // create the account MyMoneyAccount parent = wizard->parentAccount(); file->addAccount(acc, parent); // tell the wizard about the account id which it // needs to create a possible schedule and transactions wizard->setAccount(acc); // store a possible conversion rate for the currency if (acc.currencyId() != file->baseCurrency().id()) { file->addPrice(wizard->conversionRate()); } // create the opening balance transaction if any file->createOpeningBalanceTransaction(acc, wizard->openingBalance()); // create the payout transaction for loans if any MyMoneyTransaction payoutTransaction = wizard->payoutTransaction(); if (payoutTransaction.splits().count() > 0) { file->addTransaction(payoutTransaction); } // create a brokerage account if selected MyMoneyAccount brokerageAccount = wizard->brokerageAccount(); if (!(brokerageAccount == MyMoneyAccount())) { file->addAccount(brokerageAccount, parent); } // create a possible schedule MyMoneySchedule sch = wizard->schedule(); if (!(sch == MyMoneySchedule())) { MyMoneyFile::instance()->addSchedule(sch); if (acc.isLoan()) { MyMoneyAccountLoan accLoan = MyMoneyFile::instance()->account(acc.id()); accLoan.setSchedule(sch.id()); acc = accLoan; MyMoneyFile::instance()->modifyAccount(acc); } } ft.commit(); account = acc; } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to create account: %1", e.what())); } } } delete wizard; } void KMyMoneyApp::slotInvestmentNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { QString dontShowAgain = "CreateNewInvestments"; if (KMessageBox::questionYesNo(this, QString("") + i18n("The security %1 currently does not exist as sub-account of %2. " "Do you want to create it?", account.name(), parent.name()) + QString(""), i18n("Create security"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontShowAgain) == KMessageBox::Yes) { KNewInvestmentWizard dlg; dlg.setName(account.name()); if (dlg.exec() == QDialog::Accepted) { dlg.createObjects(parent.id()); account = dlg.account(); } } else { // in case the user said no but turned on the don't show again selection, we will enable // the message no matter what. Otherwise, the user is not able to use this feature // in the future anymore. KMessageBox::enableMessage(dontShowAgain); } } void KMyMoneyApp::slotInvestmentNew() { KNewInvestmentWizard dlg; if (dlg.exec() == QDialog::Accepted) { dlg.createObjects(d->m_selectedAccount.id()); } } void KMyMoneyApp::slotInvestmentEdit() { KNewInvestmentWizard dlg(d->m_selectedInvestment); if (dlg.exec() == QDialog::Accepted) { dlg.createObjects(d->m_selectedAccount.id()); } } void KMyMoneyApp::slotInvestmentDelete() { if (KMessageBox::questionYesNo(this, i18n("

Do you really want to delete the investment %1?

", d->m_selectedInvestment.name()), i18n("Delete investment"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "DeleteInvestment") == KMessageBox::Yes) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { file->removeAccount(d->m_selectedInvestment); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to delete investment: %1", e.what())); } } else { // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("DeleteInvestment")); } } } void KMyMoneyApp::slotOnlinePriceUpdate() { if (!d->m_selectedInvestment.id().isEmpty()) { QPointer dlg = new KEquityPriceUpdateDlg(0, d->m_selectedInvestment.currencyId()); if (dlg->exec() == QDialog::Accepted && dlg != 0) { dlg->storePrices(); } delete dlg; } } void KMyMoneyApp::slotManualPriceUpdate() { if (!d->m_selectedInvestment.id().isEmpty()) { try { MyMoneySecurity security = MyMoneyFile::instance()->security(d->m_selectedInvestment.currencyId()); MyMoneySecurity currency = MyMoneyFile::instance()->security(security.tradingCurrency()); const MyMoneyPrice &price = MyMoneyFile::instance()->price(security.id(), currency.id()); QPointer calc = new KCurrencyCalculator(security, currency, MyMoneyMoney::ONE, price.rate(currency.id()), price.date(), MyMoneyMoney::precToDenom(security.pricePrecision())); calc->setupPriceEditor(); // The dialog takes care of adding the price if necessary calc->exec(); delete calc; } catch (const MyMoneyException &e) { qDebug("Error in price update: %s", qPrintable(e.what())); } } } void KMyMoneyApp::createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount) { MyMoneyFile* file = MyMoneyFile::instance(); // Add the schedule only if one exists // // Remember to modify the first split to reference the newly created account if (!newSchedule.name().isEmpty()) { try { // We assume at least 2 splits in the transaction MyMoneyTransaction t = newSchedule.transaction(); if (t.splitCount() < 2) { throw MYMONEYEXCEPTION("Transaction for schedule has less than 2 splits!"); } MyMoneyFileTransaction ft; try { file->addSchedule(newSchedule); // in case of a loan account, we keep a reference to this // schedule in the account if (newAccount.accountType() == MyMoneyAccount::Loan || newAccount.accountType() == MyMoneyAccount::AssetLoan) { newAccount.setValue("schedule", newSchedule.id()); file->modifyAccount(newAccount); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } } void KMyMoneyApp::slotAccountDelete() { if (d->m_selectedAccount.id().isEmpty()) return; // need an account ID MyMoneyFile* file = MyMoneyFile::instance(); // can't delete standard accounts or account which still have transactions assigned if (file->isStandardAccount(d->m_selectedAccount.id())) return; // check if the account is referenced by a transaction or schedule MyMoneyFileBitArray skip(IMyMoneyStorage::MaxRefCheckBits); skip.fill(false); skip.setBit(IMyMoneyStorage::RefCheckAccount); skip.setBit(IMyMoneyStorage::RefCheckInstitution); skip.setBit(IMyMoneyStorage::RefCheckPayee); skip.setBit(IMyMoneyStorage::RefCheckTag); skip.setBit(IMyMoneyStorage::RefCheckSecurity); skip.setBit(IMyMoneyStorage::RefCheckCurrency); skip.setBit(IMyMoneyStorage::RefCheckPrice); bool hasReference = file->isReferenced(d->m_selectedAccount, skip); // make sure we only allow transactions in a 'category' (income/expense account) switch (d->m_selectedAccount.accountType()) { case MyMoneyAccount::Income: case MyMoneyAccount::Expense: break; default: // if the account is still referenced if (hasReference) { return; } break; } // if we get here and still have transactions referencing the account, we // need to check with the user to possibly re-assign them to a different account bool needAskUser = true; bool exit = false; MyMoneyFileTransaction ft; if (hasReference) { // show transaction reassignment dialog needAskUser = false; KCategoryReassignDlg* dlg = new KCategoryReassignDlg(this); QString categoryId = dlg->show(d->m_selectedAccount); delete dlg; // and kill the dialog if (categoryId.isEmpty()) return; // the user aborted the dialog, so let's abort as well MyMoneyAccount newCategory = file->account(categoryId); try { { KMSTATUS(i18n("Adjusting transactions...")); /* d->m_selectedAccount.id() is the old id, categoryId the new one Now search all transactions and schedules that reference d->m_selectedAccount.id() and replace that with categoryId. */ // get the list of all transactions that reference the old account MyMoneyTransactionFilter filter(d->m_selectedAccount.id()); filter.setReportAllSplits(false); QList tlist; QList::iterator it_t; file->transactionList(tlist, filter); slotStatusProgressBar(0, tlist.count()); int cnt = 0; for (it_t = tlist.begin(); it_t != tlist.end(); ++it_t) { slotStatusProgressBar(++cnt, 0); MyMoneyTransaction t = (*it_t); if (t.replaceId(categoryId, d->m_selectedAccount.id())) file->modifyTransaction(t); } slotStatusProgressBar(tlist.count(), 0); } // now fix all schedules { KMSTATUS(i18n("Adjusting scheduled transactions...")); QList slist = file->scheduleList(d->m_selectedAccount.id()); QList::iterator it_s; int cnt = 0; slotStatusProgressBar(0, slist.count()); for (it_s = slist.begin(); it_s != slist.end(); ++it_s) { slotStatusProgressBar(++cnt, 0); MyMoneySchedule sch = (*it_s); if (sch.replaceId(categoryId, d->m_selectedAccount.id())) { file->modifySchedule(sch); } } slotStatusProgressBar(slist.count(), 0); } // now fix all budgets { KMSTATUS(i18n("Adjusting budgets...")); QList blist = file->budgetList(); QList::const_iterator it_b; for (it_b = blist.constBegin(); it_b != blist.constEnd(); ++it_b) { if ((*it_b).hasReferenceTo(d->m_selectedAccount.id())) { MyMoneyBudget b = (*it_b); MyMoneyBudget::AccountGroup fromBudget = b.account(d->m_selectedAccount.id()); MyMoneyBudget::AccountGroup toBudget = b.account(categoryId); toBudget += fromBudget; b.setAccount(toBudget, categoryId); b.removeReference(d->m_selectedAccount.id()); file->modifyBudget(b); } } slotStatusProgressBar(blist.count(), 0); } } catch (MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to exchange category %1 with category %2. Reason: %3", d->m_selectedAccount.name(), newCategory.name(), e.what())); exit = true; } slotStatusProgressBar(-1, -1); } if (exit) return; // retain the account name for a possible later usage in the error message box // since the account removal notifies the views the selected account can be changed // so we make sure by doing this that we display the correct name in the error message QString selectedAccountName = d->m_selectedAccount.name(); // at this point, we must not have a reference to the account // to be deleted anymore switch (d->m_selectedAccount.accountGroup()) { // special handling for categories to allow deleting of empty subcategories case MyMoneyAccount::Income: case MyMoneyAccount::Expense: { // open a compound statement here to be able to declare variables // which would otherwise not work within a case label. // case A - only a single, unused category without subcats selected if (d->m_selectedAccount.accountList().isEmpty()) { if (!needAskUser || (KMessageBox::questionYesNo(this, QString("") + i18n("Do you really want to delete category %1?", selectedAccountName) + QString("")) == KMessageBox::Yes)) { try { file->removeAccount(d->m_selectedAccount); d->m_selectedAccount.clearId(); slotUpdateActions(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, QString("") + i18n("Unable to delete category %1. Cause: %2", selectedAccountName, e.what()) + QString("")); } } return; } // case B - we have some subcategories, maybe the user does not want to // delete them all, but just the category itself? MyMoneyAccount parentAccount = file->account(d->m_selectedAccount.parentAccountId()); QStringList accountsToReparent; int result = KMessageBox::questionYesNoCancel(this, QString("") + i18n("Do you want to delete category %1 with all its sub-categories or only " "the category itself? If you only delete the category itself, all its sub-categories " "will be made sub-categories of %2.", selectedAccountName, parentAccount.name()) + QString(""), QString(), KGuiItem(i18n("Delete all")), KGuiItem(i18n("Just the category"))); if (result == KMessageBox::Cancel) return; // cancel pressed? ok, no delete then... // "No" means "Just the category" and that means we need to reparent all subaccounts bool need_confirmation = false; // case C - User only wants to delete the category itself if (result == KMessageBox::No) accountsToReparent = d->m_selectedAccount.accountList(); else { // case D - User wants to delete all subcategories, now check all subcats of // d->m_selectedAccount and remember all that cannot be deleted and // must be "reparented" for (QStringList::const_iterator it = d->m_selectedAccount.accountList().begin(); it != d->m_selectedAccount.accountList().end(); ++it) { // reparent account if a transaction is assigned if (file->transactionCount(*it) != 0) accountsToReparent.push_back(*it); else if (!file->account(*it).accountList().isEmpty()) { // or if we have at least one sub-account that is used for transactions if (!file->hasOnlyUnusedAccounts(file->account(*it).accountList())) { accountsToReparent.push_back(*it); //qDebug() << "subaccount not empty"; } } } if (!accountsToReparent.isEmpty()) need_confirmation = true; } if (!accountsToReparent.isEmpty() && need_confirmation) { if (KMessageBox::questionYesNo(this, i18n("

Some sub-categories of category %1 cannot " "be deleted, because they are still used. They will be made sub-categories of %2. Proceed?

", selectedAccountName, parentAccount.name())) != KMessageBox::Yes) { return; // user gets wet feet... } } // all good, now first reparent selected sub-categories try { MyMoneyAccount parent = file->account(d->m_selectedAccount.parentAccountId()); for (QStringList::const_iterator it = accountsToReparent.constBegin(); it != accountsToReparent.constEnd(); ++it) { MyMoneyAccount child = file->account(*it); file->reparentAccount(child, parent); } // reload the account because the sub-account list might have changed d->m_selectedAccount = file->account(d->m_selectedAccount.id()); // now recursively delete remaining sub-categories file->removeAccountList(d->m_selectedAccount.accountList()); // don't forget to update d->m_selectedAccount, because we still have a copy of // the old account list, which is no longer valid d->m_selectedAccount = file->account(d->m_selectedAccount.id()); } catch (const MyMoneyException &e) { KMessageBox::error(this, QString("") + i18n("Unable to delete a sub-category of category %1. Reason: %2", selectedAccountName, e.what()) + QString("")); return; } } break; // the category/account is deleted after the switch default: if (!d->m_selectedAccount.accountList().isEmpty()) return; // can't delete accounts which still have subaccounts if (KMessageBox::questionYesNo(this, i18n("

Do you really want to " "delete account %1?

", selectedAccountName)) != KMessageBox::Yes) { return; // ok, you don't want to? why did you click then, hmm? } } // switch; try { file->removeAccount(d->m_selectedAccount); d->m_selectedAccount.clearId(); slotUpdateActions(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to delete account '%1'. Cause: %2", selectedAccountName, e.what())); } } void KMyMoneyApp::slotAccountEdit() { MyMoneyFile* file = MyMoneyFile::instance(); if (!d->m_selectedAccount.id().isEmpty()) { if (!file->isStandardAccount(d->m_selectedAccount.id())) { if (d->m_selectedAccount.accountType() != MyMoneyAccount::Loan && d->m_selectedAccount.accountType() != MyMoneyAccount::AssetLoan) { QString caption; bool category = false; switch (d->m_selectedAccount.accountGroup()) { default: caption = i18n("Edit account '%1'", d->m_selectedAccount.name()); break; case MyMoneyAccount::Expense: case MyMoneyAccount::Income: caption = i18n("Edit category '%1'", d->m_selectedAccount.name()); category = true; break; } // set a status message so that the application can't be closed until the editing is done slotStatusMsg(caption); QString tid = file->openingBalanceTransaction(d->m_selectedAccount); MyMoneyTransaction t; MyMoneySplit s0, s1; QPointer dlg = new KNewAccountDlg(d->m_selectedAccount, true, category, 0, caption); if (category || d->m_selectedAccount.accountType() == MyMoneyAccount::Investment) { dlg->setOpeningBalanceShown(false); dlg->setOpeningDateShown(false); tid.clear(); } else { if (!tid.isEmpty()) { try { t = file->transaction(tid); s0 = t.splitByAccount(d->m_selectedAccount.id()); s1 = t.splitByAccount(d->m_selectedAccount.id(), false); dlg->setOpeningBalance(s0.shares()); if (d->m_selectedAccount.accountGroup() == MyMoneyAccount::Liability) { dlg->setOpeningBalance(-s0.shares()); } } catch (const MyMoneyException &e) { qDebug() << "Error retrieving opening balance transaction " << tid << ": " << e.what() << "\n"; tid.clear(); } } } // check for online modules QMap::const_iterator it_plugin = d->m_onlinePlugins.constEnd(); const MyMoneyKeyValueContainer& kvp = d->m_selectedAccount.onlineBankingSettings(); if (!kvp["provider"].isEmpty()) { // if we have an online provider for this account, we need to check // that we have the corresponding plugin. If that exists, we ask it // to provide an additional tab for the account editor. it_plugin = d->m_onlinePlugins.constFind(kvp["provider"]); if (it_plugin != d->m_onlinePlugins.constEnd()) { QString name; QWidget *w = (*it_plugin)->accountConfigTab(d->m_selectedAccount, name); dlg->addTab(w, name); } } if (dlg != 0 && dlg->exec() == QDialog::Accepted) { try { MyMoneyFileTransaction ft; MyMoneyAccount account = dlg->account(); MyMoneyAccount parent = dlg->parentAccount(); if (it_plugin != d->m_onlinePlugins.constEnd()) { account.setOnlineBankingSettings((*it_plugin)->onlineBankingSettings(account.onlineBankingSettings())); } MyMoneyMoney bal = dlg->openingBalance(); if (d->m_selectedAccount.accountGroup() == MyMoneyAccount::Liability) { bal = -bal; } // we need to modify first, as reparent would override all other changes file->modifyAccount(account); if (account.parentAccountId() != parent.id()) { file->reparentAccount(account, parent); } if (!tid.isEmpty() && dlg->openingBalance().isZero()) { file->removeTransaction(t); } else if (!tid.isEmpty() && !dlg->openingBalance().isZero()) { s0.setShares(bal); s0.setValue(bal); t.modifySplit(s0); s1.setShares(-bal); s1.setValue(-bal); t.modifySplit(s1); t.setPostDate(account.openingDate()); file->modifyTransaction(t); } else if (tid.isEmpty() && !dlg->openingBalance().isZero()) { file->createOpeningBalanceTransaction(d->m_selectedAccount, bal); } ft.commit(); // reload the account object as it might have changed in the meantime slotSelectAccount(file->account(account.id())); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to modify account '%1'. Cause: %2", d->m_selectedAccount.name(), e.what())); } } delete dlg; ready(); } else { QPointer wizard = new KEditLoanWizard(d->m_selectedAccount); connect(wizard, SIGNAL(newCategory(MyMoneyAccount&)), this, SLOT(slotCategoryNew(MyMoneyAccount&))); connect(wizard, SIGNAL(createPayee(QString,QString&)), this, SLOT(slotPayeeNew(QString,QString&))); if (wizard->exec() == QDialog::Accepted && wizard != 0) { - MyMoneySchedule sch = file->schedule(d->m_selectedAccount.value("schedule").toLatin1()); + MyMoneySchedule sch; + try { + MyMoneySchedule sch = file->schedule(d->m_selectedAccount.value("schedule").toLatin1()); + } catch (const MyMoneyException &e) { + qDebug() << "schedule" << d->m_selectedAccount.value("schedule").toLatin1() << "not found"; + } if (!(d->m_selectedAccount == wizard->account()) || !(sch == wizard->schedule())) { MyMoneyFileTransaction ft; try { file->modifyAccount(wizard->account()); - sch = wizard->schedule(); + if (!sch.id().isEmpty()) { + sch = wizard->schedule(); + } try { file->schedule(sch.id()); file->modifySchedule(sch); ft.commit(); } catch (const MyMoneyException &) { try { - file->addSchedule(sch); + if(sch.transaction().splitCount() >= 2) { + file->addSchedule(sch); + } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Cannot add schedule: '%s'", qPrintable(e.what())); } } } catch (const MyMoneyException &e) { qDebug("Unable to modify account %s: '%s'", qPrintable(d->m_selectedAccount.name()), qPrintable(e.what())); } } } delete wizard; } } } } QList > KMyMoneyApp::Private::automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { static const int NR_OF_STEPS_LIMIT = 300000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; KMSTATUS(i18n("Running automatic reconciliation")); int progressBarIndex = 0; kmymoney->slotStatusProgressBar(progressBarIndex, NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS); // optimize the most common case - all transactions should be cleared QListIterator > itTransactionSplitResult(result); MyMoneyMoney transactionsBalance; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); transactionsBalance += transactionSplit.second.shares(); } if (amount == transactionsBalance) { result = transactions; return result; } kmymoney->slotStatusProgressBar(progressBarIndex++, 0); // only one transaction is uncleared itTransactionSplitResult.toFront(); int index = 0; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); if (transactionsBalance - transactionSplit.second.shares() == amount) { result.removeAt(index); return result; } index++; } kmymoney->slotStatusProgressBar(progressBarIndex++, 0); // more than one transaction is uncleared - apply the algorithm result.clear(); const MyMoneySecurity &security = MyMoneyFile::instance()->security(account.currencyId()); double precision = 0.1 / account.fraction(security); QList sumList; sumList << MyMoneyMoney(); QMap > > sumToComponentsMap; // compute the possible matches QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); QListIterator itSum(sumList); QList tempList; while (itSum.hasNext()) { const MyMoneyMoney &sum = itSum.next(); QList > splitIds; splitIds << qMakePair(transactionSplit.first.id(), transactionSplit.second.id()); if (sumToComponentsMap.contains(sum)) { if (sumToComponentsMap.value(sum).contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { continue; } splitIds.append(sumToComponentsMap.value(sum)); } tempList << transactionSplit.second.shares() + sum; sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; int size = sumToComponentsMap.size(); if (size % PROGRESSBAR_STEPS == 0) { kmymoney->slotStatusProgressBar(progressBarIndex++, 0); } if (size > NR_OF_STEPS_LIMIT) { return result; // it's taking too much resources abort the algorithm } } QList unionList; unionList.append(tempList); unionList.append(sumList); qSort(unionList); sumList.clear(); MyMoneyMoney smallestSumFromUnion = unionList.first(); sumList.append(smallestSumFromUnion); QListIterator itUnion(unionList); while (itUnion.hasNext()) { MyMoneyMoney sumFromUnion = itUnion.next(); if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { smallestSumFromUnion = sumFromUnion; sumList.append(sumFromUnion); } } } kmymoney->slotStatusProgressBar(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif kmymoney->slotStatusProgressBar(-1, -1); return result; } void KMyMoneyApp::slotAccountReconcileStart() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; // we cannot reconcile standard accounts if (!file->isStandardAccount(d->m_selectedAccount.id())) { // check if we can reconcile this account // it make's sense for asset and liability accounts try { // check if we have overdue schedules for this account QList schedules = file->scheduleList(d->m_selectedAccount.id(), MyMoneySchedule::TYPE_ANY, MyMoneySchedule::OCCUR_ANY, MyMoneySchedule::STYPE_ANY, QDate(), QDate(), true); if (schedules.count() > 0) { if (KMessageBox::questionYesNo(this, i18n("KMyMoney has detected some overdue scheduled transactions for this account. Do you want to enter those scheduled transactions now?"), i18n("Scheduled transactions found")) == KMessageBox::Yes) { QMap skipMap; bool processedOne; KMyMoneyUtils::EnterScheduleResultCodeE rc = KMyMoneyUtils::Enter; do { processedOne = false; QList::const_iterator it_sch; for (it_sch = schedules.constBegin(); (rc != KMyMoneyUtils::Cancel) && (it_sch != schedules.constEnd()); ++it_sch) { MyMoneySchedule sch(*(it_sch)); // and enter it if it is not on the skip list if (skipMap.find((*it_sch).id()) == skipMap.end()) { rc = enterSchedule(sch, false, true); if (rc == KMyMoneyUtils::Ignore) { skipMap[(*it_sch).id()] = true; } } } // reload list (maybe this schedule needs to be added again) schedules = file->scheduleList(d->m_selectedAccount.id(), MyMoneySchedule::TYPE_ANY, MyMoneySchedule::OCCUR_ANY, MyMoneySchedule::STYPE_ANY, QDate(), QDate(), true); } while (processedOne); } } account = file->account(d->m_selectedAccount.id()); // get rid of previous run. delete d->m_endingBalanceDlg; d->m_endingBalanceDlg = new KEndingBalanceDlg(account, this); if (account.isAssetLiability()) { connect(d->m_endingBalanceDlg, SIGNAL(createPayee(QString,QString&)), this, SLOT(slotPayeeNew(QString,QString&))); connect(d->m_endingBalanceDlg, SIGNAL(createCategory(MyMoneyAccount&,MyMoneyAccount)), this, SLOT(slotCategoryNew(MyMoneyAccount&,MyMoneyAccount))); if (d->m_endingBalanceDlg->exec() == QDialog::Accepted) { if (KMyMoneyGlobalSettings::autoReconciliation()) { MyMoneyMoney startBalance = d->m_endingBalanceDlg->previousBalance(); MyMoneyMoney endBalance = d->m_endingBalanceDlg->endingBalance(); QDate endDate = d->m_endingBalanceDlg->statementDate(); QList > transactionList; MyMoneyTransactionFilter filter(account.id()); filter.addState(MyMoneyTransactionFilter::cleared); filter.addState(MyMoneyTransactionFilter::notReconciled); filter.setDateFilter(QDate(), endDate); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); QList > result = d->automaticReconciliation(account, transactionList, endBalance - startBalance); if (!result.empty()) { QString message = i18n("KMyMoney has detected transactions matching your reconciliation data.\nWould you like KMyMoney to clear these transactions for you?"); if (KMessageBox::questionYesNo(this, message, i18n("Automatic reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "AcceptAutomaticReconciliation") == KMessageBox::Yes) { // mark the transactions cleared KMyMoneyRegister::SelectedTransactions oldSelection = d->m_selectedTransactions; d->m_selectedTransactions.clear(); QListIterator > itTransactionSplitResult(result); while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); d->m_selectedTransactions.append(KMyMoneyRegister::SelectedTransaction(transactionSplit.first, transactionSplit.second)); } // mark all transactions in d->m_selectedTransactions as 'Cleared' markTransaction(MyMoneySplit::Cleared); d->m_selectedTransactions = oldSelection; } } } if (d->m_myMoneyView->startReconciliation(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance())) { // check if the user requests us to create interest // or charge transactions. MyMoneyTransaction ti = d->m_endingBalanceDlg->interestTransaction(); MyMoneyTransaction tc = d->m_endingBalanceDlg->chargeTransaction(); MyMoneyFileTransaction ft; try { if (ti != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(ti); } if (tc != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(tc); } ft.commit(); } catch (const MyMoneyException &e) { qWarning("interest transaction not stored: '%s'", qPrintable(e.what())); } // reload the account object as it might have changed in the meantime d->m_reconciliationAccount = file->account(account.id()); slotUpdateActions(); } } } } catch (const MyMoneyException &) { } } } void KMyMoneyApp::slotAccountReconcileFinish() { MyMoneyFile* file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // retrieve list of all transactions that are not reconciled or cleared QList > transactionList; MyMoneyTransactionFilter filter(d->m_reconciliationAccount.id()); filter.addState(MyMoneyTransactionFilter::cleared); filter.addState(MyMoneyTransactionFilter::notReconciled); filter.setDateFilter(QDate(), d->m_endingBalanceDlg->statementDate()); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); MyMoneyMoney balance = MyMoneyFile::instance()->balance(d->m_reconciliationAccount.id(), d->m_endingBalanceDlg->statementDate()); MyMoneyMoney actBalance, clearedBalance; actBalance = clearedBalance = balance; // walk the list of transactions to figure out the balance(s) QList >::const_iterator it; for (it = transactionList.constBegin(); it != transactionList.constEnd(); ++it) { if ((*it).second.reconcileFlag() == MyMoneySplit::NotReconciled) { clearedBalance -= (*it).second.shares(); } } if (d->m_endingBalanceDlg->endingBalance() != clearedBalance) { QString message = i18n("You are about to finish the reconciliation of this account with a difference between your bank statement and the transactions marked as cleared.\n" "Are you sure you want to finish the reconciliation?"); if (KMessageBox::questionYesNo(this, message, i18n("Confirm end of reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::No) return; } MyMoneyFileTransaction ft; // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); // only update the last statement balance here, if we haven't a newer one due // to download of online statements. if (d->m_reconciliationAccount.value("lastImportedTransactionDate").isEmpty() || QDate::fromString(d->m_reconciliationAccount.value("lastImportedTransactionDate"), Qt::ISODate) < d->m_endingBalanceDlg->statementDate()) { d->m_reconciliationAccount.setValue("lastStatementBalance", d->m_endingBalanceDlg->endingBalance().toString()); // in case we override the last statement balance here, we have to make sure // that we don't show the online balance anymore, as it might be different d->m_reconciliationAccount.deletePair("lastImportedTransactionDate"); } d->m_reconciliationAccount.setLastReconciliationDate(d->m_endingBalanceDlg->statementDate()); // keep a record of this reconciliation d->m_reconciliationAccount.addReconciliation(d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); d->m_reconciliationAccount.deletePair("lastReconciledBalance"); d->m_reconciliationAccount.deletePair("statementBalance"); d->m_reconciliationAccount.deletePair("statementDate"); try { // update the account data file->modifyAccount(d->m_reconciliationAccount); /* // collect the list of cleared splits for this account filter.clear(); filter.addAccount(d->m_reconciliationAccount.id()); filter.addState(MyMoneyTransactionFilter::cleared); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); */ // walk the list of transactions/splits and mark the cleared ones as reconciled QList >::iterator it; for (it = transactionList.begin(); it != transactionList.end(); ++it) { MyMoneySplit sp = (*it).second; // skip the ones that are not marked cleared if (sp.reconcileFlag() != MyMoneySplit::Cleared) continue; // always retrieve a fresh copy of the transaction because we // might have changed it already with another split MyMoneyTransaction t = file->transaction((*it).first.id()); sp.setReconcileFlag(MyMoneySplit::Reconciled); sp.setReconcileDate(d->m_endingBalanceDlg->statementDate()); t.modifySplit(sp); // update the engine ... file->modifyTransaction(t); // ... and the list (*it) = qMakePair(t, sp); } ft.commit(); // reload account data from engine as the data might have changed in the meantime d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); emit accountReconciled(d->m_reconciliationAccount, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->previousBalance(), d->m_endingBalanceDlg->endingBalance(), transactionList); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting cleared to reconcile"); } } // Turn off reconciliation mode d->m_reconciliationAccount = MyMoneyAccount(); slotUpdateActions(); } void KMyMoneyApp::slotAccountReconcilePostpone() { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); d->m_reconciliationAccount.setValue("lastReconciledBalance", d->m_endingBalanceDlg->previousBalance().toString()); d->m_reconciliationAccount.setValue("statementBalance", d->m_endingBalanceDlg->endingBalance().toString()); d->m_reconciliationAccount.setValue("statementDate", d->m_endingBalanceDlg->statementDate().toString(Qt::ISODate)); try { file->modifyAccount(d->m_reconciliationAccount); ft.commit(); d->m_reconciliationAccount = MyMoneyAccount(); slotUpdateActions(); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting last reconcile info into account"); ft.rollback(); d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); } } } void KMyMoneyApp::slotAccountOpen(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyAccount)) return; MyMoneyFile* file = MyMoneyFile::instance(); QString id = d->m_selectedAccount.id(); // if the caller passed a non-empty object, we need to select that if (!obj.id().isEmpty()) { id = obj.id(); } // we cannot reconcile standard accounts if (!file->isStandardAccount(id)) { // check if we can open this account // currently it make's sense for asset and liability accounts try { MyMoneyAccount account = file->account(id); d->m_myMoneyView->slotLedgerSelected(account.id()); } catch (const MyMoneyException &) { } } } void KMyMoneyApp::enableCloseAccountAction(const MyMoneyAccount& acc) { switch (canCloseAccount(acc)) { case KMyMoneyUtils::AccountCanClose: { action("account_close")->setEnabled(true); break; } case KMyMoneyUtils::AccountBalanceNonZero: { action("account_close")->setEnabled(false); action("account_close")->setToolTip(i18n("The balance of the account must be zero before the account can be closed")); break; } case KMyMoneyUtils::AccountChildrenOpen: { action("account_close")->setEnabled(false); action("account_close")->setToolTip(i18n("All subaccounts must be closed before the account can be closed")); break; } case KMyMoneyUtils::AccountScheduleReference: { action("account_close")->setEnabled(false); action("account_close")->setToolTip(i18n("This account is still included in an active schedule")); break; } } } KMyMoneyUtils::CanCloseAccountCodeE KMyMoneyApp::canCloseAccount(const MyMoneyAccount& acc) const { // balance must be zero if (!acc.balance().isZero()) return KMyMoneyUtils::AccountBalanceNonZero; // all children must be already closed QStringList::const_iterator it_a; for (it_a = acc.accountList().constBegin(); it_a != acc.accountList().constEnd(); ++it_a) { MyMoneyAccount a = MyMoneyFile::instance()->account(*it_a); if (!a.isClosed()) { return KMyMoneyUtils::AccountChildrenOpen; } } // there must be no unfinished schedule referencing the account QList list = MyMoneyFile::instance()->scheduleList(); QList::const_iterator it_l; for (it_l = list.constBegin(); it_l != list.constEnd(); ++it_l) { if ((*it_l).isFinished()) continue; if ((*it_l).hasReferenceTo(acc.id())) return KMyMoneyUtils::AccountScheduleReference; } return KMyMoneyUtils::AccountCanClose; } void KMyMoneyApp::slotAccountClose() { MyMoneyAccount a; if (!d->m_selectedInvestment.id().isEmpty()) a = d->m_selectedInvestment; else if (!d->m_selectedAccount.id().isEmpty()) a = d->m_selectedAccount; if (a.id().isEmpty()) return; // need an account ID MyMoneyFileTransaction ft; try { a.setClosed(true); MyMoneyFile::instance()->modifyAccount(a); ft.commit(); if (KMyMoneyGlobalSettings::hideClosedAccounts()) { KMessageBox::information(this, QString("") + i18n("You have closed this account. It remains in the system because you have transactions which still refer to it, but it is not shown in the views. You can make it visible again by going to the View menu and selecting Show all accounts or by deselecting the Do not show closed accounts setting.") + QString(""), i18n("Information"), "CloseAccountInfo"); } } catch (const MyMoneyException &) { } } void KMyMoneyApp::slotAccountReopen() { MyMoneyAccount a; if (!d->m_selectedInvestment.id().isEmpty()) a = d->m_selectedInvestment; else if (!d->m_selectedAccount.id().isEmpty()) a = d->m_selectedAccount; if (a.id().isEmpty()) return; // need an account ID MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { while (a.isClosed()) { a.setClosed(false); file->modifyAccount(a); a = file->account(a.parentAccountId()); } ft.commit(); } catch (const MyMoneyException &) { } } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyInstitution& _dst) { MyMoneyAccount src(_src); src.setInstitutionId(_dst.id()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(src); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to institution %2. Reason: %3

", src.name(), _dst.name(), e.what())); } } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyAccount& _dst) { MyMoneyAccount src(_src); MyMoneyAccount dst(_dst); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->reparentAccount(src, dst); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to %2. Reason: %3

", src.name(), dst.name(), e.what())); } } void KMyMoneyApp::slotAccountTransactionReport() { // Generate a transaction report that contains transactions for only the // currently selected account. if (!d->m_selectedAccount.id().isEmpty()) { MyMoneyReport report( MyMoneyReport::eAccount, MyMoneyReport::eQCnumber | MyMoneyReport::eQCpayee | MyMoneyReport::eQCcategory, MyMoneyTransactionFilter::yearToDate, MyMoneyReport::eDetailAll, i18n("%1 YTD Account Transactions", d->m_selectedAccount.name()), i18n("Generated Report") ); report.setGroup(i18n("Transactions")); report.addAccount(d->m_selectedAccount.id()); d->m_myMoneyView->slotShowReport(report); } } void KMyMoneyApp::slotScheduleNew() { slotScheduleNew(MyMoneyTransaction()); } void KMyMoneyApp::slotScheduleNew(const MyMoneyTransaction& _t, MyMoneySchedule::occurrenceE occurrence) { MyMoneySchedule schedule; schedule.setOccurrence(occurrence); // if the schedule is based on an existing transaction, // we take the post date and project it to the next // schedule in a month. if (_t != MyMoneyTransaction()) { MyMoneyTransaction t(_t); schedule.setTransaction(t); if (occurrence != MyMoneySchedule::OCCUR_ONCE) schedule.setNextDueDate(schedule.nextPayment(t.postDate())); } QPointer dlg = new KEditScheduleDlg(schedule, this); TransactionEditor* transactionEditor = dlg->startEdit(); if (transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); if (dlg->exec() == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { schedule = dlg->schedule(); MyMoneyFile::instance()->addSchedule(schedule); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to add scheduled transaction: %1", e.what()), i18n("Add scheduled transaction")); } } } delete transactionEditor; delete dlg; } void KMyMoneyApp::slotScheduleEdit() { if (!d->m_selectedSchedule.id().isEmpty()) { try { MyMoneySchedule schedule = MyMoneyFile::instance()->schedule(d->m_selectedSchedule.id()); KEditScheduleDlg* sched_dlg = 0; KEditLoanWizard* loan_wiz = 0; switch (schedule.type()) { case MyMoneySchedule::TYPE_BILL: case MyMoneySchedule::TYPE_DEPOSIT: case MyMoneySchedule::TYPE_TRANSFER: sched_dlg = new KEditScheduleDlg(schedule, this); d->m_transactionEditor = sched_dlg->startEdit(); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(sched_dlg, !KMyMoneySettings::stringMatchFromStart()); if (sched_dlg->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { MyMoneySchedule sched = sched_dlg->schedule(); // Check whether the new Schedule Date // is at or before the lastPaymentDate // If it is, ask the user whether to clear the // lastPaymentDate const QDate& next = sched.nextDueDate(); const QDate& last = sched.lastPayment(); if (next.isValid() && last.isValid() && next <= last) { // Entered a date effectively no later // than previous payment. Date would be // updated automatically so we probably // want to clear it. Let's ask the user. if (KMessageBox::questionYesNo(this, QString("") + i18n("You have entered a scheduled transaction date of %1. Because the scheduled transaction was last paid on %2, KMyMoney will automatically adjust the scheduled transaction date to the next date unless the last payment date is reset. Do you want to reset the last payment date?", QLocale().toString(next, QLocale::ShortFormat), QLocale().toString(last, QLocale::ShortFormat)) + QString(""), i18n("Reset Last Payment Date"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::Yes) { sched.setLastPayment(QDate()); } } MyMoneyFile::instance()->modifySchedule(sched); // delete the editor before we emit the dataChanged() signal from the // engine. Calling this twice in a row does not hurt. deleteTransactionEditor(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } deleteTransactionEditor(); } delete sched_dlg; break; case MyMoneySchedule::TYPE_LOANPAYMENT: loan_wiz = new KEditLoanWizard(schedule.account(2)); connect(loan_wiz, SIGNAL(newCategory(MyMoneyAccount&)), this, SLOT(slotCategoryNew(MyMoneyAccount&))); connect(loan_wiz, SIGNAL(createPayee(QString,QString&)), this, SLOT(slotPayeeNew(QString,QString&))); if (loan_wiz->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifySchedule(loan_wiz->schedule()); MyMoneyFile::instance()->modifyAccount(loan_wiz->account()); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } delete loan_wiz; break; case MyMoneySchedule::TYPE_ANY: break; } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } } void KMyMoneyApp::slotScheduleDelete() { if (!d->m_selectedSchedule.id().isEmpty()) { MyMoneyFileTransaction ft; try { MyMoneySchedule sched = MyMoneyFile::instance()->schedule(d->m_selectedSchedule.id()); QString msg = i18n("

Are you sure you want to delete the scheduled transaction %1?

", d->m_selectedSchedule.name()); if (sched.type() == MyMoneySchedule::TYPE_LOANPAYMENT) { msg += QString(" "); msg += i18n("In case of loan payments it is currently not possible to recreate the scheduled transaction."); } if (KMessageBox::questionYesNo(this, msg) == KMessageBox::No) return; MyMoneyFile::instance()->removeSchedule(sched); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to remove scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } } void KMyMoneyApp::slotScheduleDuplicate() { // since we may jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->action("schedule_duplicate")->isEnabled()) { MyMoneySchedule sch = d->m_selectedSchedule; sch.clearId(); sch.setLastPayment(QDate()); sch.setName(i18nc("Copy of scheduled transaction name", "Copy of %1", sch.name())); // make sure that we set a valid next due date if the original next due date is invalid if (!sch.nextDueDate().isValid()) sch.setNextDueDate(QDate::currentDate()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addSchedule(sch); ft.commit(); // select the new schedule in the view if (!d->m_selectedSchedule.id().isEmpty()) d->m_myMoneyView->slotScheduleSelected(sch.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to duplicate scheduled transaction: '%1'", d->m_selectedSchedule.name()), e.what()); } } } void KMyMoneyApp::slotScheduleSkip() { if (!d->m_selectedSchedule.id().isEmpty()) { try { MyMoneySchedule schedule = MyMoneyFile::instance()->schedule(d->m_selectedSchedule.id()); skipSchedule(schedule); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unknown scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } } void KMyMoneyApp::skipSchedule(MyMoneySchedule& schedule) { if (!schedule.id().isEmpty()) { try { schedule = MyMoneyFile::instance()->schedule(schedule.id()); if (!schedule.isFinished()) { if (schedule.occurrence() != MyMoneySchedule::OCCUR_ONCE) { QDate next = schedule.nextDueDate(); if (!schedule.isFinished() && (KMessageBox::questionYesNo(this, QString("") + i18n("Do you really want to skip the %1 transaction scheduled for %2?", schedule.name(), QLocale().toString(next, QLocale::ShortFormat)) + QString(""))) == KMessageBox::Yes) { MyMoneyFileTransaction ft; schedule.setLastPayment(next); schedule.setNextDueDate(schedule.nextPayment(next)); MyMoneyFile::instance()->modifySchedule(schedule); ft.commit(); } } } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, QString("") + i18n("Unable to skip scheduled transaction %1.", schedule.name()) + QString(""), e.what()); } } } void KMyMoneyApp::slotScheduleEnter() { if (!d->m_selectedSchedule.id().isEmpty()) { try { MyMoneySchedule schedule = MyMoneyFile::instance()->schedule(d->m_selectedSchedule.id()); enterSchedule(schedule); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unknown scheduled transaction '%1'", d->m_selectedSchedule.name()), e.what()); } } } KMyMoneyUtils::EnterScheduleResultCodeE KMyMoneyApp::enterSchedule(MyMoneySchedule& schedule, bool autoEnter, bool extendedKeys) { KMyMoneyUtils::EnterScheduleResultCodeE rc = KMyMoneyUtils::Cancel; if (!schedule.id().isEmpty()) { try { schedule = MyMoneyFile::instance()->schedule(schedule.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); return rc; } QPointer dlg = new KEnterScheduleDlg(this, schedule); try { QDate origDueDate = schedule.nextDueDate(); dlg->showExtendedKeys(extendedKeys); d->m_transactionEditor = dlg->startEdit(); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); MyMoneyTransaction torig, taccepted; d->m_transactionEditor->createTransaction(torig, dlg->transaction(), schedule.transaction().splits().isEmpty() ? MyMoneySplit() : schedule.transaction().splits().front(), true); // force actions to be available no matter what (will be updated according to the state during // slotTransactionsEnter or slotTransactionsCancel) kmymoney->action("transaction_cancel")->setEnabled(true); kmymoney->action("transaction_enter")->setEnabled(true); KConfirmManualEnterDlg::Action action = KConfirmManualEnterDlg::ModifyOnce; if (!autoEnter || !schedule.isFixed()) { for (; dlg != 0;) { rc = KMyMoneyUtils::Cancel; if (dlg->exec() == QDialog::Accepted && dlg != 0) { rc = dlg->resultCode(); if (rc == KMyMoneyUtils::Enter) { d->m_transactionEditor->createTransaction(taccepted, torig, torig.splits().isEmpty() ? MyMoneySplit() : torig.splits().front(), true); // make sure to suppress comparison of some data: postDate torig.setPostDate(taccepted.postDate()); if (torig != taccepted) { QPointer cdlg = new KConfirmManualEnterDlg(schedule, this); cdlg->loadTransactions(torig, taccepted); if (cdlg->exec() == QDialog::Accepted) { action = cdlg->action(); delete cdlg; break; } delete cdlg; // the user has chosen 'cancel' during confirmation, // we go back to the editor continue; } } else if (rc == KMyMoneyUtils::Skip) { slotTransactionsCancel(); skipSchedule(schedule); } else { slotTransactionsCancel(); } } else { if (autoEnter) { if (KMessageBox::warningYesNo(this, i18n("Are you sure you wish to stop this scheduled transaction from being entered into the register?\n\nKMyMoney will prompt you again next time it starts unless you manually enter it later.")) == KMessageBox::No) { // the user has chosen 'No' for the above question, // we go back to the editor continue; } } slotTransactionsCancel(); } break; } } // if we still have the editor around here, the user did not cancel if ((d->m_transactionEditor != 0) && (dlg != 0)) { MyMoneyFileTransaction ft; try { MyMoneyTransaction t; // add the new transaction switch (action) { case KConfirmManualEnterDlg::UseOriginal: // setup widgets with original transaction data d->m_transactionEditor->setTransaction(dlg->transaction(), dlg->transaction().splits().isEmpty() ? MyMoneySplit() : dlg->transaction().splits().front()); // and create a transaction based on that data taccepted = MyMoneyTransaction(); d->m_transactionEditor->createTransaction(taccepted, dlg->transaction(), dlg->transaction().splits().isEmpty() ? MyMoneySplit() : dlg->transaction().splits().front(), true); break; case KConfirmManualEnterDlg::ModifyAlways: torig = taccepted; torig.setPostDate(origDueDate); schedule.setTransaction(torig); break; case KConfirmManualEnterDlg::ModifyOnce: break; } QString newId; connect(d->m_transactionEditor, SIGNAL(balanceWarning(QWidget*,MyMoneyAccount,QString)), d->m_balanceWarning, SLOT(slotShowMessage(QWidget*,MyMoneyAccount,QString))); if (d->m_transactionEditor->enterTransactions(newId, false)) { if (!newId.isEmpty()) { MyMoneyTransaction t = MyMoneyFile::instance()->transaction(newId); schedule.setLastPayment(t.postDate()); } // in case the next due date is invalid, the schedule is finished // we mark it as such by setting the next due date to one day past the end QDate nextDueDate = schedule.nextPayment(origDueDate); if (!nextDueDate.isValid()) { schedule.setNextDueDate(schedule.endDate().addDays(1)); } else { schedule.setNextDueDate(nextDueDate); } MyMoneyFile::instance()->modifySchedule(schedule); rc = KMyMoneyUtils::Enter; // delete the editor before we emit the dataChanged() signal from the // engine. Calling this twice in a row does not hurt. deleteTransactionEditor(); ft.commit(); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); } deleteTransactionEditor(); } } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); } delete dlg; } return rc; } bool KMyMoneyApp::slotPayeeNew(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Payee")) { // Ask the user if that is what he intended to do? QString msg = QLatin1String("") + i18n("Do you want to add %1 as payer/receiver?", newnameBase) + QLatin1String(""); if (KMessageBox::questionYesNo(this, msg, i18n("New payee/receiver"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewPayee") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewPayee")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->payeeByName(newname); newname = QString("%1 [%2]").arg(newnameBase).arg(++count); } catch (const MyMoneyException &) { break; } } MyMoneyPayee p; p.setName(newname); MyMoneyFile::instance()->addPayee(p); id = p.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to add payee"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); doit = false; } } return doit; } void KMyMoneyApp::slotPayeeNew() { QString id; slotPayeeNew(i18n("New Payee"), id); // the callbacks should have made sure, that the payees view has been // updated already. So we search for the id in the list of items // and select it. emit payeeCreated(id); } bool KMyMoneyApp::payeeInList(const QList& list, const QString& id) const { bool rc = false; QList::const_iterator it_p = list.begin(); while (it_p != list.end()) { if ((*it_p).id() == id) { rc = true; break; } ++it_p; } return rc; } void KMyMoneyApp::slotPayeeDelete() { if (d->m_selectedPayees.isEmpty()) return; // shouldn't happen // get confirmation from user QString prompt; if (d->m_selectedPayees.size() == 1) prompt = i18n("

Do you really want to remove the payee %1?

", d->m_selectedPayees.front().name()); else prompt = i18n("Do you really want to remove all selected payees?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Payee")) == KMessageBox::No) return; payeeReassign(KPayeeReassignDlg::TypeDelete); } void KMyMoneyApp::slotPayeeMerge() { if (d->m_selectedPayees.size() < 1) return; // shouldn't happen if (KMessageBox::questionYesNo(this, i18n("

Do you really want to merge the selected payees?"), i18n("Merge Payees")) == KMessageBox::No) return; if (payeeReassign(KPayeeReassignDlg::TypeMerge)) // clean selection since we just deleted the selected payees slotSelectPayees(QList()); } bool KMyMoneyApp::payeeReassign(int type) { if (!(type >= 0 && type < KPayeeReassignDlg::TypeCount)) return false; MyMoneyFile * file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { // create a transaction filter that contains all payees selected for removal MyMoneyTransactionFilter f = MyMoneyTransactionFilter(); for (QList::const_iterator it = d->m_selectedPayees.constBegin(); it != d->m_selectedPayees.constEnd(); ++it) { f.addPayee((*it).id()); } // request a list of all transactions that still use the payees in question QList translist = file->transactionList(f); // qDebug() << "[KPayeesView::slotDeletePayee] " << translist.count() << " transaction still assigned to payees"; // now get a list of all schedules that make use of one of the payees QList all_schedules = file->scheduleList(); QList used_schedules; for (QList::ConstIterator it = all_schedules.constBegin(); it != all_schedules.constEnd(); ++it) { // loop over all splits in the transaction of the schedule for (QList::ConstIterator s_it = (*it).transaction().splits().constBegin(); s_it != (*it).transaction().splits().constEnd(); ++s_it) { // is the payee in the split to be deleted? if (payeeInList(d->m_selectedPayees, (*s_it).payeeId())) { used_schedules.push_back(*it); // remember this schedule break; } } } // qDebug() << "[KPayeesView::slotDeletePayee] " << used_schedules.count() << " schedules use one of the selected payees"; // and a list of all loan accounts that references one of the payees QList allAccounts; QList usedAccounts; file->accountList(allAccounts); foreach (const MyMoneyAccount &account, allAccounts) { if (account.isLoan()) { MyMoneyAccountLoan loanAccount(account); foreach (const MyMoneyPayee &payee, d->m_selectedPayees) { if (loanAccount.hasReferenceTo(payee.id())) { usedAccounts.append(account); } } } } MyMoneyPayee newPayee; bool addToMatchList = false; // if at least one payee is still referenced, we need to reassign its transactions first if (!translist.isEmpty() || !used_schedules.isEmpty() || !usedAccounts.isEmpty()) { // first create list with all non-selected payees QList remainingPayees; if (type == KPayeeReassignDlg::TypeMerge) { remainingPayees = d->m_selectedPayees; } else { remainingPayees = file->payeeList(); QList::iterator it_p; for (it_p = remainingPayees.begin(); it_p != remainingPayees.end();) { if (d->m_selectedPayees.contains(*it_p)) { it_p = remainingPayees.erase(it_p); } else { ++it_p; } } } // show error message if no payees remain if (remainingPayees.isEmpty()) { KMessageBox::sorry(this, i18n("At least one transaction/scheduled transaction or loan account is still referenced by a payee. " "Currently you have all payees selected. However, at least one payee must remain so " "that the transaction/scheduled transaction or loan account can be reassigned.")); return false; } // show transaction reassignment dialog KPayeeReassignDlg * dlg = new KPayeeReassignDlg(static_cast(type), this); KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); QString payee_id = dlg->show(remainingPayees); addToMatchList = dlg->addToMatchList(); delete dlg; // and kill the dialog if (payee_id.isEmpty()) return false; // the user aborted the dialog, so let's abort as well // try to get selected payee. If not possible and we are merging payees, // then we create a new one try { newPayee = file->payee(payee_id); } catch (const MyMoneyException &e) { if (type == KPayeeReassignDlg::TypeMerge) { // it's ok to use payee_id for both arguments since the first is const, // so it's garantee not to change its content if (!slotPayeeNew(payee_id, payee_id)) return false; // the user aborted the dialog, so let's abort as well newPayee = file->payee(payee_id); } else { return false; } } // TODO : check if we have a report that explicitively uses one of our payees // and issue an appropriate warning try { QList::iterator s_it; // now loop over all transactions and reassign payee for (QList::iterator it = translist.begin(); it != translist.end(); ++it) { // create a copy of the splits list in the transaction QList splits = (*it).splits(); // loop over all splits for (s_it = splits.begin(); s_it != splits.end(); ++s_it) { // if the split is assigned to one of the selected payees, we need to modify it if (payeeInList(d->m_selectedPayees, (*s_it).payeeId())) { (*s_it).setPayeeId(payee_id); // first modify payee in current split // then modify the split in our local copy of the transaction list (*it).modifySplit(*s_it); // this does not modify the list object 'splits'! } } // for - Splits file->modifyTransaction(*it); // modify the transaction in the MyMoney object } // for - Transactions // now loop over all schedules and reassign payees for (QList::iterator it = used_schedules.begin(); it != used_schedules.end(); ++it) { // create copy of transaction in current schedule MyMoneyTransaction trans = (*it).transaction(); // create copy of lists of splits QList splits = trans.splits(); for (s_it = splits.begin(); s_it != splits.end(); ++s_it) { if (payeeInList(d->m_selectedPayees, (*s_it).payeeId())) { (*s_it).setPayeeId(payee_id); trans.modifySplit(*s_it); // does not modify the list object 'splits'! } } // for - Splits // store transaction in current schedule (*it).setTransaction(trans); file->modifySchedule(*it); // modify the schedule in the MyMoney engine } // for - Schedules // reassign the payees in the loans that reference the deleted payees foreach (const MyMoneyAccount &account, usedAccounts) { MyMoneyAccountLoan loanAccount(account); loanAccount.setPayee(payee_id); file->modifyAccount(loanAccount); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to reassign payee of transaction/split"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } else { // if !translist.isEmpty() if (type == KPayeeReassignDlg::TypeMerge) { KMessageBox::sorry(this, i18n("Nothing to merge."), i18n("Merge Payees")); return false; } } bool ignorecase; QStringList payeeNames; MyMoneyPayee::payeeMatchType matchType = newPayee.matchData(ignorecase, payeeNames); QStringList deletedPayeeNames; // now loop over all selected payees and remove them for (QList::iterator it = d->m_selectedPayees.begin(); it != d->m_selectedPayees.end(); ++it) { if (newPayee.id() != (*it).id()) { if (addToMatchList) { deletedPayeeNames << (*it).name(); } file->removePayee(*it); } } // if we initially have no matching turned on, we just ignore the case (default) if (matchType == MyMoneyPayee::matchDisabled) ignorecase = true; // update the destination payee if this was requested by the user if (addToMatchList && deletedPayeeNames.count() > 0) { // add new names to the list // TODO: it would be cool to somehow shrink the list to make better use // of regular expressions at this point. For now, we leave this task // to the user himeself. QStringList::const_iterator it_n; for (it_n = deletedPayeeNames.constBegin(); it_n != deletedPayeeNames.constEnd(); ++it_n) { if (matchType == MyMoneyPayee::matchKey) { // make sure we really need it and it is not caught by an existing regexp QStringList::const_iterator it_k; for (it_k = payeeNames.constBegin(); it_k != payeeNames.constEnd(); ++it_k) { QRegExp exp(*it_k, ignorecase ? Qt::CaseInsensitive : Qt::CaseSensitive); if (exp.indexIn(*it_n) != -1) break; } if (it_k == payeeNames.constEnd()) payeeNames << QRegExp::escape(*it_n); } else if (payeeNames.contains(*it_n) == 0) payeeNames << QRegExp::escape(*it_n); } // and update the payee in the engine context // make sure to turn on matching for this payee in the right mode newPayee.setMatchData(MyMoneyPayee::matchKey, ignorecase, payeeNames); file->modifyPayee(newPayee); } ft.commit(); // If we just deleted the payees, they sure don't exist anymore slotSelectPayees(QList()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to remove payee(s)"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } return true; } void KMyMoneyApp::slotTagNew(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Tag")) { // Ask the user if that is what he intended to do? QString msg = QString("") + i18n("Do you want to add %1 as tag?", newnameBase) + QString(""); if (KMessageBox::questionYesNo(this, msg, i18n("New tag"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewTag") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewTag")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->tagByName(newname); newname = QString("%1 [%2]").arg(newnameBase).arg(++count); } catch (const MyMoneyException &) { break; } } MyMoneyTag ta; ta.setName(newname); MyMoneyFile::instance()->addTag(ta); id = ta.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to add tag"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } } void KMyMoneyApp::slotTagNew() { QString id; slotTagNew(i18n("New Tag"), id); // the callbacks should have made sure, that the tags view has been // updated already. So we search for the id in the list of items // and select it. emit tagCreated(id); } bool KMyMoneyApp::tagInList(const QList& list, const QString& id) const { bool rc = false; QList::const_iterator it_p = list.begin(); while (it_p != list.end()) { if ((*it_p).id() == id) { rc = true; break; } ++it_p; } return rc; } void KMyMoneyApp::slotTagDelete() { if (d->m_selectedTags.isEmpty()) return; // shouldn't happen MyMoneyFile * file = MyMoneyFile::instance(); // first create list with all non-selected tags QList remainingTags = file->tagList(); QList::iterator it_ta; for (it_ta = remainingTags.begin(); it_ta != remainingTags.end();) { if (d->m_selectedTags.contains(*it_ta)) { it_ta = remainingTags.erase(it_ta); } else { ++it_ta; } } // get confirmation from user QString prompt; if (d->m_selectedTags.size() == 1) prompt = i18n("

Do you really want to remove the tag %1?

", d->m_selectedTags.front().name()); else prompt = i18n("Do you really want to remove all selected tags?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Tag")) == KMessageBox::No) return; MyMoneyFileTransaction ft; try { // create a transaction filter that contains all tags selected for removal MyMoneyTransactionFilter f = MyMoneyTransactionFilter(); for (QList::const_iterator it = d->m_selectedTags.constBegin(); it != d->m_selectedTags.constEnd(); ++it) { f.addTag((*it).id()); } // request a list of all transactions that still use the tags in question QList translist = file->transactionList(f); // qDebug() << "[KTagsView::slotDeleteTag] " << translist.count() << " transaction still assigned to tags"; // now get a list of all schedules that make use of one of the tags QList all_schedules = file->scheduleList(); QList used_schedules; for (QList::ConstIterator it = all_schedules.constBegin(); it != all_schedules.constEnd(); ++it) { // loop over all splits in the transaction of the schedule for (QList::ConstIterator s_it = (*it).transaction().splits().constBegin(); s_it != (*it).transaction().splits().constEnd(); ++s_it) { for (int i = 0; i < (*s_it).tagIdList().size(); i++) { // is the tag in the split to be deleted? if (tagInList(d->m_selectedTags, (*s_it).tagIdList()[i])) { used_schedules.push_back(*it); // remember this schedule break; } } } } // qDebug() << "[KTagsView::slotDeleteTag] " << used_schedules.count() << " schedules use one of the selected tags"; MyMoneyTag newTag; // if at least one tag is still referenced, we need to reassign its transactions first if (!translist.isEmpty() || !used_schedules.isEmpty()) { // show error message if no tags remain //FIXME-ALEX Tags are optional so we can delete all of them and simply delete every tagId from every transaction if (remainingTags.isEmpty()) { KMessageBox::sorry(this, i18n("At least one transaction/scheduled transaction is still referenced by a tag. " "Currently you have all tags selected. However, at least one tag must remain so " "that the transaction/scheduled transaction can be reassigned.")); return; } // show transaction reassignment dialog KTagReassignDlg * dlg = new KTagReassignDlg(this); KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); QString tag_id = dlg->show(remainingTags); delete dlg; // and kill the dialog if (tag_id.isEmpty()) //FIXME-ALEX Let the user choose to not reassign a to-be deleted tag to another one. return; // the user aborted the dialog, so let's abort as well newTag = file->tag(tag_id); // TODO : check if we have a report that explicitively uses one of our tags // and issue an appropriate warning try { QList::iterator s_it; // now loop over all transactions and reassign tag for (QList::iterator it = translist.begin(); it != translist.end(); ++it) { // create a copy of the splits list in the transaction QList splits = (*it).splits(); // loop over all splits for (s_it = splits.begin(); s_it != splits.end(); ++s_it) { QList tagIdList = (*s_it).tagIdList(); for (int i = 0; i < tagIdList.size(); i++) { // if the split is assigned to one of the selected tags, we need to modify it if (tagInList(d->m_selectedTags, tagIdList[i])) { tagIdList.removeAt(i); if (tagIdList.indexOf(tag_id) == -1) tagIdList.append(tag_id); i = -1; // restart from the first element } } (*s_it).setTagIdList(tagIdList); // first modify tag list in current split // then modify the split in our local copy of the transaction list (*it).modifySplit(*s_it); // this does not modify the list object 'splits'! } // for - Splits file->modifyTransaction(*it); // modify the transaction in the MyMoney object } // for - Transactions // now loop over all schedules and reassign tags for (QList::iterator it = used_schedules.begin(); it != used_schedules.end(); ++it) { // create copy of transaction in current schedule MyMoneyTransaction trans = (*it).transaction(); // create copy of lists of splits QList splits = trans.splits(); for (s_it = splits.begin(); s_it != splits.end(); ++s_it) { QList tagIdList = (*s_it).tagIdList(); for (int i = 0; i < tagIdList.size(); i++) { if (tagInList(d->m_selectedTags, tagIdList[i])) { tagIdList.removeAt(i); if (tagIdList.indexOf(tag_id) == -1) tagIdList.append(tag_id); i = -1; // restart from the first element } } (*s_it).setTagIdList(tagIdList); trans.modifySplit(*s_it); // does not modify the list object 'splits'! } // for - Splits // store transaction in current schedule (*it).setTransaction(trans); file->modifySchedule(*it); // modify the schedule in the MyMoney engine } // for - Schedules } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to reassign tag of transaction/split"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } // if !translist.isEmpty() // now loop over all selected tags and remove them for (QList::iterator it = d->m_selectedTags.begin(); it != d->m_selectedTags.end(); ++it) { file->removeTag(*it); } ft.commit(); // If we just deleted the tags, they sure don't exist anymore slotSelectTags(QList()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to remove tag(s)"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotCurrencyNew() { QString sid = QInputDialog::getText(0, i18n("New currency"), i18n("Enter ISO 4217 code for the new currency")); if (!sid.isEmpty()) { QString id(sid); MyMoneySecurity currency(id, i18n("New currency")); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addCurrency(currency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot create new currency. %1", e.what()), i18n("New currency")); } emit currencyCreated(id); } } void KMyMoneyApp::slotCurrencyUpdate(const QString ¤cyId, const QString& currencyName, const QString& currencyTradingSymbol) { MyMoneyFile* file = MyMoneyFile::instance(); try { if (currencyName != d->m_selectedCurrency.name() || currencyTradingSymbol != d->m_selectedCurrency.tradingSymbol()) { MyMoneySecurity currency = file->currency(currencyId); currency.setName(currencyName); currency.setTradingSymbol(currencyTradingSymbol); MyMoneyFileTransaction ft; try { file->modifyCurrency(currency); d->m_selectedCurrency = currency; ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot update currency. %1", e.what()), i18n("Update currency")); } } } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot update currency. %1", e.what()), i18n("Update currency")); } } void KMyMoneyApp::slotCurrencyDelete() { if (!d->m_selectedCurrency.id().isEmpty()) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->removeCurrency(d->m_selectedCurrency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot delete currency %1. %2", d->m_selectedCurrency.name(), e.what()), i18n("Delete currency")); } } } void KMyMoneyApp::slotCurrencySetBase() { if (!d->m_selectedCurrency.id().isEmpty()) { if (d->m_selectedCurrency.id() != MyMoneyFile::instance()->baseCurrency().id()) { MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->setBaseCurrency(d->m_selectedCurrency); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot set %1 as base currency: %2", d->m_selectedCurrency.name(), e.what()), i18n("Set base currency")); } } } } void KMyMoneyApp::slotBudgetNew() { QDate date = QDate::currentDate(); date.setDate(date.year(), KMyMoneyGlobalSettings::firstFiscalMonth(), KMyMoneyGlobalSettings::firstFiscalDay()); QString newname = i18n("Budget %1", date.year()); MyMoneyBudget budget; // make sure we have a unique name try { int i = 1; // Exception thrown when the name is not found while (1) { MyMoneyFile::instance()->budgetByName(newname); newname = i18n("Budget %1 %2", date.year(), i++); } } catch (const MyMoneyException &) { // all ok, the name is unique } MyMoneyFileTransaction ft; try { budget.setName(newname); budget.setBudgetStart(date); MyMoneyFile::instance()->addBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to add budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotBudgetDelete() { if (d->m_selectedBudgets.isEmpty()) return; // shouldn't happen MyMoneyFile * file = MyMoneyFile::instance(); // get confirmation from user QString prompt; if (d->m_selectedBudgets.size() == 1) prompt = i18n("

Do you really want to remove the budget %1?

", d->m_selectedBudgets.front().name()); else prompt = i18n("Do you really want to remove all selected budgets?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Budget")) == KMessageBox::No) return; MyMoneyFileTransaction ft; try { // now loop over all selected budgets and remove them for (QList::iterator it = d->m_selectedBudgets.begin(); it != d->m_selectedBudgets.end(); ++it) { file->removeBudget(*it); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to remove budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotBudgetCopy() { if (d->m_selectedBudgets.size() == 1) { MyMoneyFileTransaction ft; try { MyMoneyBudget budget = d->m_selectedBudgets.first(); budget.clearId(); budget.setName(i18n("Copy of %1", budget.name())); MyMoneyFile::instance()->addBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to add budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } void KMyMoneyApp::slotBudgetChangeYear() { if (d->m_selectedBudgets.size() == 1) { QStringList years; int current = 0; bool haveCurrent = false; MyMoneyBudget budget = *(d->m_selectedBudgets.begin()); for (int i = (QDate::currentDate().year() - 3); i < (QDate::currentDate().year() + 5); ++i) { years << QString("%1").arg(i); if (i == budget.budgetStart().year()) { haveCurrent = true; } if (!haveCurrent) ++current; } if (!haveCurrent) current = 0; bool ok = false; QString yearString = QInputDialog::getItem(this, i18n("Select year"), i18n("Budget year"), years, current, false, &ok); if (ok) { int year = yearString.toInt(0, 0); QDate newYear = QDate(year, budget.budgetStart().month(), budget.budgetStart().day()); if (newYear != budget.budgetStart()) { MyMoneyFileTransaction ft; try { budget.setBudgetStart(newYear); MyMoneyFile::instance()->modifyBudget(budget); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to modify budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } } } void KMyMoneyApp::slotBudgetForecast() { if (d->m_selectedBudgets.size() == 1) { MyMoneyFileTransaction ft; try { MyMoneyBudget budget = d->m_selectedBudgets.first(); bool calcBudget = budget.getaccounts().count() == 0; if (!calcBudget) { if (KMessageBox::warningContinueCancel(0, i18n("The current budget already contains data. Continuing will replace all current values of this budget."), i18nc("Warning message box", "Warning")) == KMessageBox::Continue) calcBudget = true; } if (calcBudget) { QDate historyStart; QDate historyEnd; QDate budgetStart; QDate budgetEnd; budgetStart = budget.budgetStart(); budgetEnd = budgetStart.addYears(1).addDays(-1); historyStart = budgetStart.addYears(-1); historyEnd = budgetEnd.addYears(-1); MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); forecast.createBudget(budget, historyStart, historyEnd, budgetStart, budgetEnd, true); MyMoneyFile::instance()->modifyBudget(budget); ft.commit(); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to modify budget: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } void KMyMoneyApp::slotKDELanguageSettings() { KMessageBox::information(this, i18n("Please be aware that changes made in the following dialog affect all KDE applications not only KMyMoney."), i18nc("Warning message box", "Warning"), "LanguageSettingsWarning"); QStringList args; args << "language"; QString error; int pid; KToolInvocation::kdeinitExec("kcmshell4", args, &error, &pid); } void KMyMoneyApp::slotNewFeature() { } void KMyMoneyApp::slotTransactionsDelete() { // since we may jump here via code, we have to make sure to react only // if the action is enabled if (!kmymoney->action("transaction_delete")->isEnabled()) return; if (d->m_selectedTransactions.isEmpty()) return; if (d->m_selectedTransactions.warnLevel() == 1) { if (KMessageBox::warningContinueCancel(0, i18n("At least one split of the selected transactions has been reconciled. " "Do you wish to delete the transactions anyway?"), i18n("Transaction already reconciled")) == KMessageBox::Cancel) return; } QString msg = i18np("Do you really want to delete the selected transaction?", "Do you really want to delete all %1 selected transactions?", d->m_selectedTransactions.count()); if (KMessageBox::questionYesNo(this, msg, i18n("Delete transaction")) == KMessageBox::Yes) { KMSTATUS(i18n("Deleting transactions")); doDeleteTransactions(); } } void KMyMoneyApp::slotTransactionDuplicate() { // since we may jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->action("transaction_duplicate")->isEnabled()) { KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int i = 0; int cnt = d->m_selectedTransactions.count(); KMSTATUS(i18n("Duplicating transactions")); slotStatusProgressBar(0, cnt); MyMoneyFileTransaction ft; MyMoneyTransaction lt; try { for (it_t = list.begin(); it_t != list.end(); ++it_t) { MyMoneyTransaction t = (*it_t).transaction(); QList::iterator it_s; // wipe out any reconciliation information for (it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s) { (*it_s).setReconcileFlag(MyMoneySplit::NotReconciled); (*it_s).setReconcileDate(QDate()); (*it_s).setBankID(QString()); } // clear invalid data t.setEntryDate(QDate()); t.clearId(); // and set the post date to today t.setPostDate(QDate::currentDate()); MyMoneyFile::instance()->addTransaction(t); lt = t; slotStatusProgressBar(i++, 0); } ft.commit(); // select the new transaction in the ledger if (!d->m_selectedAccount.id().isEmpty()) d->m_myMoneyView->slotLedgerSelected(d->m_selectedAccount.id(), lt.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to duplicate transaction(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } // switch off the progress bar slotStatusProgressBar(-1, -1); } } void KMyMoneyApp::doDeleteTransactions() { KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int cnt = list.count(); int i = 0; slotStatusProgressBar(0, cnt); MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); try { it_t = list.begin(); while (it_t != list.end()) { // only remove those transactions that do not reference a closed account if (!file->referencesClosedAccount((*it_t).transaction())) { file->removeTransaction((*it_t).transaction()); // remove all those references in the list of selected transactions // that refer to the same transaction we just removed so that we // will not be caught by an exception later on (see bko #285310) KMyMoneyRegister::SelectedTransactions::iterator it_td = it_t; ++it_td; while (it_td != list.end()) { if ((*it_t).transaction().id() == (*it_td).transaction().id()) { it_td = list.erase(it_td); i++; // bump count of deleted transactions } else { ++it_td; } } } // need to ensure "nextCheckNumber" is still correct MyMoneyAccount acc = file->account((*it_t).split().accountId()); // the "lastNumberUsed" might have been the txn number deleted // so adjust it QString deletedNum = (*it_t).split().number(); // decrement deletedNum and set new "lastNumberUsed" QString num = KMyMoneyUtils::getAdjacentNumber(deletedNum, -1); acc.setValue("lastNumberUsed", num); file->modifyAccount(acc); list.erase(it_t); it_t = list.begin(); slotStatusProgressBar(i++, 0); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to delete transaction(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } slotStatusProgressBar(-1, -1); } void KMyMoneyApp::slotTransactionsNew() { // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->action("transaction_new")->isEnabled()) { if (d->m_myMoneyView->createNewTransaction()) { d->m_transactionEditor = d->m_myMoneyView->startEdit(d->m_selectedTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(d->m_myMoneyView, !KMyMoneySettings::stringMatchFromStart()); KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !d->m_lastPayeeEnteredId.isEmpty()) { // in case we entered a new transaction before and used a payee, // we reuse it here. Save the text to the edit widget, select it // so that hitting any character will start entering another payee. payeeEdit->setSelectedItem(d->m_lastPayeeEnteredId); payeeEdit->lineEdit()->selectAll(); } if (d->m_transactionEditor) { connect(d->m_transactionEditor, SIGNAL(statusProgress(int,int)), this, SLOT(slotStatusProgressBar(int,int))); connect(d->m_transactionEditor, SIGNAL(statusMsg(QString)), this, SLOT(slotStatusMsg(QString))); connect(d->m_transactionEditor, SIGNAL(scheduleTransaction(MyMoneyTransaction,MyMoneySchedule::occurrenceE)), this, SLOT(slotScheduleNew(MyMoneyTransaction,MyMoneySchedule::occurrenceE))); } slotUpdateActions(); } } } } void KMyMoneyApp::slotTransactionsEdit() { // qDebug("KMyMoneyApp::slotTransactionsEdit()"); // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->action("transaction_edit")->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->m_myMoneyView->startEdit(d->m_selectedTransactions); KMyMoneyMVCCombo::setSubstringSearchForChildren(d->m_myMoneyView, !KMyMoneySettings::stringMatchFromStart()); slotUpdateActions(); } } void KMyMoneyApp::deleteTransactionEditor() { // make sure, we don't use the transaction editor pointer // anymore from now on TransactionEditor* p = d->m_transactionEditor; d->m_transactionEditor = 0; delete p; } void KMyMoneyApp::slotTransactionsEditSplits() { // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->action("transaction_editsplits")->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->m_myMoneyView->startEdit(d->m_selectedTransactions); slotUpdateActions(); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(d->m_myMoneyView, !KMyMoneySettings::stringMatchFromStart()); if (d->m_transactionEditor->slotEditSplits() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { QString id; connect(d->m_transactionEditor, SIGNAL(balanceWarning(QWidget*,MyMoneyAccount,QString)), d->m_balanceWarning, SLOT(slotShowMessage(QWidget*,MyMoneyAccount,QString))); d->m_transactionEditor->enterTransactions(id); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to modify transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } deleteTransactionEditor(); slotUpdateActions(); } } void KMyMoneyApp::slotTransactionsCancel() { // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->action("transaction_cancel")->isEnabled()) { // make sure, we block the enter function action("transaction_enter")->setEnabled(false); // qDebug("KMyMoneyApp::slotTransactionsCancel"); deleteTransactionEditor(); slotUpdateActions(); } } void KMyMoneyApp::slotTransactionsEnter() { // since we jump here via code, we have to make sure to react only // if the action is enabled if (kmymoney->action("transaction_enter")->isEnabled()) { // disable the action while we process it to make sure it's processed only once since // d->m_transactionEditor->enterTransactions(newId) will run QCoreApplication::processEvents // we could end up here twice which will cause a crash slotUpdateActions() will enable the action again kmymoney->action("transaction_enter")->setEnabled(false); if (d->m_transactionEditor) { QString accountId = d->m_selectedAccount.id(); QString newId; connect(d->m_transactionEditor, SIGNAL(balanceWarning(QWidget*,MyMoneyAccount,QString)), d->m_balanceWarning, SLOT(slotShowMessage(QWidget*,MyMoneyAccount,QString))); if (d->m_transactionEditor->enterTransactions(newId)) { KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !newId.isEmpty()) { d->m_lastPayeeEnteredId = payeeEdit->selectedItem(); } deleteTransactionEditor(); } if (!newId.isEmpty()) { d->m_myMoneyView->slotLedgerSelected(accountId, newId); } } slotUpdateActions(); } } void KMyMoneyApp::slotTransactionsCancelOrEnter(bool& okToSelect) { static bool oneTime = false; if (!oneTime) { oneTime = true; QString dontShowAgain = "CancelOrEditTransaction"; // qDebug("KMyMoneyApp::slotCancelOrEndEdit"); if (d->m_transactionEditor) { if (KMyMoneyGlobalSettings::focusChangeIsEnter() && kmymoney->action("transaction_enter")->isEnabled()) { slotTransactionsEnter(); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } } else { // okToSelect is preset to true if a cancel of the dialog is useful and false if it is not int rc; KGuiItem noGuiItem = KStandardGuiItem::save(); KGuiItem yesGuiItem = KStandardGuiItem::discard(); KGuiItem cancelGuiItem = KStandardGuiItem::cont(); // if the transaction can't be entered make sure that it can't be entered by pressing no either if (!kmymoney->action("transaction_enter")->isEnabled()) { noGuiItem.setEnabled(false); noGuiItem.setToolTip(kmymoney->action("transaction_enter")->toolTip()); } if (okToSelect == true) { rc = KMessageBox::warningYesNoCancel(0, i18n("

Please select what you want to do: discard the changes, save the changes or continue to edit the transaction.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, cancelGuiItem, dontShowAgain); } else { rc = KMessageBox::warningYesNo(0, i18n("

Please select what you want to do: discard the changes, save the changes or continue to edit the transaction.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, dontShowAgain); } switch (rc) { case KMessageBox::Yes: slotTransactionsCancel(); break; case KMessageBox::No: slotTransactionsEnter(); // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } break; case KMessageBox::Cancel: // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); okToSelect = false; break; } } } oneTime = false; } } void KMyMoneyApp::slotToggleReconciliationFlag() { markTransaction(MyMoneySplit::Unknown); } void KMyMoneyApp::slotMarkTransactionCleared() { markTransaction(MyMoneySplit::Cleared); } void KMyMoneyApp::slotMarkTransactionReconciled() { markTransaction(MyMoneySplit::Reconciled); } void KMyMoneyApp::slotMarkTransactionNotReconciled() { markTransaction(MyMoneySplit::NotReconciled); } void KMyMoneyApp::markTransaction(MyMoneySplit::reconcileFlagE flag) { KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; int cnt = list.count(); int i = 0; slotStatusProgressBar(0, cnt); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // turn on signals before we modify the last entry in the list cnt--; MyMoneyFile::instance()->blockSignals(cnt != 0); // get a fresh copy MyMoneyTransaction t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); MyMoneySplit sp = t.splitById((*it_t).split().id()); if (sp.reconcileFlag() != flag) { if (flag == MyMoneySplit::Unknown) { if (d->m_reconciliationAccount.id().isEmpty()) { // in normal mode we cycle through all states switch (sp.reconcileFlag()) { case MyMoneySplit::NotReconciled: sp.setReconcileFlag(MyMoneySplit::Cleared); break; case MyMoneySplit::Cleared: sp.setReconcileFlag(MyMoneySplit::Reconciled); break; case MyMoneySplit::Reconciled: sp.setReconcileFlag(MyMoneySplit::NotReconciled); break; default: break; } } else { // in reconciliation mode we skip the reconciled state switch (sp.reconcileFlag()) { case MyMoneySplit::NotReconciled: sp.setReconcileFlag(MyMoneySplit::Cleared); break; case MyMoneySplit::Cleared: sp.setReconcileFlag(MyMoneySplit::NotReconciled); break; default: break; } } } else { sp.setReconcileFlag(flag); } t.modifySplit(sp); MyMoneyFile::instance()->modifyTransaction(t); } slotStatusProgressBar(i++, 0); } slotStatusProgressBar(-1, -1); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to modify transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotTransactionsAccept() { KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; int cnt = list.count(); int i = 0; slotStatusProgressBar(0, cnt); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // reload transaction in case it got changed during the course of this loop MyMoneyTransaction t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); if (t.isImported()) { t.setImported(false); if (!d->m_selectedAccount.id().isEmpty()) { QList list = t.splits(); QList::const_iterator it_s; for (it_s = list.constBegin(); it_s != list.constEnd(); ++it_s) { if ((*it_s).accountId() == d->m_selectedAccount.id()) { if ((*it_s).reconcileFlag() == MyMoneySplit::NotReconciled) { MyMoneySplit s = (*it_s); s.setReconcileFlag(MyMoneySplit::Cleared); t.modifySplit(s); } } } } MyMoneyFile::instance()->modifyTransaction(t); } if ((*it_t).split().isMatched()) { // reload split in case it got changed during the course of this loop MyMoneySplit s = t.splitById((*it_t).split().id()); TransactionMatcher matcher(d->m_selectedAccount); matcher.accept(t, s); } slotStatusProgressBar(i++, 0); } slotStatusProgressBar(-1, -1); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to accept transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KMyMoneyApp::slotTransactionGotoAccount() { if (!d->m_accountGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass a copy, as d->myMoneyView->slotLedgerSelected() overrides // d->m_accountGoto while calling slotUpdateActions() QString accountId = d->m_accountGoto; d->m_myMoneyView->slotLedgerSelected(accountId, transactionId); } catch (const MyMoneyException &) { } } } void KMyMoneyApp::slotTransactionGotoPayee() { if (!d->m_payeeGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass copies, as d->myMoneyView->slotPayeeSelected() overrides // d->m_payeeGoto and d->m_selectedAccount while calling slotUpdateActions() QString payeeId = d->m_payeeGoto; QString accountId = d->m_selectedAccount.id(); d->m_myMoneyView->slotPayeeSelected(payeeId, accountId, transactionId); } catch (const MyMoneyException &) { } } } void KMyMoneyApp::slotTransactionCreateSchedule() { if (d->m_selectedTransactions.count() == 1) { // make sure to have the current selected split as first split in the schedule MyMoneyTransaction t = d->m_selectedTransactions[0].transaction(); MyMoneySplit s = d->m_selectedTransactions[0].split(); QString splitId = s.id(); s.clearId(); s.setReconcileFlag(MyMoneySplit::NotReconciled); s.setReconcileDate(QDate()); t.removeSplits(); t.addSplit(s); const QList& splits = d->m_selectedTransactions[0].transaction().splits(); QList::const_iterator it_s; for (it_s = splits.begin(); it_s != splits.end(); ++it_s) { if ((*it_s).id() != splitId) { MyMoneySplit s0 = (*it_s); s0.clearId(); s0.setReconcileFlag(MyMoneySplit::NotReconciled); s0.setReconcileDate(QDate()); t.addSplit(s0); } } slotScheduleNew(t); } } void KMyMoneyApp::slotTransactionAssignNumber() { if (d->m_transactionEditor) d->m_transactionEditor->assignNextNumber(); } void KMyMoneyApp::slotTransactionCombine() { qDebug("slotTransactionCombine() not implemented yet"); } void KMyMoneyApp::slotTransactionCopySplits() { MyMoneyFile* file = MyMoneyFile::instance(); if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; KMyMoneyRegister::SelectedTransaction selectedSourceTransaction; foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: selectedSourceTransaction = st; multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { MyMoneyFileTransaction ft; try { const MyMoneyTransaction& sourceTransaction = selectedSourceTransaction.transaction(); const MyMoneySplit& sourceSplit = selectedSourceTransaction.split(); foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { MyMoneyTransaction t = st.transaction(); // don't process the source transaction if (sourceTransaction.id() == t.id()) { continue; } const MyMoneySplit& baseSplit = st.split(); if (t.splitCount() == 1) { foreach (const MyMoneySplit& split, sourceTransaction.splits()) { // Don't copy the source split, as we already have that // as part of the destination transaction if (split.id() == sourceSplit.id()) { continue; } MyMoneySplit sp(split); // clear the ID and reconciliation state sp.clearId(); sp.setReconcileFlag(MyMoneySplit::NotReconciled); sp.setReconcileDate(QDate()); // in case it is a simple transaction consisting of two splits, // we can adjust the share and value part of the second split we // just created. We need to keep a possible price in mind in case // of different currencies if (sourceTransaction.splitCount() == 2) { sp.setValue(-baseSplit.value()); sp.setShares(-(baseSplit.shares() * baseSplit.price())); } t.addSplit(sp); } file->modifyTransaction(t); } } ft.commit(); } catch (const MyMoneyException &) { qDebug() << "transactionCopySplits() failed"; } } } } void KMyMoneyApp::slotMoveToAccount(const QString& id) { // close the menu, if it is still open QWidget* w = factory()->container("transaction_context_menu", this); if (w && w->isVisible()) { w->close(); } if (!d->m_selectedTransactions.isEmpty()) { MyMoneyFileTransaction ft; try { KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = d->m_selectedTransactions.constBegin(); it_t != d->m_selectedTransactions.constEnd(); ++it_t) { if (d->m_selectedAccount.accountType() == MyMoneyAccount::Investment) { d->moveInvestmentTransaction(d->m_selectedAccount.id(), id, (*it_t).transaction()); } else { QList::const_iterator it_s; bool changed = false; MyMoneyTransaction t = (*it_t).transaction(); for (it_s = (*it_t).transaction().splits().constBegin(); it_s != (*it_t).transaction().splits().constEnd(); ++it_s) { if ((*it_s).accountId() == d->m_selectedAccount.id()) { MyMoneySplit s = (*it_s); s.setAccountId(id); t.modifySplit(s); changed = true; } } if (changed) { MyMoneyFile::instance()->modifyTransaction(t); } } } ft.commit(); } catch (const MyMoneyException &) { } } } // move a stock transaction from one investment account to another void KMyMoneyApp::Private::moveInvestmentTransaction(const QString& /*fromId*/, const QString& toId, const MyMoneyTransaction& tx) { MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); MyMoneyTransaction t(tx); // first determine which stock we are dealing with. // fortunately, investment transactions have only one stock involved QString stockAccountId; QString stockSecurityId; MyMoneySplit s; for (QList::const_iterator it_s = t.splits().constBegin(); it_s != t.splits().constEnd(); ++it_s) { stockAccountId = (*it_s).accountId(); stockSecurityId = MyMoneyFile::instance()->account(stockAccountId).currencyId(); if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { s = *it_s; break; } } // Now check the target investment account to see if it // contains a stock with this id QString newStockAccountId; QStringList accountList = toInvAcc.accountList(); for (QStringList::const_iterator it_a = accountList.constBegin(); it_a != accountList.constEnd(); ++it_a) { if (MyMoneyFile::instance()->account((*it_a)).currencyId() == stockSecurityId) { newStockAccountId = (*it_a); break; } } // if it doesn't exist, we need to add it as a copy of the old one // no 'copyAccount()' function?? if (newStockAccountId.isEmpty()) { MyMoneyAccount stockAccount = MyMoneyFile::instance()->account(stockAccountId); MyMoneyAccount newStock; newStock.setName(stockAccount.name()); newStock.setNumber(stockAccount.number()); newStock.setDescription(stockAccount.description()); newStock.setInstitutionId(stockAccount.institutionId()); newStock.setOpeningDate(stockAccount.openingDate()); newStock.setAccountType(stockAccount.accountType()); newStock.setCurrencyId(stockAccount.currencyId()); newStock.setClosed(stockAccount.isClosed()); MyMoneyFile::instance()->addAccount(newStock, toInvAcc); newStockAccountId = newStock.id(); } // now update the split and the transaction s.setAccountId(newStockAccountId); t.modifySplit(s); MyMoneyFile::instance()->modifyTransaction(t); } void KMyMoneyApp::slotUpdateMoveToAccountMenu() { createTransactionMoveMenu(); // in case we were not able to create the selector, we // better get out of here. Anything else would cause // a crash later on (accountSet.load) if (!d->m_moveToAccountSelector) return; if (!d->m_selectedAccount.id().isEmpty()) { AccountSet accountSet; if (d->m_selectedAccount.accountType() == MyMoneyAccount::Investment) { accountSet.addAccountType(MyMoneyAccount::Investment); } else if (d->m_selectedAccount.isAssetLiability()) { accountSet.addAccountType(MyMoneyAccount::Checkings); accountSet.addAccountType(MyMoneyAccount::Savings); accountSet.addAccountType(MyMoneyAccount::Cash); accountSet.addAccountType(MyMoneyAccount::AssetLoan); accountSet.addAccountType(MyMoneyAccount::CertificateDep); accountSet.addAccountType(MyMoneyAccount::MoneyMarket); accountSet.addAccountType(MyMoneyAccount::Asset); accountSet.addAccountType(MyMoneyAccount::Currency); accountSet.addAccountType(MyMoneyAccount::CreditCard); accountSet.addAccountType(MyMoneyAccount::Loan); accountSet.addAccountType(MyMoneyAccount::Liability); } else if (d->m_selectedAccount.isIncomeExpense()) { accountSet.addAccountType(MyMoneyAccount::Income); accountSet.addAccountType(MyMoneyAccount::Expense); } accountSet.load(d->m_moveToAccountSelector); // remove those accounts that we currently reference KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = d->m_selectedTransactions.constBegin(); it_t != d->m_selectedTransactions.constEnd(); ++it_t) { QList::const_iterator it_s; for (it_s = (*it_t).transaction().splits().constBegin(); it_s != (*it_t).transaction().splits().constEnd(); ++it_s) { d->m_moveToAccountSelector->removeItem((*it_s).accountId()); } } // remove those accounts from the list that are denominated // in a different currency QStringList list = d->m_moveToAccountSelector->accountList(); QList::const_iterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { MyMoneyAccount acc = MyMoneyFile::instance()->account(*it_a); if (acc.currencyId() != d->m_selectedAccount.currencyId()) d->m_moveToAccountSelector->removeItem((*it_a)); } } } void KMyMoneyApp::slotTransactionMatch() { // if the menu action is retrieved it can contain an '&' character for the accelerator causing the comparison to fail if not removed QString transactionActionText = action("transaction_match")->text(); transactionActionText.remove('&'); if (transactionActionText == i18nc("Button text for match transaction", "Match")) transactionMatch(); else transactionUnmatch(); } void KMyMoneyApp::transactionUnmatch() { KMyMoneyRegister::SelectedTransactions::const_iterator it; MyMoneyFileTransaction ft; try { for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).split().isMatched()) { TransactionMatcher matcher(d->m_selectedAccount); matcher.unmatch((*it).transaction(), (*it).split()); } } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to unmatch the selected transactions"), e.what()); } } void KMyMoneyApp::transactionMatch() { if (d->m_selectedTransactions.count() != 2) return; MyMoneyTransaction startMatchTransaction; MyMoneyTransaction endMatchTransaction; MyMoneySplit startSplit; MyMoneySplit endSplit; KMyMoneyRegister::SelectedTransactions::const_iterator it; KMyMoneyRegister::SelectedTransactions toBeDeleted; for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) { if (endMatchTransaction.id().isEmpty()) { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } else { //This is a second imported transaction, we still want to merge startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } } else if (!(*it).split().isMatched()) { if (startMatchTransaction.id().isEmpty()) { startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } else { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } } } #if 0 KMergeTransactionsDlg dlg(d->m_selectedAccount); dlg.addTransaction(startMatchTransaction); dlg.addTransaction(endMatchTransaction); if (dlg.exec() == QDialog::Accepted) #endif { MyMoneyFileTransaction ft; try { if (startMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(i18n("No manually entered transaction selected for matching")); if (endMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(i18n("No imported transaction selected for matching")); TransactionMatcher matcher(d->m_selectedAccount); matcher.match(startMatchTransaction, startSplit, endMatchTransaction, endSplit, true); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to match the selected transactions"), e.what()); } } } void KMyMoneyApp::showContextMenu(const QString& containerName) { QWidget* w = factory()->container(containerName, this); QMenu *menu = dynamic_cast(w); if (menu) menu->exec(QCursor::pos()); else qDebug("menu '%s' not found: w = %p, menu = %p", qPrintable(containerName), w, menu); } void KMyMoneyApp::slotShowTransactionContextMenu() { if (d->m_selectedTransactions.isEmpty() && d->m_selectedSchedule != MyMoneySchedule()) { showContextMenu("schedule_context_menu"); } else { showContextMenu("transaction_context_menu"); } } void KMyMoneyApp::slotShowInvestmentContextMenu() { showContextMenu("investment_context_menu"); } void KMyMoneyApp::slotShowScheduleContextMenu() { showContextMenu("schedule_context_menu"); } void KMyMoneyApp::slotShowAccountContextMenu(const MyMoneyObject& obj) { // qDebug("KMyMoneyApp::slotShowAccountContextMenu"); if (typeid(obj) != typeid(MyMoneyAccount)) return; const MyMoneyAccount& acc = dynamic_cast(obj); // if the selected account is actually a stock account, we // call the right slot instead if (acc.isInvest()) { showContextMenu("investment_context_menu"); } else if (acc.isIncomeExpense()) { showContextMenu("category_context_menu"); } else { showContextMenu("account_context_menu"); } } void KMyMoneyApp::slotShowInstitutionContextMenu(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyInstitution)) return; showContextMenu("institution_context_menu"); } void KMyMoneyApp::slotShowPayeeContextMenu() { showContextMenu("payee_context_menu"); } void KMyMoneyApp::slotShowTagContextMenu() { showContextMenu("tag_context_menu"); } void KMyMoneyApp::slotShowBudgetContextMenu() { showContextMenu("budget_context_menu"); } void KMyMoneyApp::slotShowCurrencyContextMenu() { showContextMenu("currency_context_menu"); } void KMyMoneyApp::slotShowPriceContextMenu() { showContextMenu("price_context_menu"); } void KMyMoneyApp::slotShowOnlineJobContextMenu() { showContextMenu("onlinejob_context_menu"); } void KMyMoneyApp::slotPrintView() { d->m_myMoneyView->slotPrintView(); } void KMyMoneyApp::updateCaption(bool skipActions) { QString caption; caption = d->m_fileName.fileName(); if (caption.isEmpty() && d->m_myMoneyView && d->m_myMoneyView->fileOpen()) caption = i18n("Untitled"); // MyMoneyFile::instance()->dirty() throws an exception, if // there's no storage object available. In this case, we // assume that the storage object is not changed. Actually, // this can only happen if we are newly created early on. bool modified; try { modified = MyMoneyFile::instance()->dirty(); } catch (const MyMoneyException &) { modified = false; skipActions = true; } #ifdef KMM_DEBUG caption += QString(" (%1 x %2)").arg(width()).arg(height()); #endif setCaption(caption, modified); if (!skipActions) { d->m_myMoneyView->enableViewsIfFileOpen(); slotUpdateActions(); } } void KMyMoneyApp::slotUpdateActions() { MyMoneyFile* file = MyMoneyFile::instance(); bool fileOpen = d->m_myMoneyView->fileOpen(); bool modified = file->dirty(); QWidget* w; action("open_database")->setEnabled(true); action("saveas_database")->setEnabled(fileOpen); action("file_save")->setEnabled(modified && !d->m_myMoneyView->isDatabase()); action("file_save_as")->setEnabled(fileOpen); action("file_close")->setEnabled(fileOpen); action("view_personal_data")->setEnabled(fileOpen); action("file_backup")->setEnabled(fileOpen && !d->m_myMoneyView->isDatabase()); action("file_print")->setEnabled(fileOpen && d->m_myMoneyView->canPrint()); #ifdef KMM_DEBUG action("view_file_info")->setEnabled(fileOpen); action("file_dump")->setEnabled(fileOpen); #endif action("edit_find_transaction")->setEnabled(fileOpen); bool importRunning = (d->m_qifReader != 0) || (d->m_smtReader != 0); action("file_export_qif")->setEnabled(fileOpen && !importRunning); action("file_import_qif")->setEnabled(fileOpen && !importRunning); action("file_import_gnc")->setEnabled(!importRunning); action("file_import_template")->setEnabled(fileOpen && !importRunning); action("file_export_template")->setEnabled(fileOpen && !importRunning); action("tools_currency_editor")->setEnabled(fileOpen); action("tools_price_editor")->setEnabled(fileOpen); action("tools_update_prices")->setEnabled(fileOpen); action("tools_consistency_check")->setEnabled(fileOpen); action("account_new")->setEnabled(fileOpen); action("account_reconcile")->setEnabled(false); action("account_reconcile_finish")->setEnabled(false); action("account_reconcile_postpone")->setEnabled(false); action("account_edit")->setEnabled(false); action("account_delete")->setEnabled(false); action("account_open")->setEnabled(false); action("account_close")->setEnabled(false); action("account_reopen")->setEnabled(false); action("account_transaction_report")->setEnabled(false); action("account_online_map")->setEnabled(false); action("account_online_update")->setEnabled(false); action("account_online_update_all")->setEnabled(false); action("account_online_unmap")->setEnabled(false); action("account_online_new_credit_transfer")->setEnabled(onlineJobAdministration::instance()->canSendCreditTransfer()); action("account_chart")->setEnabled(false); action("category_new")->setEnabled(fileOpen); action("category_edit")->setEnabled(false); action("category_delete")->setEnabled(false); action("institution_new")->setEnabled(fileOpen); action("institution_edit")->setEnabled(false); action("institution_delete")->setEnabled(false); action("investment_new")->setEnabled(false); action("investment_edit")->setEnabled(false); action("investment_delete")->setEnabled(false); action("investment_online_price_update")->setEnabled(false); action("investment_manual_price_update")->setEnabled(false); action("schedule_edit")->setEnabled(false); action("schedule_delete")->setEnabled(false); action("schedule_enter")->setEnabled(false); action("schedule_skip")->setEnabled(false); action("payee_delete")->setEnabled(false); action("payee_rename")->setEnabled(false); action("payee_merge")->setEnabled(false); action("tag_delete")->setEnabled(false); action("tag_rename")->setEnabled(false); action("budget_delete")->setEnabled(false); action("budget_rename")->setEnabled(false); action("budget_change_year")->setEnabled(false); action("budget_new")->setEnabled(true); action("budget_copy")->setEnabled(false); action("budget_forecast")->setEnabled(false); QString tooltip = i18n("Create a new transaction"); action("transaction_new")->setEnabled(fileOpen && d->m_myMoneyView->canCreateTransactions(KMyMoneyRegister::SelectedTransactions(), tooltip)); action("transaction_new")->setToolTip(tooltip); action("transaction_edit")->setEnabled(false); action("transaction_editsplits")->setEnabled(false); action("transaction_enter")->setEnabled(false); action("transaction_cancel")->setEnabled(false); action("transaction_delete")->setEnabled(false); action("transaction_match")->setEnabled(false); action("transaction_match")->setText(i18nc("Button text for match transaction", "Match")); action("transaction_accept")->setEnabled(false); action("transaction_duplicate")->setEnabled(false); action("transaction_mark_toggle")->setEnabled(false); action("transaction_mark_cleared")->setEnabled(false); action("transaction_mark_reconciled")->setEnabled(false); action("transaction_mark_notreconciled")->setEnabled(false); action("transaction_goto_account")->setEnabled(false); action("transaction_goto_payee")->setEnabled(false); action("transaction_assign_number")->setEnabled(false); action("transaction_create_schedule")->setEnabled(false); action("transaction_combine")->setEnabled(false); action("transaction_select_all")->setEnabled(false); action("transaction_copy_splits")->setEnabled(false); action("schedule_new")->setEnabled(fileOpen); action("schedule_edit")->setEnabled(false); action("schedule_delete")->setEnabled(false); action("schedule_duplicate")->setEnabled(false); action("schedule_enter")->setEnabled(false); action("schedule_skip")->setEnabled(false); action("currency_new")->setEnabled(fileOpen); action("currency_rename")->setEnabled(false); action("currency_delete")->setEnabled(false); action("currency_setbase")->setEnabled(false); action("price_new")->setEnabled(fileOpen); action("price_edit")->setEnabled(false); action("price_delete")->setEnabled(false); action("price_update")->setEnabled(false); w = factory()->container("transaction_move_menu", this); if (w) w->setEnabled(false); w = factory()->container("transaction_mark_menu", this); if (w) w->setEnabled(false); w = factory()->container("transaction_context_mark_menu", this); if (w) w->setEnabled(false); // FIXME for now it's always on, but we should only allow it, if we // can select at least a single transaction action("transaction_select_all")->setEnabled(true); if (!d->m_selectedTransactions.isEmpty()) { // enable 'delete transaction' only if at least one of the // selected transactions does not reference a closed account bool enable = false; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = d->m_selectedTransactions.constBegin(); (enable == false) && (it_t != d->m_selectedTransactions.constEnd()); ++it_t) { enable = !(*it_t).transaction().id().isEmpty() && !file->referencesClosedAccount((*it_t).transaction()); } action("transaction_delete")->setEnabled(enable); if (!d->m_transactionEditor) { tooltip = i18n("Duplicate the current selected transactions"); action("transaction_duplicate")->setEnabled(d->m_myMoneyView->canDuplicateTransactions(d->m_selectedTransactions, tooltip) && !d->m_selectedTransactions[0].transaction().id().isEmpty()); action("transaction_duplicate")->setToolTip(tooltip); if (d->m_myMoneyView->canEditTransactions(d->m_selectedTransactions, tooltip)) { action("transaction_edit")->setEnabled(true); // editing splits is allowed only if we have one transaction selected if (d->m_selectedTransactions.count() == 1) { action("transaction_editsplits")->setEnabled(true); } if (d->m_selectedAccount.isAssetLiability() && d->m_selectedAccount.accountType() != MyMoneyAccount::Investment) { action("transaction_create_schedule")->setEnabled(d->m_selectedTransactions.count() == 1); } } action("transaction_edit")->setToolTip(tooltip); if (!d->m_selectedAccount.isClosed()) { w = factory()->container("transaction_move_menu", this); if (w) w->setEnabled(true); } w = factory()->container("transaction_mark_menu", this); if (w) w->setEnabled(true); w = factory()->container("transaction_context_mark_menu", this); if (w) w->setEnabled(true); // Allow marking the transaction if at least one is selected action("transaction_mark_cleared")->setEnabled(true); action("transaction_mark_reconciled")->setEnabled(true); action("transaction_mark_notreconciled")->setEnabled(true); action("transaction_mark_toggle")->setEnabled(true); if (!d->m_accountGoto.isEmpty()) action("transaction_goto_account")->setEnabled(true); if (!d->m_payeeGoto.isEmpty()) action("transaction_goto_payee")->setEnabled(true); // Matching is enabled as soon as one regular and one imported transaction is selected int matchedCount = 0; int importedCount = 0; KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) ++importedCount; if ((*it).split().isMatched()) ++matchedCount; } if (d->m_selectedTransactions.count() == 2 /* && action("transaction_edit")->isEnabled() */) { action("transaction_match")->setEnabled(true); } if (importedCount != 0 || matchedCount != 0) action("transaction_accept")->setEnabled(true); if (matchedCount != 0) { action("transaction_match")->setEnabled(true); action("transaction_match")->setText(i18nc("Button text for unmatch transaction", "Unmatch")); action("transaction_match")->setIcon(QIcon("process-stop")); } if (d->m_selectedTransactions.count() > 1) { action("transaction_combine")->setEnabled(true); } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { action("transaction_copy_splits")->setEnabled(true); } } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach(const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch(st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if(singleSplitTransactions > 0 && multipleSplitTransactions == 1) { action("transaction_copy_splits")->setEnabled(true); } } } else { action("transaction_assign_number")->setEnabled(d->m_transactionEditor->canAssignNumber()); action("transaction_new")->setEnabled(false); action("transaction_delete")->setEnabled(false); QString reason; action("transaction_enter")->setEnabled(d->m_transactionEditor->isComplete(reason)); //FIXME: Port to KDE4 // the next line somehow worked in KDE3 but does not have // any influence under KDE4 /// Works for me when 'reason' is set. Allan action("transaction_enter")->setToolTip(reason); action("transaction_cancel")->setEnabled(true); } } QList accList; file->accountList(accList); QList::const_iterator it_a; QMap::const_iterator it_p = d->m_onlinePlugins.constEnd(); for (it_a = accList.constBegin(); (it_p == d->m_onlinePlugins.constEnd()) && (it_a != accList.constEnd()); ++it_a) { if (!(*it_a).onlineBankingSettings().value("provider").isEmpty()) { // check if provider is available it_p = d->m_onlinePlugins.constFind((*it_a).onlineBankingSettings().value("provider")); if (it_p != d->m_onlinePlugins.constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (protocols.count() > 0) { action("account_online_update_all")->setEnabled(true); action("account_online_update_menu")->setEnabled(true); } } } } MyMoneyFileBitArray skip(IMyMoneyStorage::MaxRefCheckBits); if (!d->m_selectedAccount.id().isEmpty()) { if (!file->isStandardAccount(d->m_selectedAccount.id())) { switch (d->m_selectedAccount.accountGroup()) { case MyMoneyAccount::Asset: case MyMoneyAccount::Liability: case MyMoneyAccount::Equity: action("account_transaction_report")->setEnabled(true); action("account_edit")->setEnabled(true); action("account_delete")->setEnabled(!file->isReferenced(d->m_selectedAccount)); action("account_open")->setEnabled(true); if (d->m_selectedAccount.accountGroup() != MyMoneyAccount::Equity) { if (d->m_reconciliationAccount.id().isEmpty()) { action("account_reconcile")->setEnabled(true); action("account_reconcile")->setToolTip(i18n("Reconcile")); } else { QString tip; tip = i18n("Reconcile - disabled because you are currently reconciling %1", d->m_reconciliationAccount.name()); action("account_reconcile")->setToolTip(tip); if (!d->m_transactionEditor) { action("account_reconcile_finish")->setEnabled(d->m_selectedAccount.id() == d->m_reconciliationAccount.id()); action("account_reconcile_postpone")->setEnabled(d->m_selectedAccount.id() == d->m_reconciliationAccount.id()); } } } if (d->m_selectedAccount.accountType() == MyMoneyAccount::Investment) action("investment_new")->setEnabled(true); if (d->m_selectedAccount.isClosed()) action("account_reopen")->setEnabled(true); else enableCloseAccountAction(d->m_selectedAccount); if (!d->m_selectedAccount.onlineBankingSettings().value("provider").isEmpty()) { action("account_online_unmap")->setEnabled(true); // check if provider is available QMap::const_iterator it_p; it_p = d->m_onlinePlugins.constFind(d->m_selectedAccount.onlineBankingSettings().value("provider")); if (it_p != d->m_onlinePlugins.constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (protocols.count() > 0) { action("account_online_update")->setEnabled(true); action("account_online_update_menu")->setEnabled(true); } } } else { action("account_online_map")->setEnabled(d->m_onlinePlugins.count() > 0); } action("account_chart")->setEnabled(true); break; case MyMoneyAccount::Income : case MyMoneyAccount::Expense : action("category_edit")->setEnabled(true); // enable delete action, if category/account itself is not referenced // by any object except accounts, because we want to allow // deleting of sub-categories. Also, we allow transactions, schedules and budgets // to be present because we can re-assign them during the delete process skip.fill(false); skip.setBit(IMyMoneyStorage::RefCheckTransaction); skip.setBit(IMyMoneyStorage::RefCheckAccount); skip.setBit(IMyMoneyStorage::RefCheckSchedule); skip.setBit(IMyMoneyStorage::RefCheckBudget); action("category_delete")->setEnabled(!file->isReferenced(d->m_selectedAccount, skip)); action("account_open")->setEnabled(true); break; default: break; } } } if (!d->m_selectedInstitution.id().isEmpty()) { action("institution_edit")->setEnabled(true); action("institution_delete")->setEnabled(!file->isReferenced(d->m_selectedInstitution)); } if (!d->m_selectedInvestment.id().isEmpty()) { action("investment_edit")->setEnabled(true); action("investment_delete")->setEnabled(!file->isReferenced(d->m_selectedInvestment)); action("investment_manual_price_update")->setEnabled(true); try { MyMoneySecurity security = MyMoneyFile::instance()->security(d->m_selectedInvestment.currencyId()); if (!security.value("kmm-online-source").isEmpty()) action("investment_online_price_update")->setEnabled(true); } catch (const MyMoneyException &e) { qDebug("Error retrieving security for investment %s: %s", qPrintable(d->m_selectedInvestment.name()), qPrintable(e.what())); } if (d->m_selectedInvestment.isClosed()) action("account_reopen")->setEnabled(true); else enableCloseAccountAction(d->m_selectedInvestment); } if (!d->m_selectedSchedule.id().isEmpty()) { action("schedule_edit")->setEnabled(true); action("schedule_duplicate")->setEnabled(true); action("schedule_delete")->setEnabled(!file->isReferenced(d->m_selectedSchedule)); if (!d->m_selectedSchedule.isFinished()) { action("schedule_enter")->setEnabled(true); // a schedule with a single occurrence cannot be skipped if (d->m_selectedSchedule.occurrence() != MyMoneySchedule::OCCUR_ONCE) { action("schedule_skip")->setEnabled(true); } } } if (d->m_selectedPayees.count() >= 1) { action("payee_rename")->setEnabled(d->m_selectedPayees.count() == 1); action("payee_merge")->setEnabled(d->m_selectedPayees.count() > 1); action("payee_delete")->setEnabled(true); } if (d->m_selectedTags.count() >= 1) { action("tag_rename")->setEnabled(d->m_selectedTags.count() == 1); action("tag_delete")->setEnabled(true); } if (d->m_selectedBudgets.count() >= 1) { action("budget_delete")->setEnabled(true); if (d->m_selectedBudgets.count() == 1) { action("budget_change_year")->setEnabled(true); action("budget_copy")->setEnabled(true); action("budget_rename")->setEnabled(true); action("budget_forecast")->setEnabled(true); } } if (!d->m_selectedCurrency.id().isEmpty()) { action("currency_rename")->setEnabled(true); // no need to check each transaction. accounts are enough in this case skip.fill(false); skip.setBit(IMyMoneyStorage::RefCheckTransaction); action("currency_delete")->setEnabled(!file->isReferenced(d->m_selectedCurrency, skip)); if (d->m_selectedCurrency.id() != file->baseCurrency().id()) action("currency_setbase")->setEnabled(true); } if (!d->m_selectedPrice.from().isEmpty() && d->m_selectedPrice.source() != "KMyMoney") { action("price_edit")->setEnabled(true); action("price_delete")->setEnabled(true); //enable online update if it is a currency MyMoneySecurity security = MyMoneyFile::instance()->security(d->m_selectedPrice.from()); action("price_update")->setEnabled(security.isCurrency()); } } void KMyMoneyApp::slotResetSelections() { slotSelectAccount(); slotSelectInstitution(); slotSelectInvestment(); slotSelectSchedule(); slotSelectCurrency(); slotSelectPrice(); slotSelectPayees(QList()); slotSelectTags(QList()); slotSelectBudget(QList()); slotSelectTransactions(KMyMoneyRegister::SelectedTransactions()); slotUpdateActions(); } void KMyMoneyApp::slotSelectCurrency(const MyMoneySecurity& currency) { d->m_selectedCurrency = currency; slotUpdateActions(); emit currencySelected(d->m_selectedCurrency); } void KMyMoneyApp::slotSelectPrice(const MyMoneyPrice& price) { d->m_selectedPrice = price; slotUpdateActions(); emit priceSelected(d->m_selectedPrice); } void KMyMoneyApp::slotSelectBudget(const QList& list) { d->m_selectedBudgets = list; slotUpdateActions(); emit budgetSelected(d->m_selectedBudgets); } void KMyMoneyApp::slotSelectPayees(const QList& list) { d->m_selectedPayees = list; slotUpdateActions(); emit payeesSelected(d->m_selectedPayees); } void KMyMoneyApp::slotSelectTags(const QList& list) { d->m_selectedTags = list; slotUpdateActions(); emit tagsSelected(d->m_selectedTags); } void KMyMoneyApp::slotSelectTransactions(const KMyMoneyRegister::SelectedTransactions& list) { // list can either contain a list of transactions or a single selected scheduled transaction // in the latter case, the transaction id is actually the one of the schedule. In order // to differentiate between the two, we just ask for the schedule. If we don't find one - because // we passed the id of a real transaction - then we know that fact. We use the schedule here, // because the list of schedules is kept in a cache by MyMoneyFile. This way, we save some trips // to the backend which we would have to do if we check for the transaction. d->m_selectedTransactions.clear(); d->m_selectedSchedule = MyMoneySchedule(); d->m_accountGoto.clear(); d->m_payeeGoto.clear(); if (!list.isEmpty() && !list.first().isScheduled()) { d->m_selectedTransactions = list; if (list.count() == 1) { const MyMoneySplit& sp = d->m_selectedTransactions[0].split(); if (!sp.payeeId().isEmpty()) { try { MyMoneyPayee payee = MyMoneyFile::instance()->payee(sp.payeeId()); if (!payee.name().isEmpty()) { d->m_payeeGoto = payee.id(); QString name = payee.name(); name.replace(QRegExp("&(?!&)"), "&&"); action("transaction_goto_payee")->setText(i18n("Go to '%1'", name)); } } catch (const MyMoneyException &) { } } try { QList::const_iterator it_s; const MyMoneyTransaction& t = d->m_selectedTransactions[0].transaction(); // search the first non-income/non-expense accunt and use it for the 'goto account' const MyMoneySplit& sp = d->m_selectedTransactions[0].split(); for (it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s) { if ((*it_s).id() != sp.id()) { MyMoneyAccount acc = MyMoneyFile::instance()->account((*it_s).accountId()); if (!acc.isIncomeExpense()) { // for stock accounts we show the portfolio account if (acc.isInvest()) { acc = MyMoneyFile::instance()->account(acc.parentAccountId()); } d->m_accountGoto = acc.id(); QString name = acc.name(); name.replace(QRegExp("&(?!&)"), "&&"); action("transaction_goto_account")->setText(i18n("Go to '%1'", name)); break; } } } } catch (const MyMoneyException &) { } } slotUpdateActions(); emit transactionsSelected(d->m_selectedTransactions); } else if (!list.isEmpty()) { slotSelectSchedule(MyMoneyFile::instance()->schedule(list.first().scheduleId())); } else { slotUpdateActions(); } // make sure, we show some neutral menu entry if we don't have an object if (d->m_payeeGoto.isEmpty()) action("transaction_goto_payee")->setText(i18n("Go to payee")); if (d->m_accountGoto.isEmpty()) action("transaction_goto_account")->setText(i18n("Go to account")); } void KMyMoneyApp::slotSelectInstitution(const MyMoneyObject& institution) { if (typeid(institution) != typeid(MyMoneyInstitution)) return; d->m_selectedInstitution = dynamic_cast(institution); // qDebug("slotSelectInstitution('%s')", d->m_selectedInstitution.name().data()); slotUpdateActions(); emit institutionSelected(d->m_selectedInstitution); } void KMyMoneyApp::slotSelectAccount(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyAccount)) return; d->m_selectedAccount = MyMoneyAccount(); const MyMoneyAccount& acc = dynamic_cast(obj); if (!acc.isInvest()) d->m_selectedAccount = acc; // qDebug("slotSelectAccount('%s')", d->m_selectedAccount.name().data()); slotUpdateActions(); emit accountSelected(d->m_selectedAccount); } void KMyMoneyApp::slotSelectInvestment(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyAccount)) return; // qDebug("slotSelectInvestment('%s')", account.name().data()); d->m_selectedInvestment = MyMoneyAccount(); const MyMoneyAccount& acc = dynamic_cast(obj); if (acc.isInvest()) d->m_selectedInvestment = acc; slotUpdateActions(); emit investmentSelected(d->m_selectedInvestment); } void KMyMoneyApp::slotSelectSchedule(const MyMoneySchedule& schedule) { // qDebug("slotSelectSchedule('%s')", schedule.name().data()); d->m_selectedSchedule = schedule; slotUpdateActions(); emit scheduleSelected(d->m_selectedSchedule); } void KMyMoneyApp::slotDataChanged() { // As this method is called every time the MyMoneyFile instance // notifies a modification, it's the perfect place to start the timer if needed if (d->m_autoSaveEnabled && !d->m_autoSaveTimer->isActive()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); //miliseconds } updateCaption(); } void KMyMoneyApp::slotCurrencyDialog() { QPointer dlg = new KCurrencyEditDlg(this); connect(dlg, SIGNAL(selectObject(MyMoneySecurity)), this, SLOT(slotSelectCurrency(MyMoneySecurity))); connect(dlg, SIGNAL(openContextMenu(MyMoneySecurity)), this, SLOT(slotShowCurrencyContextMenu())); connect(this, SIGNAL(currencyRename()), dlg, SLOT(slotStartRename())); connect(dlg, SIGNAL(updateCurrency(QString,QString,QString)), this, SLOT(slotCurrencyUpdate(QString,QString,QString))); connect(this, SIGNAL(currencyCreated(QString)), dlg, SLOT(slotSelectCurrency(QString))); connect(dlg, SIGNAL(selectBaseCurrency(MyMoneySecurity)), this, SLOT(slotCurrencySetBase())); dlg->exec(); delete dlg; slotSelectCurrency(MyMoneySecurity()); } void KMyMoneyApp::slotPriceDialog() { QPointer dlg = new KMyMoneyPriceDlg(this); connect(dlg, SIGNAL(selectObject(MyMoneyPrice)), this, SLOT(slotSelectPrice(MyMoneyPrice))); connect(dlg, SIGNAL(openContextMenu(MyMoneyPrice)), this, SLOT(slotShowPriceContextMenu())); connect(this, SIGNAL(priceNew()), dlg, SLOT(slotNewPrice())); connect(this, SIGNAL(priceEdit()), dlg, SLOT(slotEditPrice())); connect(this, SIGNAL(priceDelete()), dlg, SLOT(slotDeletePrice())); connect(this, SIGNAL(priceOnlineUpdate()), dlg, SLOT(slotOnlinePriceUpdate())); dlg->exec(); } void KMyMoneyApp::slotFileConsistencyCheck() { d->consistencyCheck(true); updateCaption(); } void KMyMoneyApp::Private::consistencyCheck(bool alwaysDisplayResult) { KMSTATUS(i18n("Running consistency check...")); MyMoneyFileTransaction ft; try { m_consistencyCheckResult = MyMoneyFile::instance()->consistencyCheck(); ft.commit(); } catch (const MyMoneyException &e) { m_consistencyCheckResult.append(i18n("Consistency check failed: %1", e.what())); // always display the result if the check failed alwaysDisplayResult = true; } // in case the consistency check was OK, we get a single line as result // in all errneous cases, we get more than one line and force the // display of them. if (alwaysDisplayResult || m_consistencyCheckResult.size() > 1) { QString msg = i18n("The consistency check has found no issues in your data. Details are presented below."); if (m_consistencyCheckResult.size() > 1) msg = i18n("The consistency check has found some issues in your data. Details are presented below. Those issues that could not be corrected automatically need to be solved by the user."); // install a context menu for the list after the dialog is displayed QTimer::singleShot(500, q, SLOT(slotInstallConsistencyCheckContextMenu())); KMessageBox::informationList(0, msg, m_consistencyCheckResult, i18n("Consistency check result")); } // this data is no longer needed m_consistencyCheckResult.clear(); } void KMyMoneyApp::Private::copyConsistencyCheckResults() { QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(m_consistencyCheckResult.join(QLatin1String("\n"))); } void KMyMoneyApp::Private::saveConsistencyCheckResults() { QUrl fileUrl = QFileDialog::getSaveFileUrl(q); if (!fileUrl.isEmpty()) { QFile file(fileUrl.toLocalFile()); if (file.open(QFile::WriteOnly | QFile::Append | QFile::Text)) { QTextStream out(&file); out << m_consistencyCheckResult.join(QLatin1String("\n")); file.close(); } } } void KMyMoneyApp::Private::setCustomColors() { // setup the kmymoney custom colors if needed if (KMyMoneyGlobalSettings::useSystemColors()) { qApp->setStyleSheet(QString()); } else { qApp->setStyleSheet("QTreeView, QTableView#register, QTableView#m_register, QTableView#splittable, QListView { background-color: " + KMyMoneyGlobalSettings::listBGColor().name() + ';' + "alternate-background-color: " + KMyMoneyGlobalSettings::listColor().name() + ';' + "background-clip: content;}"); } } void KMyMoneyApp::slotCheckSchedules() { if (KMyMoneyGlobalSettings::checkSchedule() == true) { KMSTATUS(i18n("Checking for overdue scheduled transactions...")); MyMoneyFile *file = MyMoneyFile::instance(); QDate checkDate = QDate::currentDate().addDays(KMyMoneyGlobalSettings::checkSchedulePreview()); QList scheduleList = file->scheduleList(); QList::Iterator it; KMyMoneyUtils::EnterScheduleResultCodeE rc = KMyMoneyUtils::Enter; for (it = scheduleList.begin(); (it != scheduleList.end()) && (rc != KMyMoneyUtils::Cancel); ++it) { // Get the copy in the file because it might be modified by commitTransaction MyMoneySchedule schedule = file->schedule((*it).id()); if (schedule.autoEnter()) { try { while (!schedule.isFinished() && (schedule.adjustedNextDueDate() <= checkDate) && rc != KMyMoneyUtils::Ignore && rc != KMyMoneyUtils::Cancel) { rc = enterSchedule(schedule, true, true); schedule = file->schedule((*it).id()); // get a copy of the modified schedule } } catch (const MyMoneyException &) { } } if (rc == KMyMoneyUtils::Ignore) { // if the current schedule was ignored then we must make sure that the user can still enter the next scheduled transaction rc = KMyMoneyUtils::Enter; } } updateCaption(); } } void KMyMoneyApp::writeLastUsedDir(const QString& directory) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = kconfig->group("General Options"); //write path entry, no error handling since its void. grp.writeEntry("LastUsedDirectory", directory); } } void KMyMoneyApp::writeLastUsedFile(const QString& fileName) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // write path entry, no error handling since its void. // use a standard string, as fileName could contain a protocol // e.g. file:/home/thb/.... grp.writeEntry("LastUsedFile", fileName); } } QString KMyMoneyApp::readLastUsedDir() const { QString str; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); //read path entry. Second parameter is the default if the setting is not found, which will be the default document path. str = grp.readEntry("LastUsedDirectory", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); // if the path stored is empty, we use the default nevertheless if (str.isEmpty()) str = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } return str; } QString KMyMoneyApp::readLastUsedFile() const { QString str; // get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // read filename entry. str = grp.readEntry("LastUsedFile", ""); } return str; } QString KMyMoneyApp::filename() const { return d->m_fileName.url(); } QList KMyMoneyApp::instanceList() const { QList list; #ifdef KMM_DBUS QDBusReply reply = QDBusConnection::sessionBus().interface()->registeredServiceNames(); if (reply.isValid()) { QStringList apps = reply.value(); QStringList::ConstIterator it; // build a list of service names of all running kmymoney applications without this one for (it = apps.constBegin(); it != apps.constEnd(); ++it) { // please shange this method of creating a list of 'all the other kmymoney instances that are running on the system' // since assuming that D-Bus creates service names with org.kde.kmymoney-PID is an observation I don't think that it's documented somwhere if ((*it).indexOf("org.kde.kmymoney-") == 0) { #ifdef _MSC_VER uint thisProcPid = _getpid(); #else uint thisProcPid = getpid(); #endif if ((*it).indexOf(QString("org.kde.kmymoney-%1").arg(thisProcPid)) != 0) list += (*it); } } } else { qDebug("D-Bus returned the following error while obtaining instances: %s", qPrintable(reply.error().message())); } #endif return list; } void KMyMoneyApp::slotEquityPriceUpdate() { QPointer dlg = new KEquityPriceUpdateDlg(this); if (dlg->exec() == QDialog::Accepted && dlg != 0) dlg->storePrices(); delete dlg; } void KMyMoneyApp::webConnect(const QString& sourceUrl, const QByteArray& asn_id) { // // Web connect attempts to go through the known importers and see if the file // can be importing using that method. If so, it will import it using that // plugin // Q_UNUSED(asn_id) d->m_importUrlsQueue.enqueue(sourceUrl); // only start processing if this is the only import so far if (d->m_importUrlsQueue.count() == 1) { while (!d->m_importUrlsQueue.isEmpty()) { // get the value of the next item from the queue // but leave it on the queue for now QString url = d->m_importUrlsQueue.head(); // Bring this window to the forefront. This method was suggested by // Lubos Lunak of the KDE core development team. // TODO: port KF5 //KStartupInfo::setNewStartupId(this, asn_id); // Make sure we have an open file if (! d->m_myMoneyView->fileOpen() && KMessageBox::warningContinueCancel(kmymoney, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) kmymoney->slotFileOpen(); // only continue if the user really did open a file. if (d->m_myMoneyView->fileOpen()) { KMSTATUS(i18n("Importing a statement via Web Connect")); // remove the statement files d->unlinkStatementXML(); QMap::const_iterator it_plugin = d->m_importerPlugins.constBegin(); while (it_plugin != d->m_importerPlugins.constEnd()) { if ((*it_plugin)->isMyFormat(url)) { QList statements; if (!(*it_plugin)->import(url)) { KMessageBox::error(this, i18n("Unable to import %1 using %2 plugin. The plugin returned the following error: %3", url, (*it_plugin)->formatName(), (*it_plugin)->lastError()), i18n("Importing error")); } break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == d->m_importerPlugins.constEnd()) if (MyMoneyStatement::isStatementFile(url)) slotStatementImport(url); } // remove the current processed item from the queue d->m_importUrlsQueue.dequeue(); } } } void KMyMoneyApp::slotEnableMessages() { KMessageBox::enableAllMessages(); KMessageBox::information(this, i18n("All messages have been enabled."), i18n("All messages")); } void KMyMoneyApp::createInterfaces() { // Sets up the plugin interface KMyMoneyPlugin::pluginInterfaces().importInterface = new KMyMoneyPlugin::KMMImportInterface(this, this); KMyMoneyPlugin::pluginInterfaces().statementInterface = new KMyMoneyPlugin::KMMStatementInterface(this, this); KMyMoneyPlugin::pluginInterfaces().viewInterface = new KMyMoneyPlugin::KMMViewInterface(this, d->m_myMoneyView, this); // setup the calendar interface for schedules MyMoneySchedule::setProcessingCalendar(this); } void KMyMoneyApp::loadPlugins() { Q_ASSERT(!d->m_pluginLoader); d->m_pluginLoader = new KMyMoneyPlugin::PluginLoader(this); //! @todo Junior Job: Improve the config read system KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup group{ config->group("Plugins") }; const auto plugins = KPluginLoader::findPlugins("kmymoney"); d->m_pluginLoader->addPluginInfo(plugins); for (const KPluginMetaData & pluginData : plugins) { // Only load plugins which are enabled and have the right serviceType. Other serviceTypes are loaded on demand. if (isPluginEnabled(pluginData, group)) slotPluginLoad(pluginData); } connect(d->m_pluginLoader, &KMyMoneyPlugin::PluginLoader::pluginEnabled, this, &KMyMoneyApp::slotPluginLoad); connect(d->m_pluginLoader, &KMyMoneyPlugin::PluginLoader::pluginDisabled, this, &KMyMoneyApp::slotPluginUnload); } inline bool KMyMoneyApp::isPluginEnabled(const KPluginMetaData& metaData, const KConfigGroup& configGroup) { //! @fixme: there is a function in KMyMoneyPlugin::PluginLoader which has to have the same content if (metaData.serviceTypes().contains("KMyMoney/Plugin")) { const QString keyName{metaData.name() + "Enabled"}; if (configGroup.hasKey(keyName)) return configGroup.readEntry(keyName, true); return metaData.isEnabledByDefault(); } return false; } void KMyMoneyApp::slotPluginLoad(const KPluginMetaData& metaData) { std::unique_ptr loader{new QPluginLoader{metaData.fileName()}}; QObject* plugin = loader->instance(); if (!plugin) { qWarning("Could not load plugin '%s', error: %s", qPrintable(metaData.fileName()), qPrintable(loader->errorString())); return; } if ( d->m_plugins.contains(metaData.fileName()) ) { /** @fixme Handle a reload e.g. objectNames are equal but the object are different (plugin != d->m_plugins[plugin->objectName()]) * Also it could be usefull to drop the dependence on objectName() */ /* Note: there is nothing to delete here because if the plugin was loaded already, plugin points to the same object as the previously loaded one. */ return; } KMyMoneyPlugin::Plugin* kmmPlugin = qobject_cast(plugin); if (!kmmPlugin) { qWarning("Could not load plugin '%s'.", qPrintable(metaData.fileName())); return; } // check for online plugin KMyMoneyPlugin::OnlinePlugin* op = dynamic_cast(plugin); // check for extended online plugin KMyMoneyPlugin::OnlinePluginExtended* ope = dynamic_cast(plugin); // check for importer plugin KMyMoneyPlugin::ImporterPlugin* ip = dynamic_cast(plugin); // Tell the plugin it is about to get plugged kmmPlugin->plug(); // plug the plugin guiFactory()->addClient(kmmPlugin); d->m_plugins[metaData.fileName()] = kmmPlugin; if (op) d->m_onlinePlugins[plugin->objectName()] = op; if (ope) onlineJobAdministration::instance()->addPlugin(plugin->objectName(), ope); if (ip) d->m_importerPlugins[plugin->objectName()] = ip; slotUpdateActions(); } void KMyMoneyApp::slotPluginUnload(const KPluginMetaData& metaData) { KMyMoneyPlugin::Plugin* plugin = d->m_plugins[metaData.fileName()]; // Remove and test if the plugin was actually loaded if (!d->m_plugins.remove(metaData.fileName()) || plugin == nullptr) return; // check for online plugin KMyMoneyPlugin::OnlinePlugin* op = dynamic_cast(plugin); // check for importer plugin KMyMoneyPlugin::ImporterPlugin* ip = dynamic_cast(plugin); // unplug the plugin guiFactory()->removeClient(plugin); if (op) d->m_onlinePlugins.remove(plugin->objectName()); if (ip) d->m_importerPlugins.remove(plugin->objectName()); plugin->unplug(); slotUpdateActions(); } void KMyMoneyApp::slotAutoSave() { if (!d->m_inAutoSaving) { // store the focus widget so we can restore it after save QPointer focusWidget = qApp->focusWidget(); d->m_inAutoSaving = true; KMSTATUS(i18n("Auto saving...")); //calls slotFileSave if needed, and restart the timer //it the file is not saved, reinitializes the countdown. if (d->m_myMoneyView->dirty() && d->m_autoSaveEnabled) { if (!slotFileSave() && d->m_autoSavePeriod > 0) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } } d->m_inAutoSaving = false; if (focusWidget && focusWidget != qApp->focusWidget()) { // we have a valid focus widget so restore it focusWidget->setFocus(); } } } void KMyMoneyApp::slotDateChanged() { QDateTime dt = QDateTime::currentDateTime(); QDateTime nextDay(QDate(dt.date().addDays(1)), QTime(0, 0, 0)); // +1 is to make sure that we're already in the next day when the // signal is sent (this way we also avoid setting the timer to 0) QTimer::singleShot((dt.secsTo(nextDay) + 1)*1000, this, SLOT(slotDateChanged())); d->m_myMoneyView->slotRefreshViews(); } const MyMoneyAccount& KMyMoneyApp::account(const QString& key, const QString& value) const { QList list; QList::const_iterator it_a; MyMoneyFile::instance()->accountList(list); QList accList; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { // search in the account's kvp container const QString& accountKvpValue = (*it_a).value(key); // search in the account's online settings kvp container const QString& onlineSettingsKvpValue = (*it_a).onlineBankingSettings().value(key); if (accountKvpValue.contains(value) || onlineSettingsKvpValue.contains(value)) { accList << MyMoneyFile::instance()->account((*it_a).id()); } if (accountKvpValue == value || onlineSettingsKvpValue == value) { return MyMoneyFile::instance()->account((*it_a).id()); } } // if we did not find an exact match of the value, we take the one that partially // matched, but only if not more than one matched partially. if (accList.count() == 1) { return accList.first(); } // return reference to empty element return MyMoneyFile::instance()->account(QString()); } void KMyMoneyApp::setAccountOnlineParameters(const MyMoneyAccount& _acc, const MyMoneyKeyValueContainer& kvps) { MyMoneyFileTransaction ft; try { MyMoneyAccount acc = MyMoneyFile::instance()->account(_acc.id()); acc.setOnlineBankingSettings(kvps); MyMoneyFile::instance()->modifyAccount(acc); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to setup online parameters for account '%1'", _acc.name()), e.what()); } } void KMyMoneyApp::slotAccountUnmapOnline() { // no account selected if (d->m_selectedAccount.id().isEmpty()) return; // not a mapped account if (d->m_selectedAccount.onlineBankingSettings().value("provider").isEmpty()) return; if (KMessageBox::warningYesNo(this, QString("%1").arg(i18n("Do you really want to remove the mapping of account %1 to an online account? Depending on the details of the online banking method used, this action cannot be reverted.", d->m_selectedAccount.name())), i18n("Remove mapping to online account")) == KMessageBox::Yes) { MyMoneyFileTransaction ft; try { d->m_selectedAccount.setOnlineBankingSettings(MyMoneyKeyValueContainer()); // delete the kvp that is used in MyMoneyStatementReader too // we should really get rid of it, but since I don't know what it // is good for, I'll keep it around. (ipwizard) d->m_selectedAccount.deletePair("StatementKey"); MyMoneyFile::instance()->modifyAccount(d->m_selectedAccount); ft.commit(); // The mapping could disable the online task system onlineJobAdministration::instance()->updateOnlineTaskProperties(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to unmap account from online account: %1", e.what())); } } } void KMyMoneyApp::slotAccountMapOnline() { // no account selected if (d->m_selectedAccount.id().isEmpty()) return; // already an account mapped if (!d->m_selectedAccount.onlineBankingSettings().value("provider").isEmpty()) return; // check if user tries to map a brokerageAccount if (d->m_selectedAccount.name().contains(i18n(" (Brokerage)"))) { if (KMessageBox::warningContinueCancel(this, i18n("You try to map a brokerage account to an online account. This is usually not advisable. In general, the investment account should be mapped to the online account. Please cancel if you intended to map the investment account, continue otherwise"), i18n("Mapping brokerage account")) == KMessageBox::Cancel) { return; } } // if we have more than one provider let the user select the current provider QString provider; QMap::const_iterator it_p; switch (d->m_onlinePlugins.count()) { case 0: break; case 1: provider = d->m_onlinePlugins.begin().key(); break; default: { QMenu popup(this); popup.setTitle(i18n("Select online banking plugin")); // Populate the pick list with all the provider for (it_p = d->m_onlinePlugins.constBegin(); it_p != d->m_onlinePlugins.constEnd(); ++it_p) { popup.addAction(it_p.key())->setData(it_p.key()); } QAction *item = popup.actions()[0]; if (item) { popup.setActiveAction(item); } // cancelled if ((item = popup.exec(QCursor::pos(), item)) == 0) { return; } provider = item->data().toString(); } break; } if (provider.isEmpty()) return; // find the provider it_p = d->m_onlinePlugins.constFind(provider); if (it_p != d->m_onlinePlugins.constEnd()) { // plugin found, call it MyMoneyKeyValueContainer settings; if ((*it_p)->mapAccount(d->m_selectedAccount, settings)) { settings["provider"] = provider; MyMoneyAccount acc(d->m_selectedAccount); acc.setOnlineBankingSettings(settings); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(acc); ft.commit(); // The mapping could enable the online task system onlineJobAdministration::instance()->updateOnlineTaskProperties(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to map account to online account: %1", e.what())); } } } } void KMyMoneyApp::slotAccountUpdateOnlineAll() { QList accList; MyMoneyFile::instance()->accountList(accList); QList::iterator it_a; QMap::const_iterator it_p; d->m_statementResults.clear(); d->m_collectingStatements = true; // remove all those from the list, that don't have a 'provider' or the // provider is not currently present for (it_a = accList.begin(); it_a != accList.end();) { if ((*it_a).onlineBankingSettings().value("provider").isEmpty() || d->m_onlinePlugins.find((*it_a).onlineBankingSettings().value("provider")) == d->m_onlinePlugins.end()) { it_a = accList.erase(it_a); } else ++it_a; } action("account_online_update")->setEnabled(false); action("account_online_update_menu")->setEnabled(false); action("account_online_update_all")->setEnabled(false); // now work on the remaining list of accounts int cnt = accList.count() - 1; for (it_a = accList.begin(); it_a != accList.end(); ++it_a) { it_p = d->m_onlinePlugins.constFind((*it_a).onlineBankingSettings().value("provider")); (*it_p)->updateAccount(*it_a, cnt != 0); --cnt; } d->m_collectingStatements = false; if (!d->m_statementResults.isEmpty()) KMessageBox::informationList(this, i18n("The statements have been processed with the following results:"), d->m_statementResults, i18n("Statement stats")); // re-enable the disabled actions slotUpdateActions(); } void KMyMoneyApp::slotAccountUpdateOnline() { // no account selected if (d->m_selectedAccount.id().isEmpty()) return; // no online account mapped if (d->m_selectedAccount.onlineBankingSettings().value("provider").isEmpty()) return; action("account_online_update")->setEnabled(false); action("account_online_update_menu")->setEnabled(false); action("account_online_update_all")->setEnabled(false); // find the provider QMap::const_iterator it_p; it_p = d->m_onlinePlugins.constFind(d->m_selectedAccount.onlineBankingSettings().value("provider")); if (it_p != d->m_onlinePlugins.constEnd()) { // plugin found, call it d->m_collectingStatements = true; d->m_statementResults.clear(); (*it_p)->updateAccount(d->m_selectedAccount); d->m_collectingStatements = false; if (!d->m_statementResults.isEmpty()) KMessageBox::informationList(this, i18n("The statements have been processed with the following results:"), d->m_statementResults, i18n("Statement stats")); } // re-enable the disabled actions slotUpdateActions(); } void KMyMoneyApp::slotNewOnlineTransfer() { kOnlineTransferForm *transferForm = new kOnlineTransferForm(this); if (!d->m_selectedAccount.id().isEmpty()) { transferForm->setCurrentAccount(d->m_selectedAccount.id()); } connect(transferForm, SIGNAL(rejected()), transferForm, SLOT(deleteLater())); connect(transferForm, SIGNAL(acceptedForSave(onlineJob)), this, SLOT(slotOnlineJobSave(onlineJob))); connect(transferForm, SIGNAL(acceptedForSend(onlineJob)), this, SLOT(slotOnlineJobSend(onlineJob))); connect(transferForm, SIGNAL(accepted()), transferForm, SLOT(deleteLater())); transferForm->show(); } void KMyMoneyApp::slotEditOnlineJob(const QString jobId) { try { const onlineJob constJob = MyMoneyFile::instance()->getOnlineJob(jobId); slotEditOnlineJob(constJob); } catch (MyMoneyException&) { // Prevent a crash in very rare cases } } void KMyMoneyApp::slotEditOnlineJob(onlineJob job) { try { slotEditOnlineJob(onlineJobTyped(job)); } catch (MyMoneyException&) { } } void KMyMoneyApp::slotEditOnlineJob(const onlineJobTyped job) { kOnlineTransferForm *transferForm = new kOnlineTransferForm(this); transferForm->setOnlineJob(job); connect(transferForm, SIGNAL(rejected()), transferForm, SLOT(deleteLater())); connect(transferForm, SIGNAL(acceptedForSave(onlineJob)), this, SLOT(slotOnlineJobSave(onlineJob))); connect(transferForm, SIGNAL(acceptedForSend(onlineJob)), this, SLOT(slotOnlineJobSend(onlineJob))); connect(transferForm, SIGNAL(accepted()), transferForm, SLOT(deleteLater())); transferForm->show(); } void KMyMoneyApp::slotOnlineJobSave(onlineJob job) { MyMoneyFileTransaction fileTransaction; if (job.id() == MyMoneyObject::emptyId()) MyMoneyFile::instance()->addOnlineJob(job); else MyMoneyFile::instance()->modifyOnlineJob(job); fileTransaction.commit(); } /** @todo when onlineJob queue is used, continue here */ void KMyMoneyApp::slotOnlineJobSend(onlineJob job) { MyMoneyFileTransaction fileTransaction; if (job.id() == MyMoneyObject::emptyId()) MyMoneyFile::instance()->addOnlineJob(job); else MyMoneyFile::instance()->modifyOnlineJob(job); fileTransaction.commit(); QList jobList; jobList.append(job); slotOnlineJobSend(jobList); } void KMyMoneyApp::slotOnlineJobSend(QList jobs) { MyMoneyFile *const kmmFile = MyMoneyFile::instance(); QMultiMap jobsByPlugin; // Sort jobs by online plugin & lock them foreach (onlineJob job, jobs) { Q_ASSERT(job.id() != MyMoneyObject::emptyId()); // find the provider const MyMoneyAccount originAcc = job.responsibleMyMoneyAccount(); job.setLock(); job.addJobMessage(onlineJobMessage(onlineJobMessage::debug, "KMyMoneyApp::slotOnlineJobSend", "Added to queue for plugin '" + originAcc.onlineBankingSettings().value("provider") + '\'')); MyMoneyFileTransaction fileTransaction; kmmFile->modifyOnlineJob(job); fileTransaction.commit(); jobsByPlugin.insert(originAcc.onlineBankingSettings().value("provider"), job); } // Send onlineJobs to plugins QList usedPlugins = jobsByPlugin.keys(); std::sort(usedPlugins.begin(), usedPlugins.end()); const QList::iterator newEnd = std::unique(usedPlugins.begin(), usedPlugins.end()); usedPlugins.erase(newEnd, usedPlugins.end()); foreach (const QString& pluginKey, usedPlugins) { QMap::const_iterator it_p = d->m_onlinePlugins.constFind(pluginKey); if (it_p != d->m_onlinePlugins.constEnd()) { // plugin found, call it KMyMoneyPlugin::OnlinePluginExtended *pluginExt = dynamic_cast< KMyMoneyPlugin::OnlinePluginExtended* >(*it_p); if (pluginExt == 0) { qWarning("Job given for plugin which is not an extended plugin"); continue; } //! @fixme remove debug message qDebug() << "Sending " << jobsByPlugin.count(pluginKey) << " job(s) to online plugin " << pluginKey; QList jobsToExecute = jobsByPlugin.values(pluginKey); QList executedJobs = jobsToExecute; pluginExt->sendOnlineJob(executedJobs); // Save possible changes of the online job and remove lock MyMoneyFileTransaction fileTransaction; foreach (onlineJob job, executedJobs) { fileTransaction.restart(); job.setLock(false); kmmFile->modifyOnlineJob(job); fileTransaction.commit(); } if (Q_UNLIKELY(executedJobs.size() != jobsToExecute.size())) { // OnlinePlugin did not return all jobs qWarning() << "Error saving send online tasks. After restart you should see at minimum all successfully executed jobs marked send. Imperfect plugin: " << pluginExt->objectName(); } } else { qWarning() << "Error, got onlineJob for an account without online plugin."; /** @FIXME can this actually happen? */ } } } void KMyMoneyApp::slotOnlineJobLog() { QStringList jobIds = d->m_myMoneyView->getOnlineJobOutbox()->selectedOnlineJobs(); slotOnlineJobLog(jobIds); } void KMyMoneyApp::slotOnlineJobLog(const QStringList& onlineJobIds) { onlineJobMessagesView *const dialog = new onlineJobMessagesView(); onlineJobMessagesModel *const model = new onlineJobMessagesModel(dialog); model->setOnlineJob(MyMoneyFile::instance()->getOnlineJob(onlineJobIds.first())); dialog->setModel(model); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); // Note: Objects are not deleted here, Qt's parent-child system has to do that. } void KMyMoneyApp::setHolidayRegion(const QString& holidayRegion) { #ifdef KF5Holidays_FOUND //since the cost of updating the cache is now not negligible //check whether the region has been modified if (!d->m_holidayRegion || d->m_holidayRegion->regionCode() != holidayRegion) { // Delete the previous holidayRegion before creating a new one. delete d->m_holidayRegion; // Create a new holidayRegion. d->m_holidayRegion = new KHolidays::HolidayRegion(holidayRegion); //clear and update the holiday cache preloadHolidays(); } #else Q_UNUSED(holidayRegion); #endif } bool KMyMoneyApp::isProcessingDate(const QDate& date) const { #ifdef KF5Holidays_FOUND if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; if (!d->m_holidayRegion || !d->m_holidayRegion->isValid()) return true; //check first whether it's already in cache if (d->m_holidayMap.contains(date)) { return d->m_holidayMap.value(date, true); } else { bool processingDay = !d->m_holidayRegion->isHoliday(date); d->m_holidayMap.insert(date, processingDay); return processingDay; } #else Q_UNUSED(date); return true; #endif } void KMyMoneyApp::preloadHolidays() { #ifdef KF5Holidays_FOUND //clear the cache before loading d->m_holidayMap.clear(); //only do this if it is a valid region if (d->m_holidayRegion && d->m_holidayRegion->isValid()) { //load holidays for the forecast days plus 1 cycle, to be on the safe side int forecastDays = KMyMoneyGlobalSettings::forecastDays() + KMyMoneyGlobalSettings::forecastAccountCycle(); QDate endDate = QDate::currentDate().addDays(forecastDays); //look for holidays for the next 2 years as a minimum. That should give a good margin for the cache if (endDate < QDate::currentDate().addYears(2)) endDate = QDate::currentDate().addYears(2); KHolidays::Holiday::List holidayList = d->m_holidayRegion->holidays(QDate::currentDate(), endDate); KHolidays::Holiday::List::const_iterator holiday_it; for (holiday_it = holidayList.constBegin(); holiday_it != holidayList.constEnd(); ++holiday_it) { for (QDate holidayDate = (*holiday_it).observedStartDate(); holidayDate <= (*holiday_it).observedEndDate(); holidayDate = holidayDate.addDays(1)) d->m_holidayMap.insert(holidayDate, false); } for (QDate date = QDate::currentDate(); date <= endDate; date = date.addDays(1)) { //if it is not a processing day, set it to false if (!d->m_processingDays.testBit(date.dayOfWeek())) { d->m_holidayMap.insert(date, false); } else if (!d->m_holidayMap.contains(date)) { //if it is not a holiday nor a weekend, it is a processing day d->m_holidayMap.insert(date, true); } } } #endif } KMStatus::KMStatus(const QString &text) { m_prevText = kmymoney->slotStatusMsg(text); } KMStatus::~KMStatus() { kmymoney->slotStatusMsg(m_prevText); } void KMyMoneyApp::Private::unlinkStatementXML() { QDir d("/home/thb", "kmm-statement*"); for (uint i = 0; i < d.count(); ++i) { qDebug("Remove %s", qPrintable(d[i])); //FIXME: FIX on windows d.remove(QString("/home/thb/%1").arg(d[i])); } m_statementXMLindex = 0; } void KMyMoneyApp::Private::closeFile() { q->slotSelectAccount(); q->slotSelectInstitution(); q->slotSelectInvestment(); q->slotSelectSchedule(); q->slotSelectCurrency(); q->slotSelectBudget(QList()); q->slotSelectPayees(QList()); q->slotSelectTags(QList()); q->slotSelectTransactions(KMyMoneyRegister::SelectedTransactions()); m_reconciliationAccount = MyMoneyAccount(); m_myMoneyView->finishReconciliation(m_reconciliationAccount); m_myMoneyView->closeFile(); m_fileName = QUrl(); q->updateCaption(); // just create a new balance warning object delete m_balanceWarning; m_balanceWarning = new KBalanceWarning(q); emit q->fileLoaded(m_fileName); } diff --git a/kmymoney/kmymoney.kcfg b/kmymoney/kmymoney.kcfg index f6d01a52b..0045c8672 100644 --- a/kmymoney/kmymoney.kcfg +++ b/kmymoney/kmymoney.kcfg @@ -1,442 +1,451 @@ false 1 10 true true false false 10 0 20 10 0 60 false false 0 0 999 (None) 0 0 999 false false false false true true 0 0 false false false true true 0 true true false account,cashflow,payee,category,tag,memo,number,date,amount,state number,date,payee,category,tag,memo,payment,deposit,state 8,1,2,3,4,5,6,7,-9,10 100 true true false false false false false false false false false false false true true true 0 0 11 1 1 28 QDateTime::fromString("1900-01-01T00:00:00", Qt::ISODate) + + + + + + + + + true 240,240,240 255,255,255 154,154,154 255,255,0 152,251,152 255,0,0 0,0,255 255,0,0 255,242,155 255,255,221 false 1,-9,-4 1,-4 1,-9,-4 0 4 false 0 90 1 999 30 1 999 3 1 999 0 0 31 1 true true true Bar Banca,Banque Kreditkarte And. Kto. Eröffnungssaldo Rechnung false 10 0 100 2 0 256 2 0 2 diff --git a/kmymoney/mymoney/mymoneyfile.cpp b/kmymoney/mymoney/mymoneyfile.cpp index e81b02c65..c368091c5 100644 --- a/kmymoney/mymoney/mymoneyfile.cpp +++ b/kmymoney/mymoney/mymoneyfile.cpp @@ -1,3508 +1,3540 @@ /*************************************************************************** mymoneyfile.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2002, 2007-2011 by Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 "mymoneyfile.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "storage/mymoneyseqaccessmgr.h" #include "mymoneyaccount.h" #include "mymoneyreport.h" #include "mymoneybalancecache.h" #include "mymoneybudget.h" #include "mymoneyprice.h" #include "mymoneyobjectcontainer.h" #include "mymoneypayee.h" //#include "mymoneytag.h" // include the following line to get a 'cout' for debug purposes // #include -const QString MyMoneyFile::OpeningBalancesPrefix = I18N_NOOP("Opening Balances"); const QString MyMoneyFile::AccountSeperator = QChar(':'); MyMoneyFile MyMoneyFile::file; typedef QList > BalanceNotifyList; typedef QMap CacheNotifyList; /// @todo make this template based class MyMoneyNotification { public: MyMoneyNotification(MyMoneyFile::notificationModeT mode, const MyMoneyTransaction& t) : m_objType(MyMoneyFile::notifyTransaction), m_notificationMode(mode), m_id(t.id()) { } MyMoneyNotification(MyMoneyFile::notificationModeT mode, const MyMoneyAccount& acc) : m_objType(MyMoneyFile::notifyAccount), m_notificationMode(mode), m_id(acc.id()) { } MyMoneyNotification(MyMoneyFile::notificationModeT mode, const MyMoneyInstitution& institution) : m_objType(MyMoneyFile::notifyInstitution), m_notificationMode(mode), m_id(institution.id()) { } MyMoneyNotification(MyMoneyFile::notificationModeT mode, const MyMoneyPayee& payee) : m_objType(MyMoneyFile::notifyPayee), m_notificationMode(mode), m_id(payee.id()) { } MyMoneyNotification(MyMoneyFile::notificationModeT mode, const MyMoneyTag& tag) : m_objType(MyMoneyFile::notifyTag), m_notificationMode(mode), m_id(tag.id()) { } MyMoneyNotification(MyMoneyFile::notificationModeT mode, const MyMoneySchedule& schedule) : m_objType(MyMoneyFile::notifySchedule), m_notificationMode(mode), m_id(schedule.id()) { } MyMoneyNotification(MyMoneyFile::notificationModeT mode, const MyMoneySecurity& security) : m_objType(MyMoneyFile::notifySecurity), m_notificationMode(mode), m_id(security.id()) { } MyMoneyNotification(MyMoneyFile::notificationModeT mode, const onlineJob& job) : m_objType(MyMoneyFile::notifyOnlineJob), m_notificationMode(mode), m_id(job.id()) { } MyMoneyFile::notificationObjectT objectType() const { return m_objType; } MyMoneyFile::notificationModeT notificationMode() const { return m_notificationMode; } const QString& id() const { return m_id; } protected: MyMoneyNotification(MyMoneyFile::notificationObjectT obj, MyMoneyFile::notificationModeT mode, const QString& id) : m_objType(obj), m_notificationMode(mode), m_id(id) {} private: MyMoneyFile::notificationObjectT m_objType; MyMoneyFile::notificationModeT m_notificationMode; QString m_id; }; class MyMoneyFile::Private { public: Private() : m_storage(0), m_inTransaction(false) {} ~Private() { delete m_storage; } /** * This method is used to add an id to the list of objects * to be removed from the cache. If id is empty, then nothing is added to the list. * * @param id id of object to be notified * @param reload reload the object (@c true) or not (@c false). The default is @c true * @see attach, detach */ void addCacheNotification(const QString& id, bool reload = true) { if (!id.isEmpty()) m_notificationList[id] = reload; } void addCacheNotification(const QString& id, const QDate& date, bool reload = true) { if (!id.isEmpty()) { m_notificationList[id] = reload; m_balanceNotifyList.append(std::make_pair(id, date)); } } /** * This method is used to clear the notification list */ void clearCacheNotification() { // reset list to be empty m_notificationList.clear(); m_balanceNotifyList.clear(); } /** * This method is used to clear all * objects mentioned in m_notificationList from the cache. */ void notify() { QMap::ConstIterator it = m_notificationList.constBegin(); while (it != m_notificationList.constEnd()) { if (*it) m_cache.refresh(it.key()); else m_cache.clear(it.key()); ++it; } foreach (const BalanceNotifyList::value_type & i, m_balanceNotifyList) { m_balanceChangedSet += i.first; if (i.second.isValid()) { m_balanceCache.clear(i.first, i.second); } else { m_balanceCache.clear(i.first); } } clearCacheNotification(); } /** * This method checks if a storage object is attached and * throws and exception if not. */ inline void checkStorage() const { if (m_storage == 0) throw MYMONEYEXCEPTION("No storage object attached to MyMoneyFile"); } /** * This method checks that a transaction has been started with * startTransaction() and throws an exception otherwise. Calls * checkStorage() to make sure a storage object is present and attached. */ void checkTransaction(const char* txt) const { checkStorage(); if (!m_inTransaction) { throw MYMONEYEXCEPTION(QString("No transaction started for %1").arg(txt)); } } void priceChanged(const MyMoneyFile& file, const MyMoneyPrice price) { // get all affected accounts and add them to the m_valueChangedSet QList accList; file.accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { QString currencyId = account_it->currencyId(); if (currencyId != file.baseCurrency().id() && (currencyId == price.from() || currencyId == price.to())) { // this account is not in the base currency and the price affects it's value m_valueChangedSet.insert(account_it->id()); } } } /** * This member points to the storage strategy */ IMyMoneyStorage *m_storage; bool m_inTransaction; MyMoneySecurity m_baseCurrency; /** * @brief Cache for MyMoneyObjects * * It is also used to emit the objectAdded() and objectModified() signals. * => If one of these signals is used, you must use this cache. */ MyMoneyObjectContainer m_cache; MyMoneyPriceList m_priceCache; MyMoneyBalanceCache m_balanceCache; /** * This member keeps a list of ids to notify after a single * operation is completed. The boolean is used as follows * during processing of the list: * * false - don't reload the object immediately * true - reload the object immediately */ CacheNotifyList m_notificationList; /** * This member keeps a list of account ids to notify * after a single operation is completed. The balance cache * is cleared for that account and all dates on or after * the one supplied. If the date is invalid, the entire * balance cache is cleared for that account. */ BalanceNotifyList m_balanceNotifyList; /** * This member keeps a list of account ids for which * a balanceChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_balanceChangedSet; /** * This member keeps a list of account ids for which * a valueChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_valueChangedSet; /** * This member keeps the list of changes in the engine * in historical order. The type can be 'added', 'modified' * or removed. */ QList m_changeSet; }; class MyMoneyNotifier { public: MyMoneyNotifier(MyMoneyFile::Private* file) { m_file = file; m_file->clearCacheNotification(); }; ~MyMoneyNotifier() { m_file->notify(); }; private: MyMoneyFile::Private* m_file; }; MyMoneyFile::MyMoneyFile() : d(new Private) { } MyMoneyFile::~MyMoneyFile() { delete d; } MyMoneyFile::MyMoneyFile(IMyMoneyStorage *storage) : d(new Private) { attachStorage(storage); } void MyMoneyFile::attachStorage(IMyMoneyStorage* const storage) { if (d->m_storage != 0) throw MYMONEYEXCEPTION("Storage already attached"); if (storage == 0) throw MYMONEYEXCEPTION("Storage must not be 0"); d->m_storage = storage; // force reload of base currency d->m_baseCurrency = MyMoneySecurity(); // and the whole cache d->m_balanceCache.clear(); d->m_cache.clear(storage); d->m_priceCache.clear(); preloadCache(); // notify application about new data availability emit beginChangeNotification(); emit dataChanged(); emit endChangeNotification(); } void MyMoneyFile::detachStorage(IMyMoneyStorage* const /* storage */) { d->m_balanceCache.clear(); d->m_cache.clear(); d->m_priceCache.clear(); d->m_storage = 0; } IMyMoneyStorage* MyMoneyFile::storage() const { return d->m_storage; } bool MyMoneyFile::storageAttached() const { return d->m_storage != 0; } void MyMoneyFile::startTransaction() { d->checkStorage(); if (d->m_inTransaction) { throw MYMONEYEXCEPTION("Already started a transaction!"); } d->m_storage->startTransaction(); d->m_inTransaction = true; d->m_changeSet.clear(); } bool MyMoneyFile::hasTransaction() const { return d->m_inTransaction; } void MyMoneyFile::commitTransaction() { d->checkTransaction(Q_FUNC_INFO); // commit the transaction in the storage bool changed = d->m_storage->commitTransaction(); d->m_inTransaction = false; // inform the outside world about the beginning of notifications emit beginChangeNotification(); // Now it's time to send out some signals to the outside world // First we go through the d->m_changeSet and emit respective // signals about addition, modification and removal of engine objects QList::const_iterator it = d->m_changeSet.constBegin(); while (it != d->m_changeSet.constEnd()) { if ((*it).notificationMode() == notifyRemove) { emit objectRemoved((*it).objectType(), (*it).id()); // if there is a balance change recorded for this account remove it since the account itself will be removed // this can happen when deleting categories that have transactions and the reassign category feature was used d->m_balanceChangedSet.remove((*it).id()); } else { const MyMoneyObject * obj = 0; MyMoneyTransaction tr; switch((*it).objectType()) { case MyMoneyFile::notifyTransaction: tr = transaction((*it).id()); obj = &tr; break; default: obj = d->m_cache.object((*it).id()); break; } if (obj) { if ((*it).notificationMode() == notifyAdd) { emit objectAdded((*it).objectType(), obj); } else { emit objectModified((*it).objectType(), obj); } } } ++it; } // we're done with the change set, so we clear it d->m_changeSet.clear(); // now send out the balanceChanged signal for all those // accounts for which we have an indication about a possible // change. foreach (const QString& id, d->m_balanceChangedSet) { // if we notify about balance change we don't need to notify about value change // for the same account since a balance change implies a value change d->m_valueChangedSet.remove(id); const MyMoneyAccount& acc = d->m_cache.account(id); emit balanceChanged(acc); } d->m_balanceChangedSet.clear(); // now notify about the remaining value changes foreach (const QString& id, d->m_valueChangedSet) { const MyMoneyAccount& acc = d->m_cache.account(id); emit valueChanged(acc); } d->m_valueChangedSet.clear(); // as a last action, send out the global dataChanged signal if (changed) { emit dataChanged(); } // inform the outside world about the end of notifications emit endChangeNotification(); } void MyMoneyFile::rollbackTransaction() { d->checkTransaction(Q_FUNC_INFO); d->m_storage->rollbackTransaction(); d->m_inTransaction = false; preloadCache(); d->m_balanceChangedSet.clear(); d->m_valueChangedSet.clear(); d->m_changeSet.clear(); } void MyMoneyFile::addInstitution(MyMoneyInstitution& institution) { // perform some checks to see that the institution stuff is OK. For // now we assume that the institution must have a name, the ID is not set // and it does not have a parent (MyMoneyFile). if (institution.name().length() == 0 || institution.id().length() != 0) throw MYMONEYEXCEPTION("Not a new institution"); d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->addInstitution(institution); // The notifier mechanism only refreshes the cache but does not // load new objects. So we simply force loading of the new one here d->m_cache.preloadInstitution(institution); d->m_changeSet += MyMoneyNotification(notifyAdd, institution); } void MyMoneyFile::modifyInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->modifyInstitution(institution); d->addCacheNotification(institution.id()); d->m_changeSet += MyMoneyNotification(notifyModify, institution); } void MyMoneyFile::modifyTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); MyMoneyTransaction tCopy(transaction); // now check the splits bool loanAccountAffected = false; QList::ConstIterator it_s; for (it_s = transaction.splits().constBegin(); it_s != transaction.splits().constEnd(); ++it_s) { // the following line will throw an exception if the // account does not exist MyMoneyAccount acc = MyMoneyFile::account((*it_s).accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot store split with no account assigned"); if (isStandardAccount((*it_s).accountId())) throw MYMONEYEXCEPTION("Cannot store split referencing standard account"); if (acc.isLoan() && ((*it_s).action() == MyMoneySplit::ActionTransfer)) loanAccountAffected = true; } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { QList list = transaction.splits(); for (it_s = list.constBegin(); it_s != list.constEnd(); ++it_s) { if ((*it_s).action() == MyMoneySplit::ActionTransfer) { MyMoneyAccount acc = MyMoneyFile::account((*it_s).accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = (*it_s); s.setAction(MyMoneySplit::ActionAmortization); tCopy.modifySplit(s); } } } } // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the current setting of this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list // and mark all accounts that are referenced for (it_s = tr.splits().constBegin(); it_s != tr.splits().constEnd(); ++it_s) { d->addCacheNotification((*it_s).accountId(), tr.postDate()); d->addCacheNotification((*it_s).payeeId()); //FIXME-ALEX Do I need to add d->addCacheNotification((*it_s).tagList()); ?? } // make sure the value is rounded to the accounts precision fixSplitPrecision(tCopy); // perform modification d->m_storage->modifyTransaction(tCopy); // and mark all accounts that are referenced for (it_s = tCopy.splits().constBegin(); it_s != tCopy.splits().constEnd(); ++it_s) { d->addCacheNotification((*it_s).accountId(), tCopy.postDate()); d->addCacheNotification((*it_s).payeeId()); //FIXME-ALEX Do I need to add d->addCacheNotification((*it_s).tagList()); ?? } d->m_changeSet += MyMoneyNotification(notifyModify, transaction); } void MyMoneyFile::modifyAccount(const MyMoneyAccount& _account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount account(_account); MyMoneyAccount acc = MyMoneyFile::account(account.id()); // check that for standard accounts only specific parameters are changed if (isStandardAccount(account.id())) { // make sure to use the stuff we found on file account = acc; // and only use the changes that are allowed account.setName(_account.name()); account.setCurrencyId(_account.currencyId()); // now check that it is the same if (!(account == _account)) throw MYMONEYEXCEPTION("Unable to modify the standard account groups"); } if (account.accountType() != acc.accountType() && !account.isLiquidAsset() && !acc.isLiquidAsset()) throw MYMONEYEXCEPTION("Unable to change account type"); // clear all changed objects from cache MyMoneyNotifier notifier(d); // if the account was moved to another institution, we notify // the old one as well as the new one and the structure change if (acc.institutionId() != account.institutionId()) { MyMoneyInstitution inst; if (!acc.institutionId().isEmpty()) { inst = institution(acc.institutionId()); inst.removeAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } if (!account.institutionId().isEmpty()) { inst = institution(account.institutionId()); inst.addAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } d->addCacheNotification(acc.institutionId()); d->addCacheNotification(account.institutionId()); } d->m_storage->modifyAccount(account); d->addCacheNotification(account.id()); d->m_changeSet += MyMoneyNotification(notifyModify, account); } void MyMoneyFile::reparentAccount(MyMoneyAccount &acc, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); // check that it's not one of the standard account groups if (isStandardAccount(acc.id())) throw MYMONEYEXCEPTION("Unable to reparent the standard account groups"); if (acc.accountGroup() == parent.accountGroup() || (acc.accountType() == MyMoneyAccount::Income && parent.accountType() == MyMoneyAccount::Expense) || (acc.accountType() == MyMoneyAccount::Expense && parent.accountType() == MyMoneyAccount::Income)) { if (acc.isInvest() && parent.accountType() != MyMoneyAccount::Investment) throw MYMONEYEXCEPTION("Unable to reparent Stock to non-investment account"); if (parent.accountType() == MyMoneyAccount::Investment && !acc.isInvest()) throw MYMONEYEXCEPTION("Unable to reparent non-stock to investment account"); // clear all changed objects from cache MyMoneyNotifier notifier(d); // keep a notification of the current parent MyMoneyAccount curParent = account(acc.parentAccountId()); d->addCacheNotification(curParent.id()); d->m_storage->reparentAccount(acc, parent); // and also keep one for the account itself and the new parent d->addCacheNotification(acc.id()); d->addCacheNotification(parent.id()); d->m_changeSet += MyMoneyNotification(notifyModify, curParent); d->m_changeSet += MyMoneyNotification(notifyModify, parent); d->m_changeSet += MyMoneyNotification(notifyModify, acc); } else throw MYMONEYEXCEPTION("Unable to reparent to different account type"); } const MyMoneyInstitution& MyMoneyFile::institution(const QString& id) const { return d->m_cache.institution(id); } const MyMoneyAccount& MyMoneyFile::account(const QString& id) const { return d->m_cache.account(id); } const MyMoneyAccount& MyMoneyFile::subAccountByName(const MyMoneyAccount& acc, const QString& name) const { static MyMoneyAccount nullAccount; QList::const_iterator it_a; for (it_a = acc.accountList().constBegin(); it_a != acc.accountList().constEnd(); ++it_a) { const MyMoneyAccount& sacc = account(*it_a); if (sacc.name() == name) return sacc; } return nullAccount; } const MyMoneyAccount& MyMoneyFile::accountByName(const QString& name) const { return d->m_cache.accountByName(name); } void MyMoneyFile::removeTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the engine's idea about this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); QList::ConstIterator it_s; // scan the splits again to update notification list for (it_s = tr.splits().constBegin(); it_s != tr.splits().constEnd(); ++it_s) { MyMoneyAccount acc = account((*it_s).accountId()); if (acc.isClosed()) throw MYMONEYEXCEPTION(i18n("Cannot remove transaction that references a closed account.")); d->addCacheNotification((*it_s).accountId(), tr.postDate()); d->addCacheNotification((*it_s).payeeId()); //FIXME-ALEX Do I need to add d->addCacheNotification((*it_s).tagList()); ?? } d->m_storage->removeTransaction(transaction); // remove a possible notification of that same object from the changeSet QList::iterator it; for(it = d->m_changeSet.begin(); it != d->m_changeSet.end();) { if((*it).id() == transaction.id()) { it = d->m_changeSet.erase(it); } else { ++it; } } d->m_changeSet += MyMoneyNotification(notifyRemove, transaction); } bool MyMoneyFile::hasActiveSplits(const QString& id) const { d->checkStorage(); return d->m_storage->hasActiveSplits(id); } bool MyMoneyFile::isStandardAccount(const QString& id) const { d->checkStorage(); return d->m_storage->isStandardAccount(id); } void MyMoneyFile::setAccountName(const QString& id, const QString& name) const { d->checkTransaction(Q_FUNC_INFO); MyMoneyNotifier notifier(d); MyMoneyAccount acc = account(id); d->m_storage->setAccountName(id, name); d->addCacheNotification(id); d->m_changeSet += MyMoneyNotification(notifyModify, acc); } void MyMoneyFile::removeAccount(const MyMoneyAccount& account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent; MyMoneyAccount acc; MyMoneyInstitution institution; // check that the account and its parent exist // this will throw an exception if the id is unknown acc = MyMoneyFile::account(account.id()); parent = MyMoneyFile::account(account.parentAccountId()); if (!acc.institutionId().isEmpty()) institution = MyMoneyFile::institution(acc.institutionId()); // check that it's not one of the standard account groups if (isStandardAccount(account.id())) throw MYMONEYEXCEPTION("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION("Unable to remove account with active splits"); } // clear all changed objects from cache MyMoneyNotifier notifier(d); // collect all sub-ordinate accounts for notification foreach (const QString& id, acc.accountList()) { d->addCacheNotification(id); const MyMoneyAccount& acc = MyMoneyFile::account(id); d->m_changeSet += MyMoneyNotification(notifyModify, acc); } // don't forget the parent and a possible institution d->addCacheNotification(parent.id()); d->addCacheNotification(account.institutionId()); if (!institution.id().isEmpty()) { institution.removeAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(notifyModify, institution); } acc.setInstitutionId(QString()); d->m_storage->removeAccount(acc); d->addCacheNotification(acc.id(), false); d->m_cache.clear(acc.id()); d->m_balanceCache.clear(acc.id()); d->m_changeSet += MyMoneyNotification(notifyModify, parent); d->m_changeSet += MyMoneyNotification(notifyRemove, acc); } void MyMoneyFile::removeAccountList(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION("Too deep recursion in [MyMoneyFile::removeAccountList]!"); d->checkTransaction(Q_FUNC_INFO); // upon entry, we check that we could proceed with the operation if (!level) { if (!hasOnlyUnusedAccounts(account_list, 0)) { throw MYMONEYEXCEPTION("One or more accounts cannot be removed"); } } // process all accounts in the list and test if they have transactions assigned for (QStringList::ConstIterator it = account_list.constBegin(); it != account_list.constEnd(); ++it) { MyMoneyAccount a = d->m_storage->account(*it); //qDebug() << "Deleting account '"<< a.name() << "'"; // first remove all sub-accounts if (!a.accountList().isEmpty()) { removeAccountList(a.accountList(), level + 1); // then remove account itself, but we first have to get // rid of the account list that is still stored in // the MyMoneyAccount object. Easiest way is to get a fresh copy. a = d->m_storage->account(*it); } // make sure to remove the item from the cache d->m_cache.clear(a.id()); removeAccount(a); } } bool MyMoneyFile::hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION("Too deep recursion in [MyMoneyFile::hasOnlyUnusedAccounts]!"); // process all accounts in the list and test if they have transactions assigned for (QStringList::ConstIterator it = account_list.constBegin(); it != account_list.constEnd(); ++it) { if (transactionCount(*it) != 0) return false; // the current account has a transaction assigned if (!hasOnlyUnusedAccounts(account(*it).accountList(), level + 1)) return false; // some sub-account has a transaction assigned } return true; // all subaccounts unused } void MyMoneyFile::removeInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); QList::ConstIterator it_a; MyMoneyInstitution inst = MyMoneyFile::institution(institution.id()); bool blocked = signalsBlocked(); blockSignals(true); for (it_a = inst.accountList().constBegin(); it_a != inst.accountList().constEnd(); ++it_a) { MyMoneyAccount acc = account(*it_a); acc.setInstitutionId(QString()); modifyAccount(acc); d->m_changeSet += MyMoneyNotification(notifyModify, acc); } blockSignals(blocked); d->m_storage->removeInstitution(institution); d->m_changeSet += MyMoneyNotification(notifyRemove, institution); d->addCacheNotification(institution.id(), false); } void MyMoneyFile::addAccount(MyMoneyAccount& account, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution institution; // perform some checks to see that the account stuff is OK. For // now we assume that the account must have a name, has no // transaction and sub-accounts and parent account // it's own ID is not set and it does not have a pointer to (MyMoneyFile) if (account.name().length() == 0) throw MYMONEYEXCEPTION("Account has no name"); if (account.id().length() != 0) throw MYMONEYEXCEPTION("New account must have no id"); if (account.accountList().count() != 0) throw MYMONEYEXCEPTION("New account must have no sub-accounts"); if (!account.parentAccountId().isEmpty()) throw MYMONEYEXCEPTION("New account must have no parent-id"); if (account.accountType() == MyMoneyAccount::UnknownAccountType) throw MYMONEYEXCEPTION("Account has invalid type"); // make sure, that the parent account exists // if not, an exception is thrown. If it exists, // get a copy of the current data MyMoneyAccount acc = MyMoneyFile::account(parent.id()); #if 0 // TODO: remove the following code as we now can have multiple accounts // with the same name even in the same hierarchy position of the account tree // // check if the selected name is currently not among the child accounts // if we find one, then return it as the new account QStringList::const_iterator it_a; for (it_a = acc.accountList().begin(); it_a != acc.accountList().end(); ++it_a) { MyMoneyAccount a = MyMoneyFile::account(*it_a); if (account.name() == a.name()) { account = a; return; } } #endif // FIXME: make sure, that the parent has the same type // I left it out here because I don't know, if there is // a tight coupling between e.g. checking accounts and the // class asset. It certainly does not make sense to create an // expense account under an income account. Maybe it does, I don't know. // We enforce, that a stock account can never be a parent and // that the parent for a stock account must be an investment. Also, // an investment cannot have another investment account as it's parent if (parent.isInvest()) throw MYMONEYEXCEPTION("Stock account cannot be parent account"); if (account.isInvest() && parent.accountType() != MyMoneyAccount::Investment) throw MYMONEYEXCEPTION("Stock account must have investment account as parent "); if (!account.isInvest() && parent.accountType() == MyMoneyAccount::Investment) throw MYMONEYEXCEPTION("Investment account can only have stock accounts as children"); // clear all changed objects from cache MyMoneyNotifier notifier(d); // if an institution is set, verify that it exists if (account.institutionId().length() != 0) { // check the presence of the institution. if it // does not exist, an exception is thrown institution = MyMoneyFile::institution(account.institutionId()); } // if we don't have a valid opening date use today if (!account.openingDate().isValid()) { account.setOpeningDate(QDate::currentDate()); } // make sure to set the opening date for categories to a // fixed date (1900-1-1). See #313793 on b.k.o for details if (account.isIncomeExpense()) { account.setOpeningDate(QDate(1900, 1, 1)); } // if we don't have a currency assigned use the base currency if (account.currencyId().isEmpty()) { account.setCurrencyId(baseCurrency().id()); } // make sure the parent id is setup account.setParentAccountId(parent.id()); d->m_storage->addAccount(account); d->m_changeSet += MyMoneyNotification(notifyAdd, account); d->m_storage->addAccount(parent, account); d->m_changeSet += MyMoneyNotification(notifyModify, parent); if (account.institutionId().length() != 0) { institution.addAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(notifyModify, institution); d->addCacheNotification(institution.id()); } // The notifier mechanism only refreshes the cache but does not // load new objects. So we simply force loading of the new one here d->m_cache.preloadAccount(account); d->addCacheNotification(parent.id()); } MyMoneyTransaction MyMoneyFile::createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance) { MyMoneyTransaction t; // if the opening balance is not zero, we need // to create the respective transaction if (!balance.isZero()) { d->checkTransaction(Q_FUNC_INFO); MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc = openingBalanceAccount(currency); if (openAcc.openingDate() > acc.openingDate()) { openAcc.setOpeningDate(acc.openingDate()); modifyAccount(openAcc); } MyMoneySplit s; t.setPostDate(acc.openingDate()); t.setCommodity(acc.currencyId()); s.setAccountId(acc.id()); s.setShares(balance); s.setValue(balance); t.addSplit(s); s.clearId(); s.setAccountId(openAcc.id()); s.setShares(-balance); s.setValue(-balance); t.addSplit(s); addTransaction(t); } return t; } QString MyMoneyFile::openingBalanceTransaction(const MyMoneyAccount& acc) const { QString result; MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc; try { openAcc = openingBalanceAccount(currency); } catch (const MyMoneyException &) { return result; } // Iterate over all the opening balance transactions for this currency MyMoneyTransactionFilter filter; filter.addAccount(openAcc.id()); QList transactions = transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account (*it_t).splitByAccount(acc.id(), true /*match*/); // If so, we have a winner! result = (*it_t).id(); break; } catch (const MyMoneyException &) { // If not, keep searching ++it_t; } } return result; } const MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) { if (!security.isCurrency()) throw MYMONEYEXCEPTION("Opening balance for non currencies not supported"); try { return openingBalanceAccount_internal(security); } catch (const MyMoneyException &) { MyMoneyFileTransaction ft; MyMoneyAccount acc; try { acc = createOpeningBalanceAccount(security); ft.commit(); } catch (const MyMoneyException &) { qDebug("Unable to create opening balance account for security %s", qPrintable(security.id())); } return acc; } } const MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) const { return openingBalanceAccount_internal(security); } const MyMoneyAccount MyMoneyFile::openingBalanceAccount_internal(const MyMoneySecurity& security) const { if (!security.isCurrency()) throw MYMONEYEXCEPTION("Opening balance for non currencies not supported"); MyMoneyAccount acc; - QRegExp match(QString("^%1").arg((MyMoneyFile::OpeningBalancesPrefix))); - QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { - if (match.indexIn((*it).name()) != -1) { - if ((*it).currencyId() == security.id()) { + if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") + && it->currencyId() == security.id()) { + acc = *it; + break; + } + } + + if (acc.id().isEmpty()) { + for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { + if (it->name().startsWith(MyMoneyFile::openingBalancesPrefix()) + && it->currencyId() == security.id()) { acc = *it; break; } } } if (acc.id().isEmpty()) { throw MYMONEYEXCEPTION(QString("No opening balance account for %1").arg(security.tradingSymbol())); } return acc; } const MyMoneyAccount MyMoneyFile::createOpeningBalanceAccount(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount acc; - QString name(MyMoneyFile::OpeningBalancesPrefix); + QList accounts; + QList::ConstIterator it; + + accountList(accounts, equity().accountList(), true); + + // find present opening balance accounts without containing '(' + QString name; + QString parentAccountId; + QRegExp exp(QString("\\([A-Z]{3}\\)")); + + for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { + if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") + && exp.indexIn(it->name()) == -1) { + name = it->name(); + parentAccountId = it->parentAccountId(); + break; + } + } + + if (name.isEmpty()) + name = MyMoneyFile::openingBalancesPrefix(); if (security.id() != baseCurrency().id()) { name += QString(" (%1)").arg(security.id()); } acc.setName(name); acc.setAccountType(MyMoneyAccount::Equity); acc.setCurrencyId(security.id()); + acc.setValue("OpeningBalanceAccount", "Yes"); - MyMoneyAccount parent = equity(); + MyMoneyAccount parent = !parentAccountId.isEmpty() ? account(parentAccountId) : equity(); this->addAccount(acc, parent); return acc; } void MyMoneyFile::addTransaction(MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // perform some checks to see that the transaction stuff is OK. For // now we assume that // * no ids are assigned // * the date valid (must not be empty) // * the referenced accounts in the splits exist // first perform all the checks if (!transaction.id().isEmpty()) throw MYMONEYEXCEPTION("Unable to add transaction with id set"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION("Unable to add transaction with invalid postdate"); // now check the splits bool loanAccountAffected = false; QList::ConstIterator it_s; for (it_s = transaction.splits().constBegin(); it_s != transaction.splits().constEnd(); ++it_s) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts MyMoneyAccount acc = MyMoneyFile::account((*it_s).accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot add split with no account assigned"); if (acc.isLoan()) loanAccountAffected = true; if (isStandardAccount((*it_s).accountId())) throw MYMONEYEXCEPTION("Cannot add split referencing standard account"); } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { QList list = transaction.splits(); for (it_s = list.constBegin(); it_s != list.constEnd(); ++it_s) { if ((*it_s).action() == MyMoneySplit::ActionTransfer) { MyMoneyAccount acc = MyMoneyFile::account((*it_s).accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = (*it_s); s.setAction(MyMoneySplit::ActionAmortization); transaction.modifySplit(s); } } } } // check that we have a commodity if (transaction.commodity().isEmpty()) { transaction.setCommodity(baseCurrency().id()); } // make sure the value is rounded to the accounts precision fixSplitPrecision(transaction); // then add the transaction to the file global pool d->m_storage->addTransaction(transaction); // scan the splits again to update notification list for (it_s = transaction.splits().constBegin(); it_s != transaction.splits().constEnd(); ++it_s) { d->addCacheNotification((*it_s).accountId(), transaction.postDate()); d->addCacheNotification((*it_s).payeeId()); //FIXME-ALEX Do I need to add d->addCacheNotification((*it_s).tagList()); ?? } d->m_changeSet += MyMoneyNotification(notifyAdd, transaction); } const MyMoneyTransaction MyMoneyFile::transaction(const QString& id) const { d->checkStorage(); return d->m_storage->transaction(id); } const MyMoneyTransaction MyMoneyFile::transaction(const QString& account, const int idx) const { d->checkStorage(); return d->m_storage->transaction(account, idx); } void MyMoneyFile::addPayee(MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->addPayee(payee); // The notifier mechanism only refreshes the cache but does not // load new objects. So we simply force loading of the new one here d->m_cache.preloadPayee(payee); d->m_changeSet += MyMoneyNotification(notifyAdd, payee); } const MyMoneyPayee& MyMoneyFile::payee(const QString& id) const { return d->m_cache.payee(id); } const MyMoneyPayee& MyMoneyFile::payeeByName(const QString& name) const { d->checkStorage(); return d->m_cache.payee(d->m_storage->payeeByName(name).id()); } void MyMoneyFile::modifyPayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->addCacheNotification(payee.id()); d->m_storage->modifyPayee(payee); d->m_changeSet += MyMoneyNotification(notifyModify, payee); } void MyMoneyFile::removePayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the payee is not referenced anymore // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->removePayee(payee); d->addCacheNotification(payee.id(), false); d->m_changeSet += MyMoneyNotification(notifyRemove, payee); } void MyMoneyFile::addTag(MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->addTag(tag); // The notifier mechanism only refreshes the cache but does not // load new objects. So we simply force loading of the new one here d->m_cache.preloadTag(tag); d->m_changeSet += MyMoneyNotification(notifyAdd, tag); } const MyMoneyTag& MyMoneyFile::tag(const QString& id) const { return d->m_cache.tag(id); } const MyMoneyTag& MyMoneyFile::tagByName(const QString& name) const { d->checkStorage(); return d->m_cache.tag(d->m_storage->tagByName(name).id()); } void MyMoneyFile::modifyTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->addCacheNotification(tag.id()); d->m_storage->modifyTag(tag); d->m_changeSet += MyMoneyNotification(notifyModify, tag); } void MyMoneyFile::removeTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the tag is not referenced anymore // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->removeTag(tag); d->addCacheNotification(tag.id(), false); d->m_changeSet += MyMoneyNotification(notifyRemove, tag); } void MyMoneyFile::accountList(QList& list, const QStringList& idlist, const bool recursive) const { if (idlist.isEmpty()) { d->m_cache.account(list); #if 0 // TODO: I have no idea what this was good for, but it caused the networth report // to show double the numbers so I commented it out (ipwizard, 2008-05-24) if (d->m_storage && (list.isEmpty() || list.size() != d->m_storage->accountCount())) { d->m_storage->accountList(list); d->m_cache.preloadAccount(list); } #endif QList::Iterator it; for (it = list.begin(); it != list.end();) { if (isStandardAccount((*it).id())) { it = list.erase(it); } else { ++it; } } } else { QList::ConstIterator it; QList list_a; d->m_cache.account(list_a); for (it = list_a.constBegin(); it != list_a.constEnd(); ++it) { if (!isStandardAccount((*it).id())) { if (idlist.indexOf((*it).id()) != -1) { list.append(*it); if (recursive == true && !(*it).accountList().isEmpty()) { accountList(list, (*it).accountList(), true); } } } } } } void MyMoneyFile::institutionList(QList& list) const { d->m_cache.institution(list); } const QList MyMoneyFile::institutionList() const { QList list; institutionList(list); return list; } // general get functions const MyMoneyPayee& MyMoneyFile::user() const { d->checkStorage(); return d->m_storage->user(); } // general set functions void MyMoneyFile::setUser(const MyMoneyPayee& user) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->setUser(user); } bool MyMoneyFile::dirty() const { if (!d->m_storage) return false; return d->m_storage->dirty(); } void MyMoneyFile::setDirty() const { d->checkStorage(); d->m_storage->setDirty(); } unsigned int MyMoneyFile::accountCount() const { d->checkStorage(); return d->m_storage->accountCount(); } void MyMoneyFile::ensureDefaultCurrency(MyMoneyAccount& acc) const { if (acc.currencyId().isEmpty()) { if (!baseCurrency().id().isEmpty()) acc.setCurrencyId(baseCurrency().id()); } } const MyMoneyAccount& MyMoneyFile::liability() const { d->checkStorage(); return d->m_cache.account(STD_ACC_LIABILITY); } const MyMoneyAccount& MyMoneyFile::asset() const { d->checkStorage(); return d->m_cache.account(STD_ACC_ASSET); } const MyMoneyAccount& MyMoneyFile::expense() const { d->checkStorage(); return d->m_cache.account(STD_ACC_EXPENSE); } const MyMoneyAccount& MyMoneyFile::income() const { d->checkStorage(); return d->m_cache.account(STD_ACC_INCOME); } const MyMoneyAccount& MyMoneyFile::equity() const { d->checkStorage(); return d->m_cache.account(STD_ACC_EQUITY); } unsigned int MyMoneyFile::transactionCount(const QString& account) const { d->checkStorage(); return d->m_storage->transactionCount(account); } const QMap MyMoneyFile::transactionCountMap() const { d->checkStorage(); return d->m_storage->transactionCountMap(); } unsigned int MyMoneyFile::institutionCount() const { d->checkStorage(); return d->m_storage->institutionCount(); } const MyMoneyMoney MyMoneyFile::balance(const QString& id, const QDate& date) const { if (date.isValid()) { MyMoneyBalanceCacheItem bal = d->m_balanceCache.balance(id, date); if (bal.isValid()) return bal.balance(); } d->checkStorage(); MyMoneyMoney returnValue = d->m_storage->balance(id, date); if (date.isValid()) { d->m_balanceCache.insert(id, date, returnValue); } return returnValue; } const MyMoneyMoney MyMoneyFile::clearedBalance(const QString &id, const QDate& date) const { MyMoneyMoney cleared; QList list; cleared = balance(id, date); MyMoneyAccount account = this->account(id); MyMoneyMoney factor(1, 1); if (account.accountGroup() == MyMoneyAccount::Liability || account.accountGroup() == MyMoneyAccount::Equity) factor = -factor; MyMoneyTransactionFilter filter; filter.addAccount(id); filter.setDateFilter(QDate(), date); filter.setReportAllSplits(false); filter.addState(MyMoneyTransactionFilter::notReconciled); transactionList(list, filter); for (QList::const_iterator it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); for (QList::const_iterator it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { const MyMoneySplit &split = (*it_s); if (split.accountId() != id) continue; cleared -= split.shares(); } } return cleared * factor; } const MyMoneyMoney MyMoneyFile::totalBalance(const QString& id, const QDate& date) const { d->checkStorage(); return d->m_storage->totalBalance(id, date); } void MyMoneyFile::warningMissingRate(const QString& fromId, const QString& toId) const { MyMoneySecurity from, to; try { from = security(fromId); to = security(toId); qWarning("Missing price info for conversion from %s to %s", qPrintable(from.name()), qPrintable(to.name())); } catch (const MyMoneyException &e) { qWarning("Missing security caught in MyMoneyFile::warningMissingRate(): %s(%ld) %s", qPrintable(e.file()), e.line(), qPrintable(e.what())); } } void MyMoneyFile::transactionList(QList >& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } void MyMoneyFile::transactionList(QList& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } const QList MyMoneyFile::transactionList(MyMoneyTransactionFilter& filter) const { QList list; transactionList(list, filter); return list; } const QList MyMoneyFile::payeeList() const { QList list; d->m_cache.payee(list); return list; } const QList MyMoneyFile::tagList() const { QList list; d->m_cache.tag(list); return list; } QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const { MyMoneyAccount acc; QString rc; if (!accountId.isEmpty()) { acc = account(accountId); do { if (!rc.isEmpty()) rc = AccountSeperator + rc; rc = acc.name() + rc; acc = account(acc.parentAccountId()); } while (!acc.id().isEmpty() && (includeStandardAccounts || !isStandardAccount(acc.id()))); } return rc; } QString MyMoneyFile::categoryToAccount(const QString& category, MyMoneyAccount::accountTypeE type) const { QString id; // search the category in the expense accounts and if it is not found, try // to locate it in the income accounts if (type == MyMoneyAccount::UnknownAccountType || type == MyMoneyAccount::Expense) { id = locateSubAccount(MyMoneyFile::instance()->expense(), category); } if ((id.isEmpty() && type == MyMoneyAccount::UnknownAccountType) || type == MyMoneyAccount::Income) { id = locateSubAccount(MyMoneyFile::instance()->income(), category); } return id; } QString MyMoneyFile::nameToAccount(const QString& name) const { QString id; // search the category in the asset accounts and if it is not found, try // to locate it in the liability accounts id = locateSubAccount(MyMoneyFile::instance()->asset(), name); if (id.isEmpty()) id = locateSubAccount(MyMoneyFile::instance()->liability(), name); return id; } QString MyMoneyFile::parentName(const QString& name) const { return name.section(AccountSeperator, 0, -2); } QString MyMoneyFile::locateSubAccount(const MyMoneyAccount& base, const QString& category) const { MyMoneyAccount nextBase; QString level, remainder; level = category.section(AccountSeperator, 0, 0); remainder = category.section(AccountSeperator, 1); QStringList list = base.accountList(); QStringList::ConstIterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { nextBase = account(*it_a); if (nextBase.name() == level) { if (remainder.isEmpty()) { return nextBase.id(); } return locateSubAccount(nextBase, remainder); } } return QString(); } QString MyMoneyFile::value(const QString& key) const { d->checkStorage(); return d->m_storage->value(key); } void MyMoneyFile::setValue(const QString& key, const QString& val) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->setValue(key, val); } void MyMoneyFile::deletePair(const QString& key) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->deletePair(key); } void MyMoneyFile::addSchedule(MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); MyMoneyTransaction transaction = sched.transaction(); QList::ConstIterator it_s; for (it_s = transaction.splits().constBegin(); it_s != transaction.splits().constEnd(); ++it_s) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts MyMoneyAccount acc = MyMoneyFile::account((*it_s).accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot add split with no account assigned"); if (isStandardAccount((*it_s).accountId())) throw MYMONEYEXCEPTION("Cannot add split referencing standard account"); } // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->addSchedule(sched); // The notifier mechanism only refreshes the cache but does not // load new objects. So we simply force loading of the new one here d->m_cache.preloadSchedule(sched); d->m_changeSet += MyMoneyNotification(notifyAdd, sched); } void MyMoneyFile::modifySchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); MyMoneyTransaction transaction = sched.transaction(); QList::ConstIterator it_s; for (it_s = transaction.splits().constBegin(); it_s != transaction.splits().constEnd(); ++it_s) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts MyMoneyAccount acc = MyMoneyFile::account((*it_s).accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot store split with no account assigned"); if (isStandardAccount((*it_s).accountId())) throw MYMONEYEXCEPTION("Cannot store split referencing standard account"); } // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->modifySchedule(sched); d->addCacheNotification(sched.id()); d->m_changeSet += MyMoneyNotification(notifyModify, sched); } void MyMoneyFile::removeSchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->removeSchedule(sched); d->addCacheNotification(sched.id(), false); d->m_changeSet += MyMoneyNotification(notifyRemove, sched); } const MyMoneySchedule MyMoneyFile::schedule(const QString& id) const { return d->m_cache.schedule(id); } const QList MyMoneyFile::scheduleList( const QString& accountId, const MyMoneySchedule::typeE type, const MyMoneySchedule::occurrenceE occurrence, const MyMoneySchedule::paymentTypeE paymentType, const QDate& startDate, const QDate& endDate, const bool overdue) const { d->checkStorage(); return d->m_storage->scheduleList(accountId, type, occurrence, paymentType, startDate, endDate, overdue); } const QStringList MyMoneyFile::consistencyCheck() { QList list; QList::Iterator it_a; QList::Iterator it_sch; QList::Iterator it_p; QList::Iterator it_t; QList::Iterator it_r; QStringList accountRebuild; QStringList::ConstIterator it_c; QMap interestAccounts; MyMoneyAccount parent; MyMoneyAccount child; MyMoneyAccount toplevel; QString parentId; QStringList rc; int problemCount = 0; int unfixedCount = 0; QString problemAccount; // check that we have a storage object d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the current list of accounts accountList(list); // add the standard accounts list << MyMoneyFile::instance()->asset(); list << MyMoneyFile::instance()->liability(); list << MyMoneyFile::instance()->income(); list << MyMoneyFile::instance()->expense(); for (it_a = list.begin(); it_a != list.end(); ++it_a) { // no more checks for standard accounts if (isStandardAccount((*it_a).id())) { continue; } switch ((*it_a).accountGroup()) { case MyMoneyAccount::Asset: toplevel = asset(); break; case MyMoneyAccount::Liability: toplevel = liability(); break; case MyMoneyAccount::Expense: toplevel = expense(); break; case MyMoneyAccount::Income: toplevel = income(); break; case MyMoneyAccount::Equity: toplevel = equity(); break; default: qWarning("%s:%d This should never happen!", __FILE__ , __LINE__); break; } // check for loops in the hierarchy parentId = (*it_a).parentAccountId(); try { bool dropOut = false; while (!isStandardAccount(parentId) && !dropOut) { parent = account(parentId); if (parent.id() == (*it_a).id()) { // parent loops, so we need to re-parent to toplevel account // find parent account in our list problemCount++; QList::Iterator it_b; for (it_b = list.begin(); it_b != list.end(); ++it_b) { if ((*it_b).id() == parent.id()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); rc << i18n(" * Loop detected between this account and account '%1'.", (*it_b).name()); rc << i18n(" Reparenting account '%2' to top level account '%1'.", toplevel.name(), (*it_a).name()); (*it_a).setParentAccountId(toplevel.id()); if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); dropOut = true; break; } } } } parentId = parent.parentAccountId(); } } catch (const MyMoneyException &) { // if we don't know about a parent, we catch it later } // check that the parent exists parentId = (*it_a).parentAccountId(); try { parent = account(parentId); if ((*it_a).accountGroup() != parent.accountGroup()) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // the parent belongs to a different group, so we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. rc << i18n(" * Parent account '%1' belongs to a different group.", parent.name()); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account // and the one we removed this account from if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } else if (!parent.accountList().contains((*it_a).id())) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // parent exists, but does not have a reference to the account rc << i18n(" * Parent account '%1' does not contain '%2' as sub-account.", parent.name(), problemAccount); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } } catch (const MyMoneyException &) { // apparently, the parent does not exist anymore. we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The parent with id %1 does not exist anymore.", parentId); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); d->addCacheNotification((*it_a).id()); // make sure to rebuild the sub-accounts of the top account if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); } // now check that all the children exist and have the correct type for (it_c = (*it_a).accountList().begin(); it_c != (*it_a).accountList().end(); ++it_c) { // check that the child exists try { child = account(*it_c); if (child.parentAccountId() != (*it_a).id()) { throw MYMONEYEXCEPTION("Child account has a different parent"); } } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Child account with id %1 does not exist anymore.", *it_c); rc << i18n(" The child account list will be reconstructed."); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); } } // see if it is a loan account. if so, remember the assigned interest account if ((*it_a).isLoan()) { MyMoneyAccountLoan loan(*it_a); if (!loan.interestAccountId().isEmpty()) { interestAccounts[loan.interestAccountId()] = true; } try { payee(loan.payee()); } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The payee with id %1 referenced by the loan does not exist anymore.", loan.payee()); rc << i18n(" The payee will be removed."); // remove the payee - the account will be modified in the engine later (*it_a).deletePair("payee"); } } // check if it is a category and set the date to 1900-01-01 if different if ((*it_a).isIncomeExpense()) { if (((*it_a).openingDate().isValid() == false) || ((*it_a).openingDate() != QDate(1900, 1, 1))) { (*it_a).setOpeningDate(QDate(1900, 1, 1)); } } // check for clear text online password in the online settings if (!(*it_a).onlineBankingSettings().value("password").isEmpty()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Older versions of KMyMoney stored an OFX password for this account in cleartext."); rc << i18n(" Please open it in the account editor (Account/Edit account) once and press OK."); rc << i18n(" This will store the password in the KDE wallet and remove the cleartext version."); ++unfixedCount; } // if the account was modified, we need to update it in the engine if (!(d->m_storage->account((*it_a).id()) == (*it_a))) { try { d->m_storage->modifyAccount(*it_a, true); d->addCacheNotification((*it_a).id()); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data in engine."); return rc; } } } if (accountRebuild.count() != 0) { rc << i18n("* Reconstructing the child lists for"); } // clear the affected lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { rc << QString(" %1").arg((*it_a).name()); // clear the account list for (it_c = (*it_a).accountList().begin(); it_c != (*it_a).accountList().end();) { (*it_a).removeAccountId(*it_c); it_c = (*it_a).accountList().begin(); } } } // reconstruct the lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { QList::Iterator it; parentId = (*it_a).parentAccountId(); if (accountRebuild.contains(parentId)) { for (it = list.begin(); it != list.end(); ++it) { if ((*it).id() == parentId) { (*it).addAccountId((*it_a).id()); break; } } } } // update the engine objects for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { try { d->m_storage->modifyAccount(*it_a, true); d->addCacheNotification((*it_a).id()); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data for account %1 in engine", (*it_a).name()); } } } // For some reason, files exist with invalid ids. This has been found in the payee id // so we fix them here QList pList = payeeList(); QMappayeeConversionMap; for (it_p = pList.begin(); it_p != pList.end(); ++it_p) { if ((*it_p).id().length() > 7) { // found one of those with an invalid ids // create a new one and store it in the map. MyMoneyPayee payee = (*it_p); payee.clearId(); d->m_storage->addPayee(payee); payeeConversionMap[(*it_p).id()] = payee.id(); rc << i18n(" * Payee %1 recreated with fixed id", payee.name()); ++problemCount; } } // Fix the transactions QList tList; MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); d->m_storage->transactionList(tList, filter); // Generate the list of interest accounts for (it_t = tList.begin(); it_t != tList.end(); ++it_t) { const MyMoneyTransaction& t = (*it_t); QList::const_iterator it_s; for (it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s) { if ((*it_s).action() == MyMoneySplit::ActionInterest) interestAccounts[(*it_s).accountId()] = true; } } QSet supportedAccountTypes; supportedAccountTypes << MyMoneyAccount::Checkings << MyMoneyAccount::Savings << MyMoneyAccount::Cash << MyMoneyAccount::CreditCard << MyMoneyAccount::Asset << MyMoneyAccount::Liability; QSet reportedUnsupportedAccounts; for (it_t = tList.begin(); it_t != tList.end(); ++it_t) { MyMoneyTransaction t = (*it_t); QList splits = t.splits(); QList::const_iterator it_s; bool tChanged = false; QDate accountOpeningDate; QStringList accountList; for (it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { bool sChanged = false; MyMoneySplit s = (*it_s); if (payeeConversionMap.find((*it_s).payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of transaction '%1'.", t.id()); ++problemCount; } try { const MyMoneyAccount& acc = this->account(s.accountId()); // compute the newest opening date of all accounts involved in the transaction // in case the newest opening date is newer than the transaction post date, do one // of the following: // // a) for category and stock accounts: update the opening date of the account // b) for account types where the user cannot modify the opening date through // the UI issue a warning (for each account only once) // c) others will be caught later if (!acc.isIncomeExpense() && !acc.isInvest()) { if (acc.openingDate() > t.postDate()) { if (!accountOpeningDate.isValid() || acc.openingDate() > accountOpeningDate) { accountOpeningDate = acc.openingDate(); } accountList << this->accountToCategory(acc.id()); if (!supportedAccountTypes.contains(acc.accountType()) && !reportedUnsupportedAccounts.contains(acc.id())) { rc << i18n(" * Opening date of Account '%1' cannot be changed to support transaction '%2' post date.", this->accountToCategory(acc.id()), t.id()); reportedUnsupportedAccounts << acc.id(); ++unfixedCount; } } } else { if (acc.openingDate() > t.postDate()) { rc << i18n(" * Transaction '%1' post date '%2' is older than opening date '%4' of account '%3'.", t.id(), t.postDate().toString(Qt::ISODate), this->accountToCategory(acc.id()), acc.openingDate().toString(Qt::ISODate)); rc << i18n(" Account opening date updated."); MyMoneyAccount newAcc = acc; newAcc.setOpeningDate(t.postDate()); this->modifyAccount(newAcc); ++problemCount; } } // make sure, that shares and value have the same number if they // represent the same currency. if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split of transaction '%1'.", t.id()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split of transaction '%1'.", t.id()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in transaction '%1' contains a reference to invalid account %3. Please fix manually.", t.id(), (*it_s).id(), (*it_s).accountId()); ++unfixedCount; } // make sure the interest splits are marked correct as such if (interestAccounts.find(s.accountId()) != interestAccounts.end() && s.action() != MyMoneySplit::ActionInterest) { s.setAction(MyMoneySplit::ActionInterest); sChanged = true; rc << i18n(" * action marked as interest in split of transaction '%1'.", t.id()); ++problemCount; } if (sChanged) { tChanged = true; t.modifySplit(s); } } // make sure that the transaction's post date is valid if (!t.postDate().isValid()) { tChanged = true; t.setPostDate(t.entryDate().isValid() ? t.entryDate() : QDate::currentDate()); rc << i18n(" * Transaction '%1' has an invalid post date.", t.id()); rc << i18n(" The post date was updated to '%1'.", QLocale().toString(t.postDate(), QLocale::ShortFormat)); ++problemCount; } // check if the transaction's post date is after the opening date // of all accounts involved in the transaction. In case it is not, // issue a warning with the details about the transaction incl. // the account names and dates involved if (accountOpeningDate.isValid() && t.postDate() < accountOpeningDate) { QDate originalPostDate = t.postDate(); #if 0 // for now we do not activate the logic to move the post date to a later // point in time. This could cause some severe trouble if you have lots // of ancient data collected with older versions of KMyMoney that did not // enforce certain conditions like we do now. t.setPostDate(accountOpeningDate); tChanged = true; // copy the price information for investments to the new date QList::const_iterator it_t; for (it_t = t.splits().constBegin(); it_t != t.splits().constEnd(); ++it_t) { if (((*it_t).action() != "Buy") && ((*it_t).action() != "Reinvest")) { continue; } QString id = (*it_t).accountId(); MyMoneyAccount acc = this->account(id); MyMoneySecurity sec = this->security(acc.currencyId()); MyMoneyPrice price(acc.currencyId(), sec.tradingCurrency(), t.postDate(), (*it_t).price(), "Transaction"); this->addPrice(price); break; } #endif rc << i18n(" * Transaction '%1' has a post date '%2' before one of the referenced account's opening date.", t.id(), QLocale().toString(originalPostDate, QLocale::ShortFormat)); rc << i18n(" Referenced accounts: %1", accountList.join(",")); rc << i18n(" The post date was not updated to '%1'.", QLocale().toString(accountOpeningDate, QLocale::ShortFormat)); ++unfixedCount; } if (tChanged) { d->m_storage->modifyTransaction(t); } } // Fix the schedules QList schList = scheduleList(); for (it_sch = schList.begin(); it_sch != schList.end(); ++it_sch) { MyMoneySchedule sch = (*it_sch); MyMoneyTransaction t = sch.transaction(); QList splits = t.splits(); bool tChanged = false; QList::const_iterator it_s; for (it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { MyMoneySplit s = (*it_s); bool sChanged = false; if (payeeConversionMap.find((*it_s).payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of schedule '%1'.", (*it_sch).name()); ++problemCount; } if (!(*it_s).value().isZero() && (*it_s).shares().isZero()) { s.setShares(s.value()); sChanged = true; rc << i18n(" * Split in scheduled transaction '%1' contained value != 0 and shares == 0.", (*it_sch).name()); rc << i18n(" Shares set to value."); ++problemCount; } // make sure, we don't have a bankid stored with a split in a schedule if (!(*it_s).bankID().isEmpty()) { s.setBankID(QString()); sChanged = true; rc << i18n(" * Removed bankid from split in scheduled transaction '%1'.", (*it_sch).name()); ++problemCount; } // make sure, that shares and value have the same number if they // represent the same currency. try { const MyMoneyAccount& acc = this->account(s.accountId()); if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split in schedule '%1'.", (*it_sch).name()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split in schedule '%1'.", (*it_sch).name()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in schedule '%1' contains a reference to invalid account %3. Please fix manually.", (*it_sch).name(), (*it_s).id(), (*it_s).accountId()); ++unfixedCount; } if (sChanged) { t.modifySplit(s); tChanged = true; } } if (tChanged) { sch.setTransaction(t); d->m_storage->modifySchedule(sch); } } // Fix the reports QList rList = reportList(); for (it_r = rList.begin(); it_r != rList.end(); ++it_r) { MyMoneyReport r = *it_r; QStringList pList; QStringList::Iterator it_p; (*it_r).payees(pList); bool rChanged = false; for (it_p = pList.begin(); it_p != pList.end(); ++it_p) { if (payeeConversionMap.find(*it_p) != payeeConversionMap.end()) { rc << i18n(" * Payee id updated in report '%1'.", (*it_r).name()); ++problemCount; r.removeReference(*it_p); r.addPayee(payeeConversionMap[*it_p]); rChanged = true; } } if (rChanged) { d->m_storage->modifyReport(r); } } // erase old payee ids QMap::Iterator it_m; for (it_m = payeeConversionMap.begin(); it_m != payeeConversionMap.end(); ++it_m) { MyMoneyPayee payee = this->payee(it_m.key()); removePayee(payee); rc << i18n(" * Payee '%1' removed.", payee.id()); ++problemCount; } //look for accounts which have currencies other than the base currency but no price on the opening date //all accounts using base currency are excluded, since that's the base used for foreing currency calculation //thus it is considered as always present //accounts that represent Income/Expense categories are also excluded as price is irrelevant for their //fake opening date since a forex rate is required for all multi-currency transactions //get all currencies in use QStringList currencyList; QList accountForeignCurrency; QList accList; accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { MyMoneyAccount account = *account_it; if (!account.isIncomeExpense() && !currencyList.contains(account.currencyId()) && account.currencyId() != baseCurrency().id() && !account.currencyId().isEmpty()) { //add the currency and the account-currency pair currencyList.append(account.currencyId()); accountForeignCurrency.append(account); } } MyMoneyPriceList pricesList = priceList(); QMap securityPriceDate; //get the first date of the price for each security MyMoneyPriceList::const_iterator prices_it; for (prices_it = pricesList.constBegin(); prices_it != pricesList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //only check the price if the currency is in use if (currencyList.contains(firstPrice.from()) || currencyList.contains(firstPrice.to())) { //check the security in the from field //if it is there, check if it is older QPair pricePair = qMakePair(firstPrice.from(), firstPrice.to()); securityPriceDate[pricePair] = firstPrice.date(); } } //compare the dates with the opening dates of the accounts using each currency QList::const_iterator accForeignList_it; bool firstInvProblem = true; for (accForeignList_it = accountForeignCurrency.constBegin(); accForeignList_it != accountForeignCurrency.constEnd(); ++accForeignList_it) { //setup the price pair correctly QPair pricePair; //setup the reverse, which can also be used for rate conversion QPair reversePricePair; if ((*accForeignList_it).isInvest()) { //if it is a stock, we have to search for a price from its stock to the currency of the account QString securityId = (*accForeignList_it).currencyId(); QString tradingCurrencyId = security(securityId).tradingCurrency(); pricePair = qMakePair(securityId, tradingCurrencyId); reversePricePair = qMakePair(tradingCurrencyId, securityId); } else { //if it is a regular account we search for a price from the currency of the account to the base currency QString currency = (*accForeignList_it).currencyId(); QString baseCurrencyId = baseCurrency().id(); pricePair = qMakePair(currency, baseCurrencyId); reversePricePair = qMakePair(baseCurrencyId, currency); } //compare the first price with the opening date of the account if ((!securityPriceDate.contains(pricePair) || securityPriceDate.value(pricePair) > (*accForeignList_it).openingDate()) && (!securityPriceDate.contains(reversePricePair) || securityPriceDate.value(reversePricePair) > (*accForeignList_it).openingDate())) { if (firstInvProblem) { firstInvProblem = false; rc << i18n("* Potential problem with investments/currencies"); } QDate openingDate = (*accForeignList_it).openingDate(); MyMoneySecurity secError = security((*accForeignList_it).currencyId()); if (!(*accForeignList_it).isInvest()) { rc << i18n(" * The account '%1' in currency '%2' has no price set for the opening date '%3'.", (*accForeignList_it).name(), secError.name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the currency on or before the opening date."); } else { rc << i18n(" * The investment '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the investment on or before the opening date."); } ++unfixedCount; } } // Fix the budgets that somehow still reference invalid accounts QString problemBudget; QList bList = budgetList(); for (QList::const_iterator it_b = bList.constBegin(); it_b != bList.constEnd(); ++it_b) { MyMoneyBudget b = *it_b; QList baccounts = b.getaccounts(); bool bChanged = false; for (QList::const_iterator it_bacc = baccounts.constBegin(); it_bacc != baccounts.constEnd(); ++it_bacc) { try { account((*it_bacc).id()); } catch (const MyMoneyException &) { problemCount++; if (problemBudget != b.name()) { problemBudget = b.name(); rc << i18n("* Problem with budget '%1'", problemBudget); } rc << i18n(" * The account with id %1 referenced by the budget does not exist anymore.", (*it_bacc).id()); rc << i18n(" The account reference will be removed."); // remove the reference to the account b.removeReference((*it_bacc).id()); bChanged = true; } } if (bChanged) { d->m_storage->modifyBudget(b); } } // add more checks here if (problemCount == 0 && unfixedCount == 0) { rc << i18n("Finished: data is consistent."); } else { const QString problemsCorrected = i18np("%1 problem corrected.", "%1 problems corrected.", problemCount); const QString problemsRemaining = i18np("%1 problem still present.", "%1 problems still present.", unfixedCount); rc << QString(); rc << i18nc("%1 is a string, e.g. 7 problems corrected; %2 is a string, e.g. 3 problems still present", "Finished: %1 %2", problemsCorrected, problemsRemaining); } return rc; } QString MyMoneyFile::createCategory(const MyMoneyAccount& base, const QString& name) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent = base; QString categoryText; if (base.id() != expense().id() && base.id() != income().id()) throw MYMONEYEXCEPTION("Invalid base category"); QStringList subAccounts = name.split(AccountSeperator); QStringList::Iterator it; for (it = subAccounts.begin(); it != subAccounts.end(); ++it) { MyMoneyAccount categoryAccount; categoryAccount.setName(*it); categoryAccount.setAccountType(base.accountType()); if (it == subAccounts.begin()) categoryText += *it; else categoryText += (AccountSeperator + *it); // Only create the account if it doesn't exist try { QString categoryId = categoryToAccount(categoryText); if (categoryId.isEmpty()) addAccount(categoryAccount, parent); else { categoryAccount = account(categoryId); } } catch (const MyMoneyException &e) { qDebug("Unable to add account %s, %s, %s: %s", qPrintable(categoryAccount.name()), qPrintable(parent.name()), qPrintable(categoryText), qPrintable(e.what())); } parent = categoryAccount; } return categoryToAccount(name); } const QList MyMoneyFile::scheduleListEx(int scheduleTypes, int scheduleOcurrences, int schedulePaymentTypes, QDate startDate, const QStringList& accounts) const { d->checkStorage(); return d->m_storage->scheduleListEx(scheduleTypes, scheduleOcurrences, schedulePaymentTypes, startDate, accounts); } void MyMoneyFile::addSecurity(MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->addSecurity(security); // The notifier mechanism only refreshes the cache but does not // load new objects. So we simply force loading of the new one here d->m_cache.preloadSecurity(security); d->m_changeSet += MyMoneyNotification(notifyAdd, security); } void MyMoneyFile::modifySecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->modifySecurity(security); d->addCacheNotification(security.id()); d->m_changeSet += MyMoneyNotification(notifyModify, security); } void MyMoneyFile::removeSecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); // FIXME check that security is not referenced by other object // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->removeSecurity(security); d->addCacheNotification(security.id(), false); d->m_changeSet += MyMoneyNotification(notifyRemove, security); } const MyMoneySecurity& MyMoneyFile::security(const QString& id) const { if (id.isEmpty()) return baseCurrency(); return d->m_cache.security(id); } const QList MyMoneyFile::securityList() const { d->checkStorage(); return d->m_storage->securityList(); } void MyMoneyFile::addCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->addCurrency(currency); // The notifier mechanism only refreshes the cache but does not // load new objects. So we simply force loading of the new one here d->m_cache.preloadSecurity(currency); d->m_changeSet += MyMoneyNotification(notifyAdd, currency); } void MyMoneyFile::modifyCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // force reload of base currency object if (currency.id() == d->m_baseCurrency.id()) d->m_baseCurrency.clearId(); d->m_storage->modifyCurrency(currency); d->addCacheNotification(currency.id()); d->m_changeSet += MyMoneyNotification(notifyModify, currency); } void MyMoneyFile::removeCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); if (currency.id() == d->m_baseCurrency.id()) { throw MYMONEYEXCEPTION("Cannot delete base currency."); } // FIXME check that security is not referenced by other object // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->removeCurrency(currency); d->addCacheNotification(currency.id(), false); d->m_changeSet += MyMoneyNotification(notifyRemove, currency); } const MyMoneySecurity& MyMoneyFile::currency(const QString& id) const { if (id.isEmpty()) return baseCurrency(); const MyMoneySecurity& curr = d->m_cache.security(id); if (curr.id().isEmpty()) { QString msg; msg = QString("Currency '%1' not found.").arg(id); throw MYMONEYEXCEPTION(msg); } return curr; } const QMap MyMoneyFile::ancientCurrencies() const { QMap ancientCurrencies; ancientCurrencies.insert(MyMoneySecurity("ATS", i18n("Austrian Schilling"), "ÖS"), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 137603), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("DEM", i18n("German Mark"), "DM"), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 195583), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FRF", i18n("French Franc"), "FF"), MyMoneyPrice("FRF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 655957), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ITL", i18n("Italian Lira"), QChar(0x20A4)), MyMoneyPrice("ITL", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 193627), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ESP", i18n("Spanish Peseta"), QString()), MyMoneyPrice("ESP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 166386), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("NLG", i18n("Dutch Guilder"), QString()), MyMoneyPrice("NLG", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 220371), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("BEF", i18n("Belgian Franc"), "Fr"), MyMoneyPrice("BEF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("LUF", i18n("Luxembourg Franc"), "Fr"), MyMoneyPrice("LUF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("PTE", i18n("Portuguese Escudo"), QString()), MyMoneyPrice("PTE", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 200482), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("IEP", i18n("Irish Pound"), QChar(0x00A3)), MyMoneyPrice("IEP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000000, 787564), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FIM", i18n("Finnish Markka"), QString()), MyMoneyPrice("FIM", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 594573), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("GRD", i18n("Greek Drachma"), QChar(0x20AF)), MyMoneyPrice("GRD", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 34075), QLatin1Literal("KMyMoney"))); // http://en.wikipedia.org/wiki/Bulgarian_lev ancientCurrencies.insert(MyMoneySecurity("BGL", i18n("Bulgarian Lev"), "BGL"), MyMoneyPrice("BGL", "BGN", QDate(1999, 7, 5), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ROL", i18n("Romanian Leu"), "ROL"), MyMoneyPrice("ROL", "RON", QDate(2005, 6, 30), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("RUR", i18n("Russian Ruble (old)"), "RUR"), MyMoneyPrice("RUR", "RUB", QDate(1998, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("SIT", i18n("Slovenian Tolar"), "SIT"), MyMoneyPrice("SIT", "EUR", QDate(2006, 12, 31), MyMoneyMoney(1, 23964), QLatin1Literal("KMyMoney"))); // Source: http://www.tf-portfoliosolutions.net/products/turkishlira.aspx ancientCurrencies.insert(MyMoneySecurity("TRL", i18n("Turkish Lira (old)"), "TL"), MyMoneyPrice("TRL", "TRY", QDate(2004, 12, 31), MyMoneyMoney(1, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/malta-und-zypern_aid_66058.html ancientCurrencies.insert(MyMoneySecurity("MTL", i18n("Maltese Lira"), "MTL"), MyMoneyPrice("MTL", "EUR", QDate(2008, 1, 1), MyMoneyMoney(429300, 1000000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("CYP", i18n("Cyprus Pound"), QString("C%1").arg(QChar(0x00A3))), MyMoneyPrice("CYP", "EUR", QDate(2008, 1, 1), MyMoneyMoney(585274, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/waehrungszone-slowakei-ist-neuer-euro-staat_aid_359025.html ancientCurrencies.insert(MyMoneySecurity("SKK", i18n("Slovak Koruna"), "SKK"), MyMoneyPrice("SKK", "EUR", QDate(2008, 12, 31), MyMoneyMoney(1000, 30126), QLatin1Literal("KMyMoney"))); // Source: http://en.wikipedia.org/wiki/Mozambican_metical ancientCurrencies.insert(MyMoneySecurity("MZM", i18n("Mozambique Metical"), "MT"), MyMoneyPrice("MZM", "MZN", QDate(2006, 7, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); // Source https://en.wikipedia.org/wiki/Azerbaijani_manat ancientCurrencies.insert(MyMoneySecurity("AZM", i18n("Azerbaijani Manat"), "m."), MyMoneyPrice("AZM", "AZN", QDate(2006, 1, 1), MyMoneyMoney(1, 5000), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Litas ancientCurrencies.insert(MyMoneySecurity("LTL", i18n("Lithuanian Litas"), "Lt"), MyMoneyPrice("LTL", "EUR", QDate(2015, 1, 1), MyMoneyMoney(100000, 345280), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Belarusian_ruble ancientCurrencies.insert(MyMoneySecurity("BYR", i18n("Belarusian Ruble (old)"), "BYR"), MyMoneyPrice("BYR", "BYN", QDate(2016, 7, 1), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); return ancientCurrencies; } const QList MyMoneyFile::availableCurrencyList() const { QList currencyList; currencyList.append(MyMoneySecurity("AFA", i18n("Afghanistan Afghani"))); currencyList.append(MyMoneySecurity("ALL", i18n("Albanian Lek"))); currencyList.append(MyMoneySecurity("ANG", i18n("Netherland Antillian Guilder"))); currencyList.append(MyMoneySecurity("DZD", i18n("Algerian Dinar"))); currencyList.append(MyMoneySecurity("ADF", i18n("Andorran Franc"))); currencyList.append(MyMoneySecurity("ADP", i18n("Andorran Peseta"))); currencyList.append(MyMoneySecurity("AON", i18n("Angolan New Kwanza"))); currencyList.append(MyMoneySecurity("ARS", i18n("Argentine Peso"), "$")); currencyList.append(MyMoneySecurity("AWG", i18n("Aruban Florin"))); currencyList.append(MyMoneySecurity("AUD", i18n("Australian Dollar"), "$")); currencyList.append(MyMoneySecurity("AZN", i18n("Azerbaijani Manat"), "m.")); currencyList.append(MyMoneySecurity("BSD", i18n("Bahamian Dollar"), "$")); currencyList.append(MyMoneySecurity("BHD", i18n("Bahraini Dinar"), "BHD", 1000)); currencyList.append(MyMoneySecurity("BDT", i18n("Bangladeshi Taka"))); currencyList.append(MyMoneySecurity("BBD", i18n("Barbados Dollar"), "$")); currencyList.append(MyMoneySecurity("BTC", i18n("Bitcoin"), "BTC")); currencyList.append(MyMoneySecurity("BYN", i18n("Belarusian Ruble"), "Br")); currencyList.append(MyMoneySecurity("BZD", i18n("Belize Dollar"), "$")); currencyList.append(MyMoneySecurity("BMD", i18n("Bermudian Dollar"), "$")); currencyList.append(MyMoneySecurity("BTN", i18n("Bhutan Ngultrum"))); currencyList.append(MyMoneySecurity("BOB", i18n("Bolivian Boliviano"))); currencyList.append(MyMoneySecurity("BAM", i18n("Bosnian Convertible Mark"))); currencyList.append(MyMoneySecurity("BWP", i18n("Botswana Pula"))); currencyList.append(MyMoneySecurity("BRL", i18n("Brazilian Real"), "R$")); currencyList.append(MyMoneySecurity("GBP", i18n("British Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("BND", i18n("Brunei Dollar"), "$")); currencyList.append(MyMoneySecurity("BGN", i18n("Bulgarian Lev (new)"))); currencyList.append(MyMoneySecurity("BIF", i18n("Burundi Franc"))); currencyList.append(MyMoneySecurity("XAF", i18n("CFA Franc BEAC"))); currencyList.append(MyMoneySecurity("XOF", i18n("CFA Franc BCEAO"))); currencyList.append(MyMoneySecurity("XPF", i18n("CFP Franc Pacifique"), "F", 1, 100)); currencyList.append(MyMoneySecurity("KHR", i18n("Cambodia Riel"))); currencyList.append(MyMoneySecurity("CAD", i18n("Canadian Dollar"), "$")); currencyList.append(MyMoneySecurity("CVE", i18n("Cape Verde Escudo"))); currencyList.append(MyMoneySecurity("KYD", i18n("Cayman Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("CLP", i18n("Chilean Peso"))); currencyList.append(MyMoneySecurity("CNY", i18n("Chinese Yuan Renminbi"))); currencyList.append(MyMoneySecurity("COP", i18n("Colombian Peso"))); currencyList.append(MyMoneySecurity("KMF", i18n("Comoros Franc"))); currencyList.append(MyMoneySecurity("CRC", i18n("Costa Rican Colon"), QChar(0x20A1))); currencyList.append(MyMoneySecurity("HRK", i18n("Croatian Kuna"))); currencyList.append(MyMoneySecurity("CUP", i18n("Cuban Peso"))); currencyList.append(MyMoneySecurity("CZK", i18n("Czech Koruna"))); currencyList.append(MyMoneySecurity("DKK", i18n("Danish Krone"), "kr")); currencyList.append(MyMoneySecurity("DJF", i18n("Djibouti Franc"))); currencyList.append(MyMoneySecurity("DOP", i18n("Dominican Peso"))); currencyList.append(MyMoneySecurity("XCD", i18n("East Caribbean Dollar"), "$")); currencyList.append(MyMoneySecurity("EGP", i18n("Egyptian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SVC", i18n("El Salvador Colon"))); currencyList.append(MyMoneySecurity("ERN", i18n("Eritrean Nakfa"))); currencyList.append(MyMoneySecurity("EEK", i18n("Estonian Kroon"))); currencyList.append(MyMoneySecurity("ETB", i18n("Ethiopian Birr"))); currencyList.append(MyMoneySecurity("EUR", i18n("Euro"), QChar(0x20ac))); currencyList.append(MyMoneySecurity("FKP", i18n("Falkland Islands Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("FJD", i18n("Fiji Dollar"), "$")); currencyList.append(MyMoneySecurity("GMD", i18n("Gambian Dalasi"))); currencyList.append(MyMoneySecurity("GEL", i18n("Georgian Lari"))); currencyList.append(MyMoneySecurity("GHC", i18n("Ghanaian Cedi"))); currencyList.append(MyMoneySecurity("GIP", i18n("Gibraltar Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("GTQ", i18n("Guatemalan Quetzal"))); currencyList.append(MyMoneySecurity("GWP", i18n("Guinea-Bissau Peso"))); currencyList.append(MyMoneySecurity("GYD", i18n("Guyanan Dollar"), "$")); currencyList.append(MyMoneySecurity("HTG", i18n("Haitian Gourde"))); currencyList.append(MyMoneySecurity("HNL", i18n("Honduran Lempira"))); currencyList.append(MyMoneySecurity("HKD", i18n("Hong Kong Dollar"), "$")); currencyList.append(MyMoneySecurity("HUF", i18n("Hungarian Forint"), "HUF", 1, 100)); currencyList.append(MyMoneySecurity("ISK", i18n("Iceland Krona"))); currencyList.append(MyMoneySecurity("INR", i18n("Indian Rupee"), QChar(0x20A8))); currencyList.append(MyMoneySecurity("IDR", i18n("Indonesian Rupiah"), "IDR", 1)); currencyList.append(MyMoneySecurity("IRR", i18n("Iranian Rial"), "IRR", 1)); currencyList.append(MyMoneySecurity("IQD", i18n("Iraqi Dinar"), "IQD", 1000)); currencyList.append(MyMoneySecurity("ILS", i18n("Israeli New Shekel"), QChar(0x20AA))); currencyList.append(MyMoneySecurity("JMD", i18n("Jamaican Dollar"), "$")); currencyList.append(MyMoneySecurity("JPY", i18n("Japanese Yen"), QChar(0x00A5), 1)); currencyList.append(MyMoneySecurity("JOD", i18n("Jordanian Dinar"), "JOD", 1000)); currencyList.append(MyMoneySecurity("KZT", i18n("Kazakhstan Tenge"))); currencyList.append(MyMoneySecurity("KES", i18n("Kenyan Shilling"))); currencyList.append(MyMoneySecurity("KWD", i18n("Kuwaiti Dinar"), "KWD", 1000)); currencyList.append(MyMoneySecurity("KGS", i18n("Kyrgyzstan Som"))); currencyList.append(MyMoneySecurity("LAK", i18n("Laos Kip"), QChar(0x20AD))); currencyList.append(MyMoneySecurity("LVL", i18n("Latvian Lats"))); currencyList.append(MyMoneySecurity("LBP", i18n("Lebanese Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("LSL", i18n("Lesotho Loti"))); currencyList.append(MyMoneySecurity("LRD", i18n("Liberian Dollar"), "$")); currencyList.append(MyMoneySecurity("LYD", i18n("Libyan Dinar"), "LYD", 1000)); currencyList.append(MyMoneySecurity("MOP", i18n("Macau Pataca"))); currencyList.append(MyMoneySecurity("MKD", i18n("Macedonian Denar"))); currencyList.append(MyMoneySecurity("MGF", i18n("Malagasy Franc"), "MGF", 500)); currencyList.append(MyMoneySecurity("MWK", i18n("Malawi Kwacha"))); currencyList.append(MyMoneySecurity("MYR", i18n("Malaysian Ringgit"))); currencyList.append(MyMoneySecurity("MVR", i18n("Maldive Rufiyaa"))); currencyList.append(MyMoneySecurity("MLF", i18n("Mali Republic Franc"))); currencyList.append(MyMoneySecurity("MRO", i18n("Mauritanian Ouguiya"), "MRO", 5)); currencyList.append(MyMoneySecurity("MUR", i18n("Mauritius Rupee"))); currencyList.append(MyMoneySecurity("MXN", i18n("Mexican Peso"), "$")); currencyList.append(MyMoneySecurity("MDL", i18n("Moldavian Leu"))); currencyList.append(MyMoneySecurity("MNT", i18n("Mongolian Tugrik"), QChar(0x20AE))); currencyList.append(MyMoneySecurity("MAD", i18n("Moroccan Dirham"))); currencyList.append(MyMoneySecurity("MZN", i18n("Mozambique Metical"), "MT")); currencyList.append(MyMoneySecurity("MMK", i18n("Myanmar Kyat"))); currencyList.append(MyMoneySecurity("NAD", i18n("Namibian Dollar"), "$")); currencyList.append(MyMoneySecurity("NPR", i18n("Nepalese Rupee"))); currencyList.append(MyMoneySecurity("NZD", i18n("New Zealand Dollar"), "$")); currencyList.append(MyMoneySecurity("NIC", i18n("Nicaraguan Cordoba Oro"))); currencyList.append(MyMoneySecurity("NGN", i18n("Nigerian Naira"), QChar(0x20A6))); currencyList.append(MyMoneySecurity("KPW", i18n("North Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("NOK", i18n("Norwegian Kroner"), "kr")); currencyList.append(MyMoneySecurity("OMR", i18n("Omani Rial"), "OMR", 1000)); currencyList.append(MyMoneySecurity("PKR", i18n("Pakistan Rupee"))); currencyList.append(MyMoneySecurity("PAB", i18n("Panamanian Balboa"))); currencyList.append(MyMoneySecurity("PGK", i18n("Papua New Guinea Kina"))); currencyList.append(MyMoneySecurity("PYG", i18n("Paraguay Guarani"))); currencyList.append(MyMoneySecurity("PEN", i18n("Peruvian Nuevo Sol"))); currencyList.append(MyMoneySecurity("PHP", i18n("Philippine Peso"), QChar(0x20B1))); currencyList.append(MyMoneySecurity("PLN", i18n("Polish Zloty"))); currencyList.append(MyMoneySecurity("QAR", i18n("Qatari Rial"))); currencyList.append(MyMoneySecurity("RON", i18n("Romanian Leu (new)"))); currencyList.append(MyMoneySecurity("RUB", i18n("Russian Ruble"))); currencyList.append(MyMoneySecurity("RWF", i18n("Rwanda Franc"))); currencyList.append(MyMoneySecurity("WST", i18n("Samoan Tala"))); currencyList.append(MyMoneySecurity("STD", i18n("Sao Tome and Principe Dobra"))); currencyList.append(MyMoneySecurity("SAR", i18n("Saudi Riyal"))); currencyList.append(MyMoneySecurity("RSD", i18n("Serbian Dinar"))); currencyList.append(MyMoneySecurity("SCR", i18n("Seychelles Rupee"))); currencyList.append(MyMoneySecurity("SLL", i18n("Sierra Leone Leone"))); currencyList.append(MyMoneySecurity("SGD", i18n("Singapore Dollar"), "$")); currencyList.append(MyMoneySecurity("SBD", i18n("Solomon Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("SOS", i18n("Somali Shilling"))); currencyList.append(MyMoneySecurity("ZAR", i18n("South African Rand"))); currencyList.append(MyMoneySecurity("KRW", i18n("South Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("LKR", i18n("Sri Lanka Rupee"))); currencyList.append(MyMoneySecurity("SHP", i18n("St. Helena Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SDD", i18n("Sudanese Dinar"))); currencyList.append(MyMoneySecurity("SRG", i18n("Suriname Guilder"))); currencyList.append(MyMoneySecurity("SZL", i18n("Swaziland Lilangeni"))); currencyList.append(MyMoneySecurity("SEK", i18n("Swedish Krona"))); currencyList.append(MyMoneySecurity("CHF", i18n("Swiss Franc"), "SFr")); currencyList.append(MyMoneySecurity("SYP", i18n("Syrian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("TWD", i18n("Taiwan Dollar"), "$")); currencyList.append(MyMoneySecurity("TJS", i18n("Tajikistan Somoni"))); currencyList.append(MyMoneySecurity("TZS", i18n("Tanzanian Shilling"))); currencyList.append(MyMoneySecurity("THB", i18n("Thai Baht"), QChar(0x0E3F))); currencyList.append(MyMoneySecurity("TOP", i18n("Tongan Pa'anga"))); currencyList.append(MyMoneySecurity("TTD", i18n("Trinidad and Tobago Dollar"), "$")); currencyList.append(MyMoneySecurity("TND", i18n("Tunisian Dinar"), "TND", 1000)); currencyList.append(MyMoneySecurity("TRY", i18n("Turkish Lira"), QChar(0x20BA))); currencyList.append(MyMoneySecurity("TMM", i18n("Turkmenistan Manat"))); currencyList.append(MyMoneySecurity("USD", i18n("US Dollar"), "$")); currencyList.append(MyMoneySecurity("UGX", i18n("Uganda Shilling"))); currencyList.append(MyMoneySecurity("UAH", i18n("Ukraine Hryvnia"))); currencyList.append(MyMoneySecurity("CLF", i18n("Unidad de Fometo"))); currencyList.append(MyMoneySecurity("AED", i18n("United Arab Emirates Dirham"))); currencyList.append(MyMoneySecurity("UYU", i18n("Uruguayan Peso"))); currencyList.append(MyMoneySecurity("UZS", i18n("Uzbekistani Sum"))); currencyList.append(MyMoneySecurity("VUV", i18n("Vanuatu Vatu"))); currencyList.append(MyMoneySecurity("VEB", i18n("Venezuelan Bolivar"))); currencyList.append(MyMoneySecurity("VND", i18n("Vietnamese Dong"), QChar(0x20AB))); currencyList.append(MyMoneySecurity("ZMK", i18n("Zambian Kwacha"))); currencyList.append(MyMoneySecurity("ZWD", i18n("Zimbabwe Dollar"), "$")); currencyList.append(MyMoneySecurity("XAU", i18n("Gold"), "XAU", 1000000)); currencyList.append(MyMoneySecurity("XPD", i18n("Palladium"), "XPD", 1000000)); currencyList.append(MyMoneySecurity("XPT", i18n("Platinum"), "XPT", 1000000)); currencyList.append(MyMoneySecurity("XAG", i18n("Silver"), "XAG", 1000000)); currencyList.append(ancientCurrencies().keys()); return currencyList; } const QList MyMoneyFile::currencyList() const { d->checkStorage(); return d->m_storage->currencyList(); } const QString& MyMoneyFile::foreignCurrency(const QString& first, const QString& second) const { if (baseCurrency().id() == second) return first; return second; } const MyMoneySecurity& MyMoneyFile::baseCurrency() const { if (d->m_baseCurrency.id().isEmpty()) { QString id = QString(value("kmm-baseCurrency")); if (!id.isEmpty()) d->m_baseCurrency = currency(id); } return d->m_baseCurrency; } void MyMoneyFile::setBaseCurrency(const MyMoneySecurity& curr) { // make sure the currency exists MyMoneySecurity c = currency(curr.id()); // clear all changed objects from cache MyMoneyNotifier notifier(d); if (c.id() != d->m_baseCurrency.id()) { setValue("kmm-baseCurrency", curr.id()); // force reload of base currency cache d->m_baseCurrency = MyMoneySecurity(); } } void MyMoneyFile::addPrice(const MyMoneyPrice& price) { if (price.rate(QString()).isZero()) return; d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->addPrice(price); } void MyMoneyFile::removePrice(const MyMoneyPrice& price) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->removePrice(price); } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate) const { d->checkStorage(); QString to(toId); if (to.isEmpty()) to = value("kmm-baseCurrency"); // if some id is missing, we can return an empty price object if (fromId.isEmpty() || to.isEmpty()) return MyMoneyPrice(); // we don't search our tables if someone asks stupid stuff if (fromId == toId) { return MyMoneyPrice(fromId, toId, date, MyMoneyMoney::ONE, "KMyMoney"); } // if not asking for exact date, try to find the exact date match first, // either the requested price or its reciprocal value. If unsuccessful, it will move // on and look for prices of previous dates MyMoneyPrice rc = d->m_storage->price(fromId, to, date, true); if (!rc.isValid()) { // not found, search 'to-from' rate and use reciprocal value rc = d->m_storage->price(to, fromId, date, true); // not found, search previous dates, if exact date is not needed if (!exactDate && !rc.isValid()) { // search 'from-to' and 'to-from', select the most recent one MyMoneyPrice fromPrice = d->m_storage->price(fromId, to, date, exactDate); MyMoneyPrice toPrice = d->m_storage->price(to, fromId, date, exactDate); // check first whether both prices are valid if (fromPrice.isValid() && toPrice.isValid()) { if (fromPrice.date() >= toPrice.date()) { // if 'from-to' is newer or the same date, prefer that one rc = fromPrice; } else { // otherwise, use the reciprocal price rc = toPrice; } } else if (fromPrice.isValid()) { // check if any of the prices is valid, return that one rc = fromPrice; } else if (toPrice.isValid()) { rc = toPrice; } } } return rc; } const MyMoneyPriceList MyMoneyFile::priceList() const { d->checkStorage(); return d->m_storage->priceList(); } bool MyMoneyFile::hasAccount(const QString& id, const QString& name) const { MyMoneyAccount acc = d->m_cache.account(id); QStringList list = acc.accountList(); QStringList::ConstIterator it; bool rc = false; for (it = list.constBegin(); rc == false && it != list.constEnd(); ++it) { MyMoneyAccount a = d->m_cache.account(*it); if (a.name() == name) rc = true; } return rc; } const QList MyMoneyFile::reportList() const { d->checkStorage(); return d->m_storage->reportList(); } void MyMoneyFile::addReport(MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->addReport(report); } void MyMoneyFile::modifyReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->modifyReport(report); d->addCacheNotification(report.id()); } unsigned MyMoneyFile::countReports() const { d->checkStorage(); return d->m_storage->countReports(); } const MyMoneyReport MyMoneyFile::report(const QString& id) const { d->checkStorage(); return d->m_storage->report(id); } void MyMoneyFile::removeReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); MyMoneyNotifier notifier(d); d->m_storage->removeReport(report); d->addCacheNotification(report.id(), false); } const QList MyMoneyFile::budgetList() const { d->checkStorage(); return d->m_storage->budgetList(); } void MyMoneyFile::addBudget(MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->addBudget(budget); } const MyMoneyBudget MyMoneyFile::budgetByName(const QString& name) const { d->checkStorage(); return d->m_storage->budgetByName(name); } void MyMoneyFile::modifyBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->modifyBudget(budget); d->addCacheNotification(budget.id()); } unsigned MyMoneyFile::countBudgets() const { d->checkStorage(); return d->m_storage->countBudgets(); } const MyMoneyBudget MyMoneyFile::budget(const QString& id) const { d->checkStorage(); return d->m_storage->budget(id); } void MyMoneyFile::removeBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->removeBudget(budget); d->addCacheNotification(budget.id(), false); } void MyMoneyFile::addOnlineJob(onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); d->m_storage->addOnlineJob(job); d->m_cache.preloadOnlineJob(job); d->m_changeSet += MyMoneyNotification(notifyAdd, job); } void MyMoneyFile::modifyOnlineJob(const onlineJob job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyOnlineJob(job); d->m_changeSet += MyMoneyNotification(notifyModify, job); d->addCacheNotification(job.id()); } const onlineJob MyMoneyFile::getOnlineJob(const QString &jobId) const { d->checkStorage(); return d->m_storage->getOnlineJob(jobId); } const QList MyMoneyFile::onlineJobList() const { d->checkStorage(); return d->m_storage->onlineJobList(); } /** @todo improve speed by passing count job to m_storage */ int MyMoneyFile::countOnlineJobs() const { return onlineJobList().count(); } /** * @brief Remove onlineJob * @param job onlineJob to remove */ void MyMoneyFile::removeOnlineJob(const onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); if (job.isLocked()) { return; } d->addCacheNotification(job.id(), false); d->m_changeSet += MyMoneyNotification(notifyRemove, job); d->m_storage->removeOnlineJob(job); } void MyMoneyFile::removeOnlineJob(const QStringList onlineJobIds) { foreach (QString jobId, onlineJobIds) { removeOnlineJob(getOnlineJob(jobId)); } } void MyMoneyFile::costCenterList(QList< MyMoneyCostCenter >& list) const { d->checkStorage(); list = d->m_storage->costCenterList(); } bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& account, const MyMoneyAccount& category, const MyMoneyMoney& amount) { bool rc = false; try { MyMoneySplit cat; // category MyMoneySplit tax; // tax if (category.value("VatAccount").isEmpty()) return false; MyMoneyAccount vatAcc = this->account(category.value("VatAccount").toLatin1()); const MyMoneySecurity& asec = security(account.currencyId()); const MyMoneySecurity& csec = security(category.currencyId()); const MyMoneySecurity& vsec = security(vatAcc.currencyId()); if (asec.id() != csec.id() || asec.id() != vsec.id()) { qDebug("Auto VAT assignment only works if all three accounts use the same currency."); return false; } MyMoneyMoney vatRate(vatAcc.value("VatRate")); MyMoneyMoney gv, nv; // gross value, net value int fract = account.fraction(); if (!vatRate.isZero()) { tax.setAccountId(vatAcc.id()); // qDebug("vat amount is '%s'", category.value("VatAmount").toLatin1()); if (category.value("VatAmount").toLower() != QString("net")) { // split value is the gross value gv = amount; nv = gv / (MyMoneyMoney::ONE + vatRate); MyMoneySplit catSplit = transaction.splitByAccount(account.id(), false); catSplit.setShares(-nv.convert(fract)); catSplit.setValue(catSplit.shares()); transaction.modifySplit(catSplit); } else { // split value is the net value nv = amount; gv = nv * (MyMoneyMoney::ONE + vatRate); MyMoneySplit accSplit = transaction.splitByAccount(account.id()); accSplit.setValue(gv.convert(fract)); accSplit.setShares(accSplit.value()); transaction.modifySplit(accSplit); } tax.setValue(-(gv - nv).convert(fract)); tax.setShares(tax.value()); transaction.addSplit(tax); rc = true; } } catch (const MyMoneyException &) { } return rc; } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj, const MyMoneyFileBitArray& skipChecks) const { d->checkStorage(); return d->m_storage->isReferenced(obj, skipChecks); } bool MyMoneyFile::checkNoUsed(const QString& accId, const QString& no) const { // by definition, an empty string or a non-numeric string is not used QRegExp exp(QString("(.*\\D)?(\\d+)(\\D.*)?")); if (no.isEmpty() || exp.indexIn(no) == -1) return false; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { MyMoneySplit split; // Test whether the transaction also includes a split into // this account split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty() && split.number() == no) return true; } catch (const MyMoneyException &) { } ++it_t; } return false; } QString MyMoneyFile::highestCheckNo(const QString& accId) const { unsigned64 lno = 0; unsigned64 cno; QString no; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account MyMoneySplit split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty()) { // non-numerical values stored in number will return 0 in the next line cno = split.number().toULongLong(); if (cno > lno) { lno = cno; no = split.number(); } } } catch (const MyMoneyException &) { } ++it_t; } return no; } bool MyMoneyFile::hasNewerTransaction(const QString& accId, const QDate& date) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.setDateFilter(date.addDays(+1), QDate()); QList transactions = transactionList(filter); return transactions.count() > 0; } void MyMoneyFile::clearCache() { d->checkStorage(); d->m_cache.clear(); d->m_balanceCache.clear(); } void MyMoneyFile::preloadCache() { d->checkStorage(); d->m_cache.clear(); QList a_list; d->m_storage->accountList(a_list); d->m_cache.preloadAccount(a_list); d->m_cache.preloadPayee(d->m_storage->payeeList()); d->m_cache.preloadTag(d->m_storage->tagList()); d->m_cache.preloadInstitution(d->m_storage->institutionList()); d->m_cache.preloadSecurity(d->m_storage->securityList() + d->m_storage->currencyList()); d->m_cache.preloadSchedule(d->m_storage->scheduleList()); d->m_cache.preloadOnlineJob(d->m_storage->onlineJobList()); } bool MyMoneyFile::isTransfer(const MyMoneyTransaction& t) const { bool rc = false; if (t.splitCount() == 2) { QList::const_iterator it_s; for (it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s) { MyMoneyAccount acc = account((*it_s).accountId()); if (acc.isIncomeExpense()) break; } if (it_s == t.splits().end()) rc = true; } return rc; } bool MyMoneyFile::referencesClosedAccount(const MyMoneyTransaction& t) const { QList::const_iterator it_s; const QList& list = t.splits(); for (it_s = list.begin(); it_s != list.end(); ++it_s) { if (referencesClosedAccount(*it_s)) break; } return it_s != list.end(); } bool MyMoneyFile::referencesClosedAccount(const MyMoneySplit& s) const { if (s.accountId().isEmpty()) return false; try { return account(s.accountId()).isClosed(); } catch (const MyMoneyException &) { } return false; } QString MyMoneyFile::storageId() { QString id = value("kmm-id"); if (id.isEmpty()) { MyMoneyFileTransaction ft; try { QUuid uid = QUuid::createUuid(); setValue("kmm-id", uid.toString()); ft.commit(); id = uid.toString(); } catch (const MyMoneyException &) { qDebug("Unable to setup UID for new storage object"); } } return id; } +const QString MyMoneyFile::openingBalancesPrefix() +{ + return i18n("Opening Balances"); +} + bool MyMoneyFile::hasMatchingOnlineBalance(const MyMoneyAccount& _acc) const { // get current values MyMoneyAccount acc = account(_acc.id()); // if there's no last transaction import data we are done if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) return false; // otherwise, we compare the balances MyMoneyMoney balance(acc.value("lastStatementBalance")); MyMoneyMoney accBalance = this->balance(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate)); return balance == accBalance; } int MyMoneyFile::countTransactionsWithSpecificReconciliationState(const QString& accId, enum MyMoneyTransactionFilter::stateOptionE state) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.addState(state); return transactionList(filter).count(); } /** * Make sure that the splits value has the precision of the corresponding account */ void MyMoneyFile::fixSplitPrecision(MyMoneyTransaction& t) const { QList::iterator its; MyMoneySecurity transactionSecurity = security(t.commodity()); int transactionFraction = transactionSecurity.smallestAccountFraction(); for(its = t.splits().begin(); its != t.splits().end(); ++its) { MyMoneyAccount acc = account((*its).accountId()); int fraction = acc.fraction(); if(fraction == -1) { MyMoneySecurity sec = security(acc.currencyId()); fraction = acc.fraction(sec); } (*its).setShares((*its).shares().convertDenominator(fraction).canonicalize()); (*its).setValue((*its).value().convertDenominator(transactionFraction).canonicalize()); } } MyMoneyFileTransaction::MyMoneyFileTransaction() : m_isNested(MyMoneyFile::instance()->hasTransaction()), m_needRollback(!m_isNested) { if (!m_isNested) MyMoneyFile::instance()->startTransaction(); } MyMoneyFileTransaction::~MyMoneyFileTransaction() { rollback(); } void MyMoneyFileTransaction::restart() { rollback(); m_needRollback = !m_isNested; if (!m_isNested) MyMoneyFile::instance()->startTransaction(); } void MyMoneyFileTransaction::commit() { if (!m_isNested) MyMoneyFile::instance()->commitTransaction(); m_needRollback = false; } void MyMoneyFileTransaction::rollback() { if (m_needRollback) MyMoneyFile::instance()->rollbackTransaction(); m_needRollback = false; } diff --git a/kmymoney/mymoney/mymoneyfile.h b/kmymoney/mymoney/mymoneyfile.h index 2a74d0736..81e1abdac 100644 --- a/kmymoney/mymoney/mymoneyfile.h +++ b/kmymoney/mymoney/mymoneyfile.h @@ -1,1705 +1,1705 @@ /*************************************************************************** mymoneyfile.h ------------------- copyright : (C) 2002, 2007 by Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 MYMONEYFILE_H #define MYMONEYFILE_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mymoneyschedule.h" #include #include /** * @author Thomas Baumgart, Michael Edwardes, Kevin Tambascio, Christian Dávid */ class IMyMoneyStorage; class MyMoneyTransactionFilter; /** * This class represents the interface to the MyMoney engine. * For historical reasons it is still called MyMoneyFile. * It is implemented using the singleton pattern and thus only * exists once for each running instance of an application. * * The instance of the MyMoneyFile object is accessed as follows: * * @code * MyMoneyFile *file = MyMoneyFile::instance(); * file->anyMemberFunction(); * @endcode * * The first line of the above code creates a unique MyMoneyFile * object if it is called for the first time ever. All subsequent * calls to this functions return a pointer to the object created * during the first call. * * As the MyMoneyFile object represents the business logic, a storage * manager must be attached to it. This mechanism allows to use different * access methods to store the objects. The interface to access such an * storage manager is defined in the class IMyMoneyStorage. The methods * attachStorage() and detachStorage() are used to attach/detach a * storage manager object. The following code can be used to create a * functional MyMoneyFile instance: * * @code * IMyMoneyStorage *storage = .... * MyMoneyFile *file = MyMoneyFile::instance(); * file->attachStorage(storage); * @endcode * * The methods addAccount(), modifyAccount() and removeAccount() implement the * general account maintenance functions. The method reparentAccount() is * available to move an account from one superordinate account to another. * account() and accountList() are used to retrieve a single instance or a * QList of MyMoneyAccount objects. * * The methods addInstitution(), modifyInstitution() and removeInstitution() * implement the general institution maintenance functions. institution() and * institutionList() are used to retrieve a single instance or a * QList of MyMoneyInstitution objects. * * The methods addPayee(), modifyPayee() and removePayee() * implement the general payee maintenance functions. * payee() and payeeList() are used to retrieve a single instance or a * QList of MyMoneyPayee objects. * * The methods addTag(), modifyTag() and removeTag() * implement the general tag maintenance functions. * tag() and tagList() are used to retrieve a single instance or a * QList of MyMoneyTag objects. * * The methods addTransaction(), modifyTransaction() and removeTransaction() * implement the general transaction maintenance functions. * transaction() and transactionList() are used to retrieve * a single instance or a QList of MyMoneyTransaction objects. * * The methods addSecurity(), modifySecurity() and removeSecurity() * implement the general access to equities held in the engine. * * The methods addCurrency(), modifyCurrency() and removeCurrency() * implement the general access to multiple currencies held in the engine. * The methods baseCurrency() and setBaseCurrency() allow to retrieve/set * the currency selected by the user as base currency. If a currency * reference is emtpy, it will usually be interpreted as baseCurrency(). * * The methods liability(), asset(), expense(), income() and equity() are * used to retrieve the five standard accounts. isStandardAccount() * checks if a given accountId references one of the or not. * setAccountName() is used to specify a name for the standard accounts * from the GUI. * * The MyMoneyFile object emits the dataChanged() signal when data * has been changed. * * For abritrary values that have to be stored with the storage object * but are of importance to the application only, the object is derived * for MyMoneyKeyValueContainer which provides a container to store * these values indexed by an alphanumeric key. * * @exception MyMoneyException is thrown whenever an error occurs * while the engine code is running. The MyMoneyException:: object * describes the problem. */ class KMM_MYMONEY_EXPORT MyMoneyFile : public QObject { Q_OBJECT KMM_MYMONEY_UNIT_TESTABLE public: /** * notificationObject identifies the type of the object * for which this notification is stored */ typedef enum { notifyAccount = 1, notifyInstitution, notifyPayee, notifyTransaction, notifyTag, notifySchedule, notifySecurity, notifyOnlineJob } notificationObjectT; /** * notificationMode identifies the type of notifiation * (add, modify, remove) */ typedef enum { notifyAdd = 1, notifyModify, notifyRemove } notificationModeT; friend class MyMoneyNotifier; /** * This is the function to access the MyMoneyFile object. * It returns a pointer to the single instance of the object. */ static inline MyMoneyFile* instance() { return &file; } /** * This is the destructor for any MyMoneyFile object */ ~MyMoneyFile(); /** * @deprecated This is a convenience constructor. Do not use it anymore. * It will be deprecated in a future version of the engine. * * @param storage pointer to object that implements the IMyMoneyStorage * interface. */ MyMoneyFile(IMyMoneyStorage *storage); // general get functions const MyMoneyPayee& user() const; // general set functions void setUser(const MyMoneyPayee& user); /** * This method is used to attach a storage object to the MyMoneyFile object * Without an attached storage object, the MyMoneyFile object is * of no use. * * After successful completion, the dataChanged() signal is emitted. * * In case of an error condition, an exception is thrown. * The following error conditions are checked: * * - @a storage is not equal to 0 * - there is no other @a storage object attached (use detachStorage() * to revert the attachStorage() operation. * * @param storage pointer to object that implements the IMyMoneyStorage * interface. * * @sa detachStorage() */ void attachStorage(IMyMoneyStorage* const storage); /** * This method is used to detach a previously attached storage * object from the MyMoneyFile object. If no storage object * is attached to the engine, this is a NOP. * * @param storage pointer to object that implements the IMyMoneyStorage * interface. * * @sa attachStorage() */ void detachStorage(IMyMoneyStorage* const storage = 0); /** * This method returns whether a storage is currently attached to * the engine or not. * * @return true if storage object is attached, false otherwise */ bool storageAttached() const; /** * This method returns a pointer to the storage object * * @return const pointer to the current attached storage object. * If no object is attached, returns 0. */ IMyMoneyStorage* storage() const; /** * This method must be called before any single change or a series of changes * in the underlying storage area is performed. * Once all changes are complete (i.e. the transaction is completed), * commitTransaction() must be called to finalize all changes. If an error occurs * during the processing of the changes call rollbackTransaction() to undo the * changes done so far. */ void startTransaction(); /** * This method returns whether a transaction has been started (@a true) * or not (@a false). */ bool hasTransaction() const; /** * @sa startTransaction() */ void commitTransaction(); /** * @sa startTransaction() */ void rollbackTransaction(); /** * This method is used to return the standard liability account * @return MyMoneyAccount liability account(group) */ const MyMoneyAccount& liability() const; /** * This method is used to return the standard asset account * @return MyMoneyAccount asset account(group) */ const MyMoneyAccount& asset() const; /** * This method is used to return the standard expense account * @return MyMoneyAccount expense account(group) */ const MyMoneyAccount& expense() const; /** * This method is used to return the standard income account * @return MyMoneyAccount income account(group) */ const MyMoneyAccount& income() const; /** * This method is used to return the standard equity account * @return MyMoneyAccount equity account(group) */ const MyMoneyAccount& equity() const; /** * This method returns the account information for the opening * balances account for the given @p security. If the respective * account does not exist, it will be created. The name is constructed - * using MyMoneyFile::OpeningBalancesPrefix and appending " (xxx)" in + * using MyMoneyFile::openingBalancesPrefix() and appending " (xxx)" in * case the @p security is not the baseCurrency(). The account created * will be a sub-account of the standard equity account provided by equity(). * * @param security Security for which the account is searched * * @return The opening balance account * * @note No notifications will be sent! */ const MyMoneyAccount openingBalanceAccount(const MyMoneySecurity& security); /** * This method is essentially the same as the above, except it works on * const objects. If there is no opening balance account, this method * WILL NOT create one. Instead it will thrown an exception. * * @param security Security for which the account is searched * * @return The opening balance account * * @note No notifications will be sent! */ const MyMoneyAccount openingBalanceAccount(const MyMoneySecurity& security) const; /** * Create an opening balance transaction for the account @p acc * with a value of @p balance. If the corresponding opening balance account * for the account's currency does not exist it will be created. If it exists * and it's opening date is later than the opening date of @p acc, * the opening date of the opening balances account will be adjusted to the * one of @p acc. * * @param acc reference to account for which the opening balance transaction * should be created * @param balance reference to the value of the opening balance transaction * * @returns The created MyMoneyTransaction object. In case no transaction has been * created, the id of the object is empty. */ MyMoneyTransaction createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance); /** * Retrieve the opening balance transaction for the account @p acc. * If there is no opening balance transaction, QString() will be returned. * * @param acc reference to account for which the opening balance transaction * should be retrieved * @return QString id for the transaction, or QString() if no transaction exists */ QString openingBalanceTransaction(const MyMoneyAccount& acc) const; /** * This method returns an indicator if the MyMoneyFile object has been * changed after it has last been saved to permanent storage. * * @return true if changed, false if not */ bool dirty() const; /** * This method is used to force the attached storage object to * be dirty. This is used by the application to re-set the dirty * flag after a failed upload to a server when the save operation * to a local temp file was OK. */ void setDirty() const; /** * Adds an institution to the file-global institution pool. A * respective institution-ID will be generated for this object. * The ID is stored as QString in the object passed as argument. * * An exception will be thrown upon error conditions. * @param institution The complete institution information in a * MyMoneyInstitution object */ void addInstitution(MyMoneyInstitution& institution); /** * Modifies an already existing institution in the file global * institution pool. * * An exception will be thrown upon error conditions. * * @param institution The complete new institution information */ void modifyInstitution(const MyMoneyInstitution& institution); /** * Deletes an existing institution from the file global institution pool * Also modifies the accounts that reference this institution as * their institution. * * An exception will be thrown upon error conditions. * * @param institution institution to be deleted. */ void removeInstitution(const MyMoneyInstitution& institution); /** * Adds an account to the file-global account pool. A respective * account-ID will be generated within this record. The modified * members of @a account will be updated. * * A few parameters of the account to be added are checked against * the following conditions. If they do not match, an exception is * thrown. * * An account must match the following conditions: * * a) the account must have a name with length > 0 * b) the account must not have an id assigned * c) the transaction list must be empty * d) the account must not have any sub-ordinate accounts * e) the account must have no parent account * f) the account must not have any reference to a MyMoneyFile object * * An exception will be thrown upon error conditions. * * @param account The complete account information in a MyMoneyAccount object * @param parent The complete account information of the parent account */ void addAccount(MyMoneyAccount& account, MyMoneyAccount& parent); /** * Modifies an already existing account in the file global account pool. * * An exception will be thrown upon error conditions. * * @param account reference to the new account information */ void modifyAccount(const MyMoneyAccount& account); /** * This method re-parents an existing account * * An exception will be thrown upon error conditions. * * @param account MyMoneyAccount reference to account to be re-parented * @param parent MyMoneyAccount reference to new parent account */ void reparentAccount(MyMoneyAccount &account, MyMoneyAccount& parent); /** * moves splits from one account to another * * @param oldAccount id of the current account * @param newAccount if of the new account * * @return the number of modified splits */ unsigned int moveSplits(const QString& oldAccount, const QString& newAccount); /** * This method is used to determince, if the account with the * given ID is referenced by any split in m_transactionList. * * @param id id of the account to be checked for * @return true if account is referenced, false otherwise */ bool hasActiveSplits(const QString& id) const; /** * This method is used to check whether a given * account id references one of the standard accounts or not. * * An exception will be thrown upon error conditions. * * @param id account id * @return true if account-id is one of the standards, false otherwise */ bool isStandardAccount(const QString& id) const; /** * Returns @a true, if transaction @p t is a transfer transaction. * A transfer transaction has two splits, both referencing either * an asset, a liability or an equity account. */ bool isTransfer(const MyMoneyTransaction& t) const; /** * This method is used to set the name for the specified standard account * within the storage area. An exception will be thrown, if an error * occurs * * @param id QString reference to one of the standard accounts. * @param name QString reference to the name to be set * */ void setAccountName(const QString& id, const QString& name) const; /** * Deletes an existing account from the file global account pool * This method only allows to remove accounts that are not * referenced by any split. Use moveSplits() to move splits * to another account. An exception is thrown in case of a * problem. * * @param account reference to the account to be deleted. */ void removeAccount(const MyMoneyAccount& account); /** * Deletes existing accounts and their subaccounts recursivly * from the global account pool. * This method expects that all accounts and their subaccounts * are no longer assigned to any transactions or splits. * An exception is thrown in case of a problem deleting an account. * * The optional parameter level is used to keep track of the recursion level. * If the recursion level exceeds 100 (some arbitrary number which seems a good * maximum), an exception is thrown. * * @param account_list Reference to a list of account IDs to be deleted. * @param level Parameter to keep track of recursion level (do not pass a value here). */ void removeAccountList(const QStringList& account_list, unsigned int level = 0); /** * This member function checks all accounts identified by account_list * and their subaccounts whether they are assigned to transactions/splits or not. * The function calls itself recursively with the list of sub-accounts of * the currently processed account. * * The optional parameter level is used to keep track of the recursion level. * If the recursion level exceeds 100 (some arbitrary number which seems a good * maximum), an exception is thrown. * * @param account_list A QStringList with account IDs that need to be checked. * @param level (optional) Optional parameter to indicate recursion level. * @return Returns 'false' if at least one account has been found that * is still referenced by a transaction. */ bool hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level = 0); /** * Adds a transaction to the file-global transaction pool. A respective * transaction-ID will be generated for this object. The ID is stored * as QString in the object passed as argument. * Splits must reference valid accounts and valid payees. The payee * id can be empty. * * An exception will be thrown upon error conditions. * * @param transaction reference to the transaction */ void addTransaction(MyMoneyTransaction& transaction); /** * This method is used to update a specific transaction in the * transaction pool of the MyMoneyFile object. * Splits must reference valid accounts and valid payees. The payee * id can be empty. * * An exception will be thrown upon error conditions. * * @param transaction reference to transaction to be changed */ void modifyTransaction(const MyMoneyTransaction& transaction); /** * This method is used to extract a transaction from the file global * transaction pool through an id. In case of an invalid id, an * exception will be thrown. * * @param id id of transaction as QString. * @return reference to the requested transaction */ const MyMoneyTransaction transaction(const QString& id) const; /** * This method is used to extract a transaction from the file global * transaction pool through an index into an account. * * @param account id of the account as QString * @param idx number of transaction in this account * @return reference to MyMoneyTransaction object */ const MyMoneyTransaction transaction(const QString& account, const int idx) const; /** * This method is used to pull a list of transactions from the file * global transaction pool. It returns all those transactions * that match the filter passed as argument. If the filter is empty, * the whole journal will be returned. * The list returned is sorted according to the transactions posting date. * If more than one transaction exists for the same date, the order among * them is undefined. * * @param filter MyMoneyTransactionFilter object with the match criteria * * @return set of transactions in form of a QList */ const QList transactionList(MyMoneyTransactionFilter& filter) const; void transactionList(QList& list, MyMoneyTransactionFilter& filter) const; void transactionList(QList >& list, MyMoneyTransactionFilter& filter) const; /** * This method is used to remove a transaction from the transaction * pool (journal). * * @param transaction const reference to transaction to be deleted */ void removeTransaction(const MyMoneyTransaction& transaction); /** * This method is used to return the actual balance of an account * without it's sub-ordinate accounts. If a @p date is presented, * the balance at the beginning of this date (not including any * transaction on this date) is returned. Otherwise all recorded * transactions are included in the balance. * * @param id id of the account in question * @param date return balance for specific date (default = QDate()) * @return balance of the account as MyMoneyMoney object */ const MyMoneyMoney balance(const QString& id, const QDate& date = QDate()) const; /** * This method is used to return the cleared balance of an account * without it's sub-ordinate accounts for a specific date. All * recorded transactions are included in the balance. * This method is used by the reconcialition functionality * * @param id id of the account in question * @param date return cleared balance for specific date * @return balance of the account as MyMoneyMoney object */ const MyMoneyMoney clearedBalance(const QString& id, const QDate& date) const; /** * This method is used to return the actual balance of an account * including it's sub-ordinate accounts. If a @p date is presented, * the balance at the beginning of this date (not including any * transaction on this date) is returned. Otherwise all recorded * transactions are included in the balance. * * @param id id of the account in question * @param date return balance for specific date (default = QDate()) * @return balance of the account as MyMoneyMoney object */ const MyMoneyMoney totalBalance(const QString& id, const QDate& date = QDate()) const; /** * This method returns the number of transactions currently known to file * in the range 0..MAXUINT * * @param account QString reference to account id. If account is empty + all transactions (the journal) will be counted. If account * is not empty it returns the number of transactions * that have splits in this account. * * @return number of transactions in journal/account */ unsigned int transactionCount(const QString& account = QString()) const; /** * This method returns a QMap filled with the number of transactions * per account. The account id serves as index into the map. If one * needs to have all transactionCounts() for many accounts, this method * is faster than calling transactionCount(const QString& account) many * times. * * @return QMap with numbers of transactions per account */ const QMap transactionCountMap() const; /** * This method returns the number of institutions currently known to file * in the range 0..MAXUINT * * @return number of institutions known to file */ unsigned int institutionCount() const; /** * This method returns the number of accounts currently known to file * in the range 0..MAXUINT * * @return number of accounts currently known inside a MyMoneyFile object */ unsigned int accountCount() const; /** * Returns the institution of a given ID * * @param id id of the institution to locate * @return MyMoneyInstitution object filled with data. If the institution * could not be found, an exception will be thrown */ const MyMoneyInstitution& institution(const QString& id) const; /** * This method returns a list of the institutions * inside a MyMoneyFile object * * @param list reference to the list. It will be cleared by this method first */ void institutionList(QList& list) const; /** * This method returns a list of the institutions * inside a MyMoneyFile object. This is a convenience method * to the one above * * @return QList containing the institution objects */ const QList institutionList() const; /** * Returns the account addressed by its id. * * @param id id of the account to locate. * @return MyMoneyAccount object carrying the @p id. An exception is thrown * if the id is unknown */ const MyMoneyAccount& account(const QString& id) const; /** * Returns the account addressed by its name. * * @param name name of the account to locate. * @return First MyMoneyAccount object found carrying the @p name. * An empty MyMoneyAccount object will be returned if the name is not found. */ const MyMoneyAccount& accountByName(const QString& name) const; /** * Returns the sub-account addressed by its name. * * @param acc account to search in * @param name name of the account to locate. * @return First MyMoneyAccount object found carrying the @p name. * An empty MyMoneyAccount object will be returned if the name is not found. */ const MyMoneyAccount& subAccountByName(const MyMoneyAccount& acc, const QString& name) const; /** * This method returns a list of accounts inside a MyMoneyFile object. * An optional parameter is a list of id's. If this list is emtpy (the default) * the returned list contains all accounts, otherwise only those referenced * in the id-list. * * @param list reference to QList receiving the account objects * @param idlist QStringList of account ids of those accounts that * should be returned. If this list is empty, all accounts * currently known will be returned. * * @param recursive if @p true, then recurse in all found accounts. The default is @p false */ void accountList(QList& list, const QStringList& idlist = QStringList(), const bool recursive = false) const; /** * This method is used to convert an account id to a string representation * of the names which can be used as a category description. If the account * is part of a hierarchy, the category name will be the concatenation of * the single account names separated by MyMoneyAccount::AccountSeperator. * * @param accountId QString reference of the account's id * @param includeStandardAccounts if true, the standard top account will be part * of the name, otherwise it will not be included (default is @c false) * * @return QString of the constructed name. */ QString accountToCategory(const QString& accountId, bool includeStandardAccounts = false) const; /** * This method is used to convert a string representing a category to * an account id. A category can be the concatenation of multiple accounts * representing a hierarchy of accounts. They have to be separated by * MyMoneyAccount::AccountSeperator. * * @param category const reference to QString containing the category * @param type account type if a specific type is required (defaults to UnknownAccountType) * * @return QString of the corresponding account. If account was not found * the return value will be an empty string. */ QString categoryToAccount(const QString& category, MyMoneyAccount::accountTypeE type = MyMoneyAccount::UnknownAccountType) const; /** * This method is used to convert a string representing an asset or * liability account to an account id. An account name can be the * concatenation of multiple accounts representing a hierarchy of * accounts. They have to be separated by MyMoneyAccount::AccountSeperator. * * @param name const reference to QString containing the account name * * @return QString of the corresponding account. If account was not found * the return value will be an empty string. */ QString nameToAccount(const QString& name) const; /** * This method is used to extract the parent part of an account hierarchy * name who's parts are separated by MyMoneyAccount::AccountSeperator. * * @param name full account name * @return parent name (full account name excluding the last part) */ QString parentName(const QString& name) const; /** * This method is used to create a new payee * * An exception will be thrown upon error conditions * * @param payee MyMoneyPayee reference to payee information */ void addPayee(MyMoneyPayee& payee); /** * This method is used to retrieve information about a payee * An exception will be thrown upon error conditions. * * @param id QString reference to id of payee * * @return MyMoneyPayee object of payee */ const MyMoneyPayee& payee(const QString& id) const; /** * This method is used to retrieve the id to a corresponding * name of a payee/receiver. * An exception will be thrown upon error conditions. * * @param payee QString reference to name of payee * * @return MyMoneyPayee object of payee */ const MyMoneyPayee& payeeByName(const QString& payee) const; /** * This method is used to modify an existing payee * * An exception will be thrown upon error conditions * * @param payee MyMoneyPayee reference to payee information */ void modifyPayee(const MyMoneyPayee& payee); /** * This method is used to remove an existing payee. * An error condition occurs, if the payee is still referenced * by a split. * * An exception will be thrown upon error conditions * * @param payee MyMoneyPayee reference to payee information */ void removePayee(const MyMoneyPayee& payee); /** * This method returns a list of the payees * inside a MyMoneyStorage object * * @return QList containing the payee information */ const QList payeeList() const; /** * This method is used to create a new tag * * An exception will be thrown upon error conditions * * @param tag MyMoneyTag reference to tag information */ void addTag(MyMoneyTag& tag); /** * This method is used to retrieve information about a tag * An exception will be thrown upon error conditions. * * @param id QString reference to id of tag * * @return MyMoneyTag object of tag */ const MyMoneyTag& tag(const QString& id) const; /** * This method is used to retrieve the id to a corresponding * name of a tag. * An exception will be thrown upon error conditions. * * @param tag QString reference to name of tag * * @return MyMoneyTag object of tag */ const MyMoneyTag& tagByName(const QString& tag) const; /** * This method is used to modify an existing tag * * An exception will be thrown upon error conditions * * @param tag MyMoneyTag reference to tag information */ void modifyTag(const MyMoneyTag& tag); /** * This method is used to remove an existing tag. * An error condition occurs, if the tag is still referenced * by a split. * * An exception will be thrown upon error conditions * * @param tag MyMoneyTag reference to tag information */ void removeTag(const MyMoneyTag& tag); /** * This method returns a list of the tags * inside a MyMoneyStorage object * * @return QList containing the tag information */ const QList tagList() const; /** * This method returns a list of the cost centers * inside a MyMoneyStorage object * * @return QList containing the cost center information */ void costCenterList(QList< MyMoneyCostCenter >& list) const; /** * This method is used to extract a value from the storage's * KeyValueContainer. For details see MyMoneyKeyValueContainer::value(). * @note Do not use this method to return the value of the key @p kmm-id. Use * storageId() instead. * * @param key const reference to QString containing the key * @return QString containing the value */ QString value(const QString& key) const; /** * This method is used to set a value in the storage's * KeyValueContainer. For details see MyMoneyKeyValueContainer::setValue(). * * @param key const reference to QString containing the key * @param val const reference to QString containing the value * * @note Keys starting with the leadin @p kmm- are reserved for internal use * by the MyMoneyFile object. */ void setValue(const QString& key, const QString& val); /** * This method returns the unique id of the attached storage object. * In case the storage object does not have an id yet, a new one will be * assigned. * * @return QString containing the value * * An exception is thrown if no storage object is attached. */ QString storageId(); /** * This method is used to delete a key-value-pair from the * storage's KeyValueContainer identified by the parameter * @p key. For details see MyMoneyKeyValueContainer::deletePair(). * * @param key const reference to QString containing the key */ void deletePair(const QString& key); /** * This method is used to add a scheduled transaction to the engine. * It must be sure, that the id of the object is not filled. When the * method returns to the caller, the id will be filled with the * newly created object id value. * * An exception will be thrown upon erroneous situations. * * @param sched reference to the MyMoneySchedule object */ void addSchedule(MyMoneySchedule& sched); /** * This method is used to modify an existing MyMoneySchedule * object. Therefor, the id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param sched const reference to the MyMoneySchedule object to be updated */ void modifySchedule(const MyMoneySchedule& sched); /** * This method is used to remove an existing MyMoneySchedule object * from the engine. The id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param sched const reference to the MyMoneySchedule object to be updated */ void removeSchedule(const MyMoneySchedule& sched); /** * This method is used to retrieve a single MyMoneySchedule object. * The id of the object must be supplied in the parameter @p id. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneySchedule object * @return MyMoneySchedule object */ const MyMoneySchedule schedule(const QString& id) const; /** * This method is used to extract a list of scheduled transactions * according to the filter criteria passed as arguments. * * @param accountId only search for scheduled transactions that reference * account @p accountId. If accountId is the empty string, * this filter is off. Default is @p QString(). * @param type only schedules of type @p type are searched for. * See MyMoneySchedule::typeE for details. * Default is MyMoneySchedule::TYPE_ANY * @param occurrence only schedules of occurrence type @p occurrence are searched for. * See MyMoneySchedule::occurrenceE for details. * Default is MyMoneySchedule::OCCUR_ANY * @param paymentType only schedules of payment method @p paymentType * are searched for. * See MyMoneySchedule::paymentTypeE for details. * Default is MyMoneySchedule::STYPE_ANY * @param startDate only schedules with payment dates after @p startDate * are searched for. Default is all dates (QDate()). * @param endDate only schedules with payment dates ending prior to @p endDate * are searched for. Default is all dates (QDate()). * @param overdue if true, only those schedules that are overdue are * searched for. Default is false (all schedules will be returned). * * @return const QList list of schedule objects. */ const QList scheduleList(const QString& accountId = QString(), const MyMoneySchedule::typeE type = MyMoneySchedule::TYPE_ANY, const MyMoneySchedule::occurrenceE occurrence = MyMoneySchedule::OCCUR_ANY, const MyMoneySchedule::paymentTypeE paymentType = MyMoneySchedule::STYPE_ANY, const QDate& startDate = QDate(), const QDate& endDate = QDate(), const bool overdue = false) const; const QStringList consistencyCheck(); /** - * MyMoneyFile::OpeningBalancesPrefix is a special string used + * MyMoneyFile::openingBalancesPrefix() is a special string used * to generate the name for opening balances accounts. See openingBalanceAccount() * for details. */ - static const QString OpeningBalancesPrefix; + static const QString openingBalancesPrefix(); /** * MyMoneyFile::AccountSeperator is used as the separator * between account names to form a hierarchy. */ static const QString AccountSeperator; /** * createCategory creates a category from a text name. * * The whole account hierarchy is created if it does not * already exist. e.g if name = Bills:Credit Card and * base = expense(), Bills will first be checked to see if * it exists and created if not. Credit Card will then * be created with Bills as it's parent. The Credit Card account * will have it's id returned. * * @param base The base account (expense or income) * @param name The category to create * * @return The category account id or empty on error. * * @exception An exception will be thrown, if @p base is not equal * expense() or income(). **/ QString createCategory(const MyMoneyAccount& base, const QString& name); const QList scheduleListEx(int scheduleTypes, int scheduleOcurrences, int schedulePaymentTypes, QDate startDate, const QStringList& accounts = QStringList()) const; /** * This method is used to add a new security object to the engine. * The ID of the object is the trading symbol, so there is no need for an additional * ID since the symbol is guaranteed to be unique. * * An exception will be thrown upon erroneous situations. * * @param security reference to the MyMoneySecurity object */ void addSecurity(MyMoneySecurity& security); /** * This method is used to modify an existing MyMoneySchedule * object. * * An exception will be thrown upon erroneous situations. * * @param security reference to the MyMoneySecurity object to be updated */ void modifySecurity(const MyMoneySecurity& security); /** * This method is used to remove an existing MyMoneySecurity object * from the engine. * * An exception will be thrown upon erroneous situations. * * @param security reference to the MyMoneySecurity object to be removed */ void removeSecurity(const MyMoneySecurity& security); /** * This method is used to retrieve a single MyMoneySecurity object. * The id of the object must be supplied in the parameter @p id. * If no security with the given id is found, then a corresponding * currency is searched. If @p id is empty, the baseCurrency() is returned. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneySecurity object * @return MyMoneySecurity object */ const MyMoneySecurity& security(const QString& id) const; /** * This method is used to retrieve a list of all MyMoneySecurity objects. */ const QList securityList() const; /** * This method is used to add a new currency object to the engine. * The ID of the object is the trading symbol, so there is no need for an additional * ID since the symbol is guaranteed to be unique. * * An exception will be thrown upon erroneous situations. * * @param currency reference to the MyMoneySecurity object */ void addCurrency(const MyMoneySecurity& currency); /** * This method is used to modify an existing MyMoneySecurity * object. * * An exception will be thrown upon erroneous situations. * * @param currency reference to the MyMoneySecurity object */ void modifyCurrency(const MyMoneySecurity& currency); /** * This method is used to remove an existing MyMoneySecurity object * from the engine. * * An exception will be thrown upon erroneous situations. * * @param currency reference to the MyMoneySecurity object */ void removeCurrency(const MyMoneySecurity& currency); /** * This method is used to retrieve a single MyMoneySchedule object. * The id of the object must be supplied in the parameter @p id. * If @p id is empty, this method returns baseCurrency(). * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneySchedule object * @return MyMoneySchedule object */ const MyMoneySecurity& currency(const QString& id) const; /** * This method is used to retrieve the map of ancient currencies (together with their last prices) * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QMap of all MyMoneySecurity and MyMoneyPrice objects. */ const QMap ancientCurrencies() const; /** * This method is used to retrieve the list of available currencies * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneySecurity objects. */ const QList availableCurrencyList() const; /** * This method is used to retrieve the list of stored currencies * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneySecurity objects. */ const QList currencyList() const; /** * This method retrieves a MyMoneySecurity object representing * the selected base currency. If the base currency is not * selected (e.g. due to a previous call to setBaseCurrency()) * a standard MyMoneySecurity object will be returned. See * MyMoneySecurity() for details. * * An exception will be thrown upon erroneous situations. * * @return MyMoneySecurity describing base currency */ const MyMoneySecurity& baseCurrency() const; /** * This method returns the foreign currency of the given two * currency ids. If second is the base currency id then @a first * is returned otherwise @a second is returned. */ const QString& foreignCurrency(const QString& first, const QString& second) const; /** * This method allows to select the base currency. It does * not perform any changes to the data in the engine. It merely * stores a reference to the base currency. The currency * passed as argument must exist in the engine. * * An exception will be thrown upon erroneous situations. * * @param currency */ void setBaseCurrency(const MyMoneySecurity& currency); /** * This method adds/replaces a price to/from the price list */ void addPrice(const MyMoneyPrice& price); /** * This method removes a price from the price list */ void removePrice(const MyMoneyPrice& price); /** * This method is used to retrieve a price for a specific security * on a specific date. If there is no price for this date, the last * known price for this currency is used. If no price information * is available, 1.0 will be returned as price. * * @param fromId the id of the currency in question * @param toId the id of the currency to convert to (if emtpy, baseCurrency) * @param date the date for which the price should be returned (default = today) * @param exactDate if true, entry for date must exist, if false any price information * with a date less or equal to @p date will be returned * * @return price found as MyMoneyPrice object * @note This throws an exception when the base currency is not set and toId is empty */ MyMoneyPrice price(const QString& fromId, const QString& toId = QString(), const QDate& date = QDate::currentDate(), const bool exactDate = false) const; /** * This method returns a list of all prices. * * @return MyMoneyPriceList of all MyMoneyPrice objects. */ const MyMoneyPriceList priceList() const; /** * This method allows to interrogate the engine, if a known account * with id @p id has a subaccount with the name @p name. * * @param id id of the account to look at * @param name account name that needs to be searched force * @retval true account with name @p name found as subaccounts * @retval false no subaccount present with that name */ bool hasAccount(const QString& id, const QString& name) const; /** * This method is used to retrieve the list of all reports * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneyReport objects. */ const QList reportList() const; /** * Adds a report to the file-global institution pool. A * respective report-ID will be generated for this object. * The ID is stored as QString in the object passed as argument. * * An exception will be thrown upon error conditions. * * @param report The complete report information in a * MyMoneyReport object */ void addReport(MyMoneyReport& report); /** * Modifies an already existing report in the file global * report pool. * * An exception will be thrown upon error conditions. * * @param report The complete new report information */ void modifyReport(const MyMoneyReport& report); /** * This method returns the number of reports currently known to file * in the range 0..MAXUINT * * @return number of reports known to file */ unsigned countReports() const; /** * This method is used to retrieve a single MyMoneyReport object. * The id of the object must be supplied in the parameter @p id. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneyReport object * @return MyMoneyReport object */ const MyMoneyReport report(const QString& id) const; /** * This method is used to remove an existing MyMoneyReport object * from the engine. The id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param report const reference to the MyMoneyReport object to be updated */ void removeReport(const MyMoneyReport& report); /** * This method is used to retrieve the list of all budgets * known to the engine. * * An exception will be thrown upon erroneous situations. * * @return QList of all MyMoneyBudget objects. */ const QList budgetList() const; /** * Adds a budget to the file-global institution pool. A * respective budget-ID will be generated for this object. * The ID is stored as QString in the object passed as argument. * * An exception will be thrown upon error conditions. * * @param budget The complete budget information in a * MyMoneyBudget object */ void addBudget(MyMoneyBudget& budget); /** * This method is used to retrieve the id to a corresponding * name of a budget. * An exception will be thrown upon error conditions. * * @param budget QString reference to name of budget * * @return MyMoneyBudget refernce to object of budget */ const MyMoneyBudget budgetByName(const QString& budget) const; /** * Modifies an already existing budget in the file global * budget pool. * * An exception will be thrown upon error conditions. * * @param budget The complete new budget information */ void modifyBudget(const MyMoneyBudget& budget); /** * This method returns the number of budgets currently known to file * in the range 0..MAXUINT * * @return number of budgets known to file */ unsigned countBudgets() const; /** * This method is used to retrieve a single MyMoneyBudget object. * The id of the object must be supplied in the parameter @p id. * * An exception will be thrown upon erroneous situations. * * @param id QString containing the id of the MyMoneyBudget object * @return MyMoneyBudget object */ const MyMoneyBudget budget(const QString& id) const; /** * This method is used to remove an existing MyMoneyBudget object * from the engine. The id attribute of the object must be set. * * An exception will be thrown upon erroneous situations. * * @param budget const reference to the MyMoneyBudget object to be updated */ void removeBudget(const MyMoneyBudget& budget); /** * This method is used to add a VAT split to a transaction. * * @param transaction reference to the transaction * @param account reference to the account * @param category reference to the category * @param amount reference to the amount of the VAT split * * @return true if a VAT split has been added */ bool addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& account, const MyMoneyAccount& category, const MyMoneyMoney& amount); /** * This method checks, if the given @p object is referenced * by another engine object. * * @param obj const reference to object to be checked * @param skipCheck MyMoneyFileBitArray with ReferenceCheckBits set for which * the check should be skipped * * @retval false @p object is not referenced * @retval true @p institution is referenced */ bool isReferenced(const MyMoneyObject& obj, const MyMoneyFileBitArray& skipCheck = MyMoneyFileBitArray()) const; /** * Returns true if any of the accounts referenced by the splits * of transaction @a t is closed. */ bool referencesClosedAccount(const MyMoneyTransaction& t) const; /** * Returns true if the accounts referenced by the split @a s is closed. */ bool referencesClosedAccount(const MyMoneySplit& s) const; /** * This method checks if the given check no &p no is used in * a transaction referencing account &p accId. If @p accId is empty, * @p false is returned. * * @param accId id of account to checked * @param no check number to be verified if used or not * @retval false @p no is not in use * @retval true @p no is already assigned */ bool checkNoUsed(const QString& accId, const QString& no) const; /** * This method returns the highest assigned check no for * account @p accId. * * @param accId id of account to be scanned * @return highest check no. used */ QString highestCheckNo(const QString& accId) const; /** * This method checks if there is a transaction * after the date @p date for account @p accId. * * @param accId id of account to be scanned * @param date date to compare with * @retval false if there is no transaction after @p date * @retval true if there is a transaction after @p date */ bool hasNewerTransaction(const QString& accId, const QDate& date) const; /** * Clear all internal caches (used internally for performance measurements) */ void clearCache(); void forceDataChanged() { emit dataChanged(); } void preloadCache(); /** * This returns @p true if file and online balance of a specific * @p account are matching. Returns false if there is no online balance. * * @param account @p account to be checked * @retval false if @p account has balance mismatch or if there is no online balance. * @retval true if @p account has matching balances */ bool hasMatchingOnlineBalance(const MyMoneyAccount& account) const; /** * This returns the number of transactions of a specific reconciliation state @p state of account with id @p accId. * * @param accId @p accId is the account id of account to be checked * @param state @p state reconciliation state * @return number of transactions with state @p state */ int countTransactionsWithSpecificReconciliationState(const QString& accId, enum MyMoneyTransactionFilter::stateOptionE state) const; /** * @brief Saves a new onlineJob * @param job you stay owner of the object (a copy will be created) */ void addOnlineJob(onlineJob& job); /** * @brief Saves a onlineJob * @param job you stay owner of the object (a copy will be created) */ void modifyOnlineJob(const onlineJob job); /** * @brief Returns onlineJob identified by jobId * @param jobId * @return */ const onlineJob getOnlineJob(const QString &jobId) const; /** * @brief Returns all onlineJobs * @return all online jobs, caller gains ownership */ const QList onlineJobList() const; /** * @brief Returns the number of onlineJobs */ int countOnlineJobs() const; /** * @brief Remove onlineJob * * @note Removing an onlineJob fails if it is locked */ void removeOnlineJob(const onlineJob& job); /** * @brief Removes multiple onlineJobs by id * * @note Removing an onlineJob fails if it is locked */ void removeOnlineJob(const QStringList onlineJobIds); protected: /** * This is the constructor for a new empty file description */ MyMoneyFile(); Q_SIGNALS: /** * This signal is emitted when a transaction has been committed and * the notifications are about to be sent out. */ void beginChangeNotification(); /** * This signal is emitted when a transaction has been committed and * all notifications have been sent out. */ void endChangeNotification(); /** * This signal is emitted whenever any data has been changed in the engine * via any of the methods of this object */ void dataChanged(); /** * This signal is emitted by the engine whenever a new object * had been added. The data for the new object is contained in * @a obj. */ void objectAdded(MyMoneyFile::notificationObjectT objType, const MyMoneyObject * const obj); /** * This signal is emitted by the engine whenever an object * had been removed. * * @note: The data contained in @a obj is only for reference * purposes and should not be used to call any MyMoneyFile * method anymore as the object is already deleted in the storage * when the signal is emitted. */ void objectRemoved(MyMoneyFile::notificationObjectT objType, const QString& id); /** * This signal is emitted by the engine whenever an object * had been changed. The new state of the object is contained * in @a obj. */ void objectModified(MyMoneyFile::notificationObjectT objType, const MyMoneyObject * const obj); /** * This signal is emitted by the engine whenever the balance * of an account had been changed by adding, modifying or * removing transactions from the MyMoneyFile object. */ void balanceChanged(const MyMoneyAccount& acc); /** * This signal is emitted by the engine whenever the value * of an account had been changed by adding or removing * prices from the MyMoneyFile object. */ void valueChanged(const MyMoneyAccount& acc); private: static MyMoneyFile file; MyMoneyFile& operator=(MyMoneyFile&); // not allowed for singleton MyMoneyFile(const MyMoneyFile&); // not allowed for singleton QString locateSubAccount(const MyMoneyAccount& base, const QString& category) const; void ensureDefaultCurrency(MyMoneyAccount& acc) const; void warningMissingRate(const QString& fromId, const QString& toId) const; /** * This method creates an opening balances account. The name is constructed - * using MyMoneyFile::OpeningBalancesPrefix and appending " (xxx)" in + * using MyMoneyFile::openingBalancesPrefix() and appending " (xxx)" in * case the @p security is not the baseCurrency(). The account created * will be a sub-account of the standard equity account provided by equity(). * * @param security Security for which the account is searched */ const MyMoneyAccount createOpeningBalanceAccount(const MyMoneySecurity& security); const MyMoneyAccount openingBalanceAccount_internal(const MyMoneySecurity& security) const; /** * Make sure that the splits value has the precision of the corresponding account */ void fixSplitPrecision(MyMoneyTransaction& t) const; private: /// \internal d-pointer class. class Private; /// \internal d-pointer instance. Private* const d; }; class KMM_MYMONEY_EXPORT MyMoneyFileTransaction { public: MyMoneyFileTransaction(); ~MyMoneyFileTransaction(); /** * Commit the current transaction. * * @warning Make sure not to use any variable that might have been altered by * the transaction. Please keep in mind, that changing transactions * can also affect account objects. If you still need those variables * just reload them from the engine. */ void commit(); void rollback(); void restart(); private: bool m_isNested; bool m_needRollback; }; #endif diff --git a/kmymoney/mymoney/mymoneyreport.cpp b/kmymoney/mymoney/mymoneyreport.cpp index f7bb9eb49..3acb72471 100644 --- a/kmymoney/mymoney/mymoneyreport.cpp +++ b/kmymoney/mymoney/mymoneyreport.cpp @@ -1,882 +1,903 @@ /*************************************************************************** mymoneyreport.cpp ------------------- begin : Sun July 4 2004 copyright : (C) 2004-2005 by Ace Jones email : 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 "mymoneyreport.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" const QStringList MyMoneyReport::kRowTypeText = QString("none,assetliability,expenseincome,category,topcategory,account,tag,payee,month,week,topaccount,topaccount-account,equitytype,accounttype,institution,budget,budgetactual,schedule,accountinfo,accountloaninfo,accountreconcile,cashflow").split(','); const QStringList MyMoneyReport::kColumnTypeText = QString("none,months,bimonths,quarters,4,5,6,weeks,8,9,10,11,years").split(','); // if you add names here, don't forget to update the bitmap for EQueryColumns // and shift the bit for eQCend one position to the left const QStringList MyMoneyReport::kQueryColumnsText = QString("none,number,payee,category,tag,memo,account,reconcileflag,action,shares,price,performance,loan,balance,capitalgain").split(','); const MyMoneyReport::EReportType MyMoneyReport::kTypeArray[] = { eNoReport, ePivotTable, ePivotTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, eQueryTable, ePivotTable, ePivotTable, eInfoTable, eInfoTable, eInfoTable, eQueryTable, eQueryTable, eNoReport }; const QStringList MyMoneyReport::kDetailLevelText = QString("none,all,top,group,total,invalid").split(','); const QStringList MyMoneyReport::kChartTypeText = QString("none,line,bar,pie,ring,stackedbar").split(','); // This should live in mymoney/mymoneytransactionfilter.h const QStringList kTypeText = QString("all,payments,deposits,transfers,none").split(','); const QStringList kStateText = QString("all,notreconciled,cleared,reconciled,frozen,none").split(','); const QStringList kDateLockText = QString("alldates,untiltoday,currentmonth,currentyear,monthtodate,yeartodate,yeartomonth,lastmonth,lastyear,last7days,last30days,last3months,last6months,last12months,next7days,next30days,next3months,next6months,next12months,userdefined,last3tonext3months,last11Months,currentQuarter,lastQuarter,nextQuarter,currentFiscalYear,lastFiscalYear,today,next18months").split(','); const QStringList kDataLockText = QString("automatic,userdefined").split(','); const QStringList kAccountTypeText = QString("unknown,checkings,savings,cash,creditcard,loan,certificatedep,investment,moneymarket,asset,liability,currency,income,expense,assetloan,stock,equity,invalid").split(','); MyMoneyReport::MyMoneyReport() : m_name("Unconfigured Pivot Table Report"), m_detailLevel(eDetailNone), m_investmentSum(eSumSold), m_hideTransactions(false), m_convertCurrency(true), m_favorite(false), m_tax(false), m_investments(false), m_loans(false), m_reportType(kTypeArray[eExpenseIncome]), m_rowType(eExpenseIncome), m_columnType(eMonths), m_columnsAreDays(false), m_queryColumns(eQCnone), m_dateLock(MyMoneyTransactionFilter::userDefined), m_accountGroupFilter(false), m_chartType(eChartLine), m_chartDataLabels(true), m_chartCHGridLines(true), m_chartSVGridLines(true), m_chartByDefault(false), m_logYaxis(false), m_dataRangeStart('0'), m_dataRangeEnd('0'), m_dataMajorTick('0'), m_dataMinorTick('0'), m_yLabelsPrecision(2), m_dataLock(MyMoneyReport::automatic), m_includeSchedules(false), m_includeTransfers(false), m_includeBudgetActuals(false), m_includeUnusedAccounts(false), m_showRowTotals(false), m_showColumnTotals(true), m_includeForecast(false), m_includeMovingAverage(false), m_movingAverageDays(0), m_includePrice(false), m_includeAveragePrice(false), m_mixedTime(false), m_currentDateColumn(0), m_settlementPeriod(3), m_showSTLTCapitalGains(false), m_tseparator(QDate::currentDate().addYears(-1)), m_skipZero(false) { m_chartLineWidth = m_lineWidth; } MyMoneyReport::MyMoneyReport(const QString& id, const MyMoneyReport& right) : MyMoneyObject(id), m_movingAverageDays(0), m_currentDateColumn(0) { *this = right; setId(id); } MyMoneyReport::MyMoneyReport(ERowType _rt, unsigned _ct, dateOptionE _dl, EDetailLevel _ss, const QString& _name, const QString& _comment) : m_name(_name), m_comment(_comment), m_detailLevel(_ss), m_investmentSum(_ct & eQCcapitalgain ? eSumSold : eSumPeriod), m_hideTransactions(false), m_convertCurrency(true), m_favorite(false), m_tax(false), m_investments(false), m_loans(false), m_reportType(kTypeArray[_rt]), m_rowType(_rt), + m_columnType(eMonths), m_columnsAreDays(false), m_queryColumns(eQCnone), m_dateLock(_dl), m_accountGroupFilter(false), m_chartType(eChartLine), m_chartDataLabels(true), m_chartCHGridLines(true), m_chartSVGridLines(true), m_chartByDefault(false), m_logYaxis(false), m_dataRangeStart('0'), m_dataRangeEnd('0'), m_dataMajorTick('0'), m_dataMinorTick('0'), m_yLabelsPrecision(2), m_dataLock(MyMoneyReport::automatic), m_includeSchedules(false), m_includeTransfers(false), m_includeBudgetActuals(false), m_includeUnusedAccounts(false), m_showRowTotals(false), m_showColumnTotals(true), m_includeForecast(false), m_includeMovingAverage(false), m_movingAverageDays(0), m_includePrice(false), m_includeAveragePrice(false), m_mixedTime(false), m_currentDateColumn(0), m_settlementPeriod(3), m_showSTLTCapitalGains(false), m_tseparator(QDate::currentDate().addYears(-1)), m_skipZero(false) { //set initial values m_chartLineWidth = m_lineWidth; //set report type if (m_reportType == ePivotTable) m_columnType = static_cast(_ct); if (m_reportType == eQueryTable) m_queryColumns = static_cast(_ct); setDateFilter(_dl); //throw exception if the type is inconsistent if ((_rt > static_cast(sizeof(kTypeArray) / sizeof(kTypeArray[0]))) || (m_reportType == eNoReport)) throw MYMONEYEXCEPTION("Invalid report type"); //add the corresponding account groups if (_rt == MyMoneyReport::eAssetLiability) { addAccountGroup(MyMoneyAccount::Asset); addAccountGroup(MyMoneyAccount::Liability); + addAccountGroup(MyMoneyAccount::Equity); + m_showRowTotals = true; + } + if (_rt == MyMoneyReport::eAccount) { + addAccountGroup(MyMoneyAccount::Asset); + addAccountGroup(MyMoneyAccount::Liability); + addAccountGroup(MyMoneyAccount::Equity); + addAccountGroup(MyMoneyAccount::Asset); + addAccountGroup(MyMoneyAccount::Liability); + addAccountGroup(MyMoneyAccount::Equity); + addAccountGroup(MyMoneyAccount::Checkings); + addAccountGroup(MyMoneyAccount::Savings); + addAccountGroup(MyMoneyAccount::Cash); + addAccountGroup(MyMoneyAccount::CreditCard); + addAccountGroup(MyMoneyAccount::Loan); + addAccountGroup(MyMoneyAccount::Income); + addAccountGroup(MyMoneyAccount::Expense); + addAccountGroup(MyMoneyAccount::AssetLoan); + addAccountGroup(MyMoneyAccount::Stock); m_showRowTotals = true; } if (_rt == MyMoneyReport::eExpenseIncome) { addAccountGroup(MyMoneyAccount::Expense); addAccountGroup(MyMoneyAccount::Income); m_showRowTotals = true; } //FIXME take this out once we have sorted out all issues regarding budget of assets and liabilities -- asoliverez@gmail.com if (_rt == MyMoneyReport::eBudget || _rt == MyMoneyReport::eBudgetActual) { addAccountGroup(MyMoneyAccount::Expense); addAccountGroup(MyMoneyAccount::Income); } if (_rt == MyMoneyReport::eAccountInfo) { addAccountGroup(MyMoneyAccount::Asset); addAccountGroup(MyMoneyAccount::Liability); } //cash flow reports show splits for all account groups if (_rt == MyMoneyReport::eCashFlow) { addAccountGroup(MyMoneyAccount::Expense); addAccountGroup(MyMoneyAccount::Income); addAccountGroup(MyMoneyAccount::Asset); addAccountGroup(MyMoneyAccount::Liability); } } MyMoneyReport::MyMoneyReport(const QDomElement& node) : MyMoneyObject(node), m_currentDateColumn(0) { // properly initialize the object before reading it *this = MyMoneyReport(); if (!read(node)) clearId(); } void MyMoneyReport::clearTransactionFilter() { m_accountGroupFilter = false; m_accountGroups.clear(); MyMoneyTransactionFilter::clear(); } void MyMoneyReport::validDateRange(QDate& _db, QDate& _de) { _db = fromDate(); _de = toDate(); // if either begin or end date are invalid we have one of the following // possible date filters: // // a) begin date not set - first transaction until given end date // b) end date not set - from given date until last transaction // c) both not set - first transaction until last transaction // // If there is no transaction in the engine at all, we use the current // year as the filter criteria. if (!_db.isValid() || !_de.isValid()) { QList list = MyMoneyFile::instance()->transactionList(*this); QDate tmpBegin, tmpEnd; if (!list.isEmpty()) { qSort(list); // try to use the post dates tmpBegin = list.front().postDate(); tmpEnd = list.back().postDate(); // if the post dates are not valid try the entry dates if (!tmpBegin.isValid()) tmpBegin = list.front().entryDate(); if (!tmpEnd.isValid()) tmpEnd = list.back().entryDate(); } // make sure that we leave this function with valid dates no mather what if (!tmpBegin.isValid() || !tmpEnd.isValid() || tmpBegin > tmpEnd) { tmpBegin = QDate(QDate::currentDate().year(), 1, 1); // the first date in the file tmpEnd = QDate(QDate::currentDate().year(), 12, 31); // the last date in the file } if (!_db.isValid()) _db = tmpBegin; if (!_de.isValid()) _de = tmpEnd; } if (_db > _de) _db = _de; } void MyMoneyReport::setRowType(ERowType _rt) { m_rowType = _rt; m_reportType = kTypeArray[_rt]; m_accountGroupFilter = false; m_accountGroups.clear(); if (_rt == MyMoneyReport::eAssetLiability) { addAccountGroup(MyMoneyAccount::Asset); addAccountGroup(MyMoneyAccount::Liability); + addAccountGroup(MyMoneyAccount::Equity); } if (_rt == MyMoneyReport::eExpenseIncome) { addAccountGroup(MyMoneyAccount::Expense); addAccountGroup(MyMoneyAccount::Income); } } bool MyMoneyReport::accountGroups(QList& list) const { bool result = m_accountGroupFilter; if (result) { QList::const_iterator it_group = m_accountGroups.begin(); while (it_group != m_accountGroups.end()) { list += (*it_group); ++it_group; } } return result; } void MyMoneyReport::addAccountGroup(MyMoneyAccount::accountTypeE type) { if (!m_accountGroups.isEmpty() && type != MyMoneyAccount::UnknownAccountType) { if (m_accountGroups.contains(type)) return; } m_accountGroupFilter = true; if (type != MyMoneyAccount::UnknownAccountType) m_accountGroups.push_back(type); } bool MyMoneyReport::includesAccountGroup(MyMoneyAccount::accountTypeE type) const { bool result = (! m_accountGroupFilter) || (isIncludingTransfers() && m_rowType == MyMoneyReport::eExpenseIncome) || m_accountGroups.contains(type); return result; } bool MyMoneyReport::includes(const MyMoneyAccount& acc) const { bool result = false; if (includesAccountGroup(acc.accountGroup())) { switch (acc.accountGroup()) { case MyMoneyAccount::Income: case MyMoneyAccount::Expense: if (isTax()) result = (acc.value("Tax") == "Yes") && includesCategory(acc.id()); else result = includesCategory(acc.id()); break; case MyMoneyAccount::Asset: case MyMoneyAccount::Liability: if (isLoansOnly()) result = acc.isLoan() && includesAccount(acc.id()); else if (isInvestmentsOnly()) result = acc.isInvest() && includesAccount(acc.id()); else if (isIncludingTransfers() && m_rowType == MyMoneyReport::eExpenseIncome) // If transfers are included, ONLY include this account if it is NOT // included in the report itself!! result = ! includesAccount(acc.id()); else result = includesAccount(acc.id()); break; default: result = includesAccount(acc.id()); } } return result; } void MyMoneyReport::write(QDomElement& e, QDomDocument *doc, bool anonymous) const { // No matter what changes, be sure to have a 'type' attribute. Only change // the major type if it becomes impossible to maintain compatibility with // older versions of the program as new features are added to the reports. // Feel free to change the minor type every time a change is made here. // write report's internals if (m_reportType == ePivotTable) e.setAttribute("type", "pivottable 1.15"); else if (m_reportType == eQueryTable) e.setAttribute("type", "querytable 1.14"); else if (m_reportType == eInfoTable) e.setAttribute("type", "infotable 1.0"); e.setAttribute("group", m_group); e.setAttribute("id", m_id); // write general tab if (anonymous) { e.setAttribute("name", m_id); e.setAttribute("comment", QString(m_comment).fill('x')); } else { e.setAttribute("name", m_name); e.setAttribute("comment", m_comment); } e.setAttribute("convertcurrency", m_convertCurrency); e.setAttribute("favorite", m_favorite); e.setAttribute("skipZero", m_skipZero); e.setAttribute("datelock", kDateLockText[m_dateLock]); if (m_reportType == ePivotTable) { // write report's internals e.setAttribute("includesactuals", m_includeBudgetActuals); e.setAttribute("includesforecast", m_includeForecast); e.setAttribute("includesprice", m_includePrice); e.setAttribute("includesaverageprice", m_includeAveragePrice); e.setAttribute("mixedtime", m_mixedTime); e.setAttribute("investments", m_investments); // it's setable in rows/columns tab of querytable, but here it is internal setting // write rows/columns tab if (!m_budgetId.isEmpty()) e.setAttribute("budget", m_budgetId); e.setAttribute("rowtype", kRowTypeText[m_rowType]); e.setAttribute("showrowtotals", m_showRowTotals); e.setAttribute("showcolumntotals", m_showColumnTotals); e.setAttribute("detail", kDetailLevelText[m_detailLevel]); e.setAttribute("includesmovingaverage", m_includeMovingAverage); if (m_includeMovingAverage) e.setAttribute("movingaveragedays", m_movingAverageDays); e.setAttribute("includeschedules", m_includeSchedules); e.setAttribute("includestransfers", m_includeTransfers); e.setAttribute("includeunused", m_includeUnusedAccounts); e.setAttribute("columnsaredays", m_columnsAreDays); // write chart tab if (m_chartType < 0 || m_chartType >= kChartTypeText.size()) { qDebug("m_chartType out of bounds with %d on report of type %d. Default to none.", m_chartType, m_reportType); e.setAttribute("charttype", kChartTypeText[eChartNone]); } else e.setAttribute("charttype", kChartTypeText[m_chartType]); e.setAttribute("chartchgridlines", m_chartCHGridLines); e.setAttribute("chartsvgridlines", m_chartSVGridLines); e.setAttribute("chartdatalabels", m_chartDataLabels); e.setAttribute("chartbydefault", m_chartByDefault); e.setAttribute("logYaxis", m_logYaxis); e.setAttribute("chartlinewidth", m_chartLineWidth); e.setAttribute("columntype", kColumnTypeText[m_columnType]); e.setAttribute("datalock", kDataLockText[m_dataLock]); e.setAttribute("dataRangeStart", m_dataRangeStart); e.setAttribute("dataRangeEnd", m_dataRangeEnd); e.setAttribute("dataMajorTick", m_dataMajorTick); e.setAttribute("dataMinorTick", m_dataMinorTick); e.setAttribute("yLabelsPrecision", m_yLabelsPrecision); } else if (m_reportType == eQueryTable) { // write rows/columns tab e.setAttribute("rowtype", kRowTypeText[m_rowType]); QStringList columns; unsigned qc = m_queryColumns; unsigned it_qc = eQCbegin; unsigned index = 1; while (it_qc != eQCend) { if (qc & it_qc) columns += kQueryColumnsText[index]; it_qc *= 2; index++; } e.setAttribute("querycolumns", columns.join(",")); e.setAttribute("tax", m_tax); e.setAttribute("investments", m_investments); e.setAttribute("loans", m_loans); e.setAttribute("hidetransactions", m_hideTransactions); e.setAttribute("showcolumntotals", m_showColumnTotals); e.setAttribute("detail", kDetailLevelText[m_detailLevel]); // write performance tab if (m_queryColumns & eQCperformance || m_queryColumns & eQCcapitalgain) e.setAttribute("investmentsum", m_investmentSum); // write capital gains tab if (m_queryColumns & eQCcapitalgain) { if (m_investmentSum == MyMoneyReport::eSumSold) { e.setAttribute("settlementperiod", m_settlementPeriod); e.setAttribute("showSTLTCapitalGains", m_showSTLTCapitalGains); e.setAttribute("tseparator", m_tseparator.toString(Qt::ISODate)); } } } else if (m_reportType == eInfoTable) e.setAttribute("showrowtotals", m_showRowTotals); // // Text Filter // QRegExp textfilter; if (textFilter(textfilter)) { QDomElement f = doc->createElement("TEXT"); f.setAttribute("pattern", textfilter.pattern()); f.setAttribute("casesensitive", (textfilter.caseSensitivity() == Qt::CaseSensitive) ? 1 : 0); f.setAttribute("regex", (textfilter.patternSyntax() == QRegExp::Wildcard) ? 1 : 0); f.setAttribute("inverttext", m_invertText); e.appendChild(f); } // // Type & State Filters // QList typelist; if (types(typelist) && ! typelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_type = typelist.constBegin(); while (it_type != typelist.constEnd()) { QDomElement p = doc->createElement("TYPE"); p.setAttribute("type", kTypeText[*it_type]); e.appendChild(p); ++it_type; } } QList statelist; if (states(statelist) && ! statelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_state = statelist.constBegin(); while (it_state != statelist.constEnd()) { QDomElement p = doc->createElement("STATE"); p.setAttribute("state", kStateText[*it_state]); e.appendChild(p); ++it_state; } } // // Number Filter // QString nrFrom, nrTo; if (numberFilter(nrFrom, nrTo)) { QDomElement f = doc->createElement("NUMBER"); f.setAttribute("from", nrFrom); f.setAttribute("to", nrTo); e.appendChild(f); } // // Amount Filter // MyMoneyMoney from, to; if (amountFilter(from, to)) { // bool getAmountFilter(MyMoneyMoney&,MyMoneyMoney&); QDomElement f = doc->createElement("AMOUNT"); f.setAttribute("from", from.toString()); f.setAttribute("to", to.toString()); e.appendChild(f); } // // Payees Filter // QStringList payeelist; if (payees(payeelist)) { if (payeelist.empty()) { QDomElement p = doc->createElement("PAYEE"); e.appendChild(p); } else { // iterate over payees, and add each one QStringList::const_iterator it_payee = payeelist.constBegin(); while (it_payee != payeelist.constEnd()) { QDomElement p = doc->createElement("PAYEE"); p.setAttribute("id", *it_payee); e.appendChild(p); ++it_payee; } } } // // Tags Filter // QStringList taglist; if (tags(taglist)) { if (taglist.empty()) { QDomElement p = doc->createElement("TAG"); e.appendChild(p); } else { // iterate over tags, and add each one QStringList::const_iterator it_tag = taglist.constBegin(); while (it_tag != taglist.constEnd()) { QDomElement p = doc->createElement("TAG"); p.setAttribute("id", *it_tag); e.appendChild(p); ++it_tag; } } } // // Account Groups Filter // QList accountgrouplist; if (accountGroups(accountgrouplist)) { // iterate over accounts, and add each one QList::const_iterator it_group = accountgrouplist.constBegin(); while (it_group != accountgrouplist.constEnd()) { QDomElement p = doc->createElement("ACCOUNTGROUP"); p.setAttribute("group", kAccountTypeText[*it_group]); e.appendChild(p); ++it_group; } } // // Accounts Filter // QStringList accountlist; if (accounts(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = doc->createElement("ACCOUNT"); p.setAttribute("id", *it_account); e.appendChild(p); ++it_account; } } // // Categories Filter // accountlist.clear(); if (categories(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = doc->createElement("CATEGORY"); p.setAttribute("id", *it_account); e.appendChild(p); ++it_account; } } // // Date Filter // if (m_dateLock == MyMoneyTransactionFilter::userDefined) { QDate dateFrom, dateTo; if (dateFilter(dateFrom, dateTo)) { QDomElement f = doc->createElement("DATES"); if (dateFrom.isValid()) f.setAttribute("from", dateFrom.toString(Qt::ISODate)); if (dateTo.isValid()) f.setAttribute("to", dateTo.toString(Qt::ISODate)); e.appendChild(f); } } } bool MyMoneyReport::read(const QDomElement& e) { // The goal of this reading method is 100% backward AND 100% forward // compatibility. Any report ever created with any version of KMyMoney // should be able to be loaded by this method (as long as it's one of the // report types supported in this version, of course) if (e.tagName().compare("REPORT") != 0) return false; // read report's internals QString type = e.attribute("type"); if (type.startsWith("pivottable")) m_reportType = ePivotTable; else if (type.startsWith("querytable")) m_reportType = eQueryTable; else if (type.startsWith("infotable")) m_reportType = eInfoTable; else return false; m_group = e.attribute("group"); m_id = e.attribute("id"); clearTransactionFilter(); // read date tab QString datelockstr = e.attribute("datelock", "userdefined"); // Handle the pivot 1.2/query 1.1 case where the values were saved as // numbers bool ok = false; int i = datelockstr.toUInt(&ok); if (!ok) { i = kDateLockText.indexOf(datelockstr); if (i == -1) i = MyMoneyTransactionFilter::userDefined; } setDateFilter(static_cast(i)); // read general tab m_name = e.attribute("name"); m_comment = e.attribute("comment", "Extremely old report"); m_convertCurrency = e.attribute("convertcurrency", "1").toUInt(); m_favorite = e.attribute("favorite", "0").toUInt(); m_skipZero = e.attribute("skipZero", "0").toUInt(); if (m_reportType == ePivotTable) { // read report's internals m_includeBudgetActuals = e.attribute("includesactuals", "0").toUInt(); m_includeForecast = e.attribute("includesforecast", "0").toUInt(); m_includePrice = e.attribute("includesprice", "0").toUInt(); m_includeAveragePrice = e.attribute("includesaverageprice", "0").toUInt(); m_mixedTime = e.attribute("mixedtime", "0").toUInt(); m_investments = e.attribute("investments", "0").toUInt(); // read rows/columns tab if (e.hasAttribute("budget")) m_budgetId = e.attribute("budget"); i = kRowTypeText.indexOf(e.attribute("rowtype")); if (i != -1) setRowType(static_cast(i)); else setRowType(eExpenseIncome); if (e.hasAttribute("showrowtotals")) m_showRowTotals = e.attribute("showrowtotals").toUInt(); else if (rowType() == eExpenseIncome) // for backward compatibility m_showRowTotals = true; m_showColumnTotals = e.attribute("showcolumntotals", "1").toUInt(); //check for reports with older settings which didn't have the detail attribute i = kDetailLevelText.indexOf(e.attribute("detail")); if (i != -1) m_detailLevel = static_cast(i); else m_detailLevel = eDetailAll; m_includeMovingAverage = e.attribute("includesmovingaverage", "0").toUInt(); if (m_includeMovingAverage) m_movingAverageDays = e.attribute("movingaveragedays", "1").toUInt(); m_includeSchedules = e.attribute("includeschedules", "0").toUInt(); m_includeTransfers = e.attribute("includestransfers", "0").toUInt(); m_includeUnusedAccounts = e.attribute("includeunused", "0").toUInt(); m_columnsAreDays = e.attribute("columnsaredays", "0").toUInt(); // read chart tab i = kChartTypeText.indexOf(e.attribute("charttype")); if (i != -1) m_chartType = static_cast(i); else m_chartType = eChartNone; m_chartCHGridLines = e.attribute("chartchgridlines", "1").toUInt(); m_chartSVGridLines = e.attribute("chartsvgridlines", "1").toUInt(); m_chartDataLabels = e.attribute("chartdatalabels", "1").toUInt(); m_chartByDefault = e.attribute("chartbydefault", "0").toUInt(); m_logYaxis = e.attribute("logYaxis", "0").toUInt(); m_chartLineWidth = e.attribute("chartlinewidth", QString(m_lineWidth)).toUInt(); // read range tab i = kColumnTypeText.indexOf(e.attribute("columntype")); if (i != -1) setColumnType(static_cast(i)); else setColumnType(eMonths); i = kDataLockText.indexOf(e.attribute("datalock")); if (i != -1) setDataFilter(static_cast(i)); else setDataFilter(MyMoneyReport::automatic); m_dataRangeStart = e.attribute("dataRangeStart", "0"); m_dataRangeEnd = e.attribute("dataRangeEnd", "0"); m_dataMajorTick = e.attribute("dataMajorTick", "0"); m_dataMinorTick = e.attribute("dataMinorTick", "0"); m_yLabelsPrecision = e.attribute("yLabelsPrecision", "2").toUInt(); } else if (m_reportType == eQueryTable) { // read rows/columns tab i = kRowTypeText.indexOf(e.attribute("rowtype")); if (i != -1) setRowType(static_cast(i)); else setRowType(eAccount); unsigned qc = 0; QStringList columns = e.attribute("querycolumns", "none").split(','); foreach (const auto column, columns) { i = kQueryColumnsText.indexOf(column); if (i > 0) qc |= (1 << (i - 1)); } setQueryColumns(static_cast(qc)); m_tax = e.attribute("tax", "0").toUInt(); m_investments = e.attribute("investments", "0").toUInt(); m_loans = e.attribute("loans", "0").toUInt(); m_hideTransactions = e.attribute("hidetransactions", "0").toUInt(); m_showColumnTotals = e.attribute("showcolumntotals", "1").toUInt(); m_detailLevel = kDetailLevelText.indexOf(e.attribute("detail", "none")) == eDetailAll ? eDetailAll : eDetailNone; // read performance or capital gains tab if (m_queryColumns & eQCperformance) m_investmentSum = static_cast(e.attribute("investmentsum", QString().setNum(MyMoneyReport::eSumPeriod)).toInt()); // read capital gains tab if (m_queryColumns & eQCcapitalgain) { m_investmentSum = static_cast(e.attribute("investmentsum", QString().setNum(MyMoneyReport::eSumSold)).toInt()); if (m_investmentSum == MyMoneyReport::eSumSold) { m_showSTLTCapitalGains = e.attribute("showSTLTCapitalGains", "0").toUInt(); m_settlementPeriod = e.attribute("settlementperiod", "3").toUInt(); m_tseparator = QDate::fromString(e.attribute("tseparator", QDate::currentDate().addYears(-1).toString(Qt::ISODate)),Qt::ISODate); } } } else if (m_reportType == eInfoTable) { if (e.hasAttribute("showrowtotals")) m_showRowTotals = e.attribute("showrowtotals").toUInt(); else m_showRowTotals = true; } QDomNode child = e.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if ("TEXT" == c.tagName() && c.hasAttribute("pattern")) { setTextFilter(QRegExp(c.attribute("pattern"), c.attribute("casesensitive", "1").toUInt() ? Qt::CaseSensitive : Qt::CaseInsensitive, c.attribute("regex", "1").toUInt() ? QRegExp::Wildcard : QRegExp::RegExp), c.attribute("inverttext", "0").toUInt()); } if ("TYPE" == c.tagName() && c.hasAttribute("type")) { i = kTypeText.indexOf(c.attribute("type")); if (i != -1) addType(i); } if ("STATE" == c.tagName() && c.hasAttribute("state")) { i = kStateText.indexOf(c.attribute("state")); if (i != -1) addState(i); } if ("NUMBER" == c.tagName()) setNumberFilter(c.attribute("from"), c.attribute("to")); if ("AMOUNT" == c.tagName()) setAmountFilter(MyMoneyMoney(c.attribute("from", "0/100")), MyMoneyMoney(c.attribute("to", "0/100"))); if ("DATES" == c.tagName()) { QDate from, to; if (c.hasAttribute("from")) from = QDate::fromString(c.attribute("from"), Qt::ISODate); if (c.hasAttribute("to")) to = QDate::fromString(c.attribute("to"), Qt::ISODate); MyMoneyTransactionFilter::setDateFilter(from, to); } if ("PAYEE" == c.tagName()) addPayee(c.attribute("id")); if ("TAG" == c.tagName()) addTag(c.attribute("id")); if ("CATEGORY" == c.tagName() && c.hasAttribute("id")) addCategory(c.attribute("id")); if ("ACCOUNT" == c.tagName() && c.hasAttribute("id")) addAccount(c.attribute("id")); if ("ACCOUNTGROUP" == c.tagName() && c.hasAttribute("group")) { i = kAccountTypeText.indexOf(c.attribute("group")); if (i != -1) addAccountGroup(static_cast(i)); } child = child.nextSibling(); } return true; } void MyMoneyReport::writeXML(QDomDocument& document, QDomElement& parent) const { QDomElement el = document.createElement("REPORT"); write(el, &document, false); parent.appendChild(el); } bool MyMoneyReport::hasReferenceTo(const QString& id) const { QStringList list; // collect all ids accounts(list); categories(list); payees(list); tags(list); return (list.contains(id) > 0); } int MyMoneyReport::m_lineWidth = 2; void MyMoneyReport::setLineWidth(int width) { m_lineWidth = width; } diff --git a/kmymoney/mymoney/storage/mymoneystorageanon.cpp b/kmymoney/mymoney/storage/mymoneystorageanon.cpp index b6d0dc6a7..46e440a82 100644 --- a/kmymoney/mymoney/storage/mymoneystorageanon.cpp +++ b/kmymoney/mymoney/storage/mymoneystorageanon.cpp @@ -1,317 +1,317 @@ /*************************************************************************** mymoneystorageanon.cpp ------------------- begin : Thu Oct 24 2002 copyright : (C) 2000-2002 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneystorageanon.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyreport.h" #include "mymoneyinstitution.h" -QStringList MyMoneyStorageANON::zKvpNoModify = QString("kmm-baseCurrency,PreferredAccount,Tax,fixed-interest,interest-calculation,payee,schedule,term,kmm-online-source,kmm-brokerage-account,lastStatementDate,kmm-sort-reconcile,kmm-sort-std,kmm-iconpos,mm-closed,payee,schedule,term,lastImportedTransactionDate,VatAccount,VatRate,kmm-matched-tx,Imported,priceMode").split(','); +QStringList MyMoneyStorageANON::zKvpNoModify = QString("kmm-baseCurrency,OpeningBalanceAccount,PreferredAccount,Tax,fixed-interest,interest-calculation,payee,schedule,term,kmm-online-source,kmm-brokerage-account,lastStatementDate,kmm-sort-reconcile,kmm-sort-std,kmm-iconpos,mm-closed,payee,schedule,term,lastImportedTransactionDate,VatAccount,VatRate,kmm-matched-tx,Imported,priceMode").split(','); QStringList MyMoneyStorageANON::zKvpXNumber = QString("final-payment,loan-amount,periodic-payment,lastStatementBalance").split(','); MyMoneyStorageANON::MyMoneyStorageANON() : MyMoneyStorageXML() { // Choose a quasi-random 0.0-100.0 factor which will be applied to all splits this time // around. int msec; do { msec = QTime::currentTime().msec(); } while (msec == 0); m_factor = MyMoneyMoney(msec, 10).reduce(); } MyMoneyStorageANON::~MyMoneyStorageANON() { } void MyMoneyStorageANON::readFile(QIODevice* , IMyMoneySerialize*) { throw MYMONEYEXCEPTION("Cannot read a file through MyMoneyStorageANON!!"); } void MyMoneyStorageANON::writeUserInformation(QDomElement& userInfo) { MyMoneyPayee user = m_storage->user(); userInfo.setAttribute(QString("name"), hideString(user.name())); userInfo.setAttribute(QString("email"), hideString(user.email())); QDomElement address = m_doc->createElement("ADDRESS"); address.setAttribute(QString("street"), hideString(user.address())); address.setAttribute(QString("city"), hideString(user.city())); address.setAttribute(QString("county"), hideString(user.state())); address.setAttribute(QString("zipcode"), hideString(user.postcode())); address.setAttribute(QString("telephone"), hideString(user.telephone())); userInfo.appendChild(address); } void MyMoneyStorageANON::writeInstitution(QDomElement& institution, const MyMoneyInstitution& _i) { MyMoneyInstitution i(_i); // mangle fields i.setName(i.id()); i.setManager(hideString(i.manager())); i.setSortcode(hideString(i.sortcode())); i.setStreet(hideString(i.street())); i.setCity(hideString(i.city())); i.setPostcode(hideString(i.postcode())); i.setTelephone(hideString(i.telephone())); MyMoneyStorageXML::writeInstitution(institution, i); } void MyMoneyStorageANON::writePayee(QDomElement& payee, const MyMoneyPayee& _p) { MyMoneyPayee p(_p); p.setName(p.id()); p.setReference(hideString(p.reference())); p.setAddress(hideString(p.address())); p.setCity(hideString(p.city())); p.setPostcode(hideString(p.postcode())); p.setState(hideString(p.state())); p.setTelephone(hideString(p.telephone())); p.setNotes(hideString(p.notes())); bool ignoreCase; QStringList keys; MyMoneyPayee::payeeMatchType matchType = p.matchData(ignoreCase, keys); QRegExp exp("[A-Za-z]"); p.setMatchData(matchType, ignoreCase, keys.join(";").replace(exp, "x").split(';')); // Data from plugins cannot be estranged, yet. p.resetPayeeIdentifiers(); MyMoneyStorageXML::writePayee(payee, p); } void MyMoneyStorageANON::writeTag(QDomElement& tag, const MyMoneyTag& _ta) { MyMoneyTag ta(_ta); ta.setName(ta.id()); ta.setNotes(hideString(ta.notes())); MyMoneyStorageXML::writeTag(tag, ta); } void MyMoneyStorageANON::writeAccount(QDomElement& account, const MyMoneyAccount& _p) { MyMoneyAccount p(_p); p.setNumber(hideString(p.number())); p.setName(p.id()); p.setDescription(hideString(p.description())); fakeKeyValuePair(p); // Remove the online banking settings entirely. p.setOnlineBankingSettings(MyMoneyKeyValueContainer()); MyMoneyStorageXML::writeAccount(account, p); } void MyMoneyStorageANON::fakeTransaction(MyMoneyTransaction& tx) { MyMoneyTransaction tn = tx; // hide transaction data tn.setMemo(tx.id()); tn.setBankID(hideString(tx.bankID())); // hide split data QList::ConstIterator it_s; for (it_s = tx.splits().constBegin(); it_s != tx.splits().constEnd(); ++it_s) { MyMoneySplit s = (*it_s); s.setMemo(QString("%1/%2").arg(tn.id()).arg(s.id())); if (s.value() != MyMoneyMoney::autoCalc) { s.setValue((s.value() * m_factor)); s.setShares((s.shares() * m_factor)); } s.setNumber(hideString(s.number())); // obfuscate a possibly matched transaction as well if (s.isMatched()) { MyMoneyTransaction t = s.matchedTransaction(); fakeTransaction(t); s.removeMatch(); s.addMatch(t); } tn.modifySplit(s); } tx = tn; fakeKeyValuePair(tx); } void MyMoneyStorageANON::fakeKeyValuePair(MyMoneyKeyValueContainer& kvp) { QMap pairs; QMap::const_iterator it; for (it = kvp.pairs().begin(); it != kvp.pairs().end(); ++it) { if (zKvpXNumber.contains(it.key()) || it.key().left(3) == "ir-") pairs[it.key()] = hideNumber(MyMoneyMoney(it.value())).toString(); else if (zKvpNoModify.contains(it.key())) pairs[it.key()] = it.value(); else pairs[it.key()] = hideString(it.value()); } kvp.setPairs(pairs); } void MyMoneyStorageANON::writeTransaction(QDomElement& transactions, const MyMoneyTransaction& tx) { MyMoneyTransaction tn = tx; fakeTransaction(tn); MyMoneyStorageXML::writeTransaction(transactions, tn); } void MyMoneyStorageANON::writeSchedule(QDomElement& scheduledTx, const MyMoneySchedule& sx) { MyMoneySchedule sn = sx; MyMoneyTransaction tn = sn.transaction(); fakeTransaction(tn); sn.setName(sx.id()); sn.setTransaction(tn, true); MyMoneyStorageXML::writeSchedule(scheduledTx, sn); } void MyMoneyStorageANON::writeSecurity(QDomElement& securityElement, const MyMoneySecurity& security) { MyMoneySecurity s = security; s.setName(security.id()); fakeKeyValuePair(s); MyMoneyStorageXML::writeSecurity(securityElement, s); } QString MyMoneyStorageANON::hideString(const QString& _in) const { return QString(_in).fill('x'); } MyMoneyMoney MyMoneyStorageANON::hideNumber(const MyMoneyMoney& _in) const { MyMoneyMoney result; static MyMoneyMoney counter = MyMoneyMoney(100, 100); // preserve sign if (_in.isNegative()) result = MyMoneyMoney::MINUS_ONE; else result = MyMoneyMoney::ONE; result = result * counter; counter += MyMoneyMoney("10/100"); // preserve > 1000 if (_in >= MyMoneyMoney(1000, 1)) result = result * MyMoneyMoney(1000, 1); if (_in <= MyMoneyMoney(-1000, 1)) result = result * MyMoneyMoney(1000, 1); return result.convert(); } void MyMoneyStorageANON::fakeBudget(MyMoneyBudget& bx) { MyMoneyBudget bn; bn.setName(bx.id()); bn.setBudgetStart(bx.budgetStart()); bn = MyMoneyBudget(bx.id(), bn); QList list = bx.getaccounts(); QList::iterator it; for (it = list.begin(); it != list.end(); ++it) { // only add the account if there is a budget entered if (!(*it).balance().isZero()) { MyMoneyBudget::AccountGroup account; account.setId((*it).id()); account.setBudgetLevel((*it).budgetLevel()); account.setBudgetSubaccounts((*it).budgetSubaccounts()); QMap plist = (*it).getPeriods(); QMap::const_iterator it_p; for (it_p = plist.constBegin(); it_p != plist.constEnd(); ++it_p) { MyMoneyBudget::PeriodGroup pGroup; pGroup.setAmount((*it_p).amount() * m_factor); pGroup.setStartDate((*it_p).startDate()); account.addPeriod(pGroup.startDate(), pGroup); } bn.setAccount(account, account.id()); } } bx = bn; } void MyMoneyStorageANON::writeBudget(QDomElement& budgets, const MyMoneyBudget& b) { MyMoneyBudget bn = b; fakeBudget(bn); MyMoneyStorageXML::writeBudget(budgets, bn); } void MyMoneyStorageANON::writeReport(QDomElement& reports, const MyMoneyReport& r) { MyMoneyReport rn = r; rn.setName(rn.id()); rn.setComment(hideString(rn.comment())); MyMoneyStorageXML::writeReport(reports, rn); } void MyMoneyStorageANON::writeOnlineJob(QDomElement& onlineJobs, const onlineJob& job) { Q_UNUSED(onlineJobs); Q_UNUSED(job); } diff --git a/kmymoney/mymoney/tests/mymoneyfile-test.cpp b/kmymoney/mymoney/tests/mymoneyfile-test.cpp index 565a21c52..496fccc70 100644 --- a/kmymoney/mymoney/tests/mymoneyfile-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyfile-test.cpp @@ -1,2666 +1,2666 @@ /*************************************************************************** mymoneyfiletest.cpp ------------------- copyright : (C) 2002 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 "mymoneyfile-test.h" #include #include #include #include #include #include #include #include "mymoneytestutils.h" #include "payeeidentifier/ibanandbic/ibanbic.h" #include "payeeidentifier/payeeidentifierloader.h" QTEST_GUILESS_MAIN(MyMoneyFileTest) void MyMoneyFileTest::objectAdded(MyMoneyFile::notificationObjectT type, const MyMoneyObject * const obj) { Q_UNUSED(type); m_objectsAdded += obj->id(); } void MyMoneyFileTest::objectRemoved(MyMoneyFile::notificationObjectT type, const QString& id) { Q_UNUSED(type); m_objectsRemoved += id; } void MyMoneyFileTest::objectModified(MyMoneyFile::notificationObjectT type, const MyMoneyObject * const obj) { Q_UNUSED(type); m_objectsModified += obj->id(); } void MyMoneyFileTest::clearObjectLists() { m_objectsAdded.clear(); m_objectsModified.clear(); m_objectsRemoved.clear(); m_balanceChanged.clear(); m_valueChanged.clear(); } void MyMoneyFileTest::balanceChanged(const MyMoneyAccount& account) { m_balanceChanged += account.id(); } void MyMoneyFileTest::valueChanged(const MyMoneyAccount& account) { m_valueChanged += account.id(); } // this method will be called once at the beginning of the test void MyMoneyFileTest::initTestCase() { m = MyMoneyFile::instance(); connect(m, SIGNAL(objectAdded(MyMoneyFile::notificationObjectT,MyMoneyObject*const)), this, SLOT(objectAdded(MyMoneyFile::notificationObjectT,MyMoneyObject*const))); connect(m, SIGNAL(objectRemoved(MyMoneyFile::notificationObjectT,QString)), this, SLOT(objectRemoved(MyMoneyFile::notificationObjectT,QString))); connect(m, SIGNAL(objectModified(MyMoneyFile::notificationObjectT,MyMoneyObject*const)), this, SLOT(objectModified(MyMoneyFile::notificationObjectT,MyMoneyObject*const))); connect(m, SIGNAL(balanceChanged(MyMoneyAccount)), this, SLOT(balanceChanged(MyMoneyAccount))); connect(m, SIGNAL(valueChanged(MyMoneyAccount)), this, SLOT(valueChanged(MyMoneyAccount))); } // this method will be called before each testfunction void MyMoneyFileTest::init() { storage = new MyMoneySeqAccessMgr; m->attachStorage(storage); clearObjectLists(); } // this method will be called after each testfunction void MyMoneyFileTest::cleanup() { m->detachStorage(storage); delete storage; } void MyMoneyFileTest::testEmptyConstructor() { MyMoneyPayee user = m->user(); QCOMPARE(user.name(), QString()); QCOMPARE(user.address(), QString()); QCOMPARE(user.city(), QString()); QCOMPARE(user.state(), QString()); QCOMPARE(user.postcode(), QString()); QCOMPARE(user.telephone(), QString()); QCOMPARE(user.email(), QString()); QCOMPARE(m->institutionCount(), static_cast(0)); QCOMPARE(m->dirty(), false); QCOMPARE(m->accountCount(), static_cast(5)); } void MyMoneyFileTest::testAddOneInstitution() { MyMoneyInstitution institution; institution.setName("institution1"); institution.setTown("town"); institution.setStreet("street"); institution.setPostcode("postcode"); institution.setTelephone("telephone"); institution.setManager("manager"); institution.setSortcode("sortcode"); // MyMoneyInstitution institution_file("", institution); MyMoneyInstitution institution_id("I000002", institution); MyMoneyInstitution institution_noname(institution); institution_noname.setName(QString()); QString id; QCOMPARE(m->institutionCount(), static_cast(0)); storage->m_dirty = false; clearObjectLists(); MyMoneyFileTransaction ft; try { m->addInstitution(institution); ft.commit(); QCOMPARE(institution.id(), QLatin1String("I000001")); QCOMPARE(m->institutionCount(), static_cast(1)); QCOMPARE(m->dirty(), true); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(m_objectsAdded[0], QLatin1String("I000001")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } clearObjectLists(); ft.restart(); try { m->addInstitution(institution_id); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { ft.commit(); QCOMPARE(m->institutionCount(), static_cast(1)); } ft.restart(); try { m->addInstitution(institution_noname); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { ft.commit(); QCOMPARE(m->institutionCount(), static_cast(1)); } QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); } void MyMoneyFileTest::testAddTwoInstitutions() { testAddOneInstitution(); MyMoneyInstitution institution; institution.setName("institution2"); institution.setTown("town"); institution.setStreet("street"); institution.setPostcode("postcode"); institution.setTelephone("telephone"); institution.setManager("manager"); institution.setSortcode("sortcode"); QString id; storage->m_dirty = false; MyMoneyFileTransaction ft; try { m->addInstitution(institution); ft.commit(); QCOMPARE(institution.id(), QLatin1String("I000002")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), true); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } storage->m_dirty = false; try { institution = m->institution("I000001"); QCOMPARE(institution.id(), QLatin1String("I000001")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), false); institution = m->institution("I000002"); QCOMPARE(institution.id(), QLatin1String("I000002")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), false); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyFileTest::testRemoveInstitution() { testAddTwoInstitutions(); MyMoneyInstitution i; QCOMPARE(m->institutionCount(), static_cast(2)); i = m->institution("I000001"); QCOMPARE(i.id(), QLatin1String("I000001")); QCOMPARE(i.accountCount(), static_cast(0)); clearObjectLists(); storage->m_dirty = false; MyMoneyFileTransaction ft; try { m->removeInstitution(i); QCOMPARE(m_objectsRemoved.count(), 0); ft.commit(); QCOMPARE(m->institutionCount(), static_cast(1)); QCOMPARE(m->dirty(), true); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(m_objectsRemoved[0], QLatin1String("I000001")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } storage->m_dirty = false; try { m->institution("I000001"); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { QCOMPARE(m->institutionCount(), static_cast(1)); QCOMPARE(m->dirty(), false); } clearObjectLists(); ft.restart(); try { m->removeInstitution(i); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { ft.commit(); QCOMPARE(m->institutionCount(), static_cast(1)); QCOMPARE(m->dirty(), false); QCOMPARE(m_objectsRemoved.count(), 0); } } void MyMoneyFileTest::testInstitutionRetrieval() { testAddOneInstitution(); storage->m_dirty = false; MyMoneyInstitution institution; QCOMPARE(m->institutionCount(), static_cast(1)); try { institution = m->institution("I000001"); QCOMPARE(institution.id(), QLatin1String("I000001")); QCOMPARE(m->institutionCount(), static_cast(1)); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } try { institution = m->institution("I000002"); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { QCOMPARE(m->institutionCount(), static_cast(1)); } QCOMPARE(m->dirty(), false); } void MyMoneyFileTest::testInstitutionListRetrieval() { QList list; storage->m_dirty = false; list = m->institutionList(); QCOMPARE(m->dirty(), false); QCOMPARE(list.count(), 0); testAddTwoInstitutions(); storage->m_dirty = false; list = m->institutionList(); QCOMPARE(m->dirty(), false); QCOMPARE(list.count(), 2); QList::ConstIterator it; it = list.constBegin(); QVERIFY((*it).name() == "institution1" || (*it).name() == "institution2"); ++it; QVERIFY((*it).name() == "institution2" || (*it).name() == "institution1"); ++it; QVERIFY(it == list.constEnd()); } void MyMoneyFileTest::testInstitutionModify() { testAddTwoInstitutions(); MyMoneyInstitution institution; institution = m->institution("I000001"); institution.setStreet("new street"); institution.setTown("new town"); institution.setPostcode("new postcode"); institution.setTelephone("new telephone"); institution.setManager("new manager"); institution.setName("new name"); institution.setSortcode("new sortcode"); storage->m_dirty = false; clearObjectLists(); MyMoneyFileTransaction ft; try { m->modifyInstitution(institution); ft.commit(); QCOMPARE(institution.id(), QLatin1String("I000001")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), true); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(m_objectsModified[0], QLatin1String("I000001")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } MyMoneyInstitution newInstitution; newInstitution = m->institution("I000001"); QCOMPARE(newInstitution.id(), QLatin1String("I000001")); QCOMPARE(newInstitution.street(), QLatin1String("new street")); QCOMPARE(newInstitution.town(), QLatin1String("new town")); QCOMPARE(newInstitution.postcode(), QLatin1String("new postcode")); QCOMPARE(newInstitution.telephone(), QLatin1String("new telephone")); QCOMPARE(newInstitution.manager(), QLatin1String("new manager")); QCOMPARE(newInstitution.name(), QLatin1String("new name")); QCOMPARE(newInstitution.sortcode(), QLatin1String("new sortcode")); storage->m_dirty = false; ft.restart(); MyMoneyInstitution failInstitution2("I000003", newInstitution); try { m->modifyInstitution(failInstitution2); QFAIL("Exception expected"); } catch (const MyMoneyException &) { ft.commit(); QCOMPARE(failInstitution2.id(), QLatin1String("I000003")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), false); } } void MyMoneyFileTest::testSetFunctions() { MyMoneyPayee user = m->user(); QCOMPARE(user.name(), QString()); QCOMPARE(user.address(), QString()); QCOMPARE(user.city(), QString()); QCOMPARE(user.state(), QString()); QCOMPARE(user.postcode(), QString()); QCOMPARE(user.telephone(), QString()); QCOMPARE(user.email(), QString()); MyMoneyFileTransaction ft; storage->m_dirty = false; user.setName("Name"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->m_dirty = false; user.setAddress("Street"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->m_dirty = false; user.setCity("Town"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->m_dirty = false; user.setState("County"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->m_dirty = false; user.setPostcode("Postcode"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->m_dirty = false; user.setTelephone("Telephone"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->m_dirty = false; user.setEmail("Email"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->m_dirty = false; ft.commit(); user = m->user(); QCOMPARE(user.name(), QLatin1String("Name")); QCOMPARE(user.address(), QLatin1String("Street")); QCOMPARE(user.city(), QLatin1String("Town")); QCOMPARE(user.state(), QLatin1String("County")); QCOMPARE(user.postcode(), QLatin1String("Postcode")); QCOMPARE(user.telephone(), QLatin1String("Telephone")); QCOMPARE(user.email(), QLatin1String("Email")); } void MyMoneyFileTest::testAddAccounts() { testAddTwoInstitutions(); MyMoneyAccount a, b, c; a.setAccountType(MyMoneyAccount::Checkings); b.setAccountType(MyMoneyAccount::Checkings); MyMoneyInstitution institution; storage->m_dirty = false; QCOMPARE(m->accountCount(), static_cast(5)); institution = m->institution("I000001"); QCOMPARE(institution.id(), QLatin1String("I000001")); a.setName("Account1"); a.setInstitutionId(institution.id()); a.setCurrencyId("EUR"); clearObjectLists(); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->asset(); m->addAccount(a, parent); ft.commit(); QCOMPARE(m->accountCount(), static_cast(6)); QCOMPARE(a.parentAccountId(), QLatin1String("AStd::Asset")); QCOMPARE(a.id(), QLatin1String("A000001")); QCOMPARE(a.institutionId(), QLatin1String("I000001")); QCOMPARE(a.currencyId(), QLatin1String("EUR")); QCOMPARE(m->dirty(), true); QCOMPARE(m->asset().accountList().count(), 1); QCOMPARE(m->asset().accountList()[0], QLatin1String("A000001")); institution = m->institution("I000001"); QCOMPARE(institution.accountCount(), static_cast(1)); QCOMPARE(institution.accountList()[0], QLatin1String("A000001")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 2); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // try to add this account again, should not work ft.restart(); try { MyMoneyAccount parent = m->asset(); m->addAccount(a, parent); QFAIL("Expecting exception!"); } catch (const MyMoneyException &) { ft.commit(); } // check that we can modify the local object and // reload it from the file a.setName("AccountX"); a = m->account("A000001"); QCOMPARE(a.name(), QLatin1String("Account1")); storage->m_dirty = false; // check if we can get the same info to a different object c = m->account("A000001"); QCOMPARE(c.accountType(), MyMoneyAccount::Checkings); QCOMPARE(c.id(), QLatin1String("A000001")); QCOMPARE(c.name(), QLatin1String("Account1")); QCOMPARE(c.institutionId(), QLatin1String("I000001")); QCOMPARE(m->dirty(), false); // add a second account institution = m->institution("I000002"); b.setName("Account2"); b.setInstitutionId(institution.id()); b.setCurrencyId("EUR"); clearObjectLists(); ft.restart(); try { MyMoneyAccount parent = m->asset(); m->addAccount(b, parent); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(b.id(), QLatin1String("A000002")); QCOMPARE(b.currencyId(), QLatin1String("EUR")); QCOMPARE(b.parentAccountId(), QLatin1String("AStd::Asset")); QCOMPARE(m->accountCount(), static_cast(7)); institution = m->institution("I000001"); QCOMPARE(institution.accountCount(), static_cast(1)); QCOMPARE(institution.accountList()[0], QLatin1String("A000001")); institution = m->institution("I000002"); QCOMPARE(institution.accountCount(), static_cast(1)); QCOMPARE(institution.accountList()[0], QLatin1String("A000002")); QCOMPARE(m->asset().accountList().count(), 2); QCOMPARE(m->asset().accountList()[0], QLatin1String("A000001")); QCOMPARE(m->asset().accountList()[1], QLatin1String("A000002")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 2); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(QLatin1String("A000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } MyMoneyAccount p; p = m->account("A000002"); QCOMPARE(p.accountType(), MyMoneyAccount::Checkings); QCOMPARE(p.id(), QLatin1String("A000002")); QCOMPARE(p.name(), QLatin1String("Account2")); QCOMPARE(p.institutionId(), QLatin1String("I000002")); QCOMPARE(p.currencyId(), QLatin1String("EUR")); } void MyMoneyFileTest::testAddCategories() { MyMoneyAccount a, b, c; a.setAccountType(MyMoneyAccount::Income); a.setOpeningDate(QDate::currentDate()); b.setAccountType(MyMoneyAccount::Expense); storage->m_dirty = false; QCOMPARE(m->accountCount(), static_cast(5)); QCOMPARE(a.openingDate(), QDate::currentDate()); QVERIFY(!b.openingDate().isValid()); a.setName("Account1"); a.setCurrencyId("EUR"); clearObjectLists(); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->income(); m->addAccount(a, parent); ft.commit(); QCOMPARE(m->accountCount(), static_cast(6)); QCOMPARE(a.parentAccountId(), QLatin1String("AStd::Income")); QCOMPARE(a.id(), QLatin1String("A000001")); QCOMPARE(a.institutionId(), QString()); QCOMPARE(a.currencyId(), QLatin1String("EUR")); QCOMPARE(a.openingDate(), QDate(1900, 1, 1)); QCOMPARE(m->dirty(), true); QCOMPARE(m->income().accountList().count(), 1); QCOMPARE(m->income().accountList()[0], QLatin1String("A000001")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // add a second category, expense this time b.setName("Account2"); b.setCurrencyId("EUR"); clearObjectLists(); ft.restart(); try { MyMoneyAccount parent = m->expense(); m->addAccount(b, parent); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(b.id(), QLatin1String("A000002")); QCOMPARE(a.institutionId(), QString()); QCOMPARE(b.currencyId(), QLatin1String("EUR")); QCOMPARE(b.openingDate(), QDate(1900, 1, 1)); QCOMPARE(b.parentAccountId(), QLatin1String("AStd::Expense")); QCOMPARE(m->accountCount(), static_cast(7)); QCOMPARE(m->income().accountList().count(), 1); QCOMPARE(m->expense().accountList().count(), 1); QCOMPARE(m->income().accountList()[0], QLatin1String("A000001")); QCOMPARE(m->expense().accountList()[0], QLatin1String("A000002")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(QLatin1String("A000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Expense"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testModifyAccount() { testAddAccounts(); storage->m_dirty = false; MyMoneyAccount p = m->account("A000001"); MyMoneyInstitution institution; QCOMPARE(p.accountType(), MyMoneyAccount::Checkings); QCOMPARE(p.name(), QLatin1String("Account1")); p.setName("New account name"); MyMoneyFileTransaction ft; clearObjectLists(); try { m->modifyAccount(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->accountCount(), static_cast(7)); QCOMPARE(p.accountType(), MyMoneyAccount::Checkings); QCOMPARE(p.name(), QLatin1String("New account name")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("A000001"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } storage->m_dirty = false; // try to move account to new institution p.setInstitutionId("I000002"); ft.restart(); clearObjectLists(); try { m->modifyAccount(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->accountCount(), static_cast(7)); QCOMPARE(p.accountType(), MyMoneyAccount::Checkings); QCOMPARE(p.name(), QLatin1String("New account name")); QCOMPARE(p.institutionId(), QLatin1String("I000002")); institution = m->institution("I000001"); QCOMPARE(institution.accountCount(), static_cast(0)); institution = m->institution("I000002"); QCOMPARE(institution.accountCount(), static_cast(2)); QCOMPARE(institution.accountList()[0], QLatin1String("A000002")); QCOMPARE(institution.accountList()[1], QLatin1String("A000001")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 3); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000002"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } storage->m_dirty = false; // try to change to an account type that is allowed p.setAccountType(MyMoneyAccount::Savings); ft.restart(); try { m->modifyAccount(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->accountCount(), static_cast(7)); QCOMPARE(p.accountType(), MyMoneyAccount::Savings); QCOMPARE(p.name(), QLatin1String("New account name")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } storage->m_dirty = false; // try to change to an account type that is not allowed p.setAccountType(MyMoneyAccount::CreditCard); ft.restart(); try { m->modifyAccount(p); QFAIL("Expecting exception!"); } catch (const MyMoneyException &) { ft.commit(); } storage->m_dirty = false; // try to fool engine a bit p.setParentAccountId("A000001"); ft.restart(); try { m->modifyAccount(p); QFAIL("Expecting exception!"); } catch (const MyMoneyException &) { ft.commit(); } } void MyMoneyFileTest::testReparentAccount() { testAddAccounts(); storage->m_dirty = false; MyMoneyAccount p = m->account("A000001"); MyMoneyAccount q = m->account("A000002"); MyMoneyAccount o = m->account(p.parentAccountId()); // make A000001 a child of A000002 clearObjectLists(); MyMoneyFileTransaction ft; try { QVERIFY(p.parentAccountId() != q.id()); QCOMPARE(o.accountCount(), 2); QCOMPARE(q.accountCount(), 0); m->reparentAccount(p, q); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(p.parentAccountId(), q.id()); QCOMPARE(q.accountCount(), 1); QCOMPARE(q.id(), QLatin1String("A000002")); QCOMPARE(p.id(), QLatin1String("A000001")); QCOMPARE(q.accountList()[0], p.id()); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 3); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("A000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); o = m->account(o.id()); QCOMPARE(o.accountCount(), 1); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testRemoveStdAccount(const MyMoneyAccount& acc) { QString txt("Exception expected while removing account "); txt += acc.id(); MyMoneyFileTransaction ft; try { m->removeAccount(acc); QFAIL(qPrintable(txt)); } catch (const MyMoneyException &) { ft.commit(); } } void MyMoneyFileTest::testRemoveAccount() { MyMoneyInstitution institution; testAddAccounts(); QCOMPARE(m->accountCount(), static_cast(7)); storage->m_dirty = false; QString id; MyMoneyAccount p = m->account("A000001"); clearObjectLists(); MyMoneyFileTransaction ft; try { MyMoneyAccount q("Ainvalid", p); m->removeAccount(q); QFAIL("Exception expected!"); } catch (const MyMoneyException &) { ft.commit(); } ft.restart(); try { QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); m->removeAccount(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->accountCount(), static_cast(6)); institution = m->institution("I000001"); QCOMPARE(institution.accountCount(), static_cast(0)); QCOMPARE(m->asset().accountList().count(), 1); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsModified.count(), 2); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsRemoved.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); institution = m->institution("I000002"); QCOMPARE(institution.accountCount(), static_cast(1)); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // Check that the standard account-groups cannot be removed testRemoveStdAccount(m->liability()); testRemoveStdAccount(m->asset()); testRemoveStdAccount(m->expense()); testRemoveStdAccount(m->income()); } void MyMoneyFileTest::testRemoveAccountTree() { testReparentAccount(); MyMoneyAccount a = m->account("A000002"); clearObjectLists(); MyMoneyFileTransaction ft; // remove the account try { m->removeAccount(a); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsModified.count(), 3); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsRemoved.contains(QLatin1String("A000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } QCOMPARE(m->accountCount(), static_cast(6)); // make sure it's gone try { m->account("A000002"); QFAIL("Exception expected!"); } catch (const MyMoneyException &) { } // make sure that children are re-parented to parent account try { a = m->account("A000001"); QCOMPARE(a.parentAccountId(), m->asset().id()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testAccountListRetrieval() { QList list; storage->m_dirty = false; m->accountList(list); QCOMPARE(m->dirty(), false); QCOMPARE(list.count(), 0); testAddAccounts(); storage->m_dirty = false; list.clear(); m->accountList(list); QCOMPARE(m->dirty(), false); QCOMPARE(list.count(), 2); QCOMPARE(list[0].accountType(), MyMoneyAccount::Checkings); QCOMPARE(list[1].accountType(), MyMoneyAccount::Checkings); } void MyMoneyFileTest::testAddTransaction() { testAddAccounts(); MyMoneyTransaction t, p; MyMoneyAccount exp1; exp1.setAccountType(MyMoneyAccount::Expense); exp1.setName("Expense1"); MyMoneyAccount exp2; exp2.setAccountType(MyMoneyAccount::Expense); exp2.setName("Expense2"); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->expense(); m->addAccount(exp1, parent); m->addAccount(exp2, parent); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // fake the last modified flag to check that the // date is updated when we add the transaction MyMoneyAccount a = m->account("A000001"); a.setLastModified(QDate(1, 2, 3)); ft.restart(); try { m->modifyAccount(a); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } ft.restart(); QCOMPARE(m->accountCount(), static_cast(9)); a = m->account("A000001"); QCOMPARE(a.lastModified(), QDate(1, 2, 3)); // construct a transaction and add it to the pool t.setPostDate(QDate(2002, 2, 1)); t.setMemo("Memotext"); MyMoneySplit split1; MyMoneySplit split2; split1.setAccountId("A000001"); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); split2.setAccountId("A000003"); split2.setValue(MyMoneyMoney(1000, 100)); split2.setShares(MyMoneyMoney(1000, 100)); try { t.addSplit(split1); t.addSplit(split2); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } /* // FIXME: we don't have a payee and a number field right now // guess we should have a number field per split, don't know // about the payee t.setMethod(MyMoneyCheckingTransaction::Withdrawal); t.setPayee("Thomas Baumgart"); t.setNumber("1234"); t.setState(MyMoneyCheckingTransaction::Cleared); */ storage->m_dirty = false; ft.restart(); clearObjectLists(); try { m->addTransaction(t); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_balanceChanged.count(), 2); QCOMPARE(m_balanceChanged.count("A000001"), 1); QCOMPARE(m_balanceChanged.count("A000003"), 1); QCOMPARE(m_valueChanged.count(), 0); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } ft.restart(); clearObjectLists(); QCOMPARE(t.id(), QLatin1String("T000000000000000001")); QCOMPARE(t.postDate(), QDate(2002, 2, 1)); QCOMPARE(t.entryDate(), QDate::currentDate()); QCOMPARE(m->dirty(), true); // check the balance of the accounts a = m->account("A000001"); QCOMPARE(a.lastModified(), QDate::currentDate()); QCOMPARE(a.balance(), MyMoneyMoney(-1000, 100)); MyMoneyAccount b = m->account("A000003"); QCOMPARE(b.lastModified(), QDate::currentDate()); QCOMPARE(b.balance(), MyMoneyMoney(1000, 100)); storage->m_dirty = false; // locate transaction in MyMoneyFile via id try { p = m->transaction("T000000000000000001"); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(p.splitCount(), static_cast(2)); QCOMPARE(p.memo(), QLatin1String("Memotext")); QCOMPARE(p.splits()[0].accountId(), QLatin1String("A000001")); QCOMPARE(p.splits()[1].accountId(), QLatin1String("A000003")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // check if it's in the account(s) as well try { p = m->transaction("A000001", 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(p.id(), QLatin1String("T000000000000000001")); QCOMPARE(p.splitCount(), static_cast(2)); QCOMPARE(p.memo(), QLatin1String("Memotext")); QCOMPARE(p.splits()[0].accountId(), QLatin1String("A000001")); QCOMPARE(p.splits()[1].accountId(), QLatin1String("A000003")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } try { p = m->transaction("A000003", 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(p.id(), QLatin1String("T000000000000000001")); QCOMPARE(p.splitCount(), static_cast(2)); QCOMPARE(p.memo(), QLatin1String("Memotext")); QCOMPARE(p.splits()[0].accountId(), QLatin1String("A000001")); QCOMPARE(p.splits()[1].accountId(), QLatin1String("A000003")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testIsStandardAccount() { QCOMPARE(m->isStandardAccount(m->liability().id()), true); QCOMPARE(m->isStandardAccount(m->asset().id()), true); QCOMPARE(m->isStandardAccount(m->expense().id()), true); QCOMPARE(m->isStandardAccount(m->income().id()), true); QCOMPARE(m->isStandardAccount("A00001"), false); } void MyMoneyFileTest::testHasActiveSplits() { testAddTransaction(); QCOMPARE(m->hasActiveSplits("A000001"), true); QCOMPARE(m->hasActiveSplits("A000002"), false); } void MyMoneyFileTest::testModifyTransactionSimple() { // this will test that we can modify the basic attributes // of a transaction testAddTransaction(); MyMoneyTransaction t = m->transaction("T000000000000000001"); t.setMemo("New Memotext"); storage->m_dirty = false; MyMoneyFileTransaction ft; clearObjectLists(); try { m->modifyTransaction(t); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 2); QCOMPARE(m_balanceChanged.count(QLatin1String("A000001")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000003")), 1); QCOMPARE(m_valueChanged.count(), 0); t = m->transaction("T000000000000000001"); QCOMPARE(t.memo(), QLatin1String("New Memotext")); QCOMPARE(m->dirty(), true); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testModifyTransactionNewPostDate() { // this will test that we can modify the basic attributes // of a transaction testAddTransaction(); MyMoneyTransaction t = m->transaction("T000000000000000001"); t.setPostDate(QDate(2004, 2, 1)); storage->m_dirty = false; MyMoneyFileTransaction ft; clearObjectLists(); try { m->modifyTransaction(t); ft.commit(); t = m->transaction("T000000000000000001"); QCOMPARE(t.postDate(), QDate(2004, 2, 1)); t = m->transaction("A000001", 0); QCOMPARE(t.id(), QLatin1String("T000000000000000001")); QCOMPARE(m->dirty(), true); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 2); QCOMPARE(m_balanceChanged.count(QLatin1String("A000001")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000003")), 1); QCOMPARE(m_valueChanged.count(), 0); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testModifyTransactionNewAccount() { // this will test that we can modify the basic attributes // of a transaction testAddTransaction(); MyMoneyTransaction t = m->transaction("T000000000000000001"); MyMoneySplit s; s = t.splits()[0]; s.setAccountId("A000002"); t.modifySplit(s); storage->m_dirty = false; MyMoneyFileTransaction ft; clearObjectLists(); try { MyMoneyTransactionFilter f1("A000001"); MyMoneyTransactionFilter f2("A000002"); MyMoneyTransactionFilter f3("A000003"); QCOMPARE(m->transactionList(f1).count(), 1); QCOMPARE(m->transactionList(f2).count(), 0); QCOMPARE(m->transactionList(f3).count(), 1); m->modifyTransaction(t); ft.commit(); t = m->transaction("T000000000000000001"); QCOMPARE(t.postDate(), QDate(2002, 2, 1)); t = m->transaction("A000002", 0); QCOMPARE(m->dirty(), true); QCOMPARE(m->transactionList(f1).count(), 0); QCOMPARE(m->transactionList(f2).count(), 1); QCOMPARE(m->transactionList(f3).count(), 1); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 3); QCOMPARE(m_balanceChanged.count(QLatin1String("A000001")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000002")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000003")), 1); QCOMPARE(m_valueChanged.count(), 0); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testRemoveTransaction() { testModifyTransactionNewPostDate(); MyMoneyTransaction t; t = m->transaction("T000000000000000001"); storage->m_dirty = false; MyMoneyFileTransaction ft; clearObjectLists(); try { m->removeTransaction(t); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->transactionCount(), static_cast(0)); MyMoneyTransactionFilter f1("A000001"); MyMoneyTransactionFilter f2("A000002"); MyMoneyTransactionFilter f3("A000003"); QCOMPARE(m->transactionList(f1).count(), 0); QCOMPARE(m->transactionList(f2).count(), 0); QCOMPARE(m->transactionList(f3).count(), 0); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 2); QCOMPARE(m_balanceChanged.count(QLatin1String("A000001")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000003")), 1); QCOMPARE(m_valueChanged.count(), 0); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } /* * This function is currently not implemented. It's kind of tricky * because it modifies a lot of objects in a single call. This might * be a problem for the undo/redo stuff. That's why I left it out in * the first run. We migh add it, if we need it. * / void testMoveSplits() { testModifyTransactionNewPostDate(); QCOMPARE(m->account("A000001").transactionCount(), 1); QCOMPARE(m->account("A000002").transactionCount(), 0); QCOMPARE(m->account("A000003").transactionCount(), 1); try { m->moveSplits("A000001", "A000002"); QCOMPARE(m->account("A000001").transactionCount(), 0); QCOMPARE(m->account("A000002").transactionCount(), 1); QCOMPARE(m->account("A000003").transactionCount(), 1); } catch(const MyMoneyException &e) { QFAIL("Unexpected exception!"); } } */ void MyMoneyFileTest::testBalanceTotal() { testAddTransaction(); MyMoneyTransaction t; // construct a transaction and add it to the pool t.setPostDate(QDate(2002, 2, 1)); t.setMemo("Memotext"); MyMoneySplit split1; MyMoneySplit split2; MyMoneyFileTransaction ft; try { split1.setAccountId("A000002"); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); split2.setAccountId("A000004"); split2.setValue(MyMoneyMoney(1000, 100)); split2.setShares(MyMoneyMoney(1000, 100)); t.addSplit(split1); t.addSplit(split2); m->addTransaction(t); ft.commit(); ft.restart(); QCOMPARE(t.id(), QLatin1String("T000000000000000002")); QCOMPARE(m->totalBalance("A000001"), MyMoneyMoney(-1000, 100)); QCOMPARE(m->totalBalance("A000002"), MyMoneyMoney(-1000, 100)); MyMoneyAccount p = m->account("A000001"); MyMoneyAccount q = m->account("A000002"); m->reparentAccount(p, q); ft.commit(); // check totalBalance() and balance() with combinations of parameters QCOMPARE(m->totalBalance("A000001"), MyMoneyMoney(-1000, 100)); QCOMPARE(m->totalBalance("A000002"), MyMoneyMoney(-2000, 100)); QVERIFY(m->totalBalance("A000002", QDate(2002, 1, 15)).isZero()); QCOMPARE(m->balance("A000001"), MyMoneyMoney(-1000, 100)); QCOMPARE(m->balance("A000002"), MyMoneyMoney(-1000, 100)); // Date of a transaction QCOMPARE(m->balance("A000001", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); QCOMPARE(m->balance("A000002", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); // Date after last transaction QCOMPARE(m->balance("A000001", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); QCOMPARE(m->balance("A000002", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); // Date before first transaction QVERIFY(m->balance("A000001", QDate(2002, 1, 15)).isZero()); QVERIFY(m->balance("A000002", QDate(2002, 1, 15)).isZero()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // Now check for exceptions try { // Account not found for balance() QVERIFY(m->balance("A000005").isZero()); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } try { // Account not found for totalBalance() QVERIFY(m->totalBalance("A000005").isZero()); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyFileTest::testSetAccountName() { MyMoneyFileTransaction ft; clearObjectLists(); try { m->setAccountName(STD_ACC_LIABILITY, "Verbindlichkeiten"); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Liability"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } ft.restart(); clearObjectLists(); try { m->setAccountName(STD_ACC_ASSET, QString::fromUtf8("Vermögen")); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } ft.restart(); clearObjectLists(); try { m->setAccountName(STD_ACC_EXPENSE, "Ausgaben"); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Expense"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } ft.restart(); clearObjectLists(); try { m->setAccountName(STD_ACC_INCOME, "Einnahmen"); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Income"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } ft.restart(); QCOMPARE(m->liability().name(), QLatin1String("Verbindlichkeiten")); QCOMPARE(m->asset().name(), QString::fromUtf8("Vermögen")); QCOMPARE(m->expense().name(), QLatin1String("Ausgaben")); QCOMPARE(m->income().name(), QLatin1String("Einnahmen")); try { m->setAccountName("A000001", "New account name"); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyFileTest::testAddPayee() { MyMoneyPayee p; p.setName("THB"); QCOMPARE(m->dirty(), false); MyMoneyFileTransaction ft; try { m->addPayee(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(p.id(), QLatin1String("P000001")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(QLatin1String("P000001"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyFileTest::testModifyPayee() { MyMoneyPayee p; testAddPayee(); clearObjectLists(); p = m->payee("P000001"); p.setName("New name"); MyMoneyFileTransaction ft; try { m->modifyPayee(p); ft.commit(); p = m->payee("P000001"); QCOMPARE(p.name(), QLatin1String("New name")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("P000001"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyFileTest::testRemovePayee() { MyMoneyPayee p; testAddPayee(); clearObjectLists(); QCOMPARE(m->payeeList().count(), 1); p = m->payee("P000001"); MyMoneyFileTransaction ft; try { m->removePayee(p); ft.commit(); QCOMPARE(m->payeeList().count(), 0); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsRemoved.contains(QLatin1String("P000001"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyFileTest::testPayeeWithIdentifier() { MyMoneyPayee p; try { MyMoneyFileTransaction ft; m->addPayee(p); ft.commit(); p = m->payee(p.id()); payeeIdentifier ident = payeeIdentifierLoader::instance()->createPayeeIdentifier(payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()); payeeIdentifierTyped iban(ident); iban->setIban(QLatin1String("DE82 2007 0024 0066 6446 00")); ft.restart(); p.addPayeeIdentifier(iban); m->modifyPayee(p); ft.commit(); p = m->payee(p.id()); QCOMPARE(p.payeeIdentifiers().count(), 1); ident = p.payeeIdentifiers().first(); try { iban = payeeIdentifierTyped(ident); } catch (...) { QFAIL("Unexpected exception"); } QCOMPARE(iban->electronicIban(), QLatin1String("DE82200700240066644600")); } catch (const MyMoneyException& e) { unexpectedException(e); } } void MyMoneyFileTest::testAddTransactionStd() { testAddAccounts(); MyMoneyTransaction t, p; MyMoneyAccount a; a = m->account("A000001"); // construct a transaction and add it to the pool t.setPostDate(QDate(2002, 2, 1)); t.setMemo("Memotext"); MyMoneySplit split1; MyMoneySplit split2; split1.setAccountId("A000001"); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); split2.setAccountId(STD_ACC_EXPENSE); split2.setValue(MyMoneyMoney(1000, 100)); split2.setShares(MyMoneyMoney(1000, 100)); try { t.addSplit(split1); t.addSplit(split2); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } /* // FIXME: we don't have a payee and a number field right now // guess we should have a number field per split, don't know // about the payee t.setMethod(MyMoneyCheckingTransaction::Withdrawal); t.setPayee("Thomas Baumgart"); t.setNumber("1234"); t.setState(MyMoneyCheckingTransaction::Cleared); */ storage->m_dirty = false; MyMoneyFileTransaction ft; try { m->addTransaction(t); ft.commit(); QFAIL("Missing expected exception!"); } catch (const MyMoneyException &) { } QCOMPARE(m->dirty(), false); } void MyMoneyFileTest::testAttachStorage() { IMyMoneyStorage *store = new MyMoneySeqAccessMgr; MyMoneyFile *file = new MyMoneyFile; QCOMPARE(file->storageAttached(), false); try { file->attachStorage(store); QCOMPARE(file->storageAttached(), true); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } try { file->attachStorage(store); QFAIL("Exception expected!"); } catch (const MyMoneyException &) { } try { file->attachStorage(0); QFAIL("Exception expected!"); } catch (const MyMoneyException &) { } try { file->detachStorage(store); QCOMPARE(file->storageAttached(), false); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } delete store; delete file; } void MyMoneyFileTest::testAccount2Category() { testReparentAccount(); QCOMPARE(m->accountToCategory("A000001"), QLatin1String("Account2:Account1")); QCOMPARE(m->accountToCategory("A000002"), QLatin1String("Account2")); } void MyMoneyFileTest::testCategory2Account() { testAddTransaction(); MyMoneyAccount a = m->account("A000003"); MyMoneyAccount b = m->account("A000004"); MyMoneyFileTransaction ft; try { m->reparentAccount(b, a); ft.commit(); QCOMPARE(m->categoryToAccount("Expense1"), QLatin1String("A000003")); QCOMPARE(m->categoryToAccount("Expense1:Expense2"), QLatin1String("A000004")); QVERIFY(m->categoryToAccount("Acc2").isEmpty()); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testAttachedStorage() { QCOMPARE(m->storageAttached(), true); QVERIFY(m->storage() != 0); IMyMoneyStorage *p = m->storage(); m->detachStorage(p); QCOMPARE(m->storageAttached(), false); QCOMPARE(m->storage(), static_cast(0)); m->attachStorage(p); QCOMPARE(m->storageAttached(), true); QVERIFY(m->storage() != 0); } void MyMoneyFileTest::testHasAccount() { testAddAccounts(); MyMoneyAccount a, b; a.setAccountType(MyMoneyAccount::Checkings); a.setName("Account3"); b = m->account("A000001"); MyMoneyFileTransaction ft; try { m->addAccount(a, b); ft.commit(); QCOMPARE(m->accountCount(), static_cast(8)); QCOMPARE(a.parentAccountId(), QLatin1String("A000001")); QCOMPARE(m->hasAccount("A000001", "Account3"), true); QCOMPARE(m->hasAccount("A000001", "Account2"), false); QCOMPARE(m->hasAccount("A000002", "Account3"), false); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testAddEquityAccount() { MyMoneyAccount i; i.setName("Investment"); i.setAccountType(MyMoneyAccount::Investment); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->asset(); m->addAccount(i, parent); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } // keep a copy for later use m_inv = i; // make sure, that only equity accounts can be children to it MyMoneyAccount a; a.setName("Testaccount"); QList list; list << MyMoneyAccount::Checkings; list << MyMoneyAccount::Savings; list << MyMoneyAccount::Cash; list << MyMoneyAccount::CreditCard; list << MyMoneyAccount::Loan; list << MyMoneyAccount::CertificateDep; list << MyMoneyAccount::Investment; list << MyMoneyAccount::MoneyMarket; list << MyMoneyAccount::Asset; list << MyMoneyAccount::Liability; list << MyMoneyAccount::Currency; list << MyMoneyAccount::Income; list << MyMoneyAccount::Expense; list << MyMoneyAccount::AssetLoan; QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { a.setAccountType(*it); ft.restart(); try { char msg[100]; m->addAccount(a, i); sprintf(msg, "Can add non-equity type %d to investment", *it); QFAIL(msg); } catch (const MyMoneyException &) { ft.commit(); } } ft.restart(); try { a.setName("Teststock"); a.setAccountType(MyMoneyAccount::Stock); m->addAccount(a, i); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testReparentEquity() { testAddEquityAccount(); testAddEquityAccount(); MyMoneyAccount parent; // check the bad cases QList list; list << MyMoneyAccount::Checkings; list << MyMoneyAccount::Savings; list << MyMoneyAccount::Cash; list << MyMoneyAccount::CertificateDep; list << MyMoneyAccount::MoneyMarket; list << MyMoneyAccount::Asset; list << MyMoneyAccount::AssetLoan; list << MyMoneyAccount::Currency; parent = m->asset(); testReparentEquity(list, parent); list.clear(); list << MyMoneyAccount::CreditCard; list << MyMoneyAccount::Loan; list << MyMoneyAccount::Liability; parent = m->liability(); testReparentEquity(list, parent); list.clear(); list << MyMoneyAccount::Income; parent = m->income(); testReparentEquity(list, parent); list.clear(); list << MyMoneyAccount::Expense; parent = m->expense(); testReparentEquity(list, parent); // now check the good case MyMoneyAccount stock = m->account("A000002"); MyMoneyAccount inv = m->account(m_inv.id()); MyMoneyFileTransaction ft; try { m->reparentAccount(stock, inv); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testReparentEquity(QList& list, MyMoneyAccount& parent) { MyMoneyAccount a; MyMoneyAccount stock = m->account("A000002"); QList::Iterator it; MyMoneyFileTransaction ft; for (it = list.begin(); it != list.end(); ++it) { a.setName(QString("Testaccount %1").arg(*it)); a.setAccountType(*it); try { m->addAccount(a, parent); char msg[100]; m->reparentAccount(stock, a); sprintf(msg, "Can reparent stock to non-investment type %d account", *it); QFAIL(msg); } catch (const MyMoneyException &) { ft.commit(); } ft.restart(); } } void MyMoneyFileTest::testBaseCurrency() { MyMoneySecurity base("EUR", "Euro", QChar(0x20ac)); MyMoneySecurity ref; // make sure, no base currency is set try { ref = m->baseCurrency(); QVERIFY(ref.id().isEmpty()); } catch (const MyMoneyException &e) { unexpectedException(e); } // make sure, we cannot assign an unknown currency try { m->setBaseCurrency(base); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } MyMoneyFileTransaction ft; // add the currency and try again try { m->addCurrency(base); m->setBaseCurrency(base); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } ft.restart(); // make sure, the base currency is set try { ref = m->baseCurrency(); QCOMPARE(ref.id(), QLatin1String("EUR")); QCOMPARE(ref.name(), QLatin1String("Euro")); QVERIFY(ref.tradingSymbol() == QChar(0x20ac)); } catch (const MyMoneyException &e) { unexpectedException(e); } // check if it gets reset when attaching a new storage m->detachStorage(storage); MyMoneySeqAccessMgr* newStorage = new MyMoneySeqAccessMgr; m->attachStorage(newStorage); ref = m->baseCurrency(); QVERIFY(ref.id().isEmpty()); m->detachStorage(newStorage); delete newStorage; m->attachStorage(storage); ref = m->baseCurrency(); QCOMPARE(ref.id(), QLatin1String("EUR")); QCOMPARE(ref.name(), QLatin1String("Euro")); QVERIFY(ref.tradingSymbol() == QChar(0x20ac)); } void MyMoneyFileTest::testOpeningBalanceNoBase() { MyMoneyAccount openingAcc; MyMoneySecurity base; try { base = m->baseCurrency(); openingAcc = m->openingBalanceAccount(base); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } } void MyMoneyFileTest::testOpeningBalance() { MyMoneyAccount openingAcc; MyMoneySecurity second("USD", "US Dollar", "$"); testBaseCurrency(); try { openingAcc = m->openingBalanceAccount(m->baseCurrency()); QCOMPARE(openingAcc.parentAccountId(), m->equity().id()); - QCOMPARE(openingAcc.name(), MyMoneyFile::OpeningBalancesPrefix); + QCOMPARE(openingAcc.name(), MyMoneyFile::openingBalancesPrefix()); QCOMPARE(openingAcc.openingDate(), QDate::currentDate()); } catch (const MyMoneyException &e) { unexpectedException(e); } // add a second currency MyMoneyFileTransaction ft; try { m->addCurrency(second); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } - QString refName = QString("%1 (%2)").arg(MyMoneyFile::OpeningBalancesPrefix).arg("USD"); + QString refName = QString("%1 (%2)").arg(MyMoneyFile::openingBalancesPrefix()).arg("USD"); try { openingAcc = m->openingBalanceAccount(second); QCOMPARE(openingAcc.parentAccountId(), m->equity().id()); QCOMPARE(openingAcc.name(), refName); QCOMPARE(openingAcc.openingDate(), QDate::currentDate()); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testModifyStdAccount() { QVERIFY(m->asset().currencyId().isEmpty()); QCOMPARE(m->asset().name(), QLatin1String("Asset")); testBaseCurrency(); QVERIFY(m->asset().currencyId().isEmpty()); QVERIFY(!m->baseCurrency().id().isEmpty()); MyMoneyFileTransaction ft; try { MyMoneyAccount acc = m->asset(); acc.setName("Anlagen"); acc.setCurrencyId(m->baseCurrency().id()); m->modifyAccount(acc); ft.commit(); QCOMPARE(m->asset().name(), QLatin1String("Anlagen")); QCOMPARE(m->asset().currencyId(), m->baseCurrency().id()); } catch (const MyMoneyException &e) { unexpectedException(e); } ft.restart(); try { MyMoneyAccount acc = m->asset(); acc.setNumber("Test"); m->modifyAccount(acc); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { ft.rollback(); } } void MyMoneyFileTest::testAddPrice() { testAddAccounts(); testBaseCurrency(); MyMoneyAccount p; MyMoneyFileTransaction ft; try { p = m->account("A000002"); p.setCurrencyId("RON"); m->modifyAccount(p); ft.commit(); QCOMPARE(m->account("A000002").currencyId(), QLatin1String("RON")); } catch (const MyMoneyException &e) { unexpectedException(e); } clearObjectLists(); ft.restart(); MyMoneyPrice price("EUR", "RON", QDate::currentDate(), MyMoneyMoney(4.1), "Test source"); m->addPrice(price); ft.commit(); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 1); QCOMPARE(m_valueChanged.count("A000002"), 1); clearObjectLists(); ft.restart(); MyMoneyPrice priceReciprocal("RON", "EUR", QDate::currentDate(), MyMoneyMoney(1 / 4.1), "Test source reciprocal price"); m->addPrice(priceReciprocal); ft.commit(); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 1); QCOMPARE(m_valueChanged.count("A000002"), 1); } void MyMoneyFileTest::testRemovePrice() { testAddPrice(); clearObjectLists(); MyMoneyFileTransaction ft; MyMoneyPrice price("EUR", "RON", QDate::currentDate(), MyMoneyMoney(4.1), "Test source"); m->removePrice(price); ft.commit(); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 1); QCOMPARE(m_valueChanged.count("A000002"), 1); } void MyMoneyFileTest::testGetPrice() { testAddPrice(); // the price for the current date is found QVERIFY(m->price("EUR", "RON", QDate::currentDate()).isValid()); // the price for the current date is returned when asking for the next day with exact date set to false { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(1), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate()); } // no price is returned while asking for the next day with exact date set to true QVERIFY(!m->price("EUR", "RON", QDate::currentDate().addDays(1), true).isValid()); // no price is returned while asking for the previous day with exact date set to true/false because all prices are newer QVERIFY(!m->price("EUR", "RON", QDate::currentDate().addDays(-1), false).isValid()); QVERIFY(!m->price("EUR", "RON", QDate::currentDate().addDays(-1), true).isValid()); // add two more prices MyMoneyFileTransaction ft; m->addPrice(MyMoneyPrice("EUR", "RON", QDate::currentDate().addDays(3), MyMoneyMoney(4.1), "Test source")); m->addPrice(MyMoneyPrice("EUR", "RON", QDate::currentDate().addDays(5), MyMoneyMoney(4.1), "Test source")); ft.commit(); clearObjectLists(); // extra tests for the exactDate=false behavior { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(2), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate()); } { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(3), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate().addDays(3)); } { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(4), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate().addDays(3)); } { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(5), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate().addDays(5)); } { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(6), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate().addDays(5)); } } void MyMoneyFileTest::testAddAccountMissingCurrency() { testAddTwoInstitutions(); MyMoneySecurity base("EUR", "Euro", QChar(0x20ac)); MyMoneyAccount a; a.setAccountType(MyMoneyAccount::Checkings); MyMoneyInstitution institution; storage->m_dirty = false; QCOMPARE(m->accountCount(), static_cast(5)); institution = m->institution("I000001"); QCOMPARE(institution.id(), QLatin1String("I000001")); a.setName("Account1"); a.setInstitutionId(institution.id()); clearObjectLists(); MyMoneyFileTransaction ft; try { m->addCurrency(base); m->setBaseCurrency(base); MyMoneyAccount parent = m->asset(); m->addAccount(a, parent); ft.commit(); QCOMPARE(m->account("A000001").currencyId(), QLatin1String("EUR")); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testAddTransactionToClosedAccount() { QSKIP("Test not implemented yet", SkipAll); } void MyMoneyFileTest::testRemoveTransactionFromClosedAccount() { QSKIP("Test not implemented yet", SkipAll); } void MyMoneyFileTest::testModifyTransactionInClosedAccount() { QSKIP("Test not implemented yet", SkipAll); } void MyMoneyFileTest::testStorageId() { QString id; // make sure id will be setup if it does not exist MyMoneyFileTransaction ft; try { m->setValue("kmm-id", ""); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } try { // check for a new id id = m->storageId(); QVERIFY(!id.isEmpty()); // check that it is the same if we ask again QCOMPARE(id, m->storageId()); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testHasMatchingOnlineBalance_emptyAccountWithoutImportedBalance() { AddOneAccount(); MyMoneyAccount a = m->account("A000001"); QCOMPARE(m->hasMatchingOnlineBalance(a), false); } void MyMoneyFileTest::testHasMatchingOnlineBalance_emptyAccountWithEqualImportedBalance() { AddOneAccount(); MyMoneyAccount a = m->account("A000001"); a.setValue("lastImportedTransactionDate", QDate(2011, 12, 1).toString(Qt::ISODate)); a.setValue("lastStatementBalance", MyMoneyMoney().toString()); MyMoneyFileTransaction ft; m->modifyAccount(a); ft.commit(); QCOMPARE(m->hasMatchingOnlineBalance(a), true); } void MyMoneyFileTest::testHasMatchingOnlineBalance_emptyAccountWithUnequalImportedBalance() { AddOneAccount(); MyMoneyAccount a = m->account("A000001"); a.setValue("lastImportedTransactionDate", QDate(2011, 12, 1).toString(Qt::ISODate)); a.setValue("lastStatementBalance", MyMoneyMoney::ONE.toString()); MyMoneyFileTransaction ft; m->modifyAccount(a); ft.commit(); QCOMPARE(m->hasMatchingOnlineBalance(a), false); } void MyMoneyFileTest::testHasNewerTransaction_withoutAnyTransaction_afterLastImportedTransaction() { AddOneAccount(); MyMoneyAccount a = m->account("A000001"); QDate dateOfLastTransactionImport(2011, 12, 1); // There are no transactions at all: QCOMPARE(m->hasNewerTransaction(a.id(), dateOfLastTransactionImport), false); } void MyMoneyFileTest::testHasNewerTransaction_withoutNewerTransaction_afterLastImportedTransaction() { AddOneAccount(); QString accId("A000001"); QDate dateOfLastTransactionImport(2011, 12, 1); MyMoneyFileTransaction ft; MyMoneyTransaction t; // construct a transaction at the day of the last transaction import and add it to the pool t.setPostDate(dateOfLastTransactionImport); MyMoneySplit split1; split1.setAccountId(accId); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); t.addSplit(split1); ft.restart(); m->addTransaction(t); ft.commit(); QCOMPARE(m->hasNewerTransaction(accId, dateOfLastTransactionImport), false); } void MyMoneyFileTest::testHasNewerTransaction_withNewerTransaction_afterLastImportedTransaction() { AddOneAccount(); QString accId("A000001"); QDate dateOfLastTransactionImport(2011, 12, 1); QDate dateOfDayAfterLastTransactionImport(dateOfLastTransactionImport.addDays(1)); MyMoneyFileTransaction ft; MyMoneyTransaction t; // construct a transaction a day after the last transaction import and add it to the pool t.setPostDate(dateOfDayAfterLastTransactionImport); MyMoneySplit split1; split1.setAccountId(accId); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); t.addSplit(split1); ft.restart(); m->addTransaction(t); ft.commit(); QCOMPARE(m->hasNewerTransaction(accId, dateOfLastTransactionImport), true); } void MyMoneyFileTest::AddOneAccount() { QString accountId = "A000001"; MyMoneyAccount a; a.setAccountType(MyMoneyAccount::Checkings); storage->m_dirty = false; QCOMPARE(m->accountCount(), static_cast(5)); a.setName("Account1"); a.setCurrencyId("EUR"); clearObjectLists(); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->asset(); m->addAccount(a, parent); ft.commit(); QCOMPARE(m->accountCount(), static_cast(6)); QCOMPARE(a.parentAccountId(), QLatin1String("AStd::Asset")); QCOMPARE(a.id(), accountId); QCOMPARE(a.currencyId(), QLatin1String("EUR")); QCOMPARE(m->dirty(), true); QCOMPARE(m->asset().accountList().count(), 1); QCOMPARE(m->asset().accountList()[0], accountId); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(accountId.toLatin1())); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testCountTransactionsWithSpecificReconciliationState_noTransactions() { AddOneAccount(); QString accountId = "A000001"; QCOMPARE(m->countTransactionsWithSpecificReconciliationState(accountId, MyMoneyTransactionFilter::notReconciled), 0); } void MyMoneyFileTest::testCountTransactionsWithSpecificReconciliationState_transactionWithWantedReconcileState() { AddOneAccount(); QString accountId = "A000001"; // construct split & transaction MyMoneySplit split; split.setAccountId(accountId); split.setShares(MyMoneyMoney(-1000, 100)); split.setValue(MyMoneyMoney(-1000, 100)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2013, 1, 1)); transaction.addSplit(split); // add transaction MyMoneyFileTransaction ft; m->addTransaction(transaction); ft.commit(); QCOMPARE(m->countTransactionsWithSpecificReconciliationState(accountId, MyMoneyTransactionFilter::notReconciled), 1); } void MyMoneyFileTest::testCountTransactionsWithSpecificReconciliationState_transactionWithUnwantedReconcileState() { AddOneAccount(); QString accountId = "A000001"; // construct split & transaction MyMoneySplit split; split.setAccountId(accountId); split.setShares(MyMoneyMoney(-1000, 100)); split.setValue(MyMoneyMoney(-1000, 100)); split.setReconcileFlag(MyMoneySplit::Reconciled); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2013, 1, 1)); transaction.addSplit(split); // add transaction MyMoneyFileTransaction ft; m->addTransaction(transaction); ft.commit(); QCOMPARE(m->countTransactionsWithSpecificReconciliationState(accountId, MyMoneyTransactionFilter::notReconciled), 0); } void MyMoneyFileTest::testAddOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); MyMoneyFileTransaction ft; m->addOnlineJob(job); QCOMPARE(job.id(), QString("O000001")); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); #endif } void MyMoneyFileTest::testGetOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); testAddOnlineJob(); const onlineJob requestedJob = m->getOnlineJob("O000001"); QVERIFY(!requestedJob.isNull()); QCOMPARE(requestedJob.id(), QString("O000001")); } void MyMoneyFileTest::testRemoveOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); onlineJob job2(new germanOnlineTransfer()); onlineJob job3(new germanOnlineTransfer()); MyMoneyFileTransaction ft; m->addOnlineJob(job); m->addOnlineJob(job2); m->addOnlineJob(job3); ft.commit(); clearObjectLists(); ft.restart(); m->removeOnlineJob(job); m->removeOnlineJob(job2); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 2); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); #endif } void MyMoneyFileTest::testOnlineJobRollback() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); onlineJob job2(new germanOnlineTransfer()); onlineJob job3(new germanOnlineTransfer()); MyMoneyFileTransaction ft; m->addOnlineJob(job); m->addOnlineJob(job2); m->addOnlineJob(job3); ft.rollback(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); #endif } void MyMoneyFileTest::testRemoveLockedOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); job.setLock(true); QVERIFY(job.isLocked()); MyMoneyFileTransaction ft; m->addOnlineJob(job); ft.commit(); clearObjectLists(); // Try removing locked transfer ft.restart(); m->removeOnlineJob(job); ft.commit(); QVERIFY2(m_objectsRemoved.count() == 0, "Online Job was locked, removing is not allowed"); QVERIFY(m_objectsAdded.count() == 0); QVERIFY(m_objectsModified.count() == 0); QVERIFY(m_balanceChanged.count() == 0); QVERIFY(m_valueChanged.count() == 0); #endif } /** @todo */ void MyMoneyFileTest::testModifyOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); MyMoneyFileTransaction ft; m->addOnlineJob(job); ft.commit(); clearObjectLists(); // Modify online job job.setJobSend(); ft.restart(); m->modifyOnlineJob(job); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); //onlineJob modifiedJob = m->getOnlineJob( job.id() ); //QCOMPARE(modifiedJob.responsibleAccount(), QString("Std::Assert")); #endif } void MyMoneyFileTest::testClearedBalance() { testAddTransaction(); MyMoneyTransaction t1; MyMoneyTransaction t2; // construct a transaction and add it to the pool t1.setPostDate(QDate(2002, 2, 1)); t1.setMemo("Memotext"); t2.setPostDate(QDate(2002, 2, 4)); t2.setMemo("Memotext"); MyMoneySplit split1; MyMoneySplit split2; MyMoneySplit split3; MyMoneySplit split4; MyMoneyFileTransaction ft; try { split1.setAccountId("A000002"); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); split1.setReconcileFlag(MyMoneySplit::Cleared); split2.setAccountId("A000004"); split2.setValue(MyMoneyMoney(1000, 100)); split2.setShares(MyMoneyMoney(1000, 100)); split2.setReconcileFlag(MyMoneySplit::Cleared); t1.addSplit(split1); t1.addSplit(split2); m->addTransaction(t1); ft.commit(); ft.restart(); QCOMPARE(t1.id(), QLatin1String("T000000000000000002")); split3.setAccountId("A000002"); split3.setShares(MyMoneyMoney(-2000, 100)); split3.setValue(MyMoneyMoney(-2000, 100)); split3.setReconcileFlag(MyMoneySplit::Cleared); split4.setAccountId("A000004"); split4.setValue(MyMoneyMoney(2000, 100)); split4.setShares(MyMoneyMoney(2000, 100)); split4.setReconcileFlag(MyMoneySplit::Cleared); t2.addSplit(split3); t2.addSplit(split4); m->addTransaction(t2); ft.commit(); ft.restart(); QCOMPARE(m->balance("A000001", QDate(2002, 2, 4)), MyMoneyMoney(-1000, 100)); QCOMPARE(m->balance("A000002", QDate(2002, 2, 4)), MyMoneyMoney(-3000, 100)); // Date of last cleared transaction QCOMPARE(m->clearedBalance("A000002", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); // Date of last transaction QCOMPARE(m->balance("A000002", QDate(2002, 2, 4)), MyMoneyMoney(-3000, 100)); // Date before first transaction QVERIFY(m->clearedBalance("A000002", QDate(2002, 1, 15)).isZero()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testAdjustedValues() { // create a checking account, an expeense, an investment account and a stock AddOneAccount(); MyMoneyAccount exp1; exp1.setAccountType(MyMoneyAccount::Expense); exp1.setName("Expense1"); exp1.setCurrencyId("EUR"); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->expense(); m->addAccount(exp1, parent); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } testAddEquityAccount(); testBaseCurrency(); MyMoneySecurity stockSecurity(QLatin1String("Blubber"), QLatin1String("TestsockSecurity"), QLatin1String("BLUB"), 1000, 1000, 1000); stockSecurity.setTradingCurrency(QLatin1String("BLUB")); // add the security ft.restart(); try { m->addSecurity(stockSecurity); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } MyMoneyAccount i = m->accountByName("Investment"); MyMoneyAccount stock; ft.restart(); try { stock.setName("Teststock"); stock.setCurrencyId(stockSecurity.id()); stock.setAccountType(MyMoneyAccount::Stock); m->addAccount(stock, i); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } // values taken from real example on https://bugs.kde.org/show_bug.cgi?id=345655 MyMoneySplit s1, s2, s3; s1.setAccountId(QLatin1String("A000001")); s1.setShares(MyMoneyMoney(QLatin1String("-99901/1000"))); s1.setValue(MyMoneyMoney(QLatin1String("-999/10"))); s2.setAccountId(exp1.id()); s2.setShares(MyMoneyMoney(QLatin1String("-611/250"))); s2.setValue(MyMoneyMoney(QLatin1String("-61/25"))); s3.setAccountId(stock.id()); s3.setAction(MyMoneySplit::BuyShares); s3.setShares(MyMoneyMoney(QLatin1String("64901/100000"))); s3.setPrice(MyMoneyMoney(QLatin1String("157689/1000"))); s3.setValue(MyMoneyMoney(QLatin1String("102340161/1000000"))); MyMoneyTransaction t; t.setCommodity(QLatin1String("EUR")); t.setPostDate(QDate::currentDate()); t.addSplit(s1); t.addSplit(s2); t.addSplit(s3); // make sure the split sum is not zero QVERIFY(!t.splitSum().isZero()); ft.restart(); try { m->addTransaction(t); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } QCOMPARE(t.splitById(s1.id()).shares(), MyMoneyMoney(QLatin1String("-999/10"))); QCOMPARE(t.splitById(s1.id()).value(), MyMoneyMoney(QLatin1String("-999/10"))); QCOMPARE(t.splitById(s2.id()).shares(), MyMoneyMoney(QLatin1String("-61/25"))); QCOMPARE(t.splitById(s2.id()).value(), MyMoneyMoney(QLatin1String("-61/25"))); QCOMPARE(t.splitById(s3.id()).shares(), MyMoneyMoney(QLatin1String("649/1000"))); QCOMPARE(t.splitById(s3.id()).value(), MyMoneyMoney(QLatin1String("10234/100"))); QCOMPARE(t.splitById(s3.id()).price(), MyMoneyMoney(QLatin1String("157689/1000"))); QCOMPARE(t.splitSum(), MyMoneyMoney()); // now reset and check if modify also works s1.setShares(MyMoneyMoney(QLatin1String("-999/10"))); s1.setValue(MyMoneyMoney(QLatin1String("-999/10"))); s2.setShares(MyMoneyMoney(QLatin1String("-61/25"))); s2.setValue(MyMoneyMoney(QLatin1String("-61/25"))); s3.setShares(MyMoneyMoney(QLatin1String("649/1000"))); s3.setPrice(MyMoneyMoney(QLatin1String("157689/1000"))); s3.setValue(MyMoneyMoney(QLatin1String("102340161/1000000"))); t.modifySplit(s1); t.modifySplit(s2); t.modifySplit(s3); // make sure the split sum is not zero QVERIFY(!t.splitSum().isZero()); ft.restart(); try { m->modifyTransaction(t); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // we need to get the transaction from the engine, as modifyTransaction does // not return the modified values MyMoneyTransaction t2 = m->transaction(t.id()); QCOMPARE(t2.splitById(s3.id()).shares(), MyMoneyMoney(QLatin1String("649/1000"))); QCOMPARE(t2.splitById(s3.id()).value(), MyMoneyMoney(QLatin1String("10234/100"))); QCOMPARE(t2.splitById(s3.id()).price(), MyMoneyMoney(QLatin1String("157689/1000"))); QCOMPARE(t2.splitSum(), MyMoneyMoney()); } diff --git a/kmymoney/plugins/csvexport/csvwriter.cpp b/kmymoney/plugins/csvexport/csvwriter.cpp index 83365bbb4..8332b9376 100644 --- a/kmymoney/plugins/csvexport/csvwriter.cpp +++ b/kmymoney/plugins/csvexport/csvwriter.cpp @@ -1,453 +1,457 @@ /*************************************************************************** csvwriter.cpp - description -------------------- begin : Wed Mar 20 2013 copyright : (C) 2013-03-20 by Allan Anderson email : Allan Anderson agander93@gmail.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. * * * ***************************************************************************/ #include "csvwriter.h" #include "csvexportdlg.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyfile.h" -CsvWriter::CsvWriter() +CsvWriter::CsvWriter() : + m_plugin(0), + m_firstSplit(false), + m_highestSplitCount(0), + m_noError(true) { } CsvWriter::~CsvWriter() { } void CsvWriter::write(const QString& filename, const QString& accountId, const bool accountData, const bool categoryData, const QDate& startDate, const QDate& endDate, const QString& separator) { m_separator = separator; QFile csvFile(filename); if (csvFile.open(QIODevice::WriteOnly)) { QTextStream s(&csvFile); s.setCodec("UTF-8"); m_plugin->exporterDialog()->show(); try { if (categoryData) { writeCategoryEntries(s); } if (accountData) { writeAccountEntry(s, accountId, startDate, endDate); } emit signalProgress(-1, -1); } catch (const MyMoneyException &e) { QString errMsg = i18n("Unexpected exception '%1' thrown in %2, line %3 " "caught in MyMoneyCsvWriter::write()", e.what(), e.file(), e.line()); KMessageBox::error(0, errMsg); } csvFile.close(); qDebug() << i18n("Export completed.\n"); delete m_plugin->exporterDialog(); // Can now delete as export finished } else { KMessageBox::error(0, i18n("Unable to open file '%1' for writing", filename)); } } void CsvWriter::writeAccountEntry(QTextStream& stream, const QString& accountId, const QDate& startDate, const QDate& endDate) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; QString data; account = file->account(accountId); MyMoneyTransactionFilter filter(accountId); QString type = account.accountTypeToString(account.accountType()); data = QString(i18n("Account Type:")); if (type == QLatin1String("Investment")) { data += QString("%1\n\n").arg(type); m_headerLine << QString(i18n("Date")) << QString(i18n("Security")) << QString(i18n("Action/Type")) << QString(i18n("Amount")) << QString(i18n("Quantity")) << QString(i18n("Price")) << QString(i18n("Interest")) << QString(i18n("Fees")) << QString(i18n("Account")) << QString(i18n("Memo")) << QString(i18n("Status")); data += m_headerLine.join(m_separator); extractInvestmentEntries(accountId, startDate, endDate); } else { data += QString("%1\n\n").arg(type); m_headerLine << QString(i18n("Date")) << QString(i18n("Payee")) << QString(i18n("Amount")) << QString(i18n("Account/Cat")) << QString(i18n("Memo")) << QString(i18n("Status")) << QString(i18n("Number")); filter.setDateFilter(startDate, endDate); QList trList = file->transactionList(filter); QList::ConstIterator it; signalProgress(0, trList.count()); int count = 0; m_highestSplitCount = 0; for (it = trList.constBegin(); it != trList.constEnd(); ++it) { writeTransactionEntry(*it, accountId, ++count); if (m_noError) signalProgress(count, 0); } data += m_headerLine.join(m_separator); } QString result; QMap::const_iterator it_map = m_map.constBegin(); while (it_map != m_map.constEnd()) { result += it_map.value(); ++it_map; } stream << data << result << QLatin1Char('\n'); } void CsvWriter::writeCategoryEntries(QTextStream &s) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount income; MyMoneyAccount expense; income = file->income(); expense = file->expense(); QStringList list = income.accountList() + expense.accountList(); emit signalProgress(0, list.count()); QStringList::Iterator it_catList; int count = 0; for (it_catList = list.begin(); it_catList != list.end(); ++it_catList) { writeCategoryEntry(s, *it_catList, ""); emit signalProgress(++count, 0); } } void CsvWriter::writeCategoryEntry(QTextStream &s, const QString& accountId, const QString& leadIn) { MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); QString name = acc.name(); s << leadIn << name << m_separator; s << (acc.accountGroup() == MyMoneyAccount::Expense ? QLatin1Char('E') : QLatin1Char('I')); s << endl; QStringList list = acc.accountList(); QStringList::Iterator it_catList; name += m_separator; for (it_catList = list.begin(); it_catList != list.end(); ++it_catList) { writeCategoryEntry(s, *it_catList, name); } } void CsvWriter::writeTransactionEntry(const MyMoneyTransaction& t, const QString& accountId, const int count) { m_firstSplit = true; m_noError = true; MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySplit split = t.splitByAccount(accountId); QList splits = t.splits(); if (splits.count() < 2) { KMessageBox::sorry(0, i18n("Transaction number '%1' is missing an account assignment.\n" "Date '%2', Payee '%3'.\nTransaction dropped.\n", count, t.postDate().toString(Qt::ISODate), file->payee(split.payeeId()).name()), i18n("Invalid transaction")); m_noError = false; return; } QString str; str += QLatin1Char('\n'); str += QString("%1" + m_separator).arg(t.postDate().toString(Qt::ISODate)); MyMoneyPayee payee = file->payee(split.payeeId()); str += QString("%1" + m_separator).arg(payee.name()); QString txt = split.value().formatMoney("", 2, false); str += QString("%1" + m_separator).arg(txt); if (splits.count() > 1) { MyMoneySplit sp = t.splitByAccount(accountId, false); QString tmp = QString("%1").arg(file->accountToCategory(sp.accountId())); str += tmp + m_separator; } QString memo = split.memo(); memo.replace('\n', '~').remove('\''); QString localeThousands = QLocale().groupSeparator(); // In case of clash with field separator if (m_separator == localeThousands) { memo.replace(localeThousands, QString()); } str += QString("%1" + m_separator).arg(memo); switch (split.reconcileFlag()) { case MyMoneySplit::Cleared: str += QLatin1String("C") + m_separator; break; case MyMoneySplit::Reconciled: case MyMoneySplit::Frozen: str += QLatin1String("R") + m_separator; break; default: str += m_separator; break; } str += split.number(); if (splits.count() > 2) { QList::ConstIterator it; for (it = splits.constBegin(); it != splits.constEnd(); ++it) { if (!((*it) == split)) { writeSplitEntry(str, *it, splits.count() - 1); } } } QString date = t.postDate().toString(Qt::ISODate); m_map.insertMulti(date, str); } void CsvWriter::writeSplitEntry(QString &str, const MyMoneySplit& split, const int splitCount) { if (m_firstSplit) { m_firstSplit = false; str += m_separator; } MyMoneyFile* file = MyMoneyFile::instance(); QString splt = QString("%1").arg(file->accountToCategory(split.accountId())); str += splt + m_separator; if (splitCount > m_highestSplitCount) { m_highestSplitCount++; m_headerLine << QString(i18n("splitCategory")) << QString(i18n("splitMemo")) << QString(i18n("splitAmount")); m_headerLine.join(m_separator); } QString m = split.memo(); m.replace(QLatin1Char('\n'), QLatin1Char('~')); QString localeThousands = QLocale().groupSeparator(); // In case of clash with field separator if (m_separator == localeThousands) { m.replace(localeThousands, QString()); } str += QString("%1" + m_separator).arg(m); QString txt = QString("%1" + m_separator).arg(split.value().formatMoney("", 2, false)); str += txt; } void CsvWriter::extractInvestmentEntries(const QString& accountId, const QDate& startDate, const QDate& endDate) { MyMoneyFile* file = MyMoneyFile::instance(); QList accList = file->account(accountId).accountList(); QList::ConstIterator itAcc; for (itAcc = accList.constBegin(); itAcc != accList.constEnd(); ++itAcc) { MyMoneyTransactionFilter filter((*itAcc)); filter.setDateFilter(startDate, endDate); QList list = file->transactionList(filter); QList::ConstIterator itList; signalProgress(0, list.count()); int count = 0; for (itList = list.constBegin(); itList != list.constEnd(); ++itList) { writeInvestmentEntry(*itList, ++count); signalProgress(count, 0); } } } void CsvWriter::writeInvestmentEntry(const MyMoneyTransaction& t, const int count) { QString strQuantity; QString strAmount; QString strPrice; QString strAccName; QString strCheckingAccountName; QString strMemo; QString strAction; QString strStatus; QString strInterest; QString strFees; MyMoneyFile* file = MyMoneyFile::instance(); QString chkAccnt; QList lst = t.splits(); QList::Iterator itSplit; MyMoneyAccount::_accountTypeE typ; QString chkAccntId; MyMoneyMoney qty; MyMoneyMoney value; QMap map; for (int i = 0; i < lst.count(); i++) { MyMoneyAccount acc = file->account(lst[i].accountId()); QString accName = acc.name(); typ = acc.accountType(); map.insert(typ, lst[i].accountId()); if (typ == MyMoneyAccount::Stock) { switch (lst[i].reconcileFlag()) { case MyMoneySplit::Cleared: strStatus = QLatin1Char('C'); break; case MyMoneySplit::Reconciled: case MyMoneySplit::Frozen: strStatus = QLatin1Char('R'); break; default: strStatus.clear(); break; } strStatus += m_separator; } } // // Add date. // QString str = QString("\n%1" + m_separator).arg(t.postDate().toString(Qt::ISODate)); QString localeThousands = QLocale().groupSeparator(); // In case of clash with field separator for (itSplit = lst.begin(); itSplit != lst.end(); ++itSplit) { MyMoneyAccount acc = file->account((*itSplit).accountId()); // // MyMoneyAccount::Checkings. // if ((acc.accountType() == MyMoneyAccount::Checkings) || (acc.accountType() == MyMoneyAccount::Cash) || (acc.accountType() == MyMoneyAccount::Savings)) { chkAccntId = (*itSplit).accountId(); chkAccnt = file->account(chkAccntId).name(); strCheckingAccountName = file->accountToCategory(chkAccntId) + m_separator; strAmount = (*itSplit).value().formatMoney("", 2).remove(localeThousands) + m_separator; } else if (acc.accountType() == MyMoneyAccount::Income) { // // MyMoneyAccount::Income. // qty = (*itSplit).shares(); value = (*itSplit).value(); strInterest = value.formatMoney("", 2, false) + m_separator; } else if (acc.accountType() == MyMoneyAccount::Expense) { // // MyMoneyAccount::Expense. // qty = (*itSplit).shares(); value = (*itSplit).value(); strFees = value.formatMoney("", 2, false) + m_separator; } else if (acc.accountType() == MyMoneyAccount::Stock) { // // MyMoneyAccount::Stock. // strMemo = QString("%1" + m_separator).arg((*itSplit).memo()); strMemo.replace(QLatin1Char('\n'), QLatin1Char('~')).remove('\''); // // Actions. // if ((*itSplit).action() == QLatin1String("Dividend")) { strAction = QLatin1String("DivX"); } else if ((*itSplit).action() == QLatin1String("IntIncome")) { strAction = QLatin1String("IntIncX"); } if ((strAction == QLatin1String("DivX")) || (strAction == QLatin1String("IntIncX"))) { if ((map.value(MyMoneyAccount::Checkings).isEmpty()) && (map.value(MyMoneyAccount::Cash).isEmpty())) { KMessageBox::sorry(0, i18n("Transaction number '%1' is missing an account assignment.\n" "Date '%2', Amount '%3'.\nTransaction dropped.\n", count, t.postDate().toString(Qt::ISODate), strAmount), i18n("Invalid transaction")); return; } } else if ((*itSplit).action() == QLatin1String("Buy")) { qty = (*itSplit).shares(); if (qty.isNegative()) { strAction = QLatin1String("Sell"); } else { strAction = QLatin1String("Buy"); } } else if ((*itSplit).action() == QLatin1String("Add")) { qty = (*itSplit).shares(); if (qty.isNegative()) { strAction = QLatin1String("Shrsout"); } else { strAction = QLatin1String("Shrsin"); } } else if ((*itSplit).action() == QLatin1String("Reinvest")) { qty = (*itSplit).shares(); strAmount = (*itSplit).value().formatMoney("", 2).remove(localeThousands) + m_separator; strAction = QLatin1String("ReinvDiv"); } else { strAction = (*itSplit).action(); } // // Add action. // if ((strAction == QLatin1String("Buy")) || (strAction == QLatin1String("Sell")) || (strAction == QLatin1String("ReinvDiv"))) { // // Add total. // if (strAction == QLatin1String("Sell")) { value = -value; } // // Add price. // strPrice = (*itSplit).price().formatMoney("", 6, false); if (!qty.isZero()) { // // Add quantity. // if (strAction == QLatin1String("Sell")) { qty = -qty; } strQuantity = qty.formatMoney("", 2, false); } } else if ((strAction == QLatin1String("Shrsin")) || (strAction == QLatin1String("Shrsout"))) { // // Add quantity for "Shrsin" || "Shrsout". // if (strAction == QLatin1String("Shrsout")) { qty = -qty; } strQuantity = qty.formatMoney("", 2, false); } strAccName = acc.name(); strAccName += m_separator; strAction += m_separator; strQuantity += m_separator; strPrice += m_separator; } if (strCheckingAccountName.isEmpty()) { strCheckingAccountName = m_separator; } if (strInterest.isEmpty()) { strInterest = m_separator; } if (strFees.isEmpty()) { strFees = m_separator; } } // end of itSplit loop str += strAccName + strAction + strAmount + strQuantity + strPrice + strInterest + strFees + strCheckingAccountName + strMemo + strStatus; QString date = t.postDate().toString(Qt::ISODate); m_map.insertMulti(date, str); } diff --git a/kmymoney/plugins/ofximport/dialogs/kofxdirectconnectdlg.cpp b/kmymoney/plugins/ofximport/dialogs/kofxdirectconnectdlg.cpp index 2ddbb392b..f08b38038 100644 --- a/kmymoney/plugins/ofximport/dialogs/kofxdirectconnectdlg.cpp +++ b/kmymoney/plugins/ofximport/dialogs/kofxdirectconnectdlg.cpp @@ -1,229 +1,230 @@ /*************************************************************************** kofxdirectconnectdlg.cpp ------------------- begin : Sat Nov 13 2004 copyright : (C) 2002 by Ace Jones email : 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 "kofxdirectconnectdlg.h" +#include "kmymoneysettings.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include #include "mymoneyofxconnector.h" class KOfxDirectConnectDlg::Private { public: Private() : m_firstData(true) {} QFile m_fpTrace; bool m_firstData; }; KOfxDirectConnectDlg::KOfxDirectConnectDlg(const MyMoneyAccount& account, QWidget *parent) : KOfxDirectConnectDlgDecl(parent), d(new Private), m_tmpfile(0), m_connector(account), m_job(0) { } KOfxDirectConnectDlg::~KOfxDirectConnectDlg() { if (d->m_fpTrace.isOpen()) { d->m_fpTrace.close(); } delete m_tmpfile; delete d; } bool KOfxDirectConnectDlg::init() { show(); QByteArray request = m_connector.statementRequest(); if (request.isEmpty()) { hide(); return false; } // For debugging, dump out the request #if 0 QFile g("request.ofx"); g.open(QIODevice::WriteOnly); QTextStream(&g) << m_connector.url() << "\n" << QString(request); g.close(); #endif - QDir homeDir(QDir::home()); - if (homeDir.exists("ofxlog.txt")) { - d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(QDir::homePath())); + if (KMyMoneySettings::logOfxTransactions()) { + QString logPath = KMyMoneySettings::logPath(); + d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(logPath)); d->m_fpTrace.open(QIODevice::WriteOnly | QIODevice::Append); } if (d->m_fpTrace.isOpen()) { QByteArray data = m_connector.url().toUtf8(); d->m_fpTrace.write("url: ", 5); d->m_fpTrace.write(data, strlen(data)); d->m_fpTrace.write("\n", 1); d->m_fpTrace.write("request:\n", 9); QByteArray trcData(request); // make local copy trcData.replace('\r', ""); d->m_fpTrace.write(trcData, trcData.size()); d->m_fpTrace.write("\n", 1); d->m_fpTrace.write("response:\n", 10); } qDebug("creating job"); m_job = KIO::http_post(QUrl(m_connector.url()), request, KIO::HideProgressInfo); // open the temp file. We come around here twice if init() is called twice if (m_tmpfile) { qDebug() << "Already connected, using " << m_tmpfile->fileName(); delete m_tmpfile; //delete otherwise we mem leak } m_tmpfile = new QTemporaryFile(); // for debugging purposes one might want to leave the temp file around // in order to achieve this, please uncomment the next line // m_tmpfile->setAutoRemove(false); if (!m_tmpfile->open()) { qWarning("Unable to open tempfile '%s' for download.", qPrintable(m_tmpfile->fileName())); return false; } m_job->addMetaData("content-type", "Content-type: application/x-ofx"); connect(m_job, SIGNAL(result(KJob*)), this, SLOT(slotOfxFinished(KJob*))); connect(m_job, SIGNAL(data(KIO::Job*,QByteArray)), this, SLOT(slotOfxData(KIO::Job*,QByteArray))); setStatus(QString("Contacting %1...").arg(m_connector.url())); kProgress1->setMaximum(3); kProgress1->setValue(1); return true; } void KOfxDirectConnectDlg::setStatus(const QString& _status) { textLabel1->setText(_status); qDebug() << "STATUS:" << _status; } void KOfxDirectConnectDlg::setDetails(const QString& _details) { qDebug() << "DETAILS: " << _details; } void KOfxDirectConnectDlg::slotOfxData(KIO::Job*, const QByteArray& _ba) { qDebug("Got %d bytes of data", _ba.size()); if (d->m_firstData) { setStatus("Connection established, retrieving data..."); setDetails(QString("Downloading data to %1...").arg(m_tmpfile->fileName())); kProgress1->setValue(kProgress1->value() + 1); d->m_firstData = false; } m_tmpfile->write(_ba); setDetails(QString("Got %1 bytes").arg(_ba.size())); if (d->m_fpTrace.isOpen()) { QByteArray trcData(_ba); trcData.replace('\r', ""); d->m_fpTrace.write(trcData, trcData.size()); } } void KOfxDirectConnectDlg::slotOfxFinished(KJob* /* e */) { qDebug("Job finished"); kProgress1->setValue(kProgress1->value() + 1); setStatus("Completed."); if (d->m_fpTrace.isOpen()) { d->m_fpTrace.write("\nCompleted\n\n\n\n", 14); } int error = m_job->error(); if (m_tmpfile) { qDebug("Closing tempfile"); m_tmpfile->close(); } qDebug("Tempfile closed"); if (error) { qDebug("Show error message"); m_job->ui()->showErrorMessage(); } else if (m_job->isErrorPage()) { qDebug("Process error page"); QString details; if (m_tmpfile) { QFile f(m_tmpfile->fileName()); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); QString line; while (!stream.atEnd()) { details += stream.readLine(); // line of text excluding '\n' } f.close(); qDebug() << "The HTTP request failed: " << details; } } KMessageBox::detailedSorry(this, i18n("The HTTP request failed."), details, i18nc("The HTTP request failed", "Failed")); } else if (m_tmpfile) { qDebug("Emit statementReady signal with '%s'", qPrintable(m_tmpfile->fileName())); emit statementReady(m_tmpfile->fileName()); qDebug("Return from signal statementReady() processing"); } delete m_tmpfile; m_tmpfile = 0; hide(); qDebug("Finishing slotOfxFinished"); } void KOfxDirectConnectDlg::reject() { if (m_job) m_job->kill(); if (m_tmpfile) { m_tmpfile->close(); delete m_tmpfile; m_tmpfile = 0; } QDialog::reject(); } diff --git a/kmymoney/plugins/ofximport/ofxpartner.cpp b/kmymoney/plugins/ofximport/ofxpartner.cpp index ab7756506..cfd98efb8 100644 --- a/kmymoney/plugins/ofximport/ofxpartner.cpp +++ b/kmymoney/plugins/ofximport/ofxpartner.cpp @@ -1,498 +1,499 @@ /*************************************************************************** ofxpartner.cpp ---------- begin : Fri Jan 23 2009 copyright : (C) 2009 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 #include "ofxpartner.h" +#include "kmymoneysettings.h" #ifdef HAVE_UNISTD_H #include #endif // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Some standard defined stuff collides with libofx.h #ifdef Q_CC_MSVC #undef ERROR #undef DELETE #endif #define MSN 0 #define OFXHOME 1 // ---------------------------------------------------------------------------- // Project Includes namespace OfxPartner { bool post(const QString& request, const QMap& attr, const QUrl &url, const QUrl& filename); bool get(const QString& request, const QMap& attr, const QUrl &url, const QUrl& filename); const QString kBankFilename = "ofx-bank-index.xml"; const QString kCcFilename = "ofx-cc-index.xml"; const QString kInvFilename = "ofx-inv-index.xml"; #define VER "9" static QString directory; void setDirectory(const QString& dir) { directory = dir; } bool needReload(const QFileInfo& i) { return ((!i.isReadable()) || (i.lastModified().addDays(7) < QDateTime::currentDateTime()) || (i.size() < 1024)); } void ValidateIndexCache() { // TODO (Ace) Check whether these files exist and are recent enough before getting them again QUrl fname; QMap attr; #if OFXHOME fname = QUrl("file://" + directory + kBankFilename); QDir dir; dir.mkpath(directory); QFileInfo i(fname.path()); if (needReload(i)) get("", attr, QUrl(QStringLiteral("http://www.ofxhome.com/api.php?all=yes")), fname); #endif #if MSN attr["content-type"] = "application/x-www-form-urlencoded"; attr["accept"] = "*/*"; fname = directory + kBankFilename; QFileInfo i(fname.path()); if (needReload(i)) post("T=1&S=*&R=1&O=0&TEST=0", attr, QUrl("http://moneycentral.msn.com/money/2005/mnynet/service/ols/filist.aspx?SKU=3&VER=" VER), fname); fname = directory + kCcFilename; i = QFileInfo(fname.path()); if (needReload(i)) post("T=2&S=*&R=1&O=0&TEST=0", attr, QUrl("http://moneycentral.msn.com/money/2005/mnynet/service/ols/filist.aspx?SKU=3&VER=" VER) , fname); fname = directory + kInvFilename; i = QFileInfo(fname.path()); if (needReload(i)) post("T=3&S=*&R=1&O=0&TEST=0", attr, QUrl("http://moneycentral.msn.com/money/2005/mnynet/service/ols/filist.aspx?SKU=3&VER=" VER), fname); #endif } static void ParseFile(QMap& result, const QString& fileName, const QString& bankName) { QFile f(fileName); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); #if OFXHOME stream.setCodec("UTF-8"); QString msg; int errl, errc; QDomDocument doc; if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) { QDomNodeList olist = doc.elementsByTagName("institutionid"); for (int i = 0; i < olist.count(); ++i) { QDomNode onode = olist.item(i); if (onode.isElement()) { QDomElement elo = onode.toElement(); QString name = elo.attribute("name"); if (bankName.isEmpty()) result[name].clear(); else if (name == bankName) { result[elo.attribute("id")].clear(); } } } } #endif #if MSN stream.setCodec("UTF-16"); QString msg; int errl, errc; QDomDocument doc; if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) { QDomNodeList olist = doc.elementsByTagName("prov"); for (int i = 0; i < olist.count(); ++i) { QDomNode onode = olist.item(i); if (onode.isElement()) { bool collectGuid = false; QDomElement elo = onode.toElement(); QDomNodeList ilist = onode.childNodes(); for (int j = 0; j < ilist.count(); ++j) { QDomNode inode = ilist.item(j); QDomElement el = inode.toElement(); if (el.tagName() == "name") { if (bankName.isEmpty()) result[el.text()].clear(); else if (el.text() == bankName) { collectGuid = true; } } if (el.tagName() == "guid" && collectGuid) { result[el.text()].clear(); } } } } } #endif f.close(); } } QStringList BankNames() { QMap result; // Make sure the index files are up to date ValidateIndexCache(); ParseFile(result, directory + kBankFilename, QString()); #if MSN ParseFile(result, directory + kCcFilename, QString()); ParseFile(result, directory + kInvFilename, QString()); #endif // Add Innovision result["Innovision"].clear(); return QStringList() << result.keys(); } QStringList FipidForBank(const QString& bank) { QMap result; ParseFile(result, directory + kBankFilename, bank); #if MSN ParseFile(result, directory + kCcFilename, bank); ParseFile(result, directory + kInvFilename, bank); #endif // the fipid for Innovision is 1. if (bank == "Innovision") result["1"].clear(); return QStringList() << result.keys(); } QString extractNodeText(QDomElement& node, const QString& name) { QString res; QRegExp exp("([^/]+)/?([^/].*)?"); if (exp.indexIn(name) != -1) { QDomNodeList olist = node.elementsByTagName(exp.cap(1)); if (olist.count()) { QDomNode onode = olist.item(0); if (onode.isElement()) { QDomElement elo = onode.toElement(); if (exp.cap(2).isEmpty()) { res = elo.text(); } else { res = extractNodeText(elo, exp.cap(2)); } } } } return res; } QString extractNodeText(QDomDocument& doc, const QString& name) { QString res; QRegExp exp("([^/]+)/?([^/].*)?"); if (exp.indexIn(name) != -1) { QDomNodeList olist = doc.elementsByTagName(exp.cap(1)); if (olist.count()) { QDomNode onode = olist.item(0); if (onode.isElement()) { QDomElement elo = onode.toElement(); if (exp.cap(2).isEmpty()) { res = elo.text(); } else { res = extractNodeText(elo, exp.cap(2)); } } } } return res; } OfxFiServiceInfo ServiceInfo(const QString& fipid) { OfxFiServiceInfo result; memset(&result, 0, sizeof(OfxFiServiceInfo)); // Hard-coded values for Innovision test server if (fipid == "1") { strncpy(result.fid, "00000", OFX_FID_LENGTH - 1); strncpy(result.org, "ReferenceFI", OFX_ORG_LENGTH - 1); strncpy(result.url, "http://ofx.innovision.com", OFX_URL_LENGTH - 1); result.accountlist = 1; result.statements = 1; result.billpay = 1; result.investments = 1; return result; } QMap attr; QUrl guidFile(QString("file://%1fipid-%2.xml").arg(directory).arg(fipid)); // Apparently at some point in time, for VER=6 msn returned an online URL // to a static error page (http://moneycentral.msn.com/cust404.htm). // Increasing to VER=9 solved the problem. This may happen again in the // future. QFileInfo i(guidFile.path()); #if OFXHOME if (!i.isReadable() || i.lastModified().addDays(7) < QDateTime::currentDateTime()) get("", attr, QUrl(QString("http://www.ofxhome.com/api.php?lookup=%1").arg(fipid)), guidFile); QFile f(guidFile.path()); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); stream.setCodec("UTF-8"); QString msg; int errl, errc; QDomDocument doc; if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) { QString fid = extractNodeText(doc, "institution/fid"); QString org = extractNodeText(doc, "institution/org"); QString url = extractNodeText(doc, "institution/url"); strncpy(result.fid, fid.toLatin1(), OFX_FID_LENGTH - 1); strncpy(result.org, org.toLatin1(), OFX_ORG_LENGTH - 1); strncpy(result.url, url.toLatin1(), OFX_URL_LENGTH - 1); result.accountlist = true; result.statements = true; result.billpay = false; result.investments = true; } } #endif #if MSN attr["content-type"] = "application/x-www-form-urlencoded"; attr["accept"] = "*/*"; if (!i.isReadable() || i.lastModified().addDays(7) < QDateTime::currentDateTime()) get("", attr, QUrl(QString("http://moneycentral.msn.com/money/2005/mnynet/service/olsvcupd/OnlSvcBrandInfo.aspx?MSNGUID=&GUID=%1&SKU=3&VER=" VER).arg(fipid)), guidFile); QFile f(guidFile.path()); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); stream.setCodec("UTF-16"); QString msg; int errl, errc; QDomDocument doc; if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) { QString fid = extractNodeText(doc, "ProviderSettings/FID"); QString org = extractNodeText(doc, "ProviderSettings/Org"); QString url = extractNodeText(doc, "ProviderSettings/ProviderURL"); strncpy(result.fid, fid.toLatin1(), OFX_FID_LENGTH - 1); strncpy(result.org, org.toLatin1(), OFX_ORG_LENGTH - 1); strncpy(result.url, url.toLatin1(), OFX_URL_LENGTH - 1); result.accountlist = (extractNodeText(doc, "ProviderSettings/AcctListAvail") == "1"); result.statements = (extractNodeText(doc, "BankingCapabilities/Bank") == "1"); result.billpay = (extractNodeText(doc, "BillPayCapabilities/Pay") == "1"); result.investments = (extractNodeText(doc, "InvestmentCapabilities/BrkStmt") == "1"); } } #endif return result; } bool get(const QString& request, const QMap& attr, const QUrl &url, const QUrl& filename) { Q_UNUSED(request); QByteArray req; OfxHttpRequest job("GET", url, req, attr, filename, false); return job.error() == QHttp::NoError; } bool post(const QString& request, const QMap& attr, const QUrl &url, const QUrl& filename) { QByteArray req(request.toUtf8()); OfxHttpRequest job("POST", url, req, attr, filename, false); return job.error() == QHttp::NoError; } } // namespace OfxPartner class OfxHttpRequest::Private { public: QFile m_fpTrace; }; OfxHttpRequest::OfxHttpRequest(const QString& type, const QUrl &url, const QByteArray &postData, const QMap& metaData, const QUrl& dst, bool showProgressInfo) : d(new Private), m_dst(dst), m_postJob(0), m_getJob(0) { m_eventLoop = new QEventLoop(qApp->activeWindow()); - QDir homeDir(QDir::home()); - if (homeDir.exists("ofxlog.txt")) { - d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(QDir::homePath())); + if (KMyMoneySettings::logOfxTransactions()) { + QString logPath = KMyMoneySettings::logPath(); + d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(logPath)); d->m_fpTrace.open(QIODevice::WriteOnly | QIODevice::Append); } KIO::JobFlag jobFlags = KIO::DefaultFlags; if (!showProgressInfo) jobFlags = KIO::HideProgressInfo; KIO::Job* job; if(type.toLower() == QStringLiteral("get")) { job = m_getJob = KIO::copy(url, dst, jobFlags); } else { job = m_postJob = KIO::http_post(url, postData, jobFlags); m_postJob->addMetaData("content-type", "Content-type: application/x-ofx"); m_postJob->addMetaData(metaData); connect(job, SIGNAL(data(KIO::Job*,QByteArray)), this, SLOT(slotOfxData(KIO::Job*,QByteArray))); connect(job, SIGNAL(connected(KIO::Job*)), this, SLOT(slotOfxConnected(KIO::Job*))); } if (d->m_fpTrace.isOpen()) { QTextStream ts(&d->m_fpTrace); ts << "url: " << url.toDisplayString() << "\n"; ts << "request:\n" << QString(postData) << "\n" << "response:\n"; } connect(job, SIGNAL(result(KJob*)), this, SLOT(slotOfxFinished(KJob*))); job->start(); qDebug("Starting eventloop"); if (m_eventLoop) m_eventLoop->exec(); qDebug("Ending eventloop"); } OfxHttpRequest::~OfxHttpRequest() { delete m_eventLoop; if (d->m_fpTrace.isOpen()) { d->m_fpTrace.close(); } delete d; } void OfxHttpRequest::slotOfxConnected(KIO::Job*) { m_file.setFileName(m_dst.path()); m_file.open(QIODevice::WriteOnly); } void OfxHttpRequest::slotOfxData(KIO::Job*, const QByteArray& _ba) { if (m_file.isOpen()) { m_file.write(_ba); if (d->m_fpTrace.isOpen()) { d->m_fpTrace.write(_ba); } } } void OfxHttpRequest::slotOfxFinished(KJob* /* e */) { if (m_file.isOpen()) { m_file.close(); if (d->m_fpTrace.isOpen()) { d->m_fpTrace.write("\nCompleted\n\n\n\n", 14); } } if(m_postJob) { int error = m_postJob->error(); if (error) { // TODO: port to KF5 //m_job->ui()->setWindow(0); m_postJob->ui()->showErrorMessage(); QFile::remove(m_dst.path()); } else if (m_postJob->isErrorPage()) { QString details; QFile f(m_dst.path()); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); QString line; while (!stream.atEnd()) { details += stream.readLine(); // line of text excluding '\n' } f.close(); } KMessageBox::detailedSorry(0, i18n("The HTTP request failed."), details, i18nc("The HTTP request failed", "Failed")); QFile::remove(m_dst.path()); } } else if(m_getJob) { int error = m_getJob->error(); if (error) { // TODO: port to KF5 //m_job->ui()->setWindow(0); m_getJob->ui()->showErrorMessage(); QFile::remove(m_dst.path()); } } qDebug("Finishing eventloop"); if (m_eventLoop) m_eventLoop->exit(); } diff --git a/kmymoney/reports/objectinfotable.cpp b/kmymoney/reports/objectinfotable.cpp index a419cbae8..2b11c91dc 100644 --- a/kmymoney/reports/objectinfotable.cpp +++ b/kmymoney/reports/objectinfotable.cpp @@ -1,353 +1,354 @@ /*************************************************************************** objectinfotable.cpp ------------------- begin : Sat 28 jun 2008 copyright : (C) 2004-2005 by Ace Jones 2008 by Alvaro Soliverez ***************************************************************************/ /*************************************************************************** * * * 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 "objectinfotable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyreport.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "reportaccount.h" #include "reportdebug.h" namespace reports { // **************************************************************************** // // ObjectInfoTable implementation // // **************************************************************************** /** * TODO * * - Collapse 2- & 3- groups when they are identical * - Way more test cases (especially splits & transfers) * - Option to collapse splits * - Option to exclude transfers * */ ObjectInfoTable::ObjectInfoTable(const MyMoneyReport& _report): ListTable(_report) { // separated into its own method to allow debugging (setting breakpoints // directly in ctors somehow does not work for me (ipwizard)) // TODO: remove the init() method and move the code back to the ctor init(); } void ObjectInfoTable::init() { switch (m_config.rowType()) { case MyMoneyReport::eSchedule: constructScheduleTable(); m_columns = "nextduedate,name"; break; case MyMoneyReport::eAccountInfo: constructAccountTable(); m_columns = "institution,type,name"; break; case MyMoneyReport::eAccountLoanInfo: constructAccountLoanTable(); m_columns = "institution,type,name"; break; default: break; } // Sort the data to match the report definition m_subtotal = "value"; switch (m_config.rowType()) { case MyMoneyReport::eSchedule: m_group = "type"; m_subtotal = "value"; break; case MyMoneyReport::eAccountInfo: case MyMoneyReport::eAccountLoanInfo: m_group = "topcategory,institution"; m_subtotal = "currentbalance"; break; default: throw MYMONEYEXCEPTION("ObjectInfoTable::ObjectInfoTable(): unhandled row type"); } QString sort = m_group + ',' + m_columns + ",id,rank"; switch (m_config.rowType()) { case MyMoneyReport::eSchedule: if (m_config.detailLevel() == MyMoneyReport::eDetailAll) { m_columns = "name,payee,paymenttype,occurence,nextduedate,category"; // krazy:exclude=spelling } else { m_columns = "name,payee,paymenttype,occurence,nextduedate"; // krazy:exclude=spelling } break; case MyMoneyReport::eAccountInfo: m_columns = "type,name,number,description,openingdate,currencyname,balancewarning,maxbalancelimit,creditwarning,maxcreditlimit,tax,favorite"; break; case MyMoneyReport::eAccountLoanInfo: m_columns = "type,name,number,description,openingdate,currencyname,payee,loanamount,interestrate,nextinterestchange,periodicpayment,finalpayment,favorite"; break; default: m_columns = ""; } TableRow::setSortCriteria(sort); qSort(m_rows); } void ObjectInfoTable::constructScheduleTable() { MyMoneyFile* file = MyMoneyFile::instance(); QList schedules; schedules = file->scheduleList("", MyMoneySchedule::TYPE_ANY, MyMoneySchedule::OCCUR_ANY, MyMoneySchedule::STYPE_ANY, m_config.fromDate(), m_config.toDate()); QList::const_iterator it_schedule = schedules.constBegin(); while (it_schedule != schedules.constEnd()) { MyMoneySchedule schedule = *it_schedule; ReportAccount account = schedule.account(); if (m_config.includes(account)) { //get fraction for account int fraction = account.fraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = MyMoneyFile::instance()->baseCurrency().smallestAccountFraction(); TableRow scheduleRow; //convert to base currency if needed MyMoneyMoney xr = MyMoneyMoney::ONE; if (m_config.isConvertCurrency() && account.isForeignCurrency()) { xr = account.baseCurrencyPrice(QDate::currentDate()).reduce(); } // help for sort and render functions scheduleRow["rank"] = '0'; //schedule data scheduleRow["id"] = schedule.id(); scheduleRow["name"] = schedule.name(); scheduleRow["nextduedate"] = schedule.nextDueDate().toString(Qt::ISODate); scheduleRow["type"] = KMyMoneyUtils::scheduleTypeToString(schedule.type()); scheduleRow["occurence"] = i18nc("Frequency of schedule", schedule.occurrenceToString().toLatin1()); // krazy:exclude=spelling scheduleRow["paymenttype"] = KMyMoneyUtils::paymentMethodToString(schedule.paymentType()); //scheduleRow["category"] = account.name(); //to get the payee we must look into the splits of the transaction MyMoneyTransaction transaction = schedule.transaction(); MyMoneySplit split = transaction.splitByAccount(account.id(), true); scheduleRow["value"] = (split.value() * xr).toString(); MyMoneyPayee payee = file->payee(split.payeeId()); scheduleRow["payee"] = payee.name(); m_rows += scheduleRow; //the text matches the main split bool transaction_text = m_config.match(&split); if (m_config.detailLevel() == MyMoneyReport::eDetailAll) { //get the information for all splits QList splits = transaction.splits(); QList::const_iterator split_it = splits.constBegin(); for (; split_it != splits.constEnd(); ++split_it) { TableRow splitRow; ReportAccount splitAcc = (*split_it).accountId(); splitRow["rank"] = '1'; splitRow["id"] = schedule.id(); splitRow["name"] = schedule.name(); splitRow["type"] = KMyMoneyUtils::scheduleTypeToString(schedule.type()); splitRow["nextduedate"] = schedule.nextDueDate().toString(Qt::ISODate); if ((*split_it).value() == MyMoneyMoney::autoCalc) { splitRow["split"] = MyMoneyMoney::autoCalc.toString(); } else if (! splitAcc.isIncomeExpense()) { splitRow["split"] = (*split_it).value().toString(); } else { splitRow["split"] = (- (*split_it).value()).toString(); } //if it is an assett account, mark it as a transfer if (! splitAcc.isIncomeExpense()) { splitRow["category"] = ((* split_it).value().isNegative()) ? i18n("Transfer from %1" , splitAcc.fullName()) : i18n("Transfer to %1" , splitAcc.fullName()); } else { splitRow ["category"] = splitAcc.fullName(); } //add the split only if it matches the text or it matches the main split if (m_config.match(&(*split_it)) || transaction_text) m_rows += splitRow; } } } ++it_schedule; } } void ObjectInfoTable::constructAccountTable() { MyMoneyFile* file = MyMoneyFile::instance(); //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { TableRow accountRow; ReportAccount account = *it_account; if (m_config.includes(account) && account.accountType() != MyMoneyAccount::Stock && !account.isClosed()) { MyMoneyMoney value; accountRow["rank"] = '0'; accountRow["topcategory"] = KMyMoneyUtils::accountTypeToString(account.accountGroup()); accountRow["institution"] = (file->institution(account.institutionId())).name(); accountRow["type"] = KMyMoneyUtils::accountTypeToString(account.accountType()); accountRow["name"] = account.name(); accountRow["number"] = account.number(); accountRow["description"] = account.description(); accountRow["openingdate"] = account.openingDate().toString(Qt::ISODate); //accountRow["currency"] = (file->currency(account.currencyId())).tradingSymbol(); accountRow["currencyname"] = (file->currency(account.currencyId())).name(); accountRow["balancewarning"] = account.value("minBalanceEarly"); accountRow["maxbalancelimit"] = account.value("minBalanceAbsolute"); accountRow["creditwarning"] = account.value("maxCreditEarly"); accountRow["maxcreditlimit"] = account.value("maxCreditAbsolute"); accountRow["tax"] = account.value("Tax") == QLatin1String("Yes") ? i18nc("Is this a tax account?", "Yes") : QString(); + accountRow["openingbalance"] = account.value("OpeningBalanceAccount") == QLatin1String("Yes") ? i18nc("Is this an opening balance account?", "Yes") : QString(); accountRow["favorite"] = account.value("PreferredAccount") == QLatin1String("Yes") ? i18nc("Is this a favorite account?", "Yes") : QString(); //investment accounts show the balances of all its subaccounts if (account.accountType() == MyMoneyAccount::Investment) { value = investmentBalance(account); } else { value = file->balance(account.id()); } //convert to base currency if needed if (m_config.isConvertCurrency() && account.isForeignCurrency()) { MyMoneyMoney xr = account.baseCurrencyPrice(QDate::currentDate()).reduce(); value = value * xr; } accountRow["currentbalance"] = value.toString(); m_rows += accountRow; } ++it_account; } } void ObjectInfoTable::constructAccountLoanTable() { MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { TableRow accountRow; ReportAccount account = *it_account; MyMoneyAccountLoan loan = *it_account; if (m_config.includes(account) && account.isLoan() && !account.isClosed()) { //convert to base currency if needed MyMoneyMoney xr = MyMoneyMoney::ONE; if (m_config.isConvertCurrency() && account.isForeignCurrency()) { xr = account.baseCurrencyPrice(QDate::currentDate()).reduce(); } accountRow["rank"] = '0'; accountRow["topcategory"] = KMyMoneyUtils::accountTypeToString(account.accountGroup()); accountRow["institution"] = (file->institution(account.institutionId())).name(); accountRow["type"] = KMyMoneyUtils::accountTypeToString(account.accountType()); accountRow["name"] = account.name(); accountRow["number"] = account.number(); accountRow["description"] = account.description(); accountRow["openingdate"] = account.openingDate().toString(Qt::ISODate); //accountRow["currency"] = (file->currency(account.currencyId())).tradingSymbol(); accountRow["currencyname"] = (file->currency(account.currencyId())).name(); accountRow["payee"] = file->payee(loan.payee()).name(); accountRow["loanamount"] = (loan.loanAmount() * xr).toString(); accountRow["interestrate"] = (loan.interestRate(QDate::currentDate()) / MyMoneyMoney(100, 1) * xr).toString(); accountRow["nextinterestchange"] = loan.nextInterestChange().toString(Qt::ISODate); accountRow["periodicpayment"] = (loan.periodicPayment() * xr).toString(); accountRow["finalpayment"] = (loan.finalPayment() * xr).toString(); accountRow["favorite"] = account.value("PreferredAccount") == QLatin1String("Yes") ? i18nc("Is this a favorite account?", "Yes") : QString(); MyMoneyMoney value = file->balance(account.id()); value = value * xr; accountRow["currentbalance"] = value.toString(); m_rows += accountRow; } ++it_account; } } MyMoneyMoney ObjectInfoTable::investmentBalance(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyMoney value = file->balance(acc.id()); QStringList accList = acc.accountList(); QStringList::const_iterator it_a = accList.constBegin(); for (; it_a != acc.accountList().constEnd(); ++it_a) { MyMoneyAccount stock = file->account(*it_a); try { MyMoneyMoney val; MyMoneyMoney balance = file->balance(stock.id()); MyMoneySecurity security = file->security(stock.currencyId()); const MyMoneyPrice &price = file->price(stock.currencyId(), security.tradingCurrency()); val = balance * price.rate(security.tradingCurrency()); // adjust value of security to the currency of the account MyMoneySecurity accountCurrency = file->currency(acc.currencyId()); val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id()); val = val.convert(acc.fraction()); value += val; } catch (const MyMoneyException &e) { qWarning("%s", qPrintable(QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e.what()))); } } return value; } } diff --git a/kmymoney/reports/pivottable.cpp b/kmymoney/reports/pivottable.cpp index b5e9947f5..006995c95 100644 --- a/kmymoney/reports/pivottable.cpp +++ b/kmymoney/reports/pivottable.cpp @@ -1,2369 +1,2370 @@ /*************************************************************************** pivottable.cpp ------------------- begin : Mon May 17 2004 copyright : (C) 2004-2005 by Ace Jones email : Thomas Baumgart Alvaro Soliverez ***************************************************************************/ /*************************************************************************** * * * 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 "pivottable.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "pivotgrid.h" #include "reportdebug.h" #include "kreportchartview.h" #include "kmymoneyglobalsettings.h" #include "kmymoneyutils.h" #include "mymoneyforecast.h" #include namespace reports { using KChart::Widget; QString Debug::m_sTabs; bool Debug::m_sEnabled = DEBUG_ENABLED_BY_DEFAULT; QString Debug::m_sEnableKey; Debug::Debug(const QString& _name): m_methodName(_name), m_enabled(m_sEnabled) { if (!m_enabled && _name == m_sEnableKey) m_enabled = true; if (m_enabled) { qDebug("%s%s(): ENTER", qPrintable(m_sTabs), qPrintable(m_methodName)); m_sTabs.append("--"); } } Debug::~Debug() { if (m_enabled) { m_sTabs.remove(0, 2); qDebug("%s%s(): EXIT", qPrintable(m_sTabs), qPrintable(m_methodName)); if (m_methodName == m_sEnableKey) m_enabled = false; } } void Debug::output(const QString& _text) { if (m_enabled) qDebug("%s%s(): %s", qPrintable(m_sTabs), qPrintable(m_methodName), qPrintable(_text)); } PivotTable::PivotTable(const MyMoneyReport& _config_f): ReportTable(), m_runningSumsCalculated(false), m_config_f(_config_f) { init(); } void PivotTable::init() { DEBUG_ENTER(Q_FUNC_INFO); // // Initialize locals // MyMoneyFile* file = MyMoneyFile::instance(); // // Initialize member variables // //make sure we have all subaccounts of investment accounts includeInvestmentSubAccounts(); m_config_f.validDateRange(m_beginDate, m_endDate); // If we need to calculate running sums, it does not make sense // to show a row total column if (m_config_f.isRunningSum()) m_config_f.setShowingRowTotals(false); if (m_config_f.isRunningSum() && !m_config_f.isIncludingPrice() && !m_config_f.isIncludingAveragePrice() && !m_config_f.isIncludingMovingAverage() && !m_config_f.isIncludingForecast() && !m_config_f.isIncludingSchedules()) m_startColumn = 1; else m_startColumn = 0; m_numColumns = columnValue(m_endDate) - columnValue(m_beginDate) + 1 + m_startColumn; // 1 for m_beginDate values and m_startColumn for opening balance values //Load what types of row the report is going to show loadRowTypeList(); // // Initialize outer groups of the grid // if (m_config_f.rowType() == MyMoneyReport::eAssetLiability) { m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Asset), PivotOuterGroup(m_numColumns)); m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Liability), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder, true /* inverted */)); + m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Equity), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder, true /* inverted */)); } else { m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Income), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 2)); m_grid.insert(KMyMoneyUtils::accountTypeToString(MyMoneyAccount::Expense), PivotOuterGroup(m_numColumns, PivotOuterGroup::m_kDefaultSortOrder - 1, true /* inverted */)); // // Create rows for income/expense reports with all accounts included // if (m_config_f.isIncludingUnusedAccounts()) createAccountRows(); } // // Initialize grid totals // m_grid.m_total = PivotGridRowSet(m_numColumns); // // Get opening balances // Only net worth report qualifies if (m_config_f.isRunningSum() && !m_config_f.isIncludingPrice() && !m_config_f.isIncludingAveragePrice() && !m_config_f.isIncludingMovingAverage() && !m_config_f.isIncludingForecast() && !m_config_f.isIncludingSchedules()) calculateOpeningBalances(); // // Calculate budget mapping // (for budget reports only) // if (m_config_f.hasBudget()) calculateBudgetMapping(); // // Populate all transactions into the row/column pivot grid // QList transactions; m_config_f.setReportAllSplits(false); m_config_f.setConsiderCategory(true); try { transactions = file->transactionList(m_config_f); } catch (const MyMoneyException &e) { qDebug("ERR: %s thrown in %s(%ld)", qPrintable(e.what()), qPrintable(e.file()), e.line()); throw e; } DEBUG_OUTPUT(QString("Found %1 matching transactions").arg(transactions.count())); // Include scheduled transactions if required if (m_config_f.isIncludingSchedules()) { // Create a custom version of the report filter, excluding date // We'll use this to compare the transaction against MyMoneyTransactionFilter schedulefilter(m_config_f); schedulefilter.setDateFilter(QDate(), QDate()); // Get the real dates from the config filter QDate configbegin, configend; m_config_f.validDateRange(configbegin, configend); QList schedules = file->scheduleList(); QList::const_iterator it_schedule = schedules.constBegin(); while (it_schedule != schedules.constEnd()) { // If the transaction meets the filter MyMoneyTransaction tx = (*it_schedule).transaction(); if (!(*it_schedule).isFinished() && schedulefilter.match(tx)) { // Keep the id of the schedule with the transaction so that // we can do the autocalc later on in case of a loan payment tx.setValue("kmm-schedule-id", (*it_schedule).id()); // Get the dates when a payment will be made within the report window QDate nextpayment = (*it_schedule).adjustedNextPayment(configbegin); if (nextpayment.isValid()) { // Add one transaction for each date QList paymentDates = (*it_schedule).paymentDates(nextpayment, configend); QList::const_iterator it_date = paymentDates.constBegin(); while (it_date != paymentDates.constEnd()) { //if the payment occurs in the past, enter it tomorrow if (QDate::currentDate() >= *it_date) { tx.setPostDate(QDate::currentDate().addDays(1)); } else { tx.setPostDate(*it_date); } if (tx.postDate() <= configend && tx.postDate() >= configbegin) { transactions += tx; } DEBUG_OUTPUT(QString("Added transaction for schedule %1 on %2").arg((*it_schedule).id()).arg((*it_date).toString())); ++it_date; } } } ++it_schedule; } } // whether asset & liability transactions are actually to be considered // transfers bool al_transfers = (m_config_f.rowType() == MyMoneyReport::eExpenseIncome) && (m_config_f.isIncludingTransfers()); //this is to store balance for loan accounts when not included in the report QMap loanBalances; QList::const_iterator it_transaction = transactions.constBegin(); int colofs = columnValue(m_beginDate) - m_startColumn; while (it_transaction != transactions.constEnd()) { MyMoneyTransaction tx = (*it_transaction); QDate postdate = tx.postDate(); if (postdate < m_beginDate) { qDebug("MyMoneyFile::transactionList returned a transaction that is outside the date filter, skipping it"); ++it_transaction; continue; } int column = columnValue(postdate) - colofs; // check if we need to call the autocalculation routine if (tx.isLoanPayment() && tx.hasAutoCalcSplit() && (tx.value("kmm-schedule-id").length() > 0)) { // make sure to consider any autocalculation for loan payments MyMoneySchedule sched = file->schedule(tx.value("kmm-schedule-id")); const MyMoneySplit& split = tx.amortizationSplit(); if (!split.id().isEmpty()) { ReportAccount splitAccount = file->account(split.accountId()); MyMoneyAccount::accountTypeE type = splitAccount.accountGroup(); QString outergroup = KMyMoneyUtils::accountTypeToString(type); //if the account is included in the report, calculate the balance from the cells if (m_config_f.includes(splitAccount)) { loanBalances[splitAccount.id()] = cellBalance(outergroup, splitAccount, column, false); } else { //if it is not in the report and also not in loanBalances, get the balance from the file if (!loanBalances.contains(splitAccount.id())) { QDate dueDate = sched.nextDueDate(); //if the payment is overdue, use current date if (dueDate < QDate::currentDate()) dueDate = QDate::currentDate(); //get the balance from the file for the date loanBalances[splitAccount.id()] = file->balance(splitAccount.id(), dueDate.addDays(-1)); } } KMyMoneyUtils::calculateAutoLoan(sched, tx, loanBalances); //if the loan split is not included in the report, update the balance for the next occurrence if (!m_config_f.includes(splitAccount)) { QList::ConstIterator it_loanSplits; for (it_loanSplits = tx.splits().constBegin(); it_loanSplits != tx.splits().constEnd(); ++it_loanSplits) { if ((*it_loanSplits).isAmortizationSplit() && (*it_loanSplits).accountId() == splitAccount.id()) loanBalances[splitAccount.id()] = loanBalances[splitAccount.id()] + (*it_loanSplits).shares(); } } } } QList splits = tx.splits(); QList::const_iterator it_split = splits.constBegin(); while (it_split != splits.constEnd()) { ReportAccount splitAccount = (*it_split).accountId(); // Each split must be further filtered, because if even one split matches, // the ENTIRE transaction is returned with all splits (even non-matching ones) if (m_config_f.includes(splitAccount) && m_config_f.match(&(*it_split))) { // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse(splitAccount.isIncomeExpense() ? -1 : 1, 1); MyMoneyMoney value; // the outer group is the account class (major account type) MyMoneyAccount::accountTypeE type = splitAccount.accountGroup(); QString outergroup = KMyMoneyUtils::accountTypeToString(type); value = (*it_split).shares(); bool stockSplit = tx.isStockSplit(); if (!stockSplit) { // retrieve the value in the account's underlying currency if (value != MyMoneyMoney::autoCalc) { value = value * reverse; } else { qDebug("PivotTable::PivotTable(): This must not happen"); value = MyMoneyMoney(); // keep it 0 so far } // Except in the case of transfers on an income/expense report if (al_transfers && (type == MyMoneyAccount::Asset || type == MyMoneyAccount::Liability)) { outergroup = i18n("Transfers"); value = -value; } } // add the value to its correct position in the pivot table assignCell(outergroup, splitAccount, column, value, false, stockSplit); } ++it_split; } ++it_transaction; } // // Get forecast data // if (m_config_f.isIncludingForecast()) calculateForecast(); // //Insert Price data // if (m_config_f.isIncludingPrice()) fillBasePriceUnit(ePrice); // //Insert Average Price data // if (m_config_f.isIncludingAveragePrice()) { fillBasePriceUnit(eActual); calculateMovingAverage(); } // // Collapse columns to match column type // if (m_config_f.columnPitch() > 1) collapseColumns(); // // Calculate the running sums // (for running sum reports only) // if (m_config_f.isRunningSum()) calculateRunningSums(); // // Calculate Moving Average // if (m_config_f.isIncludingMovingAverage()) calculateMovingAverage(); // // Calculate Budget Difference // if (m_config_f.isIncludingBudgetActuals()) calculateBudgetDiff(); // // Convert all values to the deep currency // convertToDeepCurrency(); // // Convert all values to the base currency // if (m_config_f.isConvertCurrency()) convertToBaseCurrency(); // // Determine column headings // calculateColumnHeadings(); // // Calculate row and column totals // calculateTotals(); // // If using mixed time, calculate column for current date // m_config_f.setCurrentDateColumn(currentDateColumn()); } void PivotTable::collapseColumns() { DEBUG_ENTER(Q_FUNC_INFO); int columnpitch = m_config_f.columnPitch(); if (columnpitch != 1) { int sourcemonth = (m_config_f.isColumnsAreDays()) // use the user's locale to determine the week's start ? (m_beginDate.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7 : m_beginDate.month(); int sourcecolumn = m_startColumn; int destcolumn = m_startColumn; while (sourcecolumn < m_numColumns) { if (sourcecolumn != destcolumn) { #if 0 // TODO: Clean up this rather inefficient kludge. We really should jump by an entire // destcolumn at a time on RS reports, and calculate the proper sourcecolumn to use, // allowing us to clear and accumulate only ONCE per destcolumn if (m_config_f.isRunningSum()) clearColumn(destcolumn); #endif accumulateColumn(destcolumn, sourcecolumn); } if (++sourcecolumn < m_numColumns) { if ((sourcemonth++ % columnpitch) == 0) { if (sourcecolumn != ++destcolumn) clearColumn(destcolumn); } } } m_numColumns = destcolumn + 1; } } void PivotTable::accumulateColumn(int destcolumn, int sourcecolumn) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("From Column %1 to %2").arg(sourcecolumn).arg(destcolumn)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= sourcecolumn) throw MYMONEYEXCEPTION(QString("Sourcecolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); if ((*it_row)[eActual].count() <= destcolumn) throw MYMONEYEXCEPTION(QString("Destcolumn %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(sourcecolumn).arg((*it_row)[eActual].count())); (*it_row)[eActual][destcolumn] += (*it_row)[eActual][sourcecolumn]; ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::clearColumn(int column) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Column %1").arg(column)); // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // iterate over inner groups PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { // iterator over rows PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { if ((*it_row)[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::accumulateColumn").arg(column).arg((*it_row)[eActual].count())); (*it_row++)[eActual][column] = PivotCell(); } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateColumnHeadings() { DEBUG_ENTER(Q_FUNC_INFO); // one column for the opening balance if (m_config_f.isRunningSum() && !m_config_f.isIncludingPrice() && !m_config_f.isIncludingAveragePrice() && !m_config_f.isIncludingMovingAverage() && !m_config_f.isIncludingForecast() && !m_config_f.isIncludingSchedules()) m_columnHeadings.append("Opening"); int columnpitch = m_config_f.columnPitch(); if (columnpitch == 0) { // output the warning but don't crash by dividing with 0 qWarning("PivotTable::calculateColumnHeadings() Invalid column pitch"); return; } // if this is a days-based report if (m_config_f.isColumnsAreDays()) { if (columnpitch == 1) { QDate columnDate = m_beginDate; int column = m_startColumn; while (column++ < m_numColumns) { QString heading = QLocale().monthName(columnDate.month(), QLocale::ShortFormat) + ' ' + QString::number(columnDate.day()); columnDate = columnDate.addDays(1); m_columnHeadings.append(heading); } } else { QDate day = m_beginDate; QDate prv = m_beginDate; // use the user's locale to determine the week's start int dow = (day.dayOfWeek() + 8 - QLocale().firstDayOfWeek()) % 7; while (day <= m_endDate) { if (((dow % columnpitch) == 0) || (day == m_endDate)) { m_columnHeadings.append(QString("%1 %2 - %3 %4") .arg(QLocale().monthName(prv.month(), QLocale::ShortFormat)) .arg(prv.day()) .arg(QLocale().monthName(day.month(), QLocale::ShortFormat)) .arg(day.day())); prv = day.addDays(1); } day = day.addDays(1); dow++; } } } // else it's a months-based report else { if (columnpitch == 12) { int year = m_beginDate.year(); int column = m_startColumn; while (column++ < m_numColumns) m_columnHeadings.append(QString::number(year++)); } else { int year = m_beginDate.year(); bool includeyear = (m_beginDate.year() != m_endDate.year()); int segment = (m_beginDate.month() - 1) / columnpitch; int column = m_startColumn; while (column++ < m_numColumns) { QString heading = QLocale().monthName(1 + segment * columnpitch, QLocale::ShortFormat); if (columnpitch != 1) heading += '-' + QLocale().monthName((1 + segment) * columnpitch, QLocale::ShortFormat); if (includeyear) heading += ' ' + QString::number(year); m_columnHeadings.append(heading); if (++segment >= 12 / columnpitch) { segment -= 12 / columnpitch; ++year; } } } } } void PivotTable::createAccountRows() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account = *it_account; // only include this item if its account group is included in this report // and if the report includes this account if (m_config_f.includes(*it_account)) { DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = KMyMoneyUtils::accountTypeToString(account.accountGroup()); // place into the 'opening' column... assignCell(outergroup, account, 0, MyMoneyMoney()); } ++it_account; } } void PivotTable::calculateOpeningBalances() { DEBUG_ENTER(Q_FUNC_INFO); // First, determine the inclusive dates of the report. Normally, that's just // the begin & end dates of m_config_f. However, if either of those dates are // blank, we need to use m_beginDate and/or m_endDate instead. QDate from = m_config_f.fromDate(); QDate to = m_config_f.toDate(); if (! from.isValid()) from = m_beginDate; if (! to.isValid()) to = m_endDate; MyMoneyFile* file = MyMoneyFile::instance(); QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { ReportAccount account = *it_account; // only include this item if its account group is included in this report // and if the report includes this account if (m_config_f.includes(*it_account)) { //do not include account if it is closed and it has no transactions in the report period if (account.isClosed()) { //check if the account has transactions for the report timeframe MyMoneyTransactionFilter filter; filter.addAccount(account.id()); filter.setDateFilter(m_beginDate, m_endDate); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if a closed account has no transactions in that timeframe, do not include it if (transactions.size() == 0) { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); ++it_account; continue; } } DEBUG_OUTPUT(QString("Includes account %1").arg(account.name())); // the row group is the account class (major account type) QString outergroup = KMyMoneyUtils::accountTypeToString(account.accountGroup()); // extract the balance of the account for the given begin date, which is // the opening balance plus the sum of all transactions prior to the begin // date // this is in the underlying currency MyMoneyMoney value = file->balance(account.id(), from.addDays(-1)); // place into the 'opening' column... assignCell(outergroup, account, 0, value); } else { DEBUG_OUTPUT(QString("DOES NOT INCLUDE account %1").arg(account.name())); } ++it_account; } } void PivotTable::calculateRunningSums(PivotInnerGroup::iterator& it_row) { MyMoneyMoney runningsum = it_row.value()[eActual][0].calculateRunningSum(MyMoneyMoney()); int column = m_startColumn; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = it_row.value()[eActual][column].calculateRunningSum(runningsum); ++column; } } void PivotTable::calculateRunningSums() { DEBUG_ENTER(Q_FUNC_INFO); m_runningSumsCalculated = true; PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { #if 0 MyMoneyMoney runningsum = it_row.value()[0]; int column = m_startColumn; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateRunningSums").arg(column).arg(it_row.value()[eActual].count())); runningsum = (it_row.value()[eActual][column] += runningsum); ++column; } #endif calculateRunningSums(it_row); ++it_row; } ++it_innergroup; } ++it_outergroup; } } MyMoneyMoney PivotTable::cellBalance(const QString& outergroup, const ReportAccount& _row, int _column, bool budget) { if (m_runningSumsCalculated) { qDebug("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()"); throw MYMONEYEXCEPTION(QString("You must not call PivotTable::cellBalance() after calling PivotTable::calculateRunningSums()")); } // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config_f.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return MyMoneyMoney(); row = newrow; } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= _column) throw MYMONEYEXCEPTION(QString("Column %1 out of m_numColumns range (%2) in PivotTable::cellBalance").arg(_column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= _column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(_column).arg(m_grid[outergroup][innergroup][row][eActual].count())); MyMoneyMoney balance; if (budget) balance = m_grid[outergroup][innergroup][row][eBudget][0].cellBalance(MyMoneyMoney()); else balance = m_grid[outergroup][innergroup][row][eActual][0].cellBalance(MyMoneyMoney()); int column = m_startColumn; while (column < _column) { if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::cellBalance").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); balance = m_grid[outergroup][innergroup][row][eActual][column].cellBalance(balance); ++column; } return balance; } void PivotTable::calculateBudgetMapping() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); // Only do this if there is at least one budget in the file if (file->countBudgets()) { // Select a budget // // It will choose the first budget in the list for the start year of the report if no budget is selected MyMoneyBudget budget = MyMoneyBudget(); QList budgets = file->budgetList(); bool validBudget = false; //check that the selected budget is valid if (m_config_f.budget() != "Any") { QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the budget by id if ((*budgets_it).id() == m_config_f.budget()) { budget = file->budget((*budgets_it).id()); validBudget = true; break; } ++budgets_it; } } //if no valid budget has been selected if (!validBudget) { //if the budget list is empty, just return if (budgets.count() == 0) { return; } QList::const_iterator budgets_it = budgets.constBegin(); while (budgets_it != budgets.constEnd()) { //pick the first budget that matches the report start year if ((*budgets_it).budgetStart().year() == QDate::currentDate().year()) { budget = file->budget((*budgets_it).id()); break; } ++budgets_it; } //if it can't find a matching budget, take the first one on the list if (budget.id().isEmpty()) { budget = budgets[0]; } //assign the budget to the report m_config_f.setBudget(budget.id(), m_config_f.isIncludingBudgetActuals()); } // Dump the budget //qDebug() << "Budget " << budget.name() << ": "; // Go through all accounts in the system to build the mapping QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); while (it_account != accounts.constEnd()) { //include only the accounts selected for the report if (m_config_f.includes(*it_account)) { QString id = (*it_account).id(); QString acid = id; // If the budget contains this account outright if (budget.contains(id)) { // Add it to the mapping m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " self-maps / type =" << budget.account(id).budgetLevel(); } // Otherwise, search for a parent account which includes sub-accounts else { //if includeBudgetActuals, include all accounts regardless of whether in budget or not if (m_config_f.isIncludingBudgetActuals()) { m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); } do { id = file->account(id).parentAccountId(); if (budget.contains(id)) { if (budget.account(id).budgetSubaccounts()) { m_budgetMap[acid] = id; // qDebug() << ReportAccount(acid).debugName() << " maps to " << ReportAccount(id).debugName(); break; } } } while (! id.isEmpty()); } } ++it_account; } // end while looping through the accounts in the file // Place the budget values into the budget grid QList baccounts = budget.getaccounts(); QList::const_iterator it_bacc = baccounts.constBegin(); while (it_bacc != baccounts.constEnd()) { ReportAccount splitAccount = (*it_bacc).id(); //include the budget account only if it is included in the report if (m_config_f.includes(splitAccount)) { MyMoneyAccount::accountTypeE type = splitAccount.accountGroup(); QString outergroup = KMyMoneyUtils::accountTypeToString(type); // reverse sign to match common notation for cash flow direction, only for expense/income splits MyMoneyMoney reverse((splitAccount.accountType() == MyMoneyAccount::Expense) ? -1 : 1, 1); const QMap& periods = (*it_bacc).getPeriods(); // skip the account if it has no periods if (periods.count() < 1) { ++it_bacc; continue; } MyMoneyMoney value = (*periods.begin()).amount() * reverse; int column = m_startColumn; // based on the kind of budget it is, deal accordingly switch ((*it_bacc).budgetLevel()) { case MyMoneyBudget::AccountGroup::eYearly: // divide the single yearly value by 12 and place it in each column value /= MyMoneyMoney(12, 1); // intentional fall through case MyMoneyBudget::AccountGroup::eNone: case MyMoneyBudget::AccountGroup::eMax: case MyMoneyBudget::AccountGroup::eMonthly: // place the single monthly value in each column of the report // only add the value if columns are monthly or longer if (m_config_f.columnType() == MyMoneyReport::eBiMonths || m_config_f.columnType() == MyMoneyReport::eMonths || m_config_f.columnType() == MyMoneyReport::eYears || m_config_f.columnType() == MyMoneyReport::eQuarters) { QDate budgetDate = budget.budgetStart(); while (column < m_numColumns && budget.budgetStart().addYears(1) > budgetDate) { //only show budget values if the budget year and the column date match //no currency conversion is done here because that is done for all columns later if (budgetDate > columnDate(column)) { ++column; } else { if (budgetDate >= m_beginDate.addDays(-m_beginDate.day() + 1) && budgetDate <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) && budgetDate > (columnDate(column).addMonths(-m_config_f.columnType()))) { assignCell(outergroup, splitAccount, column, value, true /*budget*/); } budgetDate = budgetDate.addMonths(1); } } } break; case MyMoneyBudget::AccountGroup::eMonthByMonth: // place each value in the appropriate column // budget periods are supposed to come in order just like columns { QMap::const_iterator it_period = periods.begin(); while (it_period != periods.end() && column < m_numColumns) { if ((*it_period).startDate() > columnDate(column)) { ++column; } else { switch (m_config_f.columnType()) { case MyMoneyReport::eYears: case MyMoneyReport::eBiMonths: case MyMoneyReport::eQuarters: case MyMoneyReport::eMonths: { if ((*it_period).startDate() >= m_beginDate.addDays(-m_beginDate.day() + 1) && (*it_period).startDate() <= m_endDate.addDays(m_endDate.daysInMonth() - m_endDate.day()) && (*it_period).startDate() > (columnDate(column).addMonths(-m_config_f.columnType()))) { //no currency conversion is done here because that is done for all columns later value = (*it_period).amount() * reverse; assignCell(outergroup, splitAccount, column, value, true /*budget*/); } ++it_period; break; } default: break; } } } break; } } } ++it_bacc; } } // end if there was a budget } void PivotTable::convertToBaseCurrency() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); int fraction = file->baseCurrency().smallestAccountFraction(); QList rowTypeList = m_rowTypeList; rowTypeList.removeOne(eAverage); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToBaseCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get base price for that date MyMoneyMoney conversionfactor = it_row.key().baseCurrencyPrice(valuedate, m_config_f.isSkippingZero()); int pricePrecision; if (it_row.key().isInvest()) pricePrecision = file->security(it_row.key().currencyId()).pricePrecision(); else pricePrecision = MyMoneyMoney::denomToPrec(fraction); foreach (const auto rowType, rowTypeList) { //calculate base value MyMoneyMoney oldval = it_row.value()[rowType][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //convert to lowest fraction if (rowType == ePrice) it_row.value()[rowType][column] = PivotCell(value.convertPrecision(pricePrecision)); else it_row.value()[rowType][column] = PivotCell(value.convert(fraction)); DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.value()[rowType][column].toDouble()))); } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::convertToDeepCurrency() { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; while (column < m_numColumns) { if (it_row.value()[eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::convertToDeepCurrency").arg(column).arg(it_row.value()[eActual].count())); QDate valuedate = columnDate(column); //get conversion factor for the account and date MyMoneyMoney conversionfactor = it_row.key().deepCurrencyPrice(valuedate, m_config_f.isSkippingZero()); //use the fraction relevant to the account at hand int fraction = it_row.key().currency().smallestAccountFraction(); //use base currency fraction if not initialized if (fraction == -1) fraction = file->baseCurrency().smallestAccountFraction(); //convert to deep currency MyMoneyMoney oldval = it_row.value()[eActual][column]; MyMoneyMoney value = (oldval * conversionfactor).reduce(); //reduce to lowest fraction it_row.value()[eActual][column] = PivotCell(value.convert(fraction)); //convert price data if (m_config_f.isIncludingPrice()) { MyMoneyMoney oldPriceVal = it_row.value()[ePrice][column]; MyMoneyMoney priceValue = (oldPriceVal * conversionfactor).reduce(); it_row.value()[ePrice][column] = PivotCell(priceValue.convert(10000)); } DEBUG_OUTPUT_IF(conversionfactor != MyMoneyMoney::ONE , QString("Factor of %1, value was %2, now %3").arg(conversionfactor).arg(DEBUG_SENSITIVE(oldval)).arg(DEBUG_SENSITIVE(it_row.value()[eActual][column].toDouble()))); ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateTotals() { //insert the row type that is going to be used for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { m_grid.m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Outer groups // // iterate over outer groups PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_outergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Inner Groups // PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { for (int k = 0; k < m_numColumns; ++k) { (*it_innergroup).m_total[ m_rowTypeList[i] ].append(PivotCell()); } } // // Rows // PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // int column = m_startColumn; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (it_row.value()[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, row columns").arg(column).arg(it_row.value()[ m_rowTypeList[i] ].count())); if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate total MyMoneyMoney value = it_row.value()[ m_rowTypeList[i] ][column]; (*it_innergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_row)[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_row; } // // Inner Row Group Totals // int column = m_startColumn; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if ((*it_innergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, inner group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); if ((*it_outergroup).m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, outer group totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate totals MyMoneyMoney value = (*it_innergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ][column] += value; (*it_innergroup).m_total[ m_rowTypeList[i] ].m_total += value; } ++column; } ++it_innergroup; } // // Outer Row Group Totals // const bool isIncomeExpense = (m_config_f.rowType() == MyMoneyReport::eExpenseIncome); const bool invert_total = (*it_outergroup).m_inverted; int column = m_startColumn; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(column).arg((*it_innergroup).m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = (*it_outergroup).m_total[ m_rowTypeList[i] ][column]; (*it_outergroup).m_total[ m_rowTypeList[i] ].m_total += value; //so far the invert only applies to actual and budget if (invert_total && m_rowTypeList[i] != eBudgetDiff && m_rowTypeList[i] != eForecast) value = -value; // forecast income expense reports should be inverted as oposed to asset/liability reports if (invert_total && isIncomeExpense && m_rowTypeList[i] == eForecast) value = -value; m_grid.m_total[ m_rowTypeList[i] ][column] += value; } ++column; } ++it_outergroup; } // // Report Totals // int totalcolumn = m_startColumn; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { if (m_grid.m_total[ m_rowTypeList[i] ].count() <= totalcolumn) throw MYMONEYEXCEPTION(QString("Total column %1 out of grid range (%2) in PivotTable::calculateTotals, grid totals").arg(totalcolumn).arg(m_grid.m_total[ m_rowTypeList[i] ].count())); //calculate actual totals MyMoneyMoney value = m_grid.m_total[ m_rowTypeList[i] ][totalcolumn]; m_grid.m_total[ m_rowTypeList[i] ].m_total += value; } ++totalcolumn; } } void PivotTable::assignCell(const QString& outergroup, const ReportAccount& _row, int column, MyMoneyMoney value, bool budget, bool stockSplit) { DEBUG_ENTER(Q_FUNC_INFO); DEBUG_OUTPUT(QString("Parameters: %1,%2,%3,%4,%5").arg(outergroup).arg(_row.debugName()).arg(column).arg(DEBUG_SENSITIVE(value.toDouble())).arg(budget)); // for budget reports, if this is the actual value, map it to the account which // holds its budget ReportAccount row = _row; if (!budget && m_config_f.hasBudget()) { QString newrow = m_budgetMap[row.id()]; // if there was no mapping found, then the budget report is not interested // in this account. if (newrow.isEmpty()) return; row = newrow; } // ensure the row already exists (and its parental hierarchy) createRow(outergroup, row, true); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (m_numColumns <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of m_numColumns range (%2) in PivotTable::assignCell").arg(column).arg(m_numColumns)); if (m_grid[outergroup][innergroup][row][eActual].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eActual].count())); if (m_grid[outergroup][innergroup][row][eBudget].count() <= column) throw MYMONEYEXCEPTION(QString("Column %1 out of grid range (%2) in PivotTable::assignCell").arg(column).arg(m_grid[outergroup][innergroup][row][eBudget].count())); if (!stockSplit) { // Determine whether the value should be inverted before being placed in the row if (m_grid[outergroup].m_inverted) value = -value; // Add the value to the grid cell if (budget) { m_grid[outergroup][innergroup][row][eBudget][column] += value; } else { // If it is loading an actual value for a budget report // check whether it is a subaccount of a budget account (include subaccounts) // If so, check if is the same currency and convert otherwise if (m_config_f.hasBudget() && row.id() != _row.id() && row.currencyId() != _row.currencyId()) { ReportAccount origAcc = _row; MyMoneyMoney rate = origAcc.foreignCurrencyPrice(row.currencyId(), columnDate(column), false); m_grid[outergroup][innergroup][row][eActual][column] += (value * rate).reduce(); } else { m_grid[outergroup][innergroup][row][eActual][column] += value; } } } else { m_grid[outergroup][innergroup][row][eActual][column] += PivotCell::stockSplit(value); } } void PivotTable::createRow(const QString& outergroup, const ReportAccount& row, bool recursive) { DEBUG_ENTER(Q_FUNC_INFO); // Determine the inner group from the top-most parent account QString innergroup(row.topParentName()); if (! m_grid.contains(outergroup)) { DEBUG_OUTPUT(QString("Adding group [%1]").arg(outergroup)); m_grid[outergroup] = PivotOuterGroup(m_numColumns); } if (! m_grid[outergroup].contains(innergroup)) { DEBUG_OUTPUT(QString("Adding group [%1][%2]").arg(outergroup).arg(innergroup)); m_grid[outergroup][innergroup] = PivotInnerGroup(m_numColumns); } if (! m_grid[outergroup][innergroup].contains(row)) { DEBUG_OUTPUT(QString("Adding row [%1][%2][%3]").arg(outergroup).arg(innergroup).arg(row.debugName())); m_grid[outergroup][innergroup][row] = PivotGridRowSet(m_numColumns); if (recursive && !row.isTopLevel()) createRow(outergroup, row.parent(), recursive); } } int PivotTable::columnValue(const QDate& _date) const { if (m_config_f.isColumnsAreDays()) return (m_beginDate.daysTo(_date)); else return (_date.year() * 12 + _date.month()); } QDate PivotTable::columnDate(int column) const { if (m_config_f.isColumnsAreDays()) return m_beginDate.addDays(m_config_f.columnPitch() * column - m_startColumn); else return m_beginDate.addMonths(m_config_f.columnPitch() * column).addDays(-m_startColumn); } QString PivotTable::renderCSV() const { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); int pricePrecision = 0; int currencyPrecision = 0; int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); // // Report Title // QString result = QString("\"Report: %1\"\n").arg(m_config_f.name()); result += i18nc("Report date range", "%1 through %2\n", QLocale().toString(m_config_f.fromDate(), QLocale::ShortFormat), QLocale().toString(m_config_f.toDate(), QLocale::ShortFormat)); if (m_config_f.isConvertCurrency()) result += i18n("All currencies converted to %1\n", MyMoneyFile::instance()->baseCurrency().name()); else result += i18n("All values shown in %1 unless otherwise noted\n", MyMoneyFile::instance()->baseCurrency().name()); // // Table Header // result += i18n("Account"); int column = m_startColumn; while (column < m_numColumns) { result += QString(",%1").arg(QString(m_columnHeadings[column++])); if (m_rowTypeList.size() > 1) { QString separator; separator = separator.fill(',', m_rowTypeList.size() - 1); result += separator; } } //show total columns if (m_config_f.isShowingRowTotals()) result += QString(",%1").arg(i18nc("Total balance", "Total")); result += '\n'; // Row Type Header if (m_rowTypeList.size() > 1) { int column = m_startColumn; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString(",%1").arg(m_columnTypeHeaderList[i]); } } result += '\n'; } // // Outer groups // // iterate over outer groups PivotGrid::const_iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { // // Outer Group Header // if (!(m_config_f.isIncludingPrice() || m_config_f.isIncludingAveragePrice())) result += it_outergroup.key() + '\n'; // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { ReportAccount rowname = it_row.key(); // // Columns // QString rowdata; int column = m_startColumn; bool isUsed = false; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= it_row.value()[ m_rowTypeList[i] ][0].isUsed(); if (it_row.key().accountType() != MyMoneyAccount::Investment) { while (column < m_numColumns) { //show columns foreach (const auto rowType, m_rowTypeList) { if (rowType == ePrice) { if (pricePrecision == 0) { if (it_row.key().isInvest()) { pricePrecision = file->currency(it_row.key().currencyId()).pricePrecision(); precision = pricePrecision; } else precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); } else precision = pricePrecision; } else { if (currencyPrecision == 0) { if (it_row.key().isInvest()) // stock account isn't eveluated in currency, so take investment account instead currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().parent().fraction()); else currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().fraction()); precision = currencyPrecision; } else precision = currencyPrecision; } rowdata += QString(",\"%1\"").arg(it_row.value()[rowType][column].formatMoney(QString(), precision, false)); isUsed |= it_row.value()[rowType][column].isUsed(); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) rowdata += QString(",\"%1\"").arg((*it_row)[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } } else { for (auto i = 0; i < m_numColumns + m_rowTypeList.size(); ++i) rowdata.append(',');; } // // Row Header // if (!rowname.isClosed() || isUsed) { innergroupdata += "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); // if we don't convert the currencies to the base currency and the // current row contains a foreign currency, then we append the currency // to the name of the account if (!m_config_f.isConvertCurrency() && rowname.isForeignCurrency()) innergroupdata += QString(" (%1)").arg(rowname.currencyId()); innergroupdata += '\"'; if (isUsed) innergroupdata += rowdata; innergroupdata += '\n'; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; if (m_config_f.detailLevel() == MyMoneyReport::eDetailAll && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config_f.isConvertCurrency() && m_config_f.isShowingColumnTotals()) { // Start the TOTALS row finalRow = i18nc("Total balance", "Total"); isUsed = true; } else { ++rownum; finishrow = false; } } else { // Start the single INDIVIDUAL ACCOUNT row ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = "\"" + QString().fill(' ', rowname.hierarchyDepth() - 1) + rowname.name(); if (!m_config_f.isConvertCurrency() && rowname.isForeignCurrency()) finalRow += QString(" (%1)").arg(rowname.currencyId()); finalRow += "\""; } // Finish the row started above, unless told not to if (finishrow) { int column = m_startColumn; for (int i = 0; i < m_rowTypeList.size(); ++i) isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][0].isUsed(); while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) { isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(QString(), precision, false)); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) finalRow += QString(",\"%1\"").arg((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } finalRow += '\n'; } if (isUsed) { result += finalRow; ++rownum; } ++it_innergroup; } // // Outer Row Group Totals // if (m_config_f.isConvertCurrency() && m_config_f.isShowingColumnTotals()) { result += QString("%1 %2").arg(i18nc("Total balance", "Total")).arg(it_outergroup.key()); int column = m_startColumn; while (column < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ][column].formatMoney(QString(), precision, false)); column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } result += '\n'; } ++it_outergroup; } // // Report Totals // if (m_config_f.isConvertCurrency() && m_config_f.isShowingColumnTotals()) { result += i18n("Grand Total"); int totalcolumn = m_startColumn; while (totalcolumn < m_numColumns) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn].formatMoney(QString(), precision, false)); totalcolumn++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) result += QString(",\"%1\"").arg(m_grid.m_total[ m_rowTypeList[i] ].m_total.formatMoney(QString(), precision, false)); } result += '\n'; } return result; } QString PivotTable::renderBody() const { DEBUG_ENTER(Q_FUNC_INFO); MyMoneyFile* file = MyMoneyFile::instance(); int pricePrecision = 0; int currencyPrecision = 0; int precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); QString colspan = QString(" colspan=\"%1\"").arg(m_numColumns + 1 + (m_config_f.isShowingRowTotals() ? 1 : 0)); // // Report Title // QString result = QString("

%1

\n").arg(m_config_f.name()); //actual dates of the report result += QString("
"); result += i18nc("Report date range", "%1 through %2", QLocale().toString(m_config_f.fromDate(), QLocale::ShortFormat), QLocale().toString(m_config_f.toDate(), QLocale::ShortFormat)); result += QString("
\n"); result += QString("
 
\n"); //currency conversion message result += QString("
"); if (m_config_f.isConvertCurrency()) result += i18n("All currencies converted to %1", MyMoneyFile::instance()->baseCurrency().name()); else result += i18n("All values shown in %1 unless otherwise noted", MyMoneyFile::instance()->baseCurrency().name()); result += QString("
\n"); result += QString("
 
\n"); // setup a leftborder for better readability of budget vs actual reports QString leftborder; if (m_rowTypeList.size() > 1) leftborder = " class=\"leftborder\""; // // Table Header // result += QString("\n\n\n" "\n").arg(i18n("Account")); QString headerspan; int span = m_rowTypeList.size(); headerspan = QString(" colspan=\"%1\"").arg(span); int column = m_startColumn; while (column < m_numColumns) result += QString("%2").arg(headerspan, QString(m_columnHeadings[column++]).replace(QRegExp(" "), "
")); if (m_config_f.isShowingRowTotals()) result += QString("%2").arg(headerspan).arg(i18nc("Total balance", "Total")); result += "
\n"; // // Header for multiple columns // if (span > 1) { result += ""; int column = m_startColumn; while (column < m_numColumns) { QString lb; if (column != m_startColumn) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? lb : QString()); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(m_columnTypeHeaderList[i]) .arg(i == 0 ? leftborder : QString()); } } result += ""; } // Skip the body of the report if the report only calls for totals to be shown if (m_config_f.detailLevel() != MyMoneyReport::eDetailTotal) { // // Outer groups // // Need to sort the outergroups. They can't always be sorted by name. So we create a list of // map iterators, and sort that. Then we'll iterate through the map iterators and use those as // before. // // I hope this doesn't bog the performance of reports, given that we're copying the entire report // data. If this is a perf hit, we could change to storing outergroup pointers, I think. QList outergroups; PivotGrid::const_iterator it_outergroup_map = m_grid.begin(); while (it_outergroup_map != m_grid.end()) { outergroups.push_back(it_outergroup_map.value()); // copy the name into the outergroup, because we will now lose any association with // the map iterator outergroups.back().m_displayName = it_outergroup_map.key(); ++it_outergroup_map; } qSort(outergroups.begin(), outergroups.end()); QList::const_iterator it_outergroup = outergroups.constBegin(); while (it_outergroup != outergroups.constEnd()) { // // Outer Group Header // if (!(m_config_f.isIncludingPrice() || m_config_f.isIncludingAveragePrice())) result += QString("\n").arg(colspan).arg((*it_outergroup).m_displayName); // Skip the inner groups if the report only calls for outer group totals to be shown if (m_config_f.detailLevel() != MyMoneyReport::eDetailGroup) { // // Inner Groups // PivotOuterGroup::const_iterator it_innergroup = (*it_outergroup).begin(); int rownum = 0; while (it_innergroup != (*it_outergroup).end()) { // // Rows // QString innergroupdata; PivotInnerGroup::const_iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { // // Columns // QString rowdata; int column = m_startColumn; pricePrecision = 0; // new row => new account => new precision currencyPrecision = 0; bool isUsed = it_row.value()[eActual][0].isUsed(); if (it_row.key().accountType() != MyMoneyAccount::Investment) { while (column < m_numColumns) { QString lb; if (column > m_startColumn) lb = leftborder; foreach (const auto rowType, m_rowTypeList) { if (rowType == ePrice) { if (pricePrecision == 0) { if (it_row.key().isInvest()) { pricePrecision = file->currency(it_row.key().currencyId()).pricePrecision(); precision = pricePrecision; } else precision = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); } else precision = pricePrecision; } else { if (currencyPrecision == 0) { if (it_row.key().isInvest()) // stock account isn't eveluated in currency, so take investment account instead currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().parent().fraction()); else currencyPrecision = MyMoneyMoney::denomToPrec(it_row.key().fraction()); precision = currencyPrecision; } else precision = currencyPrecision; } rowdata += QString("%1") .arg(coloredAmount(it_row.value()[rowType][column], QString(), precision)) .arg(lb); lb.clear(); isUsed |= it_row.value()[rowType][column].isUsed(); } ++column; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { rowdata += QString("%1") .arg(coloredAmount(it_row.value()[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } } else rowdata += QString(QLatin1Literal("")).arg(m_numColumns + m_rowTypeList.size()); // // Row Header // ReportAccount rowname = it_row.key(); // don't show closed accounts if they have not been used if (!rowname.isClosed() || isUsed) { innergroupdata += QString("%5%6") .arg(rownum & 0x01 ? "even" : "odd") .arg(rowname.isTopLevel() ? " id=\"topparent\"" : "") .arg("") //.arg((*it_row).m_total.isZero() ? colspan : "") // colspan the distance if this row will be blank .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config_f.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); // Don't print this row if it's going to be all zeros // TODO: Uncomment this, and deal with the case where the data // is zero, but the budget is non-zero //if ( !(*it_row).m_total.isZero() ) innergroupdata += rowdata; innergroupdata += "\n"; } ++it_row; } // // Inner Row Group Totals // bool finishrow = true; QString finalRow; bool isUsed = false; if (m_config_f.detailLevel() == MyMoneyReport::eDetailAll && ((*it_innergroup).size() > 1)) { // Print the individual rows result += innergroupdata; if (m_config_f.isConvertCurrency() && m_config_f.isShowingColumnTotals()) { // Start the TOTALS row finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") .arg(i18nc("Total balance", "Total")); // don't suppress display of totals isUsed = true; } else { finishrow = false; ++rownum; } } else { // Start the single INDIVIDUAL ACCOUNT row // FIXME: There is a bit of a bug here with class=leftX. There's only a finite number // of classes I can define in the .CSS file, and the user can theoretically nest deeper. // The right solution is to use style=Xem, and calculate X. Let's see if anyone complains // first :) Also applies to the row header case above. // FIXED: I found it in one of my reports and changed it to the proposed method. // This works for me (ipwizard) ReportAccount rowname = (*it_innergroup).begin().key(); isUsed |= !rowname.isClosed(); finalRow = QString("") .arg(rownum & 0x01 ? "even" : "odd") .arg(m_config_f.detailLevel() == MyMoneyReport::eDetailAll ? "id=\"solo\"" : "") .arg(rowname.hierarchyDepth() - 1) .arg(rowname.name().replace(QRegExp(" "), " ")) .arg((m_config_f.isConvertCurrency() || !rowname.isForeignCurrency()) ? QString() : QString(" (%1)").arg(rowname.currency().id())); } // Finish the row started above, unless told not to if (finishrow) { int column = m_startColumn; isUsed |= (*it_innergroup).m_total[eActual][0].isUsed(); while (column < m_numColumns) { QString lb; if (column != m_startColumn) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ][column], QString(), precision)) .arg(i == 0 ? lb : QString()); isUsed |= (*it_innergroup).m_total[ m_rowTypeList[i] ][column].isUsed(); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { finalRow += QString("%1") .arg(coloredAmount((*it_innergroup).m_total[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } finalRow += "\n"; if (isUsed) { result += finalRow; ++rownum; } } ++it_innergroup; } // end while iterating on the inner groups } // end if detail level is not "group" // // Outer Row Group Totals // if (m_config_f.isConvertCurrency() && m_config_f.isShowingColumnTotals()) { result += QString("").arg(i18nc("Total balance", "Total")).arg((*it_outergroup).m_displayName); int column = m_startColumn; while (column < m_numColumns) { QString lb; if (column != m_startColumn) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ][column], QString(), precision)) .arg(i == 0 ? lb : QString()); } column++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount((*it_outergroup).m_total[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } ++it_outergroup; } // end while iterating on the outergroups } // end if detail level is not "total" // // Report Totals // if (m_config_f.isConvertCurrency() && m_config_f.isShowingColumnTotals()) { result += QString("\n"); result += QString("").arg(i18n("Grand Total")); int totalcolumn = m_startColumn; while (totalcolumn < m_numColumns) { QString lb; if (totalcolumn != m_startColumn) lb = leftborder; for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ][totalcolumn], QString(), precision)) .arg(i == 0 ? lb : QString()); } totalcolumn++; } if (m_config_f.isShowingRowTotals()) { for (int i = 0; i < m_rowTypeList.size(); ++i) { result += QString("%1") .arg(coloredAmount(m_grid.m_total[ m_rowTypeList[i] ].m_total, QString(), precision)) .arg(i == 0 ? leftborder : QString()); } } result += "\n"; } result += QString("\n"); result += QString("\n"); result += "
%1
%2
  %2
%5%6
%1 %2
 
%1
 
 
\n"; return result; } void PivotTable::dump(const QString& file, const QString& /* context */) const { QFile g(file); g.open(QIODevice::WriteOnly); QTextStream(&g) << renderBody(); g.close(); } void PivotTable::drawChart(KReportChartView& chartView) const { chartView.drawPivotChart(m_grid, m_config_f, m_numColumns, m_columnHeadings, m_rowTypeList, m_columnTypeHeaderList); } QString PivotTable::coloredAmount(const MyMoneyMoney& amount, const QString& currencySymbol, int prec) const { QString result; if (amount.isNegative()) result += QString("") .arg(KMyMoneyGlobalSettings::listNegativeValueColor().red()) .arg(KMyMoneyGlobalSettings::listNegativeValueColor().green()) .arg(KMyMoneyGlobalSettings::listNegativeValueColor().blue()); result += amount.formatMoney(currencySymbol, prec); if (amount.isNegative()) result += QString(""); return result; } void PivotTable::calculateBudgetDiff() { PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; switch (it_row.key().accountGroup()) { case MyMoneyAccount::Income: case MyMoneyAccount::Asset: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = it_row.value()[eActual][column] - it_row.value()[eBudget][column]; ++column; } break; case MyMoneyAccount::Expense: case MyMoneyAccount::Liability: while (column < m_numColumns) { it_row.value()[eBudgetDiff][column] = it_row.value()[eBudget][column] - it_row.value()[eActual][column]; ++column; } break; default: break; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::calculateForecast() { //setup forecast MyMoneyForecast forecast = KMyMoneyGlobalSettings::forecast(); //since this is a net worth forecast we want to include all account even those that are not in use forecast.setIncludeUnusedAccounts(true); //setup forecast dates if (m_endDate > QDate::currentDate()) { forecast.setForecastEndDate(m_endDate); forecast.setForecastStartDate(QDate::currentDate()); forecast.setForecastDays(QDate::currentDate().daysTo(m_endDate)); } else { forecast.setForecastStartDate(m_beginDate); forecast.setForecastEndDate(m_endDate); forecast.setForecastDays(m_beginDate.daysTo(m_endDate) + 1); } //adjust history dates if beginning date is before today if (m_beginDate < QDate::currentDate()) { forecast.setHistoryEndDate(m_beginDate.addDays(-1)); forecast.setHistoryStartDate(forecast.historyEndDate().addDays(-forecast.accountsCycle()*forecast.forecastCycles())); } //run forecast if (m_config_f.rowType() == MyMoneyReport::eAssetLiability) { //asset and liability forecast.doForecast(); } else { //income and expenses MyMoneyBudget budget; forecast.createBudget(budget, m_beginDate.addYears(-1), m_beginDate.addDays(-1), m_beginDate, m_endDate, false); } //go through the data and add forecast PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; QDate forecastDate = m_beginDate; //check whether columns are days or months if (m_config_f.isColumnsAreDays()) { while (column < m_numColumns) { it_row.value()[eForecast][column] = forecast.forecastBalance(it_row.key(), forecastDate); forecastDate = forecastDate.addDays(1); ++column; } } else { //if columns are months while (column < m_numColumns) { // the forecast balance is on the first day of the month see MyMoneyForecast::calculateScheduledMonthlyBalances() forecastDate = QDate(forecastDate.year(), forecastDate.month(), 1); //check that forecastDate is not over ending date if (forecastDate > m_endDate) forecastDate = m_endDate; //get forecast balance and set the corresponding column it_row.value()[eForecast][column] = forecast.forecastBalance(it_row.key(), forecastDate); forecastDate = forecastDate.addMonths(1); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::loadRowTypeList() { if ((m_config_f.isIncludingBudgetActuals()) || (!m_config_f.hasBudget() && !m_config_f.isIncludingForecast() && !m_config_f.isIncludingMovingAverage() && !m_config_f.isIncludingPrice() && !m_config_f.isIncludingAveragePrice()) ) { m_rowTypeList.append(eActual); m_columnTypeHeaderList.append(i18n("Actual")); } if (m_config_f.hasBudget()) { m_rowTypeList.append(eBudget); m_columnTypeHeaderList.append(i18n("Budget")); } if (m_config_f.isIncludingBudgetActuals()) { m_rowTypeList.append(eBudgetDiff); m_columnTypeHeaderList.append(i18n("Difference")); } if (m_config_f.isIncludingForecast()) { m_rowTypeList.append(eForecast); m_columnTypeHeaderList.append(i18n("Forecast")); } if (m_config_f.isIncludingMovingAverage()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average")); } if (m_config_f.isIncludingAveragePrice()) { m_rowTypeList.append(eAverage); m_columnTypeHeaderList.append(i18n("Moving Average Price")); } if (m_config_f.isIncludingPrice()) { m_rowTypeList.append(ePrice); m_columnTypeHeaderList.append(i18n("Price")); } } void PivotTable::calculateMovingAverage() { int delta = m_config_f.movingAverageDays() / 2; //go through the data and add the moving average PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; //check whether columns are days or months if (m_config_f.columnType() == MyMoneyReport::eDays) { while (column < m_numColumns) { MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageStart = columnDate(column).addDays(-delta); QDate averageEnd = columnDate(column).addDays(delta); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config_f.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } //calculate the average price MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); //get the actual value, multiply by the average price and save that value MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; it_row.value()[eAverage][column] = averageValue.convert(10000); ++column; } } else { //if columns are months while (column < m_numColumns) { QDate averageStart = columnDate(column); //set the right start date depending on the column type switch (m_config_f.columnType()) { case MyMoneyReport::eYears: { averageStart = QDate(columnDate(column).year(), 1, 1); break; } case MyMoneyReport::eBiMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::eQuarters: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1).addMonths(-1); break; } case MyMoneyReport::eMonths: { averageStart = QDate(columnDate(column).year(), columnDate(column).month(), 1); break; } case MyMoneyReport::eWeeks: { averageStart = columnDate(column).addDays(-columnDate(column).dayOfWeek() + 1); break; } default: break; } //gather the actual data and calculate the average MyMoneyMoney totalPrice = MyMoneyMoney(); QDate averageEnd = columnDate(column); for (QDate averageDate = averageStart; averageDate <= averageEnd; averageDate = averageDate.addDays(1)) { if (m_config_f.isConvertCurrency()) { totalPrice += it_row.key().deepCurrencyPrice(averageDate) * it_row.key().baseCurrencyPrice(averageDate); } else { totalPrice += it_row.key().deepCurrencyPrice(averageDate); } totalPrice = totalPrice.convert(10000); } MyMoneyMoney averagePrice = totalPrice / MyMoneyMoney((averageStart.daysTo(averageEnd) + 1), 1); MyMoneyMoney averageValue = it_row.value()[eActual][column] * averagePrice; //fill in the average it_row.value()[eAverage][column] = averageValue.convert(10000); ++column; } } ++it_row; } ++it_innergroup; } ++it_outergroup; } } void PivotTable::fillBasePriceUnit(ERowType rowType) { MyMoneyFile* file = MyMoneyFile::instance(); QString baseCurrencyId = file->baseCurrency().id(); //get the first price date for securities QMap securityDates = securityFirstPrice(); //go through the data PivotGrid::iterator it_outergroup = m_grid.begin(); while (it_outergroup != m_grid.end()) { PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { int column = m_startColumn; //if it is a base currency fill all the values bool firstPriceExists = false; if (it_row.key().currencyId() == baseCurrencyId) { firstPriceExists = true; } while (column < m_numColumns) { //check whether the date for that column is on or after the first price if (!firstPriceExists && securityDates.contains(it_row.key().currencyId()) && columnDate(column) >= securityDates.value(it_row.key().currencyId())) { firstPriceExists = true; } //only add the dummy value if there is a price for that date if (firstPriceExists) { //insert a unit of currency for each account it_row.value()[rowType][column] = MyMoneyMoney::ONE; } ++column; } ++it_row; } ++it_innergroup; } ++it_outergroup; } } QMap PivotTable::securityFirstPrice() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPriceList priceList = file->priceList(); QMap securityPriceDate; MyMoneyPriceList::const_iterator prices_it; for (prices_it = priceList.constBegin(); prices_it != priceList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //check the security in the from field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.from())) { if (securityPriceDate.value(firstPrice.from()) > firstPrice.date()) { securityPriceDate[firstPrice.from()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.from(), firstPrice.date()); } //check the security in the to field //if it is there, check if it is older if (securityPriceDate.contains(firstPrice.to())) { if (securityPriceDate.value(firstPrice.to()) > firstPrice.date()) { securityPriceDate[firstPrice.to()] = firstPrice.date(); } } else { securityPriceDate.insert(firstPrice.to(), firstPrice.date()); } } return securityPriceDate; } void PivotTable::includeInvestmentSubAccounts() { // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected QStringList accountList; if (m_config_f.accounts(accountList)) { if (!KMyMoneyGlobalSettings::expertMode()) { QStringList::const_iterator it_a, it_b; for (it_a = accountList.constBegin(); it_a != accountList.constEnd(); ++it_a) { MyMoneyAccount acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == MyMoneyAccount::Investment) { for (it_b = acc.accountList().constBegin(); it_b != acc.accountList().constEnd(); ++it_b) { if (!accountList.contains(*it_b)) { m_config_f.addAccount(*it_b); } } } } } } } int PivotTable::currentDateColumn() { //return -1 if the columns do not include the current date if (m_beginDate > QDate::currentDate() || m_endDate < QDate::currentDate()) { return -1; } //check the date of each column and return if it is the one for the current date //if columns are not days, return the one for the current month or year int column = m_startColumn; while (column < m_numColumns) { if (columnDate(column) >= QDate::currentDate()) { break; } column++; } //if there is no column matching the current date, return -1 if (column == m_numColumns) { column = -1; } return column; } } // namespace diff --git a/kmymoney/reports/tests/reportstestcommon.cpp b/kmymoney/reports/tests/reportstestcommon.cpp index f64d3b6d2..db614f5d1 100644 --- a/kmymoney/reports/tests/reportstestcommon.cpp +++ b/kmymoney/reports/tests/reportstestcommon.cpp @@ -1,474 +1,477 @@ /*************************************************************************** reportstestcommon.cpp ------------------- copyright : (C) 2002-2005 by Thomas Baumgart email : ipwizard@users.sourceforge.net Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kreportsview-test.h" #include #include #include #include "reportstestcommon.h" #include "pivottable.h" #include "querytable.h" using namespace reports; #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneystoragedump.h" #include "mymoneyreport.h" #include "mymoneystatement.h" #include "mymoneystoragexml.h" namespace test { const MyMoneyMoney moCheckingOpen(0.0); const MyMoneyMoney moCreditOpen(-0.0); const MyMoneyMoney moConverterCheckingOpen(1418.0); const MyMoneyMoney moConverterCreditOpen(-418.0); const MyMoneyMoney moZero(0.0); const MyMoneyMoney moSolo(234.12); const MyMoneyMoney moParent1(88.01); const MyMoneyMoney moParent2(133.22); const MyMoneyMoney moParent(moParent1 + moParent2); const MyMoneyMoney moChild(14.00); const MyMoneyMoney moThomas(5.11); const MyMoneyMoney moNoPayee(8944.70); QString acAsset; QString acLiability; QString acExpense; QString acIncome; QString acChecking; QString acCredit; QString acSolo; QString acParent; QString acChild; QString acSecondChild; QString acGrandChild1; QString acGrandChild2; QString acForeign; QString acCanChecking; QString acJpyChecking; QString acCanCash; QString acJpyCash; QString inBank; QString eqStock1; QString eqStock2; QString eqStock3; QString eqStock4; QString acInvestment; QString acStock1; QString acStock2; QString acStock3; QString acStock4; QString acDividends; QString acInterest; QString acFees; QString acTax; QString acCash; TransactionHelper::TransactionHelper(const QDate& _date, const QString& _action, MyMoneyMoney _value, const QString& _accountid, const QString& _categoryid, const QString& _currencyid, const QString& _payee) { // _currencyid is the currency of the transaction, and of the _value // both the account and category can have their own currency (athough the category having // a foreign currency is not yet supported by the program, the reports will still allow it, // so it must be tested.) MyMoneyFile* file = MyMoneyFile::instance(); bool haspayee = ! _payee.isEmpty(); MyMoneyPayee payeeTest = file->payeeByName(_payee); MyMoneyFileTransaction ft; setPostDate(_date); QString currencyid = _currencyid; if (currencyid.isEmpty()) currencyid = MyMoneyFile::instance()->baseCurrency().id(); setCommodity(currencyid); MyMoneyMoney price; MyMoneySplit splitLeft; if (haspayee) splitLeft.setPayeeId(payeeTest.id()); splitLeft.setAction(_action); splitLeft.setValue(-_value); price = MyMoneyFile::instance()->price(currencyid, file->account(_accountid).currencyId(), _date).rate(file->account(_accountid).currencyId()); splitLeft.setShares(-_value * price); splitLeft.setAccountId(_accountid); addSplit(splitLeft); MyMoneySplit splitRight; if (haspayee) splitRight.setPayeeId(payeeTest.id()); splitRight.setAction(_action); splitRight.setValue(_value); price = MyMoneyFile::instance()->price(currencyid, file->account(_categoryid).currencyId(), _date).rate(file->account(_categoryid).currencyId()); splitRight.setShares(_value * price); splitRight.setAccountId(_categoryid); addSplit(splitRight); MyMoneyFile::instance()->addTransaction(*this); ft.commit(); } TransactionHelper::~TransactionHelper() { MyMoneyFileTransaction ft; MyMoneyFile::instance()->removeTransaction(*this); ft.commit(); } void TransactionHelper::update() { MyMoneyFileTransaction ft; MyMoneyFile::instance()->modifyTransaction(*this); ft.commit(); } InvTransactionHelper::InvTransactionHelper(const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _price, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid, MyMoneyMoney _fee) { init(_date, _action, _shares, _price, _fee, _stockaccountid, _transferid, _categoryid); } void InvTransactionHelper::init(const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _price, MyMoneyMoney _fee, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount stockaccount = file->account(_stockaccountid); MyMoneyMoney value = _shares * _price; setPostDate(_date); setCommodity("USD"); MyMoneySplit s1; s1.setValue(value); s1.setAccountId(_stockaccountid); if (_action == MyMoneySplit::ActionReinvestDividend) { s1.setShares(_shares); s1.setAction(MyMoneySplit::ActionReinvestDividend); MyMoneySplit s2; s2.setAccountId(_categoryid); s2.setShares(-value); s2.setValue(-value); addSplit(s2); } else if (_action == MyMoneySplit::ActionDividend || _action == MyMoneySplit::ActionYield) { s1.setAccountId(_categoryid); s1.setShares(-value); s1.setValue(-value); // 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.setValue(MyMoneyMoney()); s2.setShares(MyMoneyMoney()); s2.setAction(_action); s2.setAccountId(_stockaccountid); addSplit(s2); MyMoneySplit s3; s3.setAccountId(_transferid); s3.setShares(value); s3.setValue(value); addSplit(s3); } else if (_action == MyMoneySplit::ActionBuyShares) { s1.setShares(_shares); s1.setValue(value); s1.setAction(MyMoneySplit::ActionBuyShares); MyMoneySplit s3; s3.setAccountId(_transferid); s3.setShares(-value - _fee); s3.setValue(-value - _fee); addSplit(s3); if (!_categoryid.isEmpty() && !_fee.isZero()) { MyMoneySplit s2; s2.setAccountId(_categoryid); s2.setValue(_fee); s2.setShares(_fee); addSplit(s2); } } addSplit(s1); //qDebug() << "created transaction, now adding..."; MyMoneyFileTransaction ft; file->addTransaction(*this); //qDebug() << "updating price..."; // update the price, while we're here QString stockid = stockaccount.currencyId(); QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price(stockid, basecurrencyid, _date, true); if (!price.isValid()) { MyMoneyPrice newprice(stockid, basecurrencyid, _date, _price, "test"); file->addPrice(newprice); } ft.commit(); //qDebug() << "successfully added " << id(); } -QString makeAccount(const QString& _name, MyMoneyAccount::accountTypeE _type, MyMoneyMoney _balance, const QDate& _open, const QString& _parent, QString _currency, bool _taxReport) +QString makeAccount(const QString& _name, MyMoneyAccount::accountTypeE _type, MyMoneyMoney _balance, const QDate& _open, const QString& _parent, QString _currency, bool _taxReport, bool _openingBalance) { MyMoneyAccount info; MyMoneyFileTransaction ft; info.setName(_name); info.setAccountType(_type); info.setOpeningDate(_open); if (!_currency.isEmpty()) info.setCurrencyId(_currency); else info.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); if (_taxReport) info.setValue("Tax", "Yes"); + if (_openingBalance) + info.setValue("OpeningBalanceAccount", "Yes"); + MyMoneyAccount parent = MyMoneyFile::instance()->account(_parent); MyMoneyFile::instance()->addAccount(info, parent); // create the opening balance transaction if any if (!_balance.isZero()) { MyMoneySecurity sec = MyMoneyFile::instance()->currency(info.currencyId()); MyMoneyFile::instance()->openingBalanceAccount(sec); MyMoneyFile::instance()->createOpeningBalanceTransaction(info, _balance); } ft.commit(); return info.id(); } void makePrice(const QString& _currencyid, const QDate& _date, const MyMoneyMoney& _price) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity curr = file->currency(_currencyid); MyMoneyPrice price(_currencyid, file->baseCurrency().id(), _date, _price, "test"); file->addPrice(price); ft.commit(); } QString makeEquity(const QString& _name, const QString& _symbol) { MyMoneySecurity equity; MyMoneyFileTransaction ft; equity.setName(_name); equity.setTradingSymbol(_symbol); equity.setSmallestAccountFraction(1000); equity.setSecurityType(MyMoneySecurity::SECURITY_NONE /*MyMoneyEquity::ETYPE_STOCK*/); MyMoneyFile::instance()->addSecurity(equity); ft.commit(); return equity.id(); } void makeEquityPrice(const QString& _id, const QDate& _date, const MyMoneyMoney& _price) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price(_id, basecurrencyid, _date, true); if (!price.isValid()) { MyMoneyPrice newprice(_id, basecurrencyid, _date, _price, "test"); file->addPrice(newprice); } ft.commit(); } void writeRCFtoXMLDoc(const MyMoneyReport& filter, QDomDocument* doc) { QDomProcessingInstruction instruct = doc->createProcessingInstruction(QString("xml"), QString("version=\"1.0\" encoding=\"utf-8\"")); doc->appendChild(instruct); QDomElement root = doc->createElement("KMYMONEY-FILE"); doc->appendChild(root); QDomElement reports = doc->createElement("REPORTS"); root.appendChild(reports); QDomElement report = doc->createElement("REPORT"); filter.write(report, doc); reports.appendChild(report); } void writeTabletoHTML(const PivotTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.html").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderBody(); g.close(); } void writeTabletoHTML(const QueryTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.html").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderBody(); g.close(); } void writeTabletoCSV(const PivotTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.csv").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderCSV(); g.close(); } void writeTabletoCSV(const QueryTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("qreport-%1%2.csv").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderCSV(); g.close(); } void writeRCFtoXML(const MyMoneyReport& filter, const QString& _filename) { static unsigned filenum = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.xml").arg(QString::number(filenum).rightJustified(2, '0')); ++filenum; } QDomDocument* doc = new QDomDocument("KMYMONEY-FILE"); Q_CHECK_PTR(doc); writeRCFtoXMLDoc(filter, doc); QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream stream(&g); stream.setCodec("UTF-8"); stream << doc->toString(); g.close(); delete doc; } bool readRCFfromXMLDoc(QList& list, QDomDocument* doc) { bool result = false; QDomElement rootElement = doc->documentElement(); if (!rootElement.isNull()) { QDomNode child = rootElement.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement childElement = child.toElement(); if ("REPORTS" == childElement.tagName()) { result = true; QDomNode subchild = child.firstChild(); while (!subchild.isNull() && subchild.isElement()) { MyMoneyReport filter; if (filter.read(subchild.toElement())) { list += filter; } subchild = subchild.nextSibling(); } } child = child.nextSibling(); } } return result; } bool readRCFfromXML(QList& list, const QString& filename) { int result = false; QFile f(filename); f.open(QIODevice::ReadOnly); QDomDocument* doc = new QDomDocument; if (doc->setContent(&f, false)) { result = readRCFfromXMLDoc(list, doc); } delete doc; return result; } void XMLandback(MyMoneyReport& filter) { // this function writes the filter to XML, and then reads // it back from XML overwriting the original filter; // in all cases, the result should be the same if the read // & write methods are working correctly. QDomDocument* doc = new QDomDocument("KMYMONEY-FILE"); Q_CHECK_PTR(doc); writeRCFtoXMLDoc(filter, doc); QList list; if (readRCFfromXMLDoc(list, doc) && !list.isEmpty()) filter = list[0]; else throw MYMONEYEXCEPTION("Failed to load report from XML"); delete doc; } MyMoneyMoney searchHTML(const QString& _html, const QString& _search) { QRegExp re(QString("%1[<>/td]*([\\-.0-9,]*)").arg(_search)); re.indexIn(_html); QString found = re.cap(1); found.remove(','); return MyMoneyMoney(found.toDouble()); } } // end namespace test diff --git a/kmymoney/reports/tests/reportstestcommon.h b/kmymoney/reports/tests/reportstestcommon.h index e110ec6e4..d4888b897 100644 --- a/kmymoney/reports/tests/reportstestcommon.h +++ b/kmymoney/reports/tests/reportstestcommon.h @@ -1,138 +1,138 @@ /*************************************************************************** reportstestcommon.h ------------------- copyright : (C) 2002-2005 by Thomas Baumgart email : ipwizard@users.sourceforge.net Ace Jones ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef REPORTSTESTCOMMON_H #define REPORTSTESTCOMMON_H #include class QDomDocument; #include "mymoneyaccount.h" #include "mymoneytransaction.h" #include "mymoneymoney.h" class MyMoneyReport; namespace reports { class PivotTable; class QueryTable; } namespace test { extern const MyMoneyMoney moCheckingOpen; extern const MyMoneyMoney moCreditOpen; extern const MyMoneyMoney moConverterCheckingOpen; extern const MyMoneyMoney moConverterCreditOpen; extern const MyMoneyMoney moZero; extern const MyMoneyMoney moSolo; extern const MyMoneyMoney moParent1; extern const MyMoneyMoney moParent2; extern const MyMoneyMoney moParent; extern const MyMoneyMoney moChild; extern const MyMoneyMoney moThomas; extern const MyMoneyMoney moNoPayee; extern QString acAsset; extern QString acLiability; extern QString acExpense; extern QString acIncome; extern QString acChecking; extern QString acCredit; extern QString acSolo; extern QString acParent; extern QString acChild; extern QString acSecondChild; extern QString acGrandChild1; extern QString acGrandChild2; extern QString acForeign; extern QString acCanChecking; extern QString acJpyChecking; extern QString acCanCash; extern QString acJpyCash; extern QString inBank; extern QString eqStock1; extern QString eqStock2; extern QString eqStock3; extern QString eqStock4; extern QString acInvestment; extern QString acStock1; extern QString acStock2; extern QString acStock3; extern QString acStock4; extern QString acDividends; extern QString acInterest; extern QString acFees; extern QString acTax; extern QString acCash; class TransactionHelper: public MyMoneyTransaction { private: QString m_id; public: TransactionHelper(const QDate& _date, const QString& _action, MyMoneyMoney _value, const QString& _accountid, const QString& _categoryid, const QString& _currencyid = QString(), const QString& _payee = "Test Payee"); ~TransactionHelper(); void update(); protected: TransactionHelper() {} }; class InvTransactionHelper: public TransactionHelper { public: InvTransactionHelper(const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _value, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid, MyMoneyMoney _fee = MyMoneyMoney()); void init(const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _price, MyMoneyMoney _fee, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid); }; class BudgetEntryHelper { private: QDate m_date; QString m_categoryid; bool m_applytosub; MyMoneyMoney m_amount; public: BudgetEntryHelper(): m_applytosub(false) {} BudgetEntryHelper(const QDate& _date, const QString& _categoryid, bool _applytosub, const MyMoneyMoney& _amount): m_date(_date), m_categoryid(_categoryid), m_applytosub(_applytosub), m_amount(_amount) {} }; class BudgetHelper: public QList { MyMoneyMoney budgetAmount(const QDate& _date, const QString& _categoryid, bool& _applytosub); }; -extern QString makeAccount(const QString& _name, MyMoneyAccount::accountTypeE _type, MyMoneyMoney _balance, const QDate& _open, const QString& _parent, QString _currency = "", bool _taxReport = false); +extern QString makeAccount(const QString& _name, MyMoneyAccount::accountTypeE _type, MyMoneyMoney _balance, const QDate& _open, const QString& _parent, QString _currency = "", bool _taxReport = false, bool _openingBalance = false); extern void makePrice(const QString& _currencyid, const QDate& _date, const MyMoneyMoney& _price); QString makeEquity(const QString& _name, const QString& _symbol); extern void makeEquityPrice(const QString& _id, const QDate& _date, const MyMoneyMoney& _price); extern void writeRCFtoXMLDoc(const MyMoneyReport& filter, QDomDocument* doc); extern void writeTabletoHTML(const reports::PivotTable& table, const QString& _filename = QString()); extern void writeTabletoHTML(const reports::QueryTable& table, const QString& _filename = QString()); extern void writeTabletoCSV(const reports::PivotTable& table, const QString& _filename = QString()); extern void writeTabletoCSV(const reports::QueryTable& table, const QString& _filename = QString()); extern void writeRCFtoXML(const MyMoneyReport& filter, const QString& _filename = QString()); extern bool readRCFfromXMLDoc(QList& list, QDomDocument* doc); extern bool readRCFfromXML(QList& list, const QString& filename); extern void XMLandback(MyMoneyReport& filter); extern MyMoneyMoney searchHTML(const QString& _html, const QString& _search); } // end namespace test #endif // REPORTSTESTCOMMON_H diff --git a/kmymoney/templates/de_DE/skr03.kmt b/kmymoney/templates/de_DE/skr03.kmt index 8ab66033e..e89b870ad 100644 --- a/kmymoney/templates/de_DE/skr03.kmt +++ b/kmymoney/templates/de_DE/skr03.kmt @@ -1,141 +1,143 @@ Kontenrahmen SKR03 Standardkontenrahmen SKR03 Beta Version des Kontenrahmes SKR03 zum Erweitern und Umstrukturieren. Der Kontenrahmen sollte den eigenen Bedürfnissen angepasst werden was Struktur und Kontenbezeichnungen angeht. WICHTIG!: Die Privatkonten fließen nicht in die Berechnung des Berichtes Bilanz, sind jedoch im Bericht Bilanz aufgeführt. Die im Bericht Bilanz aufgeführte Passiva "Gewinnrücklagen" gibt den Saldo der GuV aus. Die Anlage dieses Kontenrahmens wurde von der Firma LiHAS - Linuxhaus Stuttgart - unterstützt. - + + + diff --git a/kmymoney/templates/de_DE/skr04.kmt b/kmymoney/templates/de_DE/skr04.kmt index d3b963608..029605185 100644 --- a/kmymoney/templates/de_DE/skr04.kmt +++ b/kmymoney/templates/de_DE/skr04.kmt @@ -1,1383 +1,1385 @@ Kontenrahmen SKR04 Standardkontenrahmen SKR04, Stand 2005 BETA-Version eines Kontenrahmes SKR04 für 2005. Mehr Informationen unter http://wiki.gnucash.org/wiki/De/SKR04 - + + + diff --git a/kmymoney/templates/kmt.dtd b/kmymoney/templates/kmt.dtd index 54528384e..8905ca4a2 100644 --- a/kmymoney/templates/kmt.dtd +++ b/kmymoney/templates/kmt.dtd @@ -1,34 +1,35 @@ diff --git a/kmymoney/views/kgloballedgerview.cpp b/kmymoney/views/kgloballedgerview.cpp index 8883b4d77..2b4a2f5e1 100644 --- a/kmymoney/views/kgloballedgerview.cpp +++ b/kmymoney/views/kgloballedgerview.cpp @@ -1,1603 +1,1604 @@ /*************************************************************************** kgloballedgerview.cpp - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 "kgloballedgerview.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kmymoneytitlelabel.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransaction.h" #include "kmymoneyglobalsettings.h" #include "registersearchline.h" #include "kfindtransactiondlg.h" #include "kmymoney.h" #include "scheduledtransaction.h" #include "models.h" class KGlobalLedgerView::Private { public: Private(); // used to store the id of an item and the id of an immeadiate unselected sibling void storeId(KMyMoneyRegister::RegisterItem *item, QString &id, QString &backupId) { if (item) { // the id of the item id = item->id(); // the id of the item's previous/next unselected item for (KMyMoneyRegister::RegisterItem *it = item->prevItem(); it != 0 && backupId.isEmpty(); it = it->prevItem()) { if (!it->isSelected()) { backupId = it->id(); } } // if we didn't found previous unselected items search trough the next items for (KMyMoneyRegister::RegisterItem *it = item->nextItem(); it != 0 && backupId.isEmpty(); it = it->nextItem()) { if (!it->isSelected()) { backupId = it->id(); } } } } // use to match an item by it's id or a backup id which has a lower precedence void matchItemById(KMyMoneyRegister::RegisterItem **item, KMyMoneyRegister::Transaction* t, QString &id, QString &backupId) { if (!backupId.isEmpty() && t->id() == backupId) *item = t; if (!id.isEmpty() && t->id() == id) { // we found the real thing there's no need for the backup anymore backupId.clear(); *item = t; } } MousePressFilter* m_mousePressFilter; KMyMoneyRegister::RegisterSearchLineWidget* m_registerSearchLine; QString m_reconciliationAccount; QDate m_reconciliationDate; MyMoneyMoney m_endingBalance; int m_precision; bool m_recursion; bool m_showDetails; KMyMoneyRegister::Action m_action; // models AccountNamesFilterProxyModel *m_filterProxyModel; // widgets KMyMoneyAccountCombo* m_accountComboBox; MyMoneyMoney m_totalBalance; bool m_balanceIsApproximated; }; MousePressFilter::MousePressFilter(QWidget* parent) : QObject(parent), m_lastMousePressEvent(0), m_filterActive(true) { } void MousePressFilter::addWidget(QWidget* w) { m_parents.append(w); } void MousePressFilter::setFilterActive(bool state) { m_filterActive = state; } bool MousePressFilter::isChildOf(QWidget* child, QWidget *parent) { while (child) { if (child == parent) return true; // If one of the ancestors is a KPassivePopup or a KDialog or a popup widget then // it's as if it is a child of our own because these widgets could // appear during transaction entry (message boxes, completer widgets) if (dynamic_cast(child) || dynamic_cast(child) || ((child->windowFlags() & Qt::Popup) && child != kmymoney)) return true; child = child->parentWidget(); } return false; } bool MousePressFilter::eventFilter(QObject* o, QEvent* e) { if (m_filterActive) { if (e->type() == QEvent::MouseButtonPress && !m_lastMousePressEvent) { QList::const_iterator it_w; for (it_w = m_parents.constBegin(); it_w != m_parents.constEnd(); ++it_w) { if (isChildOf((QWidget*)o, (*it_w))) { m_lastMousePressEvent = e; break; } } if (it_w == m_parents.constEnd()) { m_lastMousePressEvent = e; bool rc = false; emit mousePressedOnExternalWidget(rc); } } if (e->type() != QEvent::MouseButtonPress) { m_lastMousePressEvent = 0; } } return false; } KGlobalLedgerView::Private::Private() : m_mousePressFilter(0), m_registerSearchLine(0), m_recursion(false), m_showDetails(false), m_filterProxyModel(0), m_accountComboBox(0), m_balanceIsApproximated(false) { } QDate KGlobalLedgerView::m_lastPostDate; KGlobalLedgerView::KGlobalLedgerView(QWidget *parent, const char *name) : KMyMoneyViewBase(parent, name, i18n("Ledgers")), d(new Private), m_needReload(false), m_newAccountLoaded(true), m_inEditMode(false) { d->m_mousePressFilter = new MousePressFilter((QWidget*)this); d->m_action = KMyMoneyRegister::ActionNone; // the proxy filter model d->m_filterProxyModel = new AccountNamesFilterProxyModel(this); d->m_filterProxyModel->addAccountGroup(MyMoneyAccount::Asset); d->m_filterProxyModel->addAccountGroup(MyMoneyAccount::Liability); + d->m_filterProxyModel->addAccountGroup(MyMoneyAccount::Equity); d->m_filterProxyModel->setSourceModel(Models::instance()->accountsModel()); d->m_filterProxyModel->sort(0); // create the toolbar frame at the top of the view m_toolbarFrame = new QFrame(this); QHBoxLayout* toolbarLayout = new QHBoxLayout(m_toolbarFrame); toolbarLayout->setContentsMargins(0, 0, 0, 0); toolbarLayout->setSpacing(6); // the account selector widget d->m_accountComboBox = new KMyMoneyAccountCombo(d->m_filterProxyModel, m_toolbarFrame); toolbarLayout->addWidget(d->m_accountComboBox); layout()->addWidget(m_toolbarFrame); toolbarLayout->setStretchFactor(d->m_accountComboBox, 60); // create the register frame m_registerFrame = new QFrame(this); QVBoxLayout* registerFrameLayout = new QVBoxLayout(m_registerFrame); registerFrameLayout->setContentsMargins(0, 0, 0, 0); registerFrameLayout->setSpacing(0); layout()->addWidget(m_registerFrame); layout()->setStretchFactor(m_registerFrame, 2); m_register = new KMyMoneyRegister::Register(m_registerFrame); m_register->setUsedWithEditor(true); registerFrameLayout->addWidget(m_register); m_register->installEventFilter(this); connect(m_register, SIGNAL(openContextMenu()), this, SIGNAL(openContextMenu())); connect(m_register, SIGNAL(transactionsSelected(KMyMoneyRegister::SelectedTransactions)), this, SLOT(slotUpdateSummaryLine(KMyMoneyRegister::SelectedTransactions))); connect(m_register->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotSortOptions())); connect(m_register, SIGNAL(reconcileStateColumnClicked(KMyMoneyRegister::Transaction*)), this, SLOT(slotToggleTransactionMark(KMyMoneyRegister::Transaction*))); // insert search line widget d->m_registerSearchLine = new KMyMoneyRegister::RegisterSearchLineWidget(m_register, m_toolbarFrame); toolbarLayout->addWidget(d->m_registerSearchLine); toolbarLayout->setStretchFactor(d->m_registerSearchLine, 100); // create the summary frame m_summaryFrame = new QFrame(this); QHBoxLayout* summaryFrameLayout = new QHBoxLayout(m_summaryFrame); summaryFrameLayout->setContentsMargins(0, 0, 0, 0); summaryFrameLayout->setSpacing(0); m_leftSummaryLabel = new QLabel(m_summaryFrame); m_centerSummaryLabel = new QLabel(m_summaryFrame); m_rightSummaryLabel = new QLabel(m_summaryFrame); summaryFrameLayout->addWidget(m_leftSummaryLabel); QSpacerItem* spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_centerSummaryLabel); spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_rightSummaryLabel); layout()->addWidget(m_summaryFrame); // create the button frame m_buttonFrame = new QFrame(this); QVBoxLayout* buttonLayout = new QVBoxLayout(m_buttonFrame); buttonLayout->setContentsMargins(0, 0, 0, 0); buttonLayout->setSpacing(0); layout()->addWidget(m_buttonFrame); m_buttonbar = new KToolBar(m_buttonFrame, 0, true); m_buttonbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); buttonLayout->addWidget(m_buttonbar); m_buttonbar->addAction(kmymoney->action("transaction_new")); m_buttonbar->addAction(kmymoney->action("transaction_delete")); m_buttonbar->addAction(kmymoney->action("transaction_edit")); m_buttonbar->addAction(kmymoney->action("transaction_enter")); m_buttonbar->addAction(kmymoney->action("transaction_cancel")); m_buttonbar->addAction(kmymoney->action("transaction_accept")); m_buttonbar->addAction(kmymoney->action("transaction_match")); // create the transaction form frame m_formFrame = new QFrame(this); QVBoxLayout* frameLayout = new QVBoxLayout(m_formFrame); frameLayout->setContentsMargins(5, 5, 5, 5); frameLayout->setSpacing(0); m_form = new KMyMoneyTransactionForm::TransactionForm(m_formFrame); frameLayout->addWidget(m_form->tabBar(m_formFrame)); frameLayout->addWidget(m_form); m_formFrame->setFrameShape(QFrame::Panel); m_formFrame->setFrameShadow(QFrame::Raised); layout()->addWidget(m_formFrame); connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotLoadView())); connect(m_register, SIGNAL(focusChanged(KMyMoneyRegister::Transaction*)), m_form, SLOT(slotSetTransaction(KMyMoneyRegister::Transaction*))); connect(m_register, SIGNAL(focusChanged()), kmymoney, SLOT(slotUpdateActions())); connect(d->m_accountComboBox, SIGNAL(accountSelected(QString)), this, SLOT(slotSelectAccount(QString))); connect(m_register, SIGNAL(transactionsSelected(KMyMoneyRegister::SelectedTransactions)), this, SIGNAL(transactionsSelected(KMyMoneyRegister::SelectedTransactions))); connect(m_register, SIGNAL(editTransaction()), this, SIGNAL(startEdit())); connect(m_register, SIGNAL(emptyItemSelected()), this, SLOT(slotNewTransaction())); connect(m_register, SIGNAL(aboutToSelectItem(KMyMoneyRegister::RegisterItem*,bool&)), this, SLOT(slotAboutToSelectItem(KMyMoneyRegister::RegisterItem*,bool&))); // TODO: port to KF5 - the current implementation is no longer working //connect(d->m_mousePressFilter, SIGNAL(mousePressedOnExternalWidget(bool&)), this, SIGNAL(cancelOrEndEdit(bool&))); connect(m_form, SIGNAL(newTransaction(KMyMoneyRegister::Action)), this, SLOT(slotNewTransaction(KMyMoneyRegister::Action))); // setup mouse press filter d->m_mousePressFilter->addWidget(m_formFrame); d->m_mousePressFilter->addWidget(m_buttonFrame); d->m_mousePressFilter->addWidget(m_summaryFrame); d->m_mousePressFilter->addWidget(m_registerFrame); m_tooltipPosn = QPoint(); } KGlobalLedgerView::~KGlobalLedgerView() { delete d; } void KGlobalLedgerView::slotAboutToSelectItem(KMyMoneyRegister::RegisterItem* item, bool& okToSelect) { Q_UNUSED(item); emit cancelOrEndEdit(okToSelect); } void KGlobalLedgerView::slotLoadView() { m_needReload = true; if (isVisible()) { if (!m_inEditMode) { setUpdatesEnabled(false); loadView(); setUpdatesEnabled(true); m_needReload = false; // force a new account if the current one is empty m_newAccountLoaded = m_account.id().isEmpty(); } } } void KGlobalLedgerView::clear() { // clear current register contents m_register->clear(); // setup header font QFont font = KMyMoneyGlobalSettings::listHeaderFont(); QFontMetrics fm(font); int height = fm.lineSpacing() + 6; m_register->horizontalHeader()->setMinimumHeight(height); m_register->horizontalHeader()->setMaximumHeight(height); m_register->horizontalHeader()->setFont(font); // setup cell font font = KMyMoneyGlobalSettings::listCellFont(); m_register->setFont(font); // clear the form m_form->clear(); // the selected transactions list m_transactionList.clear(); // and the selected account in the combo box d->m_accountComboBox->setSelected(QString()); // fraction defaults to two digits d->m_precision = 2; } void KGlobalLedgerView::loadView() { MYMONEYTRACER(tracer); // setup form visibility m_formFrame->setVisible(KMyMoneyGlobalSettings::transactionForm()); // no account selected emit accountSelected(MyMoneyAccount()); // no transaction selected KMyMoneyRegister::SelectedTransactions list; emit transactionsSelected(list); QMap isSelected; QString focusItemId; QString backUpFocusItemId; // in case the focus item is removed QString anchorItemId; QString backUpAnchorItemId; // in case the anchor item is removed if (!m_newAccountLoaded) { // remember the current selected transactions KMyMoneyRegister::RegisterItem* item = m_register->firstItem(); for (; item; item = item->nextItem()) { if (item->isSelected()) { isSelected[item->id()] = true; } } // remember the item that has the focus d->storeId(m_register->focusItem(), focusItemId, backUpFocusItemId); // and the one that has the selection anchor d->storeId(m_register->anchorItem(), anchorItemId, backUpAnchorItemId); } else { d->m_registerSearchLine->searchLine()->reset(); } // clear the current contents ... clear(); // ... load the combobox widget and select current account ... loadAccounts(); // ... setup the register columns ... m_register->setupRegister(m_account); // ... setup the form ... m_form->setupForm(m_account); if (m_account.id().isEmpty()) { // if we don't have an account we bail out setEnabled(false); return; } setEnabled(true); m_register->setUpdatesEnabled(false); // ... and recreate it KMyMoneyRegister::RegisterItem* focusItem = 0; KMyMoneyRegister::RegisterItem* anchorItem = 0; QMap actBalance, clearedBalance, futureBalance; QMap::iterator it_b; try { // setup the filter to select the transactions we want to display // and update the sort order QString sortOrder; QString key; QDate reconciliationDate = d->m_reconciliationDate; MyMoneyTransactionFilter filter(m_account.id()); // if it's an investment account, we also take care of // the sub-accounts (stock accounts) if (m_account.accountType() == MyMoneyAccount::Investment) filter.addAccount(m_account.accountList()); if (isReconciliationAccount()) { key = "kmm-sort-reconcile"; sortOrder = KMyMoneyGlobalSettings::sortReconcileView(); filter.addState(MyMoneyTransactionFilter::notReconciled); filter.addState(MyMoneyTransactionFilter::cleared); } else { filter.setDateFilter(KMyMoneyGlobalSettings::startDate().date(), QDate()); key = "kmm-sort-std"; sortOrder = KMyMoneyGlobalSettings::sortNormalView(); if (KMyMoneyGlobalSettings::hideReconciledTransactions() && !m_account.isIncomeExpense()) { filter.addState(MyMoneyTransactionFilter::notReconciled); filter.addState(MyMoneyTransactionFilter::cleared); } } filter.setReportAllSplits(true); // check if we have an account override of the sort order if (!m_account.value(key).isEmpty()) sortOrder = m_account.value(key); // setup sort order m_register->setSortOrder(sortOrder); // retrieve the list from the engine MyMoneyFile::instance()->transactionList(m_transactionList, filter); kmymoney->slotStatusProgressBar(0, m_transactionList.count()); // create the elements for the register QList >::const_iterator it; QMapuniqueMap; int i = 0; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Transaction* t = KMyMoneyRegister::Register::transactionFactory(m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); actBalance[t->split().accountId()] = MyMoneyMoney(); kmymoney->slotStatusProgressBar(++i, 0); // if we're in reconciliation and the state is cleared, we // force the item to show in dimmed intensity to get a visual focus // on those items, that we need to work on if (isReconciliationAccount() && (*it).second.reconcileFlag() == MyMoneySplit::Cleared) { t->setReducedIntensity(true); } } // create dummy entries for the scheduled transactions if sorted by postdate int period = KMyMoneyGlobalSettings::schedulePreview(); if (m_register->primarySortKey() == KMyMoneyRegister::PostDateSort) { // show scheduled transactions which have a scheduled postdate // within the next 'period' days. In reconciliation mode, the // period starts on the statement date. QDate endDate = QDate::currentDate().addDays(period); if (isReconciliationAccount()) endDate = reconciliationDate.addDays(period); QList scheduleList = MyMoneyFile::instance()->scheduleList(m_account.id()); while (scheduleList.count() > 0) { MyMoneySchedule& s = scheduleList.first(); for (;;) { if (s.isFinished() || s.adjustedNextDueDate() > endDate) { break; } MyMoneyTransaction t(s.id(), KMyMoneyUtils::scheduledTransaction(s)); // if the transaction is scheduled and overdue, it can't // certainly be posted in the past. So we take today's date // as the alternative if (s.isOverdue()) { t.setPostDate(s.adjustedDate(QDate::currentDate(), s.weekendOption())); } else { t.setPostDate(s.adjustedNextDueDate()); } const QList& splits = t.splits(); QList::const_iterator it_s; for (it_s = splits.begin(); it_s != splits.end(); ++it_s) { if ((*it_s).accountId() == m_account.id()) { new KMyMoneyRegister::StdTransactionScheduled(m_register, t, *it_s, uniqueMap[t.id()]); } } // keep track of this payment locally (not in the engine) if (s.isOverdue()) { s.setLastPayment(QDate::currentDate()); } else { s.setLastPayment(s.nextDueDate()); } // if this is a one time schedule, we can bail out here as we're done if (s.occurrence() == MyMoneySchedule::OCCUR_ONCE) break; // for all others, we check if the next payment date is still 'in range' QDate nextDueDate = s.nextPayment(s.nextDueDate()); if (nextDueDate.isValid()) { s.setNextDueDate(nextDueDate); } else { break; } } scheduleList.pop_front(); } } // add the group markers m_register->addGroupMarkers(); // sort the transactions according to the sort setting m_register->sortItems(); // remove trailing and adjacent markers m_register->removeUnwantedGroupMarkers(); // add special markers for reconciliation now so that they do not get // removed by m_register->removeUnwantedGroupMarkers(). Needs resorting // of items but that's ok. KMyMoneyRegister::StatementGroupMarker* statement = 0; KMyMoneyRegister::StatementGroupMarker* dStatement = 0; KMyMoneyRegister::StatementGroupMarker* pStatement = 0; if (isReconciliationAccount()) { switch (m_register->primarySortKey()) { case KMyMoneyRegister::PostDateSort: statement = new KMyMoneyRegister::StatementGroupMarker(m_register, KMyMoneyRegister::Deposit, reconciliationDate, i18n("Statement Details")); m_register->sortItems(); break; case KMyMoneyRegister::TypeSort: dStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, KMyMoneyRegister::Deposit, reconciliationDate, i18n("Statement Deposit Details")); pStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, KMyMoneyRegister::Payment, reconciliationDate, i18n("Statement Payment Details")); m_register->sortItems(); break; default: break; } } // we need at least the balance for the account we currently show actBalance[m_account.id()] = MyMoneyMoney(); if (m_account.accountType() == MyMoneyAccount::Investment) { QList::const_iterator it_a; for (it_a = m_account.accountList().begin(); it_a != m_account.accountList().end(); ++it_a) { actBalance[*it_a] = MyMoneyMoney(); } } // determine balances (actual, cleared). We do this by getting the actual // balance of all entered transactions from the engine and walk the list // of transactions backward. Also re-select a transaction if it was // selected before and setup the focus item. MyMoneyMoney factor(1, 1); if (m_account.accountGroup() == MyMoneyAccount::Liability || m_account.accountGroup() == MyMoneyAccount::Equity) factor = -factor; QMap deposits; QMap payments; QMap depositAmount; QMap paymentAmount; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyMoney balance = MyMoneyFile::instance()->balance(it_b.key()); balance = balance * factor; clearedBalance[it_b.key()] = futureBalance[it_b.key()] = (*it_b) = balance; deposits[it_b.key()] = payments[it_b.key()] = 0; depositAmount[it_b.key()] = MyMoneyMoney(); paymentAmount[it_b.key()] = MyMoneyMoney(); } tracer.printf("total balance of %s = %s", qPrintable(m_account.name()), qPrintable(actBalance[m_account.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_account.name()), qPrintable(futureBalance[m_account.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_account.name()), qPrintable(clearedBalance[m_account.id()].formatMoney("", 2))); KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); focusItem = 0; // take care of possibly trailing scheduled transactions (bump up the future balance) while (p) { if (p->isSelectable()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isScheduled()) { MyMoneyMoney balance = futureBalance[t->split().accountId()]; const MyMoneySplit& split = t->split(); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance = balance * split.shares(); } else { balance += split.shares() * factor; } futureBalance[split.accountId()] = balance; } else if (t && !focusItem) focusItem = p; } p = p->prevItem(); } p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (isSelected.contains(t->id())) t->setSelected(true); d->matchItemById(&focusItem, t, focusItemId, backUpFocusItemId); d->matchItemById(&anchorItem, t, anchorItemId, backUpAnchorItemId); const MyMoneySplit& split = t->split(); MyMoneyMoney balance = futureBalance[split.accountId()]; t->setBalance(balance); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance /= split.shares(); } else { balance -= split.shares() * factor; } if (!t->isScheduled()) { if (isReconciliationAccount() && t->transaction().postDate() <= reconciliationDate && split.reconcileFlag() == MyMoneySplit::Cleared) { if (split.shares().isNegative()) { payments[split.accountId()]++; paymentAmount[split.accountId()] += split.shares(); } else { deposits[split.accountId()]++; depositAmount[split.accountId()] += split.shares(); } } if (t->transaction().postDate() > QDate::currentDate()) { tracer.printf("Reducing actual balance by %s because %s/%s(%s) is in the future", qPrintable((split.shares() * factor).formatMoney("", 2)), qPrintable(t->transaction().id()), qPrintable(split.id()), qPrintable(t->transaction().postDate().toString(Qt::ISODate))); actBalance[split.accountId()] -= split.shares() * factor; } } futureBalance[split.accountId()] = balance; } p = p->prevItem(); } clearedBalance[m_account.id()] = MyMoneyFile::instance()->clearedBalance(m_account.id(), reconciliationDate); tracer.printf("total balance of %s = %s", qPrintable(m_account.name()), qPrintable(actBalance[m_account.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_account.name()), qPrintable(futureBalance[m_account.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_account.name()), qPrintable(clearedBalance[m_account.id()].formatMoney("", 2))); // update statement information if (statement) { const QString aboutDeposits = i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_account.id()], depositAmount[m_account.id()].abs().formatMoney(m_account.fraction())); const QString aboutPayments = i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_account.id()], paymentAmount[m_account.id()].abs().formatMoney(m_account.fraction())); statement->setText(i18nc("%1 is a string, e.g. 7 deposits; %2 is a string, e.g. 4 payments", "%1, %2", aboutDeposits, aboutPayments)); } if (pStatement) { pStatement->setText(i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_account.id()] , paymentAmount[m_account.id()].abs().formatMoney(m_account.fraction()))); } if (dStatement) { dStatement->setText(i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_account.id()] , depositAmount[m_account.id()].abs().formatMoney(m_account.fraction()))); } // add a last empty entry for new transactions // leave some information about the current account MyMoneySplit split; split.setReconcileFlag(MyMoneySplit::NotReconciled); // make sure to use the value specified in the option during reconciliation if (isReconciliationAccount()) split.setReconcileFlag(static_cast(KMyMoneyGlobalSettings::defaultReconciliationState())); KMyMoneyRegister::Register::transactionFactory(m_register, MyMoneyTransaction(), split, 0); m_register->updateRegister(true); if (focusItem) { // in case we have some selected items we just set the focus item // in other cases, we make the focusitem also the selected item if (anchorItem && (anchorItem != focusItem)) { m_register->setFocusItem(focusItem); m_register->setAnchorItem(anchorItem); } else m_register->selectItem(focusItem, true); } else { // just use the empty line at the end if nothing else exists in the ledger p = m_register->lastItem(); m_register->setFocusItem(p); m_register->selectItem(p); focusItem = p; } updateSummaryLine(actBalance, clearedBalance); kmymoney->slotStatusProgressBar(-1, -1); } catch (const MyMoneyException &) { m_account = MyMoneyAccount(); clear(); } d->m_showDetails = KMyMoneyGlobalSettings::showRegisterDetailed(); // and tell everyone what's selected emit accountSelected(m_account); KMyMoneyRegister::SelectedTransactions actualSelection(m_register); emit transactionsSelected(actualSelection); } void KGlobalLedgerView::updateSummaryLine(const QMap& actBalance, const QMap& clearedBalance) { MyMoneyFile* file = MyMoneyFile::instance(); m_leftSummaryLabel->show(); m_centerSummaryLabel->show(); m_rightSummaryLabel->show(); if (isReconciliationAccount()) { if (m_account.accountType() != MyMoneyAccount::Investment) { m_leftSummaryLabel->setText(i18n("Statement: %1", d->m_endingBalance.formatMoney("", d->m_precision))); m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_account.id()].formatMoney("", d->m_precision))); d->m_totalBalance = clearedBalance[m_account.id()] - d->m_endingBalance; } } else { // update summary line in normal mode QDate reconcileDate = m_account.lastReconciliationDate(); if (reconcileDate.isValid()) { m_leftSummaryLabel->setText(i18n("Last reconciled: %1", QLocale().toString(reconcileDate, QLocale::ShortFormat))); } else { m_leftSummaryLabel->setText(i18n("Never reconciled")); } QPalette palette = m_rightSummaryLabel->palette(); palette.setColor(m_rightSummaryLabel->foregroundRole(), m_leftSummaryLabel->palette().color(foregroundRole())); if (m_account.accountType() != MyMoneyAccount::Investment) { m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_account.id()].formatMoney("", d->m_precision))); d->m_totalBalance = actBalance[m_account.id()]; } else { m_centerSummaryLabel->hide(); MyMoneyMoney balance; MyMoneySecurity base = file->baseCurrency(); QMap::const_iterator it_b; // reset the approximated flag d->m_balanceIsApproximated = false; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyAccount stock = file->account(it_b.key()); QString currencyId = stock.currencyId(); MyMoneySecurity sec = file->security(currencyId); MyMoneyMoney rate(1, 1); if (stock.isInvest()) { currencyId = sec.tradingCurrency(); const MyMoneyPrice &priceInfo = file->price(sec.id(), currencyId); d->m_balanceIsApproximated |= !priceInfo.isValid(); rate = priceInfo.rate(sec.tradingCurrency()); } if (currencyId != base.id()) { const MyMoneyPrice &priceInfo = file->price(sec.tradingCurrency(), base.id()); d->m_balanceIsApproximated |= !priceInfo.isValid(); rate = (rate * priceInfo.rate(base.id())).convertPrecision(sec.pricePrecision()); } balance += ((*it_b) * rate).convert(base.smallestAccountFraction()); } d->m_totalBalance = balance; } m_rightSummaryLabel->setPalette(palette); } // determine the number of selected transactions KMyMoneyRegister::SelectedTransactions selection; m_register->selectedTransactions(selection); slotUpdateSummaryLine(selection); } void KGlobalLedgerView::slotUpdateSummaryLine(const KMyMoneyRegister::SelectedTransactions& selection) { if (selection.count() > 1) { MyMoneyMoney balance; foreach (const KMyMoneyRegister::SelectedTransaction& t, selection) { if (!t.isScheduled()) { balance += t.split().shares(); } } m_rightSummaryLabel->setText(QString("%1: %2").arg(QChar(0x2211), balance.formatMoney("", d->m_precision))); } else { if (isReconciliationAccount()) { m_rightSummaryLabel->setText(i18n("Difference: %1", d->m_totalBalance.formatMoney("", d->m_precision))); } else { if (m_account.accountType() != MyMoneyAccount::Investment) { m_rightSummaryLabel->setText(i18n("Balance: %1", d->m_totalBalance.formatMoney("", d->m_precision))); bool showNegative = d->m_totalBalance.isNegative(); if (m_account.accountGroup() == MyMoneyAccount::Liability && !d->m_totalBalance.isZero()) showNegative = !showNegative; if (showNegative) { QPalette palette = m_rightSummaryLabel->palette(); palette.setColor(m_rightSummaryLabel->foregroundRole(), KMyMoneyGlobalSettings::listNegativeValueColor()); m_rightSummaryLabel->setPalette(palette); } } else { m_rightSummaryLabel->setText(i18n("Investment value: %1%2", d->m_balanceIsApproximated ? "~" : "", d->m_totalBalance.formatMoney(MyMoneyFile::instance()->baseCurrency().tradingSymbol(), d->m_precision))); } } } } void KGlobalLedgerView::resizeEvent(QResizeEvent* ev) { m_register->resize(KMyMoneyRegister::DetailColumn); m_form->resize(KMyMoneyTransactionForm::ValueColumn1); KMyMoneyViewBase::resizeEvent(ev); } void KGlobalLedgerView::loadAccounts() { MyMoneyFile* file = MyMoneyFile::instance(); // check if the current account still exists and make it the // current account if (!m_account.id().isEmpty()) { try { m_account = file->account(m_account.id()); } catch (const MyMoneyException &) { m_account = MyMoneyAccount(); return; } } // TODO: check why the invalidate is needed here d->m_filterProxyModel->invalidate(); d->m_filterProxyModel->sort(AccountsModel::Account); d->m_filterProxyModel->setHideClosedAccounts(KMyMoneyGlobalSettings::hideClosedAccounts() && !kmymoney->toggleAction("view_show_all_accounts")->isChecked()); d->m_filterProxyModel->setHideEquityAccounts(!KMyMoneyGlobalSettings::expertMode()); d->m_accountComboBox->expandAll(); if (m_account.id().isEmpty()) { // find the first favorite account QModelIndexList list = Models::instance()->accountsModel()->match(Models::instance()->accountsModel()->index(0, 0), AccountsModel::AccountFavoriteRole, QVariant(true), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (list.count() > 0) { QVariant accountId = list.front().data(AccountsModel::AccountIdRole); if (accountId.isValid()) { m_account = file->account(accountId.toString()); } } if (m_account.id().isEmpty()) { // there are no favorite accounts find any account QModelIndexList list = Models::instance()->accountsModel()->match(Models::instance()->accountsModel()->index(0, 0), Qt::DisplayRole, QVariant(QString("*")), -1, Qt::MatchFlags(Qt::MatchWildcard | Qt::MatchRecursive)); for (QModelIndexList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { if (!it->parent().isValid()) continue; // skip the top level accounts QVariant accountId = (*it).data(AccountsModel::AccountIdRole); if (accountId.isValid()) { MyMoneyAccount a = file->account(accountId.toString()); if (!a.isInvest()) { m_account = a; break; } } } } } if (!m_account.id().isEmpty()) { d->m_accountComboBox->setSelected(m_account.id()); try { d->m_precision = MyMoneyMoney::denomToPrec(m_account.fraction()); } catch (const MyMoneyException &) { qDebug("Security %s for account %s not found", qPrintable(m_account.currencyId()), qPrintable(m_account.name())); d->m_precision = 2; } } } void KGlobalLedgerView::selectTransaction(const QString& id) { if (!id.isEmpty()) { KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->transaction().id() == id) { m_register->selectItem(t); m_register->ensureItemVisible(t); break; } } p = p->prevItem(); } } } void KGlobalLedgerView::slotSelectAllTransactions() { m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = m_register->firstItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->isVisible() && t->isSelectable() && !t->isScheduled() && !t->id().isEmpty()) { t->setSelected(true); } } p = p->nextItem(); } // this is here only to re-paint the items without selecting anything because the data (including the selection) is not really held in the model right now m_register->selectAll(); // inform everyone else about the selected items KMyMoneyRegister::SelectedTransactions list(m_register); emit transactionsSelected(list); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate, const MyMoneyMoney& endingBalance) { if (d->m_reconciliationAccount != acc.id()) { // make sure the account is selected if (!acc.id().isEmpty()) slotSelectAccount(acc.id()); d->m_reconciliationAccount = acc.id(); d->m_reconciliationDate = reconciliationDate; d->m_endingBalance = endingBalance; if (acc.accountGroup() == MyMoneyAccount::Liability) d->m_endingBalance = -endingBalance; m_newAccountLoaded = true; if (acc.id().isEmpty()) { m_buttonbar->removeAction(kmymoney->action("account_reconcile_postpone")); m_buttonbar->removeAction(kmymoney->action("account_reconcile_finish")); } else { m_buttonbar->addAction(kmymoney->action("account_reconcile_postpone")); m_buttonbar->addAction(kmymoney->action("account_reconcile_finish")); // when we start reconciliation, we need to reload the view // because no data has been changed. When postponing or finishing // reconciliation, the data change in the engine takes care of updateing // the view. slotLoadView(); } } } bool KGlobalLedgerView::isReconciliationAccount() const { return m_account.id() == d->m_reconciliationAccount; } bool KGlobalLedgerView::slotSelectAccount(const MyMoneyObject& obj) { if (typeid(obj) != typeid(MyMoneyAccount)) return false; if (d->m_recursion) return false; d->m_recursion = true; const MyMoneyAccount& acc = dynamic_cast(obj); bool rc = slotSelectAccount(acc.id()); d->m_recursion = false; return rc; } bool KGlobalLedgerView::slotSelectAccount(const QString& id, const QString& transactionId) { bool rc = true; if (!id.isEmpty()) { if (m_account.id() != id) { try { m_account = MyMoneyFile::instance()->account(id); // if a stock account is selected, we show the // the corresponding parent (investment) account if (m_account.isInvest()) { m_account = MyMoneyFile::instance()->account(m_account.parentAccountId()); } m_newAccountLoaded = true; slotLoadView(); } catch (const MyMoneyException &) { qDebug("Unable to retrieve account %s", qPrintable(id)); rc = false; } } else { // we need to refresh m_account.m_accountList, a child could have been deleted m_account = MyMoneyFile::instance()->account(id); emit accountSelected(m_account); } selectTransaction(transactionId); } return rc; } void KGlobalLedgerView::slotNewTransaction(KMyMoneyRegister::Action id) { if (!m_inEditMode) { d->m_action = id; emit newTransaction(); } } void KGlobalLedgerView::slotNewTransaction() { slotNewTransaction(KMyMoneyRegister::ActionNone); } void KGlobalLedgerView::setupDefaultAction() { switch (m_account.accountType()) { case MyMoneyAccount::Asset: case MyMoneyAccount::AssetLoan: case MyMoneyAccount::Savings: d->m_action = KMyMoneyRegister::ActionDeposit; break; default: d->m_action = KMyMoneyRegister::ActionWithdrawal; break; } } bool KGlobalLedgerView::selectEmptyTransaction() { bool rc = false; if (!m_inEditMode) { // in case we don't know the type of transaction to be created, // have at least one selected transaction and the id of // this transaction is not empty, we take it as template for the // transaction to be created KMyMoneyRegister::SelectedTransactions list(m_register); if ((d->m_action == KMyMoneyRegister::ActionNone) && (!list.isEmpty()) && (!list[0].transaction().id().isEmpty())) { // the new transaction to be created will have the same type // as the one that currently has the focus KMyMoneyRegister::Transaction* t = dynamic_cast(m_register->focusItem()); if (t) d->m_action = t->actionType(); m_register->clearSelection(); } // if we still don't have an idea which type of transaction // to create, we use the default. if (d->m_action == KMyMoneyRegister::ActionNone) { setupDefaultAction(); } m_register->selectItem(m_register->lastItem()); m_register->updateRegister(); rc = true; } return rc; } TransactionEditor* KGlobalLedgerView::startEdit(const KMyMoneyRegister::SelectedTransactions& list) { // we use the warnlevel to keep track, if we have to warn the // user that some or all splits have been reconciled or if the // user cannot modify the transaction if at least one split // has the status frozen. The following value are used: // // 0 - no sweat, user can modify // 1 - user should be warned that at least one split has been reconciled // already // 2 - user will be informed, that this transaction cannot be changed anymore int warnLevel = list.warnLevel(); Q_ASSERT(warnLevel < 2); // otherwise the edit action should not be enabled switch (warnLevel) { case 0: break; case 1: if (KMessageBox::warningContinueCancel(0, i18n( "At least one split of the selected transactions has been reconciled. " "Do you wish to continue to edit the transactions anyway?" ), i18n("Transaction already reconciled"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "EditReconciledTransaction") == KMessageBox::Cancel) { warnLevel = 2; } break; case 2: KMessageBox::sorry(0, i18n("At least one split of the selected transactions has been frozen. " "Editing the transactions is therefore prohibited."), i18n("Transaction already frozen")); break; case 3: KMessageBox::sorry(0, i18n("At least one split of the selected transaction references an account that has been closed. " "Editing the transactions is therefore prohibited."), i18n("Account closed")); break; } if (warnLevel > 1) return 0; TransactionEditor* editor = 0; KMyMoneyRegister::Transaction* item = dynamic_cast(m_register->focusItem()); if (item) { // in case the current focus item is not selected, we move the focus to the first selected transaction if (!item->isSelected()) { KMyMoneyRegister::RegisterItem* p; for (p = m_register->firstItem(); p; p = p->nextItem()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isSelected()) { m_register->setFocusItem(t); item = t; break; } } } // decide, if we edit in the register or in the form TransactionEditorContainer* parent; if (m_formFrame->isVisible()) parent = m_form; else { parent = m_register; } editor = item->createEditor(parent, list, m_lastPostDate); // check that we use the same transaction commodity in all selected transactions // if not, we need to update this in the editor's list. The user can also bail out // of this operation which means that we have to stop editing here. if (editor) { if (!editor->fixTransactionCommodity(m_account)) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { if (parent == m_register) { // make sure, the height of the table is correct m_register->updateRegister(KMyMoneyGlobalSettings::ledgerLens() | !KMyMoneyGlobalSettings::transactionForm()); } m_inEditMode = true; connect(editor, SIGNAL(transactionDataSufficient(bool)), kmymoney->action("transaction_enter"), SLOT(setEnabled(bool))); connect(editor, SIGNAL(returnPressed()), kmymoney->action("transaction_enter"), SLOT(trigger())); connect(editor, SIGNAL(escapePressed()), kmymoney->action("transaction_cancel"), SLOT(trigger())); connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), editor, SLOT(slotReloadEditWidgets())); connect(editor, SIGNAL(finishEdit(KMyMoneyRegister::SelectedTransactions)), this, SLOT(slotLeaveEditMode(KMyMoneyRegister::SelectedTransactions))); connect(editor, SIGNAL(objectCreation(bool)), d->m_mousePressFilter, SLOT(setFilterDeactive(bool))); connect(editor, SIGNAL(createPayee(QString,QString&)), kmymoney, SLOT(slotPayeeNew(QString,QString&))); connect(editor, SIGNAL(createTag(QString,QString&)), kmymoney, SLOT(slotTagNew(QString,QString&))); connect(editor, SIGNAL(createCategory(MyMoneyAccount&,MyMoneyAccount)), kmymoney, SLOT(slotCategoryNew(MyMoneyAccount&,MyMoneyAccount))); connect(editor, SIGNAL(createSecurity(MyMoneyAccount&,MyMoneyAccount)), kmymoney, SLOT(slotInvestmentNew(MyMoneyAccount&,MyMoneyAccount))); connect(editor, SIGNAL(assignNumber()), kmymoney, SLOT(slotTransactionAssignNumber())); connect(editor, SIGNAL(lastPostDateUsed(QDate)), this, SLOT(slotKeepPostDate(QDate))); // create the widgets, place them in the parent and load them with data // setup tab order m_tabOrderWidgets.clear(); editor->setup(m_tabOrderWidgets, m_account, d->m_action); Q_ASSERT(!m_tabOrderWidgets.isEmpty()); // install event filter in all taborder widgets QWidgetList::const_iterator it_w = m_tabOrderWidgets.constBegin(); for (; it_w != m_tabOrderWidgets.constEnd(); ++it_w) { (*it_w)->installEventFilter(this); } // Install a filter that checks if a mouse press happened outside // of one of our own widgets. qApp->installEventFilter(d->m_mousePressFilter); // Check if the editor has some preference on where to set the focus // If not, set the focus to the first widget in the tab order QWidget* focusWidget = editor->firstWidget(); if (!focusWidget) focusWidget = m_tabOrderWidgets.first(); // for some reason, this only works reliably if delayed a bit QTimer::singleShot(10, focusWidget, SLOT(setFocus())); // preset to 'I have no idea which type to create' for the next round. d->m_action = KMyMoneyRegister::ActionNone; } } return editor; } void KGlobalLedgerView::slotLeaveEditMode(const KMyMoneyRegister::SelectedTransactions& list) { m_inEditMode = false; qApp->removeEventFilter(d->m_mousePressFilter); // a possible focusOut event may have removed the focus, so we // install it back again. m_register->focusItem()->setFocus(true); // if we come back from editing a new item, we make sure that // we always select the very last known transaction entry no // matter if the transaction has been created or not. if (list.count() && list[0].transaction().id().isEmpty()) { // block signals to prevent some infinite loops that might occur here. m_register->blockSignals(true); m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); if (p && p->prevItem()) p = p->prevItem(); m_register->selectItem(p); m_register->updateRegister(true); m_register->blockSignals(false); // we need to update the form manually as sending signals was blocked KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) m_form->slotSetTransaction(t); } else { if (!KMyMoneySettings::transactionForm()) { // update the row height of the transactions because it might differ between viewing/editing mode when not using the transaction form m_register->blockSignals(true); m_register->updateRegister(true); m_register->blockSignals(false); } } if (m_needReload) slotLoadView(); m_register->setFocus(); } bool KGlobalLedgerView::focusNextPrevChild(bool next) { bool rc = false; // qDebug("KGlobalLedgerView::focusNextPrevChild(editmode=%s)", m_inEditMode ? "true" : "false"); if (m_inEditMode) { QWidget *w = 0; w = qApp->focusWidget(); // qDebug("w = %p", w); int currentWidgetIndex = m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", qPrintable(w->objectName())); w = w->parentWidget(); currentWidgetIndex = m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { // if(w) qDebug("tab order is at '%s'", qPrintable(w->objectName())); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = m_tabOrderWidgets[currentWidgetIndex]; // qDebug("currentWidgetIndex = %d, w = %p", currentWidgetIndex, w); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' (%p) as focus", qPrintable(w->objectName()), w); w->setFocus(); rc = true; } } } else rc = KMyMoneyViewBase::focusNextPrevChild(next); return rc; } void KGlobalLedgerView::showEvent(QShowEvent* event) { emit aboutToShow(); if (m_needReload) { if (!m_inEditMode) { setUpdatesEnabled(false); loadView(); setUpdatesEnabled(true); m_needReload = false; m_newAccountLoaded = false; } } else { emit accountSelected(m_account); KMyMoneyRegister::SelectedTransactions list(m_register); emit transactionsSelected(list); } // don't forget base class implementation KMyMoneyViewBase::showEvent(event); } bool KGlobalLedgerView::eventFilter(QObject* o, QEvent* e) { bool rc = false; // Need to capture mouse position here as QEvent::ToolTip is too slow m_tooltipPosn = QCursor::pos(); if (e->type() == QEvent::KeyPress) { if (m_inEditMode) { // qDebug("object = %s, key = %d", o->className(), k->key()); if (o == m_register) { // we hide all key press events from the register // while editing a transaction rc = true; } } } if (!rc) rc = KMyMoneyViewBase::eventFilter(o, e); return rc; } void KGlobalLedgerView::showTooltip(const QString msg) const { QToolTip::showText(m_tooltipPosn, msg); } void KGlobalLedgerView::slotSortOptions() { QPointer dlg = new KSortOptionDlg(this); QString key; QString sortOrder, def; if (isReconciliationAccount()) { key = "kmm-sort-reconcile"; def = KMyMoneyGlobalSettings::sortReconcileView(); } else { key = "kmm-sort-std"; def = KMyMoneyGlobalSettings::sortNormalView(); } // check if we have an account override of the sort order if (!m_account.value(key).isEmpty()) sortOrder = m_account.value(key); QString oldOrder = sortOrder; dlg->setSortOption(sortOrder, def); if (dlg->exec() == QDialog::Accepted) { if (dlg != 0) { sortOrder = dlg->sortOption(); if (sortOrder != oldOrder) { if (sortOrder.isEmpty()) { m_account.deletePair(key); } else { m_account.setValue(key, sortOrder); } MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(m_account); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to update sort order for account '%s': %s", qPrintable(m_account.name()), qPrintable(e.what())); } } } } delete dlg; } void KGlobalLedgerView::slotToggleTransactionMark(KMyMoneyRegister::Transaction* /* t */) { if (!m_inEditMode) { emit toggleReconciliationFlag(); } } void KGlobalLedgerView::slotKeepPostDate(const QDate& date) { m_lastPostDate = date; } bool KGlobalLedgerView::canCreateTransactions(QString& tooltip) const { bool rc = true; if (m_account.id().isEmpty()) { tooltip = i18n("Cannot create transactions when no account is selected."); rc = false; } if (m_account.accountGroup() == MyMoneyAccount::Income || m_account.accountGroup() == MyMoneyAccount::Expense) { tooltip = i18n("Cannot create transactions in the context of a category."); showTooltip(tooltip); rc = false; } if (m_account.isClosed()) { tooltip = i18n("Cannot create transactions in a closed account."); showTooltip(tooltip); rc = false; } return rc; } bool KGlobalLedgerView::canProcessTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { MyMoneyAccount acc; QString closedAccount; if (m_register->focusItem() == 0) return false; bool rc = true; if (list.warnLevel() == 3) { //Closed account somewhere KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = list.begin(); rc && it_t != list.end(); ++it_t) { QList splitList = (*it_t).transaction().splits(); QString id = splitList.first().accountId(); acc = MyMoneyFile::instance()->account(id); if (!acc.isClosed()) { //wrong split, try other id = splitList.last().accountId(); acc = MyMoneyFile::instance()->account(id); } closedAccount = acc.name(); break; } tooltip = i18n("Cannot process transactions in account %1, which is closed.", closedAccount); showTooltip(tooltip); return false; } if (!m_register->focusItem()->isSelected()) { tooltip = i18n("Cannot process transaction with focus if it is not selected."); showTooltip(tooltip); return false; } tooltip.clear(); return !list.isEmpty(); } bool KGlobalLedgerView::canModifyTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { return canProcessTransactions(list, tooltip) && list.canModify(); } bool KGlobalLedgerView::canDuplicateTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { return canProcessTransactions(list, tooltip) && list.canDuplicate(); } bool KGlobalLedgerView::canEditTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { // check if we can edit the list of transactions. We can edit, if // // a) no mix of standard and investment transactions exist // b) if a split transaction is selected, this is the only selection // c) none of the splits is frozen // d) the transaction having the current focus is selected // check for d) if (!canProcessTransactions(list, tooltip)) return false; // check for c) if (list.warnLevel() == 2) { tooltip = i18n("Cannot edit transactions with frozen splits."); showTooltip(tooltip); return false; } bool rc = true; int investmentTransactions = 0; int normalTransactions = 0; if (m_account.accountGroup() == MyMoneyAccount::Income || m_account.accountGroup() == MyMoneyAccount::Expense) { tooltip = i18n("Cannot edit transactions in the context of a category."); showTooltip(tooltip); rc = false; } if (m_account.isClosed()) { tooltip = i18n("Cannot create or edit any transactions in Account %1 as it is closed", m_account.name()); showTooltip(tooltip); rc = false; } KMyMoneyRegister::SelectedTransactions::const_iterator it_t; QString action; for (it_t = list.begin(); rc && it_t != list.end(); ++it_t) { if ((*it_t).transaction().id().isEmpty()) { tooltip.clear(); rc = false; continue; } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) { if (action.isEmpty()) { action = (*it_t).split().action(); continue; } if (action == (*it_t).split().action()) { continue; } else { tooltip = (i18n("Cannot edit mixed investment action/type transactions together.")); showTooltip(tooltip); rc = false; break; } } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) ++investmentTransactions; else ++normalTransactions; // check for a) if (investmentTransactions != 0 && normalTransactions != 0) { tooltip = i18n("Cannot edit investment transactions and non-investment transactions together."); showTooltip(tooltip); rc = false; break; } // check for b) but only for normalTransactions if ((*it_t).transaction().splitCount() > 2 && normalTransactions != 0) { if (list.count() > 1) { tooltip = i18n("Cannot edit multiple split transactions at once."); showTooltip(tooltip); rc = false; break; } } } // check for multiple transactions being selected in an investment account // we do not allow editing in this case: https://bugs.kde.org/show_bug.cgi?id=240816 // later on, we might allow to edit investment transactions of the same type /// Can now disable the following check. /* if (rc == true && investmentTransactions > 1) { tooltip = i18n("Cannot edit multiple investment transactions at once"); rc = false; }*/ // now check that we have the correct account type for investment transactions if (rc == true && investmentTransactions != 0) { if (m_account.accountType() != MyMoneyAccount::Investment) { tooltip = i18n("Cannot edit investment transactions in the context of this account."); rc = false; } } return rc; } diff --git a/kmymoney/views/kpayeesview.cpp b/kmymoney/views/kpayeesview.cpp index ddc4bd2b2..66aca9e7b 100644 --- a/kmymoney/views/kpayeesview.cpp +++ b/kmymoney/views/kpayeesview.cpp @@ -1,971 +1,973 @@ /*************************************************************************** kpayeesview.cpp --------------- begin : Thu Jan 24 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Andreas Nicolai ***************************************************************************/ /*************************************************************************** * * * 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 "kpayeesview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "config-kmymoney.h" #include "mymoneyfile.h" #include "kmymoneyglobalsettings.h" #include "kmymoney.h" #include "models.h" #include "mymoneysecurity.h" // *** KPayeeListItem Implementation *** KPayeeListItem::KPayeeListItem(QListWidget *parent, const MyMoneyPayee& payee) : QListWidgetItem(parent, QListWidgetItem::UserType), m_payee(payee) { setText(payee.name()); // allow in column rename setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); } KPayeeListItem::~KPayeeListItem() { } // *** KPayeesView Implementation *** KPayeesView::KPayeesView(QWidget *parent) : QWidget(parent), m_contact(new MyMoneyContact(this)), m_needReload(false), m_inSelection(false), m_allowEditing(true), m_payeeFilterType(0) { setupUi(this); m_filterProxyModel = new AccountNamesFilterProxyModel(this); + m_filterProxyModel->setHideEquityAccounts(!KMyMoneyGlobalSettings::expertMode()); m_filterProxyModel->addAccountGroup(MyMoneyAccount::Asset); m_filterProxyModel->addAccountGroup(MyMoneyAccount::Liability); + m_filterProxyModel->addAccountGroup(MyMoneyAccount::Equity); m_filterProxyModel->addAccountGroup(MyMoneyAccount::Income); m_filterProxyModel->addAccountGroup(MyMoneyAccount::Expense); m_filterProxyModel->setSourceModel(Models::instance()->accountsModel()); m_filterProxyModel->sort(0); comboDefaultCategory->setModel(m_filterProxyModel); matchTypeCombo->addItem(i18nc("@item No matching", "No matching"), MyMoneyPayee::matchDisabled); matchTypeCombo->addItem(i18nc("@item Match Payees name partially", "Match Payees name (partial)"), MyMoneyPayee::matchName); matchTypeCombo->addItem(i18nc("@item Match Payees name exactly", "Match Payees name (exact)"), MyMoneyPayee::matchNameExact); matchTypeCombo->addItem(i18nc("@item Search match in list", "Match on a name listed below"), MyMoneyPayee::matchKey); // create the searchline widget // and insert it into the existing layout m_searchWidget = new KListWidgetSearchLine(this, m_payeesList); m_searchWidget->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); m_payeesList->setContextMenuPolicy(Qt::CustomContextMenu); m_listTopHLayout->insertWidget(0, m_searchWidget); //load the filter type m_filterBox->addItem(i18nc("@item Show all payees", "All")); m_filterBox->addItem(i18nc("@item Show only used payees", "Used")); m_filterBox->addItem(i18nc("@item Show only unused payees", "Unused")); m_filterBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); KGuiItem newButtonItem(QString(""), QIcon::fromTheme(QStringLiteral("list-add-user")), i18n("Creates a new payee"), i18n("Use this to create a new payee.")); KGuiItem::assign(m_newButton, newButtonItem); m_newButton->setToolTip(newButtonItem.toolTip()); KGuiItem renameButtonItem(QString(""), QIcon::fromTheme(QStringLiteral("user-properties"), QIcon::fromTheme(QStringLiteral("text-editor"))), i18n("Rename the current selected payee"), i18n("Use this to start renaming the selected payee.")); KGuiItem::assign(m_renameButton, renameButtonItem); m_renameButton->setToolTip(renameButtonItem.toolTip()); KGuiItem deleteButtonItem(QString(""), QIcon::fromTheme(QStringLiteral("list-remove-user")), i18n("Delete selected payee(s)"), i18n("Use this to delete the selected payee. You can also select " "multiple payees to be deleted.")); KGuiItem::assign(m_deleteButton, deleteButtonItem); m_deleteButton->setToolTip(deleteButtonItem.toolTip()); KGuiItem mergeButtonItem(QString(""), QIcon::fromTheme(QStringLiteral("merge")), i18n("Merge multiple selected payees"), i18n("Use this to merge multiple selected payees.")); KGuiItem::assign(m_mergeButton, mergeButtonItem); m_mergeButton->setToolTip(mergeButtonItem.toolTip()); KGuiItem updateButtonItem(i18nc("Update payee", "Update"), QIcon::fromTheme(QStringLiteral("dialog-ok"), QIcon::fromTheme(QStringLiteral("finish"))), i18n("Accepts the entered data and stores it"), i18n("Use this to accept the modified data.")); KGuiItem::assign(m_updateButton, updateButtonItem); KGuiItem syncButtonItem(i18nc("Sync payee", "Sync"), QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Fetches the payee's data from your addressbook."), i18n("Use this to fetch payee's data.")); KGuiItem::assign(m_syncAddressbook, syncButtonItem); KGuiItem sendMailButtonItem(i18nc("Send mail", "Send"), QIcon::fromTheme(QStringLiteral("mail-message"), QIcon::fromTheme(QStringLiteral("internet-mail"))), i18n("Creates new e-mail to your payee."), i18n("Use this to create new e-mail to your payee.")); KGuiItem::assign(m_sendMail, sendMailButtonItem); m_updateButton->setEnabled(false); m_syncAddressbook->setEnabled(false); #ifndef KMM_ADDRESSBOOK_FOUND m_syncAddressbook->hide(); #endif matchTypeCombo->setCurrentIndex(0); checkMatchIgnoreCase->setEnabled(false); checkEnableDefaultCategory->setChecked(false); labelDefaultCategory->setEnabled(false); comboDefaultCategory->setEnabled(false); QList cols; cols << KMyMoneyRegister::DateColumn; cols << KMyMoneyRegister::AccountColumn; cols << KMyMoneyRegister::DetailColumn; cols << KMyMoneyRegister::ReconcileFlagColumn; cols << KMyMoneyRegister::PaymentColumn; cols << KMyMoneyRegister::DepositColumn; m_register->setupRegister(MyMoneyAccount(), cols); m_register->setSelectionMode(QTableWidget::SingleSelection); m_register->setDetailsColumnType(KMyMoneyRegister::AccountFirst); m_balanceLabel->hide(); connect(m_contact, SIGNAL(contactFetched(ContactData)), this, SLOT(slotContactFetched(ContactData))); connect(m_payeesList, SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)), this, SLOT(slotSelectPayee(QListWidgetItem*,QListWidgetItem*))); connect(m_payeesList, SIGNAL(itemSelectionChanged()), this, SLOT(slotSelectPayee())); connect(m_payeesList, SIGNAL(itemDoubleClicked(QListWidgetItem*)), this, SLOT(slotStartRename(QListWidgetItem*))); connect(m_payeesList, SIGNAL(itemChanged(QListWidgetItem*)), this, SLOT(slotRenamePayee(QListWidgetItem*))); connect(m_payeesList, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotOpenContextMenu(QPoint))); connect(m_renameButton, SIGNAL(clicked()), this, SLOT(slotRenameButtonCliked())); connect(m_deleteButton, SIGNAL(clicked()), kmymoney->action("payee_delete"), SLOT(trigger())); connect(m_mergeButton, SIGNAL(clicked()), kmymoney->action("payee_merge"), SLOT(trigger())); connect(m_newButton, SIGNAL(clicked()), this, SLOT(slotPayeeNew())); connect(addressEdit, SIGNAL(textChanged()), this, SLOT(slotPayeeDataChanged())); connect(postcodeEdit, SIGNAL(textChanged(QString)), this, SLOT(slotPayeeDataChanged())); connect(telephoneEdit, SIGNAL(textChanged(QString)), this, SLOT(slotPayeeDataChanged())); connect(emailEdit, SIGNAL(textChanged(QString)), this, SLOT(slotPayeeDataChanged())); connect(notesEdit, SIGNAL(textChanged()), this, SLOT(slotPayeeDataChanged())); connect(matchKeyEditList, SIGNAL(changed()), this, SLOT(slotKeyListChanged())); connect(matchTypeCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotPayeeDataChanged())); connect(checkMatchIgnoreCase, SIGNAL(toggled(bool)), this, SLOT(slotPayeeDataChanged())); connect(checkEnableDefaultCategory, SIGNAL(toggled(bool)), this, SLOT(slotPayeeDataChanged())); connect(comboDefaultCategory, SIGNAL(accountSelected(QString)), this, SLOT(slotPayeeDataChanged())); connect(buttonSuggestACategory, SIGNAL(clicked()), this, SLOT(slotChooseDefaultAccount())); connect(m_updateButton, SIGNAL(clicked()), this, SLOT(slotUpdatePayee())); connect(m_syncAddressbook, SIGNAL(clicked()), this, SLOT(slotSyncAddressBook())); connect(m_helpButton, SIGNAL(clicked()), this, SLOT(slotHelp())); connect(m_sendMail, SIGNAL(clicked()), this, SLOT(slotSendMail())); connect(m_register, SIGNAL(editTransaction()), this, SLOT(slotSelectTransaction())); connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotLoadPayees())); connect(m_filterBox, SIGNAL(currentIndexChanged(int)), this, SLOT(slotChangeFilter(int))); connect(payeeIdentifiers, SIGNAL(dataChanged()), this, SLOT(slotPayeeDataChanged())); // use the size settings of the last run (if any) KConfigGroup grp = KSharedConfig::openConfig()->group("Last Use Settings"); m_splitter->restoreState(grp.readEntry("KPayeesViewSplitterSize", QByteArray())); m_splitter->setChildrenCollapsible(false); //At start we haven't any payee selected m_tabWidget->setEnabled(false); // disable tab widget m_deleteButton->setEnabled(false); //disable delete, rename and merge buttons m_renameButton->setEnabled(false); m_mergeButton->setEnabled(false); m_payee = MyMoneyPayee(); // make sure we don't access an undefined payee clearItemData(); } KPayeesView::~KPayeesView() { // remember the splitter settings for startup KConfigGroup grp = KSharedConfig::openConfig()->group("Last Use Settings"); grp.writeEntry("KPayeesViewSplitterSize", m_splitter->saveState()); grp.sync(); } void KPayeesView::slotChooseDefaultAccount() { MyMoneyFile* file = MyMoneyFile::instance(); QMap account_count; KMyMoneyRegister::RegisterItem* item = m_register->firstItem(); while (item) { //only walk through selectable items. eg. transactions and not group markers if (item->isSelectable()) { KMyMoneyRegister::Transaction* t = dynamic_cast(item); MyMoneySplit s = t->transaction().splitByPayee(m_payee.id()); const MyMoneyAccount& acc = file->account(s.accountId()); QString txt; if (s.action() != MyMoneySplit::ActionAmortization && acc.accountType() != MyMoneyAccount::AssetLoan && !file->isTransfer(t->transaction()) && t->transaction().splitCount() == 2) { MyMoneySplit s0 = t->transaction().splitByAccount(s.accountId(), false); if (account_count.contains(s0.accountId())) { account_count[s0.accountId()]++; } else { account_count[s0.accountId()] = 1; } } } item = item->nextItem(); } QMap::Iterator most_frequent, iter; most_frequent = account_count.begin(); for (iter = account_count.begin(); iter != account_count.end(); ++iter) { if (iter.value() > most_frequent.value()) { most_frequent = iter; } } if (most_frequent != account_count.end()) { checkEnableDefaultCategory->setChecked(true); comboDefaultCategory->setSelected(most_frequent.key()); setDirty(); } } void KPayeesView::slotStartRename(QListWidgetItem* item) { m_allowEditing = true; m_payeesList->editItem(item); } void KPayeesView::slotRenameButtonCliked() { if (m_payeesList->currentItem() && m_payeesList->selectedItems().count() == 1) { slotStartRename(m_payeesList->currentItem()); } } // This variant is only called when a single payee is selected and renamed. void KPayeesView::slotRenamePayee(QListWidgetItem* p) { //if there is no current item selected, exit if (m_allowEditing == false || !m_payeesList->currentItem() || p != m_payeesList->currentItem()) return; //qDebug() << "[KPayeesView::slotRenamePayee]"; // create a copy of the new name without appended whitespaces QString new_name = p->text(); if (m_payee.name() != new_name) { MyMoneyFileTransaction ft; try { // check if we already have a payee with the new name try { // this function call will throw an exception, if the payee // hasn't been found. MyMoneyFile::instance()->payeeByName(new_name); // the name already exists, ask the user whether he's sure to keep the name if (KMessageBox::questionYesNo(this, i18n("A payee with the name '%1' already exists. It is not advisable to have " "multiple payees with the same identification name. Are you sure you would like " "to rename the payee?", new_name)) != KMessageBox::Yes) { p->setText(m_payee.name()); return; } } catch (const MyMoneyException &) { // all ok, the name is unique } m_payee.setName(new_name); m_newName = new_name; MyMoneyFile::instance()->modifyPayee(m_payee); // the above call to modifyPayee will reload the view so // all references and pointers to the view have to be // re-established. // make sure, that the record is visible even if it moved // out of sight due to the rename operation ensurePayeeVisible(m_payee.id()); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to modify payee"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } else { p->setText(new_name); } } void KPayeesView::ensurePayeeVisible(const QString& id) { for (int i = 0; i < m_payeesList->count(); ++i) { KPayeeListItem* p = dynamic_cast(m_payeesList->item(0)); if (p && p->payee().id() == id) { m_payeesList->scrollToItem(p, QAbstractItemView::PositionAtCenter); m_payeesList->setCurrentItem(p); // active item and deselect all others m_payeesList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it break; } } } void KPayeesView::selectedPayees(QList& payeesList) const { QList selectedItems = m_payeesList->selectedItems(); QList::ConstIterator itemsIt = selectedItems.constBegin(); while (itemsIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*itemsIt); if (item) payeesList << item->payee(); ++itemsIt; } } void KPayeesView::slotSelectPayee(QListWidgetItem* cur, QListWidgetItem* prev) { Q_UNUSED(cur); Q_UNUSED(prev); m_allowEditing = false; } void KPayeesView::slotSelectPayee() { // check if the content of a currently selected payee was modified // and ask to store the data if (isDirty()) { QString question = QString("%1").arg(i18n("Do you want to save the changes for %1?", m_newName)); if (KMessageBox::questionYesNo(this, question, i18n("Save changes")) == KMessageBox::Yes) { m_inSelection = true; slotUpdatePayee(); m_inSelection = false; } } // make sure we always clear the selected list when listing again m_selectedPayeesList.clear(); // loop over all payees and count the number of payees, also // obtain last selected payee selectedPayees(m_selectedPayeesList); emit selectObjects(m_selectedPayeesList); if (m_selectedPayeesList.isEmpty()) { m_tabWidget->setEnabled(false); // disable tab widget m_balanceLabel->hide(); m_deleteButton->setEnabled(false); //disable delete, rename and merge buttons m_renameButton->setEnabled(false); m_mergeButton->setEnabled(false); clearItemData(); m_payee = MyMoneyPayee(); m_syncAddressbook->setEnabled(false); return; // make sure we don't access an undefined payee } m_deleteButton->setEnabled(true); //re-enable delete button m_syncAddressbook->setEnabled(true); // if we have multiple payees selected, clear and disable the payee information if (m_selectedPayeesList.count() > 1) { m_tabWidget->setEnabled(false); // disable tab widget m_renameButton->setEnabled(false); // disable also the rename button m_mergeButton->setEnabled(true); m_balanceLabel->hide(); clearItemData(); } else { m_mergeButton->setEnabled(false); m_renameButton->setEnabled(true); } // otherwise we have just one selected, enable payee information widget m_tabWidget->setEnabled(true); m_balanceLabel->show(); // as of now we are updating only the last selected payee, and until // selection mode of the QListView has been changed to Extended, this // will also be the only selection and behave exactly as before - Andreas try { m_payee = m_selectedPayeesList[0]; m_newName = m_payee.name(); addressEdit->setEnabled(true); addressEdit->setText(m_payee.address()); postcodeEdit->setEnabled(true); postcodeEdit->setText(m_payee.postcode()); telephoneEdit->setEnabled(true); telephoneEdit->setText(m_payee.telephone()); emailEdit->setEnabled(true); emailEdit->setText(m_payee.email()); notesEdit->setText(m_payee.notes()); QStringList keys; bool ignorecase = false; MyMoneyPayee::payeeMatchType type = m_payee.matchData(ignorecase, keys); matchTypeCombo->setCurrentIndex(matchTypeCombo->findData(type)); matchKeyEditList->clear(); matchKeyEditList->insertStringList(keys); checkMatchIgnoreCase->setChecked(ignorecase); checkEnableDefaultCategory->setChecked(m_payee.defaultAccountEnabled()); comboDefaultCategory->setSelected(m_payee.defaultAccountId()); payeeIdentifiers->setSource(m_payee); slotPayeeDataChanged(); showTransactions(); } catch (const MyMoneyException &e) { qDebug("exception during display of payee: %s at %s:%ld", qPrintable(e.what()), qPrintable(e.file()), e.line()); m_register->clear(); m_selectedPayeesList.clear(); m_payee = MyMoneyPayee(); } m_allowEditing = true; } void KPayeesView::clearItemData() { addressEdit->setText(QString()); postcodeEdit->setText(QString()); telephoneEdit->setText(QString()); emailEdit->setText(QString()); notesEdit->setText(QString()); showTransactions(); } void KPayeesView::showTransactions() { MyMoneyMoney balance; MyMoneyFile *file = MyMoneyFile::instance(); MyMoneySecurity base = file->baseCurrency(); // setup sort order m_register->setSortOrder(KMyMoneyGlobalSettings::sortSearchView()); // clear the register m_register->clear(); if (m_selectedPayeesList.isEmpty() || !m_tabWidget->isEnabled()) { m_balanceLabel->setText(i18n("Balance: %1", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); return; } // setup the list and the pointer vector MyMoneyTransactionFilter filter; for (QList::const_iterator it = m_selectedPayeesList.constBegin(); it != m_selectedPayeesList.constEnd(); ++it) filter.addPayee((*it).id()); filter.setDateFilter(KMyMoneyGlobalSettings::startDate().date(), QDate()); // retrieve the list from the engine file->transactionList(m_transactionList, filter); // create the elements for the register QList >::const_iterator it; QMap uniqueMap; MyMoneyMoney deposit, payment; int splitCount = 0; bool balanceAccurate = true; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { const MyMoneySplit& split = (*it).second; MyMoneyAccount acc = file->account(split.accountId()); ++splitCount; uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Register::transactionFactory(m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); // take care of foreign currencies MyMoneyMoney val = split.shares().abs(); if (acc.currencyId() != base.id()) { const MyMoneyPrice &price = file->price(acc.currencyId(), base.id()); // in case the price is valid, we use it. Otherwise, we keep // a flag that tells us that the balance is somewhat inaccurate if (price.isValid()) { val *= price.rate(base.id()); } else { balanceAccurate = false; } } if (split.shares().isNegative()) { payment += val; } else { deposit += val; } } balance = deposit - payment; // add the group markers m_register->addGroupMarkers(); // sort the transactions according to the sort setting m_register->sortItems(); // remove trailing and adjacent markers m_register->removeUnwantedGroupMarkers(); m_register->updateRegister(true); // we might end up here with updates disabled on the register so // make sure that we enable updates here m_register->setUpdatesEnabled(true); m_balanceLabel->setText(i18n("Balance: %1%2", balanceAccurate ? "" : "~", balance.formatMoney(file->baseCurrency().smallestAccountFraction()))); } void KPayeesView::slotKeyListChanged() { bool rc = false; bool ignorecase = false; QStringList keys; m_payee.matchData(ignorecase, keys); if (matchTypeCombo->currentData().toUInt() == MyMoneyPayee::matchKey) { rc |= (keys != matchKeyEditList->items()); } setDirty(rc); } void KPayeesView::slotPayeeDataChanged() { bool rc = false; if (m_tabWidget->isEnabled()) { rc |= ((m_payee.email().isEmpty() != emailEdit->text().isEmpty()) || (!emailEdit->text().isEmpty() && m_payee.email() != emailEdit->text())); rc |= ((m_payee.address().isEmpty() != addressEdit->toPlainText().isEmpty()) || (!addressEdit->toPlainText().isEmpty() && m_payee.address() != addressEdit->toPlainText())); rc |= ((m_payee.postcode().isEmpty() != postcodeEdit->text().isEmpty()) || (!postcodeEdit->text().isEmpty() && m_payee.postcode() != postcodeEdit->text())); rc |= ((m_payee.telephone().isEmpty() != telephoneEdit->text().isEmpty()) || (!telephoneEdit->text().isEmpty() && m_payee.telephone() != telephoneEdit->text())); rc |= ((m_payee.name().isEmpty() != m_newName.isEmpty()) || (!m_newName.isEmpty() && m_payee.name() != m_newName)); rc |= ((m_payee.notes().isEmpty() != notesEdit->toPlainText().isEmpty()) || (!notesEdit->toPlainText().isEmpty() && m_payee.notes() != notesEdit->toPlainText())); bool ignorecase = false; QStringList keys; MyMoneyPayee::payeeMatchType type = m_payee.matchData(ignorecase, keys); rc |= (static_cast(type) != matchTypeCombo->currentData().toUInt()); checkMatchIgnoreCase->setEnabled(false); matchKeyEditList->setEnabled(false); if (matchTypeCombo->currentData().toUInt() != MyMoneyPayee::matchDisabled) { checkMatchIgnoreCase->setEnabled(true); // if we turn matching on, we default to 'ignore case' // TODO maybe make the default a user option if (type == MyMoneyPayee::matchDisabled && matchTypeCombo->currentData().toUInt() != MyMoneyPayee::matchDisabled) checkMatchIgnoreCase->setChecked(true); rc |= (ignorecase != checkMatchIgnoreCase->isChecked()); if (matchTypeCombo->currentData().toUInt() == MyMoneyPayee::matchKey) { matchKeyEditList->setEnabled(true); rc |= (keys != matchKeyEditList->items()); } } rc |= (checkEnableDefaultCategory->isChecked() != m_payee.defaultAccountEnabled()); if (checkEnableDefaultCategory->isChecked()) { comboDefaultCategory->setEnabled(true); labelDefaultCategory->setEnabled(true); // this is only going to understand the first in the list of selected accounts if (comboDefaultCategory->getSelected().isEmpty()) { rc |= !m_payee.defaultAccountId().isEmpty(); } else { QString temp = comboDefaultCategory->getSelected(); rc |= (temp.isEmpty() != m_payee.defaultAccountId().isEmpty()) || (!m_payee.defaultAccountId().isEmpty() && temp != m_payee.defaultAccountId()); } } else { comboDefaultCategory->setEnabled(false); labelDefaultCategory->setEnabled(false); } rc |= (m_payee.payeeIdentifiers() != payeeIdentifiers->identifiers()); } setDirty(rc); } void KPayeesView::slotUpdatePayee() { if (isDirty()) { MyMoneyFileTransaction ft; setDirty(false); try { m_payee.setName(m_newName); m_payee.setAddress(addressEdit->toPlainText()); m_payee.setPostcode(postcodeEdit->text()); m_payee.setTelephone(telephoneEdit->text()); m_payee.setEmail(emailEdit->text()); m_payee.setNotes(notesEdit->toPlainText()); m_payee.setMatchData(static_cast(matchTypeCombo->currentData().toUInt()), checkMatchIgnoreCase->isChecked(), matchKeyEditList->items()); m_payee.setDefaultAccountId(); m_payee.resetPayeeIdentifiers(payeeIdentifiers->identifiers()); if (checkEnableDefaultCategory->isChecked()) { QString temp; if (!comboDefaultCategory->getSelected().isEmpty()) { temp = comboDefaultCategory->getSelected(); m_payee.setDefaultAccountId(temp); } } MyMoneyFile::instance()->modifyPayee(m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to modify payee"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } } void KPayeesView::slotSyncAddressBook() { if (m_payeeRows.isEmpty()) { // empty list means no syncing is pending... foreach (auto item, m_payeesList->selectedItems()) { m_payeeRows.append(m_payeesList->row(item)); // ...so initialize one } m_payeesList->clearSelection(); // otherwise slotSelectPayee will be run after every payee update // m_syncAddressbook->setEnabled(false); // disallow concurent syncs } if (m_payeeRows.count() <= m_payeeRow) { KPayeeListItem* item = dynamic_cast(m_payeesList->currentItem()); if (item) { // update ui if something is selected m_payee = item->payee(); addressEdit->setText(m_payee.address()); postcodeEdit->setText(m_payee.postcode()); telephoneEdit->setText(m_payee.telephone()); } m_payeeRows.clear(); // that means end of sync m_payeeRow = 0; return; } KPayeeListItem* item = dynamic_cast(m_payeesList->item(m_payeeRows.at(m_payeeRow))); if (item) m_payee = item->payee(); ++m_payeeRow; m_contact->fetchContact(m_payee.email()); // search for payee's data in addressbook and receive it in slotContactFetched } void KPayeesView::slotContactFetched(const ContactData &identity) { if (!identity.email.isEmpty()) { // empty e-mail means no identity fetched QString txt; if (!identity.street.isEmpty()) txt.append(identity.street + "\n"); if (!identity.locality.isEmpty()) { txt.append(identity.locality); if (!identity.postalCode.isEmpty()) txt.append(' ' + identity.postalCode + "\n"); else txt.append("\n"); } if (!identity.country.isEmpty()) txt.append(identity.country + "\n"); if (!txt.isEmpty() && m_payee.address().compare(txt) != 0) m_payee.setAddress(txt); if (!identity.postalCode.isEmpty() && m_payee.postcode().compare(identity.postalCode) != 0) m_payee.setPostcode(identity.postalCode); if (!identity.phoneNumber.isEmpty() && m_payee.telephone().compare(identity.phoneNumber) != 0) m_payee.setTelephone(identity.phoneNumber); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyPayee(m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Unable to modify payee"), i18n("%1 thrown in %2:%3", e.what(), e.file(), e.line())); } } slotSyncAddressBook(); // process next payee } void KPayeesView::slotSendMail() { QRegularExpression re(".+@.+"); if (re.match(m_payee.email()).hasMatch()) QDesktopServices::openUrl(QUrl(QStringLiteral("mailto:?to=") + m_payee.email(), QUrl::TolerantMode)); } void KPayeesView::showEvent(QShowEvent* event) { emit aboutToShow(); if (m_needReload) { loadPayees(); m_needReload = false; } // don't forget base class implementation QWidget::showEvent(event); QList list; selectedPayees(list); emit selectObjects(list); } void KPayeesView::slotLoadPayees() { if (isVisible()) { if (m_inSelection) QTimer::singleShot(0, this, SLOT(slotLoadPayees())); else loadPayees(); } else { m_needReload = true; } } void KPayeesView::loadPayees() { if (m_inSelection) return; QMap isSelected; QString id; MyMoneyFile* file = MyMoneyFile::instance(); // remember which items are selected in the list QList selectedItems = m_payeesList->selectedItems(); QList::const_iterator payeesIt = selectedItems.constBegin(); while (payeesIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*payeesIt); if (item) isSelected[item->payee().id()] = true; ++payeesIt; } // keep current selected item KPayeeListItem *currentItem = static_cast(m_payeesList->currentItem()); if (currentItem) id = currentItem->payee().id(); m_allowEditing = false; // clear the list m_searchWidget->clear(); m_searchWidget->updateSearch(); m_payeesList->clear(); m_register->clear(); currentItem = 0; QListlist = file->payeeList(); QList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { if (m_payeeFilterType == eAllPayees || (m_payeeFilterType == eReferencedPayees && file->isReferenced(*it)) || (m_payeeFilterType == eUnusedPayees && !file->isReferenced(*it))) { KPayeeListItem* item = new KPayeeListItem(m_payeesList, *it); if (item->payee().id() == id) currentItem = item; if (isSelected[item->payee().id()]) item->setSelected(true); } } m_payeesList->sortItems(); if (currentItem) { m_payeesList->setCurrentItem(currentItem); m_payeesList->scrollToItem(currentItem); } m_filterProxyModel->invalidate(); comboDefaultCategory->expandAll(); slotSelectPayee(0, 0); m_allowEditing = true; } void KPayeesView::slotSelectTransaction() { QList list = m_register->selectedItems(); if (!list.isEmpty()) { KMyMoneyRegister::Transaction* t = dynamic_cast(list[0]); if (t) emit transactionSelected(t->split().accountId(), t->transaction().id()); } } void KPayeesView::slotSelectPayeeAndTransaction(const QString& payeeId, const QString& accountId, const QString& transactionId) { if (!isVisible()) return; try { // clear filter m_searchWidget->clear(); m_searchWidget->updateSearch(); // deselect all other selected items QList selectedItems = m_payeesList->selectedItems(); QList::const_iterator payeesIt = selectedItems.constBegin(); while (payeesIt != selectedItems.constEnd()) { KPayeeListItem* item = dynamic_cast(*payeesIt); if (item) item->setSelected(false); ++payeesIt; } // find the payee in the list QListWidgetItem* it; for (int i = 0; i < m_payeesList->count(); ++i) { it = m_payeesList->item(i); KPayeeListItem* item = dynamic_cast(it); if (item && item->payee().id() == payeeId) { m_payeesList->scrollToItem(it, QAbstractItemView::PositionAtCenter); m_payeesList->setCurrentItem(it); // active item and deselect all others m_payeesList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it //make sure the payee selection is updated and transactions are updated accordingly slotSelectPayee(); KMyMoneyRegister::RegisterItem *item = 0; for (int i = 0; i < m_register->rowCount(); ++i) { item = m_register->itemAtRow(i); KMyMoneyRegister::Transaction* t = dynamic_cast(item); if (t) { if (t->transaction().id() == transactionId && t->transaction().accountReferenced(accountId)) { m_register->selectItem(item); m_register->ensureItemVisible(item); break; } } } // quit out of for() loop break; } } } catch (const MyMoneyException &e) { qWarning("Unexpected exception in KPayeesView::slotSelectPayeeAndTransaction %s", qPrintable(e.what())); } } void KPayeesView::slotOpenContextMenu(const QPoint& /*p*/) { KPayeeListItem* item = dynamic_cast(m_payeesList->currentItem()); if (item) { slotSelectPayee(); emit openContextMenu(item->payee()); } } void KPayeesView::slotPayeeNew() { kmymoney->action("payee_new")->trigger(); } void KPayeesView::slotHelp() { KHelpClient::invokeHelp("details.payees"); } void KPayeesView::slotChangeFilter(int index) { //update the filter type then reload the payees list m_payeeFilterType = index; loadPayees(); } bool KPayeesView::isDirty() const { return m_updateButton->isEnabled(); } void KPayeesView::setDirty(bool dirty) { m_updateButton->setEnabled(dirty); } diff --git a/kmymoney/widgets/kaccounttemplateselector.cpp b/kmymoney/widgets/kaccounttemplateselector.cpp index 1ed6b9da6..aa250df9f 100644 --- a/kmymoney/widgets/kaccounttemplateselector.cpp +++ b/kmymoney/widgets/kaccounttemplateselector.cpp @@ -1,276 +1,278 @@ /*************************************************************************** kaccounttemplateselector.cpp - description ------------------- begin : Tue Feb 5 2008 copyright : (C) 2008 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 #include "kaccounttemplateselector.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include class KAccountTemplateSelector::Private { public: - Private(KAccountTemplateSelector* p) { + Private(KAccountTemplateSelector* p) : + id(0) + { m_parent = p; } #ifndef KMM_DESIGNER QList selectedTemplates() const; QTreeWidgetItem* hierarchyItem(const QString& parent, const QString& name); void loadHierarchy(); #endif public: KAccountTemplateSelector* m_parent; QMap m_templateHierarchy; #ifndef KMM_DESIGNER QMap m_templates; // a map of country name or country name (language name) -> localeId (lang_country) so be careful how you use it QMap countries; QString currentLocaleId; QMap::iterator it_m; QStringList dirlist; int id; #endif }; #ifndef KMM_DESIGNER QTreeWidgetItem* KAccountTemplateSelector::Private::hierarchyItem(const QString& parent, const QString& name) { if (!m_templateHierarchy.contains(parent) || m_templateHierarchy[parent] == 0) { QRegExp exp("(.*):(.*)"); if (exp.indexIn(parent) != -1) m_templateHierarchy[parent] = hierarchyItem(exp.cap(1), exp.cap(2)); } QTreeWidgetItem *item = new QTreeWidgetItem(m_templateHierarchy[parent]); item->setText(0, name); return item; } void KAccountTemplateSelector::Private::loadHierarchy() { m_templateHierarchy.clear(); QTreeWidgetItemIterator it(m_parent->m_groupList, QTreeWidgetItemIterator::Selected); QTreeWidgetItem* it_v; while ((it_v = *it) != 0) { m_templates[it_v->data(0, IdRole).toInt()].hierarchy(m_templateHierarchy); ++it; } // I need to think about this some more. The code works and shows // the current account hierarchy. It might be useful, to show // existing accounts dimmed and the new ones in bold or so. #if 0 // add the hierarchy from the MyMoneyFile object QList aList; QList::const_iterator it_a; MyMoneyFile* file = MyMoneyFile::instance(); file->accountList(aList); if (aList.count() > 0) { m_templateHierarchy[file->accountToCategory(file->asset().id(), true)] = 0; m_templateHierarchy[file->accountToCategory(file->liability().id(), true)] = 0; m_templateHierarchy[file->accountToCategory(file->income().id(), true)] = 0; m_templateHierarchy[file->accountToCategory(file->expense().id(), true)] = 0; m_templateHierarchy[file->accountToCategory(file->equity().id(), true)] = 0; } for (it_a = aList.begin(); it_a != aList.end(); ++it_a) { m_templateHierarchy[file->accountToCategory((*it_a).id(), true)] = 0; } #endif m_parent->m_accountList->clear(); QRegExp exp("(.*):(.*)"); for (QMap::iterator it_m = m_templateHierarchy.begin(); it_m != m_templateHierarchy.end(); ++it_m) { if (exp.indexIn(it_m.key()) == -1) { (*it_m) = new QTreeWidgetItem(m_parent->m_accountList); (*it_m)->setText(0, it_m.key()); } else { (*it_m) = hierarchyItem(exp.cap(1), exp.cap(2)); } (*it_m)->setExpanded(true); } m_parent->m_description->clear(); if (m_parent->m_groupList->currentItem()) { m_parent->m_description->setText(m_templates[m_parent->m_groupList->currentItem()->data(0, IdRole).toInt()].longDescription()); } } QList KAccountTemplateSelector::Private::selectedTemplates() const { QList list; QTreeWidgetItemIterator it(m_parent->m_groupList, QTreeWidgetItemIterator::Selected); QTreeWidgetItem* it_v; while ((it_v = *it) != 0) { list << m_templates[it_v->data(0, IdRole).toInt()]; ++it; } return list; } #endif KAccountTemplateSelector::KAccountTemplateSelector(QWidget* parent) : KAccountTemplateSelectorDecl(parent), d(new Private(this)) { m_accountList->header()->hide(); m_groupList->setSelectionMode(QAbstractItemView::ExtendedSelection); connect(m_groupList, SIGNAL(itemSelectionChanged()), this, SLOT(slotLoadHierarchy())); // kick off loading of account template data QTimer::singleShot(0, this, SLOT(slotLoadTemplateList())); } KAccountTemplateSelector::~KAccountTemplateSelector() { delete d; } void KAccountTemplateSelector::slotLoadTemplateList() { #ifndef KMM_DESIGNER QStringList dirs; // get list of template subdirs and scan them for the list of subdirs d->dirlist = QStandardPaths::locateAll(QStandardPaths::DataLocation, "templates", QStandardPaths::LocateDirectory); QStringList::iterator it; for (it = d->dirlist.begin(); it != d->dirlist.end(); ++it) { QDir dir(*it); dirs = dir.entryList(QStringList("*"), QDir::Dirs); QStringList::iterator it_d; for (it_d = dirs.begin(); it_d != dirs.end(); ++it_d) { // we don't care about . and .. if ((*it_d) == ".." || (*it_d) == "." || (*it_d) == "C") continue; QLocale templateLocale(*it_d); if (templateLocale.language() != QLocale::C) { QString country = QLocale().countryToString(templateLocale.country()); QString lang = QLocale().languageToString(templateLocale.language()); if (d->countries.contains(country)) { if (d->countries[country] != *it_d) { QString otherName = d->countries[country]; QLocale otherTemplateLocale(otherName); QString otherCountry = QLocale().countryToString(otherTemplateLocale.country()); QString otherLang = QLocale().languageToString(otherTemplateLocale.language()); d->countries.remove(country); d->countries[QString("%1 (%2)").arg(otherCountry, otherLang)] = otherName; d->countries[QString("%1 (%2)").arg(country, lang)] = *it_d; // retain the item corresponding to the current locale if (QLocale().country() == templateLocale.country()) { d->currentLocaleId = *it_d; } } } else { d->countries[country] = *it_d; // retain the item corresponding to the current locale if (QLocale().country() == templateLocale.country()) { d->currentLocaleId = *it_d; } } } else { qDebug("'%s/%s' not scanned", qPrintable(*it), qPrintable(*it_d)); } } } // now that we know, what we can get at max, we scan everything // and parse the templates into memory m_groupList->clear(); d->m_templates.clear(); d->it_m = d->countries.begin(); d->id = 1; if (d->it_m != d->countries.end()) QTimer::singleShot(0, this, SLOT(slotLoadCountry())); else { d->loadHierarchy(); } #endif } void KAccountTemplateSelector::slotLoadCountry() { #ifndef KMM_DESIGNER QTreeWidgetItem *parent = new QTreeWidgetItem(m_groupList); parent->setText(0, d->it_m.key()); parent->setFlags(parent->flags() & ~Qt::ItemIsSelectable); for (QStringList::iterator it = d->dirlist.begin(); it != d->dirlist.end(); ++it) { QDir dir(QString("%1/%2").arg(*it).arg(*(d->it_m))); if (dir.exists()) { QStringList files = dir.entryList(QStringList("*"), QDir::Files); for (QStringList::iterator it_f = files.begin(); it_f != files.end(); ++it_f) { MyMoneyTemplate templ(QUrl::fromUserInput(QString("%1/%2").arg(dir.canonicalPath(), *it_f))); d->m_templates[d->id] = templ; QTreeWidgetItem *item = new QTreeWidgetItem(parent); item->setText(0, templ.title()); item->setText(1, templ.shortDescription()); item->setData(0, IdRole, QString("%1").arg(d->id)); ++d->id; } } } // make visible the templates of the current locale if (d->it_m.value() == d->currentLocaleId) { m_groupList->setCurrentItem(parent); m_groupList->expandItem(parent); m_groupList->scrollToItem(parent, QTreeView::PositionAtTop); } ++d->it_m; if (d->it_m != d->countries.end()) QTimer::singleShot(0, this, SLOT(slotLoadCountry())); else { d->loadHierarchy(); } #endif } void KAccountTemplateSelector::slotLoadHierarchy() { #ifndef KMM_DESIGNER d->loadHierarchy(); #endif } QList KAccountTemplateSelector::selectedTemplates() const { #ifndef KMM_DESIGNER return d->selectedTemplates(); #else return QList(); #endif } diff --git a/kmymoney/widgets/kmymoneybriefschedule.cpp b/kmymoney/widgets/kmymoneybriefschedule.cpp index d1f937cd7..ede31097f 100644 --- a/kmymoney/widgets/kmymoneybriefschedule.cpp +++ b/kmymoney/widgets/kmymoneybriefschedule.cpp @@ -1,176 +1,177 @@ /*************************************************************************** kmymoneybriefschedule.cpp - description ------------------- begin : Sun Jul 6 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kmymoneybriefschedule.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyschedule.h" #include "kmymoneyutils.h" KMyMoneyBriefSchedule::KMyMoneyBriefSchedule(QWidget *parent) - : kScheduleBriefWidget(parent/*,name, Qt::WStyle_Customize | Qt::WStyle_NoBorder*/) + : kScheduleBriefWidget(parent/*,name, Qt::WStyle_Customize | Qt::WStyle_NoBorder*/), + m_index(0) { m_nextButton->setIcon(QIcon::fromTheme(QString::fromLatin1("arrow-right"))); m_prevButton->setIcon(QIcon::fromTheme(QString::fromLatin1("arrow-left"))); connect(m_prevButton, SIGNAL(clicked()), this, SLOT(slotPrevClicked())); connect(m_nextButton, SIGNAL(clicked()), this, SLOT(slotNextClicked())); connect(m_closeButton, SIGNAL(clicked()), this, SLOT(hide())); connect(m_skipButton, SIGNAL(clicked()), this, SLOT(slotSkipClicked())); connect(m_buttonEnter, SIGNAL(clicked()), this, SLOT(slotEnterClicked())); KGuiItem skipGuiItem(i18n("&Skip"), QIcon::fromTheme("media-seek-forward"), i18n("Skip this transaction"), i18n("Use this button to skip this transaction")); KGuiItem::assign(m_skipButton, skipGuiItem); KGuiItem enterGuiItem(i18n("&Enter"), QIcon::fromTheme(QLatin1String("key-enter")), i18n("Record this transaction into the register"), i18n("Use this button to record this transaction")); KGuiItem::assign(m_buttonEnter, enterGuiItem); } KMyMoneyBriefSchedule::~KMyMoneyBriefSchedule() { } void KMyMoneyBriefSchedule::setSchedules(QList list, const QDate& date) { m_scheduleList = list; m_date = date; m_index = 0; if (list.count() >= 1) { loadSchedule(); } } void KMyMoneyBriefSchedule::loadSchedule() { try { if (m_index < m_scheduleList.count()) { MyMoneySchedule sched = m_scheduleList[m_index]; m_indexLabel->setText(i18n("%1 of %2", m_index + 1, m_scheduleList.count())); m_name->setText(sched.name()); m_type->setText(KMyMoneyUtils::scheduleTypeToString(sched.type())); m_account->setText(sched.account().name()); QString text; MyMoneyMoney amount = sched.transaction().splitByAccount(sched.account().id()).value(); amount = amount.abs(); if (sched.willEnd()) { int transactions = sched.paymentDates(m_date, sched.endDate()).count() - 1; text = i18np("Payment on %2 for %3 with %1 transaction remaining occurring %4.", "Payment on %2 for %3 with %1 transactions remaining occurring %4.", transactions, QLocale().toString(m_date, QLocale::ShortFormat), amount.formatMoney(sched.account().fraction()), i18n(sched.occurrenceToString().toLatin1())); } else { text = i18n("Payment on %1 for %2 occurring %3.", QLocale().toString(m_date, QLocale::ShortFormat), amount.formatMoney(sched.account().fraction()), i18n(sched.occurrenceToString().toLatin1())); } if (m_date < QDate::currentDate()) { if (sched.isOverdue()) { QDate startD = (sched.lastPayment().isValid()) ? sched.lastPayment() : sched.startDate(); if (m_date.isValid()) startD = m_date; int days = startD.daysTo(QDate::currentDate()); int transactions = sched.paymentDates(startD, QDate::currentDate()).count(); text += "
"; text += i18np("%1 day overdue", "%1 days overdue", days); text += QString(" "); text += i18np("(%1 occurrence.)", "(%1 occurrences.)", transactions); text += ""; } } m_details->setText(text); m_prevButton->setEnabled(true); m_nextButton->setEnabled(true); m_skipButton->setEnabled(sched.occurrencePeriod() != MyMoneySchedule::OCCUR_ONCE); if (m_index == 0) m_prevButton->setEnabled(false); if (m_index == (m_scheduleList.count() - 1)) m_nextButton->setEnabled(false); } } catch (const MyMoneyException &) { } } void KMyMoneyBriefSchedule::slotPrevClicked() { if (m_index >= 1) { --m_index; loadSchedule(); } } void KMyMoneyBriefSchedule::slotNextClicked() { if (m_index < (m_scheduleList.count() - 1)) { m_index++; loadSchedule(); } } void KMyMoneyBriefSchedule::slotEnterClicked() { hide(); emit enterClicked(m_scheduleList[m_index], m_date); } void KMyMoneyBriefSchedule::slotSkipClicked() { hide(); emit skipClicked(m_scheduleList[m_index], m_date); } diff --git a/kmymoney/widgets/kmymoneycalculator.cpp b/kmymoney/widgets/kmymoneycalculator.cpp index 565c4cc26..022444543 100644 --- a/kmymoney/widgets/kmymoneycalculator.cpp +++ b/kmymoney/widgets/kmymoneycalculator.cpp @@ -1,453 +1,453 @@ /*************************************************************************** kmymoneycalculator.cpp - description ------------------- begin : Sat Oct 19 2002 copyright : (C) 2000-2002 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kmymoneycalculator.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes kMyMoneyCalculator::kMyMoneyCalculator(QWidget* parent) : QFrame(parent) { m_comma = QLocale().decimalPoint(); m_clearOperandOnDigit = false; QGridLayout* grid = new QGridLayout(this); display = new QLabel(this); QPalette palette; palette.setColor(display->backgroundRole(), QColor("#BDFFB4")); display->setPalette(palette); display->setFrameStyle(QFrame::Panel | QFrame::Sunken); display->setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid->addWidget(display, 0, 0, 1, 5); buttons[0] = new QPushButton("0", this); buttons[1] = new QPushButton("1", this); buttons[2] = new QPushButton("2", this); buttons[3] = new QPushButton("3", this); buttons[4] = new QPushButton("4", this); buttons[5] = new QPushButton("5", this); buttons[6] = new QPushButton("6", this); buttons[7] = new QPushButton("7", this); buttons[8] = new QPushButton("8", this); buttons[9] = new QPushButton("9", this); buttons[PLUS] = new QPushButton("+", this); buttons[MINUS] = new QPushButton("-", this); buttons[STAR] = new QPushButton("X", this); buttons[COMMA] = new QPushButton(m_comma, this); buttons[EQUAL] = new QPushButton("=", this); buttons[SLASH] = new QPushButton("/", this); buttons[CLEAR] = new QPushButton("C", this); buttons[CLEARALL] = new QPushButton("AC", this); buttons[PLUSMINUS] = new QPushButton("+-", this); buttons[PERCENT] = new QPushButton("%", this); grid->addWidget(buttons[7], 1, 0); grid->addWidget(buttons[8], 1, 1); grid->addWidget(buttons[9], 1, 2); grid->addWidget(buttons[4], 2, 0); grid->addWidget(buttons[5], 2, 1); grid->addWidget(buttons[6], 2, 2); grid->addWidget(buttons[1], 3, 0); grid->addWidget(buttons[2], 3, 1); grid->addWidget(buttons[3], 3, 2); grid->addWidget(buttons[0], 4, 1); grid->addWidget(buttons[COMMA], 4, 0); grid->addWidget(buttons[PLUS], 3, 3); grid->addWidget(buttons[MINUS], 4, 3); grid->addWidget(buttons[STAR], 3, 4); grid->addWidget(buttons[SLASH], 4, 4); grid->addWidget(buttons[EQUAL], 4, 2); grid->addWidget(buttons[PLUSMINUS], 2, 3); grid->addWidget(buttons[PERCENT], 2, 4); grid->addWidget(buttons[CLEAR], 1, 3); grid->addWidget(buttons[CLEARALL], 1, 4); buttons[EQUAL]->setFocus(); op1 = 0.0; - stackedOp = op = 0; + stackedOp = op = op0 = 0; operand.clear(); changeDisplay("0"); // connect the digit signals through a signal mapper QSignalMapper* mapper = new QSignalMapper(this); for (int i = 0; i < 10; ++i) { mapper->setMapping(buttons[i], i); connect(buttons[i], SIGNAL(clicked()), mapper, SLOT(map())); } connect(mapper, SIGNAL(mapped(int)), this, SLOT(digitClicked(int))); // connect the calculation operations through another mapper mapper = new QSignalMapper(this); for (int i = PLUS; i <= EQUAL; ++i) { mapper->setMapping(buttons[i], i); connect(buttons[i], SIGNAL(clicked()), mapper, SLOT(map())); } connect(mapper, SIGNAL(mapped(int)), this, SLOT(calculationClicked(int))); // connect all remaining signals connect(buttons[COMMA], SIGNAL(clicked()), SLOT(commaClicked())); connect(buttons[PLUSMINUS], SIGNAL(clicked()), SLOT(plusminusClicked())); connect(buttons[PERCENT], SIGNAL(clicked()), SLOT(percentClicked())); connect(buttons[CLEAR], SIGNAL(clicked()), SLOT(clearClicked())); connect(buttons[CLEARALL], SIGNAL(clicked()), SLOT(clearAllClicked())); for (int i = 0; i < MAX_BUTTONS; ++i) { buttons[i]->setMinimumSize(40, 30); buttons[i]->setMaximumSize(40, 30); } // keep the size determined by the size of the contained buttons no matter what setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); } kMyMoneyCalculator::~kMyMoneyCalculator() { } void kMyMoneyCalculator::digitClicked(int button) { if (m_clearOperandOnDigit) { operand.clear(); m_clearOperandOnDigit = false; } operand += QChar(button + 0x30); if (operand.length() > 16) operand = operand.left(16); changeDisplay(operand); } void kMyMoneyCalculator::commaClicked() { if (operand.length() == 0) operand = '0'; if (operand.contains('.', Qt::CaseInsensitive) == 0) operand.append('.'); if (operand.length() > 16) operand = operand.left(16); changeDisplay(operand); } void kMyMoneyCalculator::plusminusClicked() { if (operand.length() == 0 && m_result.length() > 0) operand = m_result; if (operand.length() > 0) { if (operand.indexOf('-') != -1) operand.remove('-'); else operand.prepend('-'); changeDisplay(operand); } } void kMyMoneyCalculator::calculationClicked(int button) { if (operand.length() == 0 && op != 0 && button == EQUAL) { op = 0; m_result = normalizeString(op1); changeDisplay(m_result); } else if (operand.length() > 0 && op != 0) { // perform operation double op2 = operand.toDouble(); bool error = false; // if the pending operation is addition and we now do multiplication // we just stack op1 and remember the operation in if ((op == PLUS || op == MINUS) && (button == STAR || button == SLASH)) { op0 = op1; stackedOp = op; op = 0; } switch (op) { case PLUS: op2 = op1 + op2; break; case MINUS: op2 = op1 - op2; break; case STAR: op2 = op1 * op2; break; case SLASH: if (op2 == 0.0) error = true; else op2 = op1 / op2; break; } // if we have a pending addition operation, and the next operation is // not multiplication, we calculate the stacked operation if (stackedOp && button != STAR && button != SLASH) { switch (stackedOp) { case PLUS: op2 = op0 + op2; break; case MINUS: op2 = op0 - op2; break; } stackedOp = 0; } if (error) { op = 0; changeDisplay("Error"); operand.clear(); } else { op1 = op2; m_result = normalizeString(op1); changeDisplay(m_result); } } else if (operand.length() > 0 && op == 0) { op1 = operand.toDouble(); m_result = normalizeString(op1); changeDisplay(m_result); } if (button != EQUAL) { op = button; } else { op = 0; emit signalResultAvailable(); } operand.clear(); } QString kMyMoneyCalculator::normalizeString(const double& val) { QString str; str.setNum(val, 'f'); int i = str.length(); while (i > 1 && str[i-1] == '0') { --i; } // cut off trailing 0's str.remove(i, str.length()); if (str.length() > 0) { // possibly remove trailing period if (str[str.length()-1] == '.') { str.remove(str.length() - 1, 1); } } return str; } void kMyMoneyCalculator::clearClicked() { if (operand.length() > 0) { operand = operand.left(operand.length() - 1); } if (operand.length() == 0) changeDisplay("0"); else changeDisplay(operand); } void kMyMoneyCalculator::clearAllClicked() { operand.clear(); op = 0; changeDisplay("0"); } void kMyMoneyCalculator::percentClicked() { if (op != 0) { double op2 = operand.toDouble(); switch (op) { case PLUS: case MINUS: op2 = (op1 * op2) / 100; break; case STAR: case SLASH: op2 /= 100; break; } operand = normalizeString(op2); changeDisplay(operand); } } const QString kMyMoneyCalculator::result() const { QString txt = m_result; txt.replace(QRegExp("\\."), m_comma); if (txt[0] == '-') { txt = txt.mid(1); // get rid of the minus sign QString mask; // TODO: port this to kf5 #if 0 switch (KLocale::global()->negativeMonetarySignPosition()) { case KLocale::ParensAround: mask = "(%1)"; break; case KLocale::AfterQuantityMoney: mask = "%1-"; break; case KLocale::AfterMoney: case KLocale::BeforeMoney: mask = "%1 -"; break; case KLocale::BeforeQuantityMoney: mask = "-%1"; break; } #else mask = "-%1"; #endif txt = QString(mask).arg(txt); } return txt; } void kMyMoneyCalculator::changeDisplay(const QString& str) { QString txt = str; txt.replace(QRegExp("\\."), m_comma); display->setText("" + txt + ""); } void kMyMoneyCalculator::keyPressEvent(QKeyEvent* ev) { int button = -1; switch (ev->key()) { case Qt::Key_0: case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: case Qt::Key_8: case Qt::Key_9: if (m_clearOperandOnDigit) { operand.clear(); m_clearOperandOnDigit = false; } button = ev->key() - Qt::Key_0; break; case Qt::Key_Plus: button = PLUS; break; case Qt::Key_Minus: button = MINUS; break; case Qt::Key_Comma: case Qt::Key_Period: if (m_clearOperandOnDigit) { operand.clear(); m_clearOperandOnDigit = false; } button = COMMA; break; case Qt::Key_Slash: button = SLASH; break; case Qt::Key_Backspace: button = CLEAR; if(ev->modifiers() & Qt::ShiftModifier) { button = CLEARALL; } break; case Qt::Key_Asterisk: button = STAR; break; case Qt::Key_Return: case Qt::Key_Enter: case Qt::Key_Equal: button = EQUAL; break; case Qt::Key_Escape: emit signalQuit(); break; case Qt::Key_Percent: button = PERCENT; break; default: ev->ignore(); break; } if (button != -1) buttons[button]->animateClick(); m_clearOperandOnDigit = false; } void kMyMoneyCalculator::setInitialValues(const QString& value, QKeyEvent* ev) { bool negative = false; // setup operand operand = value; // TODO: port this to kf5 //operand.replace(QRegExp(QString('\\') + ""/* TODO: port to kf5 - KLocale::global()->thousandsSeparator()*/), QChar()); operand.replace(QRegExp(QString('\\') + m_comma), "."); if (operand.contains('(')) { negative = true; operand.remove('('); operand.remove(')'); } if (operand.contains('-')) { negative = true; operand.remove('-'); } if (operand.isEmpty()) operand = '0'; else if (negative) operand = QString("-%1").arg(operand); changeDisplay(operand); // and operation op = 0; if (ev) keyPressEvent(ev); else m_clearOperandOnDigit = true; } diff --git a/kmymoney/widgets/kmymoneycurrencyselector.cpp b/kmymoney/widgets/kmymoneycurrencyselector.cpp index 21f4a5cf5..aa935276b 100644 --- a/kmymoney/widgets/kmymoneycurrencyselector.cpp +++ b/kmymoney/widgets/kmymoneycurrencyselector.cpp @@ -1,168 +1,169 @@ /*************************************************************************** kmymoneycurrencyselector.cpp - description ------------------- begin : Tue Apr 6 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kmymoneycurrencyselector.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes KMyMoneySecuritySelector::KMyMoneySecuritySelector(QWidget *parent) : KComboBox(parent), m_displayItem(FullName), + m_selectedItemId(0), m_displayOnly(false), m_displayType(TypeAll) { // update(QString()); } KMyMoneySecuritySelector::~KMyMoneySecuritySelector() { } void KMyMoneySecuritySelector::selectDisplayItem(KMyMoneySecuritySelector::displayItemE item) { m_displayItem = item; update(QString()); } void KMyMoneySecuritySelector::setDisplayType(displayTypeE type) { m_displayType = type; } void KMyMoneySecuritySelector::update(const QString& id) { MyMoneySecurity curr = MyMoneyFile::instance()->baseCurrency(); QString baseCurrency = curr.id(); if (!id.isEmpty()) curr = m_currency; this->clear(); m_list.clear(); if (m_displayType & TypeCurrencies) m_list += MyMoneyFile::instance()->currencyList(); if (m_displayType & TypeSecurities) m_list += MyMoneyFile::instance()->securityList(); // sort qSort(m_list); QList::ConstIterator it; // construct a transparent 16x16 pixmap static unsigned char empty_png[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0xF3, 0xFF, 0x61, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x06, 0x62, 0x4B, 0x47, 0x44, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xA0, 0xBD, 0xA7, 0x93, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0B, 0x13, 0x00, 0x00, 0x0B, 0x13, 0x01, 0x00, 0x9A, 0x9C, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4D, 0x45, 0x07, 0xDB, 0x07, 0x08, 0x0B, 0x16, 0x09, 0xAA, 0xA8, 0x50, 0x21, 0x00, 0x00, 0x00, 0x12, 0x49, 0x44, 0x41, 0x54, 0x38, 0xCB, 0x63, 0x60, 0x18, 0x05, 0xA3, 0x60, 0x14, 0x8C, 0x02, 0x08, 0x00, 0x00, 0x04, 0x10, 0x00, 0x01, 0x85, 0x3F, 0xAA, 0x72, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; QPixmap empty; empty.loadFromData(empty_png, sizeof(empty_png), 0, Qt::AutoColor); QIcon emptyIcon(empty); int itemId = 0; int m_selectedItemId = 0; for (it = m_list.constBegin(); it != m_list.constEnd(); ++it) { QString display; switch (m_displayItem) { default: case FullName: if ((*it).isCurrency()) { display = QString("%2 (%1)").arg((*it).id()).arg((*it).name()); } else display = QString("%2 (%1)").arg((*it).tradingSymbol()).arg((*it).name()); break; break; case Symbol: if ((*it).isCurrency()) display = (*it).id(); else display = (*it).tradingSymbol(); break; } if ((*it).id() == baseCurrency) { insertItem(itemId, QIcon::fromTheme(QStringLiteral("view-bank-account"), QIcon::fromTheme(QStringLiteral("account"), QIcon::fromTheme(QStringLiteral("unknown")))), display); } else { insertItem(itemId, emptyIcon, display); } if (curr.id() == (*it).id()) { m_selectedItemId = itemId; m_currency = (*it); } itemId++; } setCurrentIndex(m_selectedItemId); } const MyMoneySecurity& KMyMoneySecuritySelector::security() const { int index = currentIndex(); if ((0 <= index) && (index < m_list.size())) return m_list[index]; else return m_currency; } void KMyMoneySecuritySelector::setSecurity(const MyMoneySecurity& currency) { m_currency = currency; update(QString("x")); } KMyMoneyCurrencySelector::KMyMoneyCurrencySelector(QWidget *parent) : KMyMoneySecuritySelector(parent) { setDisplayType(TypeCurrencies); } diff --git a/kmymoney/widgets/kmymoneymvccombo.cpp b/kmymoney/widgets/kmymoneymvccombo.cpp index 29e378f1f..449b98a3c 100644 --- a/kmymoney/widgets/kmymoneymvccombo.cpp +++ b/kmymoney/widgets/kmymoneymvccombo.cpp @@ -1,809 +1,810 @@ /*************************************************************************** kmymoneymvccombo.cpp - description ------------------- begin : Sat Jan 09 2010 copyright : (C) 2010 by Thomas Baumgart Cristian Onet Alvaro Soliverez ***************************************************************************/ /*************************************************************************** * * * 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 "kmymoneymvccombo.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneysettings.h" class KMyMoneyMVCCombo::Private { public: Private() : m_canCreateObjects(false), m_inFocusOutEvent(false), m_completer(0) {} /** * Flag to control object creation. Use * KMyMoneyMVCCombo::setSuppressObjectCreation() * to modify it's setting. Defaults to @a false. */ bool m_canCreateObjects; /** * Flag to check whether a focusOutEvent processing is underway or not */ bool m_inFocusOutEvent; QCompleter *m_completer; }; KMyMoneyMVCCombo::KMyMoneyMVCCombo(QWidget* parent) : KComboBox(parent), d(new Private) { view()->setAlternatingRowColors(true); connect(this, SIGNAL(activated(int)), SLOT(activated(int))); } KMyMoneyMVCCombo::KMyMoneyMVCCombo(bool editable, QWidget* parent) : KComboBox(editable, parent), d(new Private) { d->m_completer = new QCompleter(this); d->m_completer->setCaseSensitivity(Qt::CaseInsensitive); d->m_completer->setModel(model()); setCompleter(d->m_completer); // setSubstringSearch(!KMyMoneySettings::stringMatchFromStart()); view()->setAlternatingRowColors(true); setInsertPolicy(QComboBox::NoInsert); // don't insert new objects due to object creation connect(this, SIGNAL(activated(int)), SLOT(activated(int))); } KMyMoneyMVCCombo::~KMyMoneyMVCCombo() { delete d; } void KMyMoneyMVCCombo::setEditable(bool editable) { KComboBox::setEditable(editable); if(editable) { if(!d->m_completer) { d->m_completer = new QCompleter(this); d->m_completer->setCaseSensitivity(Qt::CaseInsensitive); d->m_completer->setModel(model()); } setCompleter(d->m_completer); } } void KMyMoneyMVCCombo::setSubstringSearch(bool enabled) { d->m_completer->setCompletionMode(QCompleter::PopupCompletion); d->m_completer->setModel(model()); d->m_completer->setFilterMode(enabled ? Qt::MatchContains : Qt::MatchStartsWith); } void KMyMoneyMVCCombo::setSubstringSearchForChildren(QWidget*const widget, bool enabled) { Q_CHECK_PTR(widget); QList comboList; comboList = widget->findChildren(); foreach (KMyMoneyMVCCombo *combo, comboList) { combo->setSubstringSearch(enabled); } } void KMyMoneyMVCCombo::setPlaceholderText(const QString& hint) const { KLineEdit* le = qobject_cast(lineEdit()); if (le) { le->setPlaceholderText(hint); } } const QString& KMyMoneyMVCCombo::selectedItem() const { QVariant data = itemData(currentIndex()); if (data.isValid()) m_id = data.toString(); else m_id.clear(); return m_id; } void KMyMoneyMVCCombo::setSelectedItem(const QString& id) { m_id = id; setCurrentIndex(findData(QVariant(m_id))); } void KMyMoneyMVCCombo::activated(int index) { QVariant data = itemData(index); if (data.isValid()) { m_id = data.toString(); emit itemSelected(m_id); } } void KMyMoneyMVCCombo::connectNotify(const QMetaMethod & signal) { if (signal != QMetaMethod::fromSignal(&KMyMoneyMVCCombo::createItem)) { d->m_canCreateObjects = true; } } void KMyMoneyMVCCombo::disconnectNotify(const QMetaMethod & signal) { if (signal != QMetaMethod::fromSignal(&KMyMoneyMVCCombo::createItem)) { d->m_canCreateObjects = false; } } void KMyMoneyMVCCombo::focusOutEvent(QFocusEvent* e) { // when showing m_completion we'll receive a focus out event even if the focus // will still remain at this widget since this widget is the completion's focus proxy // so ignore the focus out event caused by showin a widget of type Qt::Popup if (e->reason() == Qt::PopupFocusReason) return; if (d->m_inFocusOutEvent) { KComboBox::focusOutEvent(e); return; } //check if the focus went to a widget in TransactionFrom or in the Register if (e->reason() == Qt::MouseFocusReason) { QObject *w = this->parent(); QObject *q = qApp->focusWidget()->parent(); // KMyMoneyTagCombo is inside KTagContainer, KMyMoneyPayeeCombo isn't it if (w->inherits("KTagContainer")) w = w->parent(); while (q && q->objectName() != "qt_scrollarea_viewport") q = q->parent(); if (q != w && qApp->focusWidget()->objectName() != "register") { KComboBox::focusOutEvent(e); return; } } d->m_inFocusOutEvent = true; if (isEditable() && !currentText().isEmpty() && e->reason() != Qt::ActiveWindowFocusReason) { if (d->m_canCreateObjects) { // in case we tab out, we make sure that if the current completion // contains the current text that we set the current text to // the full completion text but only if the completion box is visible. // BUG 254984 is resolved with the visbility check if (e->reason() != Qt::MouseFocusReason) { if (d->m_completer->popup() && d->m_completer->popup()->isVisible() && d->m_completer->currentCompletion().contains(currentText(), Qt::CaseInsensitive)) { lineEdit()->setText(d->m_completer->currentCompletion()); } } //check if the current text is contained in the internal list, if not ask the user if want to create a new item. checkCurrentText(); // else if we cannot create objects, and the current text is not // in the list, then we clear the text and the selection. } else if (!contains(currentText())) { clearEditText(); } //this is to cover the case when you highlight an item but don't activate it with Enter if (currentText() != itemText(currentIndex())) { setCurrentIndex(findText(currentText(), Qt::MatchExactly)); emit activated(currentIndex()); } } KComboBox::focusOutEvent(e); // force update of hint and id if there is no text in the widget if (isEditable() && currentText().isEmpty()) { QString id = m_id; m_id.clear(); if (!id.isEmpty()) emit itemSelected(m_id); update(); } d->m_inFocusOutEvent = false; // This is used only be KMyMoneyTagCombo at this time emit lostFocus(); } void KMyMoneyMVCCombo::checkCurrentText() { if (!contains(currentText())) { QString id; // annouce that we go into a possible dialog to create an object // This can be used by upstream widgets to disable filters etc. emit objectCreation(true); emit createItem(currentText(), id); // Announce that we return from object creation emit objectCreation(false); // update the field to a possibly created object m_id = id; setCurrentTextById(id); } } void KMyMoneyMVCCombo::setCurrentTextById(const QString& id) { clearEditText(); if (!id.isEmpty()) { int index = findData(QVariant(id), Qt::UserRole, Qt::MatchExactly); if (index > -1) { setCompletedText(itemText(index)); setEditText(itemText(index)); setCurrentIndex(index); } } } void KMyMoneyMVCCombo::protectItem(int id, bool protect) { QStandardItemModel* standardModel = qobject_cast (model()); QStandardItem* standardItem = standardModel->item(id); standardItem->setSelectable(!protect); } KMyMoneyPayeeCombo::KMyMoneyPayeeCombo(QWidget* parent) : KMyMoneyMVCCombo(true, parent) { } void KMyMoneyPayeeCombo::loadPayees(const QList& list) { clear(); //add a blank item, since the field is optional addItem(QString(), QVariant(QString())); //add all payees QList::const_iterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { addItem((*it).name(), QVariant((*it).id())); } //sort the model, which will sort the list in the combo model()->sort(Qt::DisplayRole, Qt::AscendingOrder); //set the text to empty and the index to the first item on the list setCurrentIndex(0); clearEditText(); } KMyMoneyTagCombo::KMyMoneyTagCombo(QWidget* parent) : KMyMoneyMVCCombo(true, parent) { } void KMyMoneyTagCombo::loadTags(const QList& list) { clear(); //add a blank item, since the field is optional addItem(QString(), QVariant(QString())); //add all not closed tags QList::const_iterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { if (!(*it).isClosed()) addItem((*it).name(), QVariant((*it).id())); else { m_closedIdList.append((*it).id()); m_closedTagNameList.append((*it).name()); } } //sort the model, which will sort the list in the combo model()->sort(Qt::DisplayRole, Qt::AscendingOrder); //set the text to empty and the index to the first item on the list setCurrentIndex(0); clearEditText(); } void KMyMoneyTagCombo::setUsedTagList(QList& usedIdList, QList& usedTagNameList) { m_usedIdList = usedIdList; m_usedTagNameList = usedTagNameList; for (int i = 0; i < m_usedIdList.size(); ++i) { int index = findData(QVariant(m_usedIdList.at(i)), Qt::UserRole, Qt::MatchExactly); if (index != -1) removeItem(index); } } void KMyMoneyTagCombo::checkCurrentText() { if (!contains(currentText())) { if (m_closedTagNameList.contains(currentText())) { // Tell the user what's happened QString msg = QString("") + i18n("Closed tags cannot be used.") + QString(""); KMessageBox::information(this, msg, i18n("Closed tag"), "Closed tag"); setCurrentText(); return; } else if (m_usedTagNameList.contains(currentText())) { // Tell the user what's happened QString msg = QString("") + i18n("The tag is already present.") + QString(""); KMessageBox::information(this, msg, i18n("Duplicate tag"), "Duplicate tag"); setCurrentText(); return; } QString id; // annouce that we go into a possible dialog to create an object // This can be used by upstream widgets to disable filters etc. emit objectCreation(true); emit createItem(currentText(), id); // Announce that we return from object creation emit objectCreation(false); // update the field to a possibly created object //m_id = id; setCurrentTextById(id); } } KTagLabel::KTagLabel(const QString& id, const QString& name, QWidget* parent) : QFrame(parent) { QToolButton *t = new QToolButton(this); t->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); t->setAutoRaise(true); QLabel *l = new QLabel(name, this); m_tagId = id; QHBoxLayout *layout = new QHBoxLayout; layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); this->setLayout(layout); layout->addWidget(t); layout->addWidget(l); connect(t, SIGNAL(clicked(bool)), this, SIGNAL(clicked(bool))); //this->setFrameStyle(QFrame::Panel | QFrame::Plain); } KTagContainer::KTagContainer(QWidget* parent) : QWidget(parent) { m_tagCombo = new KMyMoneyTagCombo; QHBoxLayout *layout = new QHBoxLayout; layout->setContentsMargins(0, 0, 5, 0); layout->setSpacing(0); layout->addWidget(m_tagCombo, 100); this->setLayout(layout); this->setFocusProxy(m_tagCombo); connect(m_tagCombo, SIGNAL(lostFocus()), this, SLOT(slotAddTagWidget())); } void KTagContainer::loadTags(const QList& list) { m_list = list; m_tagCombo->loadTags(list); } const QList KTagContainer::selectedTags() { return m_tagIdList; } void KTagContainer::addTagWidget(const QString& id) { if (id.isNull() || m_tagIdList.contains(id)) return; const QString tagName = m_tagCombo->itemText(m_tagCombo->findData(QVariant(id), Qt::UserRole, Qt::MatchExactly)); KTagLabel *t = new KTagLabel(id, tagName, this); connect(t, SIGNAL(clicked(bool)), this, SLOT(slotRemoveTagWidget())); m_tagLabelList.append(t); m_tagNameList.append(tagName); m_tagIdList.append(id); this->layout()->addWidget(t); m_tagCombo->loadTags(m_list); m_tagCombo->setUsedTagList(m_tagIdList, m_tagNameList); m_tagCombo->setCurrentIndex(0); m_tagCombo->setFocus(); } void KTagContainer::RemoveAllTagWidgets() { m_tagIdList.clear(); m_tagNameList.clear(); while (!m_tagLabelList.isEmpty()) delete m_tagLabelList.takeLast(); m_tagCombo->loadTags(m_list); m_tagCombo->setUsedTagList(m_tagIdList, m_tagNameList); m_tagCombo->setCurrentIndex(0); } void KTagContainer::slotAddTagWidget() { addTagWidget(m_tagCombo->selectedItem()); } void KTagContainer::slotRemoveTagWidget() { this->tagCombo()->setFocus(); KTagLabel *t = (KTagLabel *)sender(); int index = m_tagLabelList.indexOf(t); m_tagLabelList.removeAt(index); m_tagIdList.removeAt(index); m_tagNameList.removeAt(index); delete t; m_tagCombo->loadTags(m_list); m_tagCombo->setUsedTagList(m_tagIdList, m_tagNameList); m_tagCombo->setCurrentIndex(0); } KMyMoneyReconcileCombo::KMyMoneyReconcileCombo(QWidget* w) : KMyMoneyMVCCombo(false, w) { // add the items in reverse order of appearance (see KMyMoneySelector::newItem() for details) addItem(i18n("Reconciled"), QVariant("R")); addItem(i18nc("Reconciliation state 'Cleared'", "Cleared"), QVariant("C")); addItem(i18n("Not reconciled"), QVariant(" ")); addItem(" ", QVariant("U")); connect(this, SIGNAL(itemSelected(QString)), this, SLOT(slotSetState(QString))); } void KMyMoneyReconcileCombo::slotSetState(const QString& state) { setSelectedItem(state); } void KMyMoneyReconcileCombo::removeDontCare() { //Remove unknown state removeItem(3); } void KMyMoneyReconcileCombo::setState(MyMoneySplit::reconcileFlagE state) { QString id; switch (state) { case MyMoneySplit::NotReconciled: id = ' '; break; case MyMoneySplit::Cleared: id = 'C'; break; case MyMoneySplit::Reconciled: id = 'R'; break; case MyMoneySplit::Frozen: id = 'F'; break; case MyMoneySplit::Unknown: id = 'U'; break; default: qDebug() << "Unknown reconcile state '" << state << "' in KMyMoneyReconcileCombo::setState()\n"; break; } setSelectedItem(id); } MyMoneySplit::reconcileFlagE KMyMoneyReconcileCombo::state() const { MyMoneySplit::reconcileFlagE state = MyMoneySplit::NotReconciled; QVariant data = itemData(currentIndex()); QString dataVal; if (data.isValid()) dataVal = data.toString(); else return state; if (!dataVal.isEmpty()) { if (dataVal == "C") state = MyMoneySplit::Cleared; if (dataVal == "R") state = MyMoneySplit::Reconciled; if (dataVal == "F") state = MyMoneySplit::Frozen; if (dataVal == "U") state = MyMoneySplit::Unknown; } return state; } KMyMoneyCashFlowCombo::KMyMoneyCashFlowCombo(QWidget* w, MyMoneyAccount::accountTypeE accountType) : - KMyMoneyMVCCombo(false, w) + KMyMoneyMVCCombo(false, w), + m_dir(KMyMoneyRegister::Unknown) { addItem(" ", QVariant(KMyMoneyRegister::Unknown)); if (accountType == MyMoneyAccount::Income || accountType == MyMoneyAccount::Expense) { // this is used for income/expense accounts to just show the reverse sense addItem(i18nc("Activity for income categories", "Received"), QVariant(KMyMoneyRegister::Payment)); addItem(i18nc("Activity for expense categories", "Paid"), QVariant(KMyMoneyRegister::Deposit)); } else { addItem(i18n("Pay to"), QVariant(KMyMoneyRegister::Payment)); addItem(i18n("From"), QVariant(KMyMoneyRegister::Deposit)); } connect(this, SIGNAL(itemSelected(QString)), this, SLOT(slotSetDirection(QString))); } void KMyMoneyCashFlowCombo::setDirection(KMyMoneyRegister::CashFlowDirection dir) { m_dir = dir; QString num; setSelectedItem(num.setNum(dir)); } void KMyMoneyCashFlowCombo::slotSetDirection(const QString& id) { QString num; for (int i = KMyMoneyRegister::Deposit; i <= KMyMoneyRegister::Unknown; ++i) { num.setNum(i); if (num == id) { m_dir = static_cast(i); break; } } emit directionSelected(m_dir); update(); } void KMyMoneyCashFlowCombo::removeDontCare() { removeItem(findData(QVariant(KMyMoneyRegister::Unknown), Qt::UserRole, Qt::MatchExactly)); } KMyMoneyActivityCombo::KMyMoneyActivityCombo(QWidget* w) : KMyMoneyMVCCombo(false, w), m_activity(MyMoneySplit::UnknownTransactionType) { addItem(i18n("Buy shares"), QVariant(MyMoneySplit::BuyShares)); addItem(i18n("Sell shares"), QVariant(MyMoneySplit::SellShares)); addItem(i18n("Dividend"), QVariant(MyMoneySplit::Dividend)); addItem(i18n("Reinvest dividend"), QVariant(MyMoneySplit::ReinvestDividend)); addItem(i18n("Yield"), QVariant(MyMoneySplit::Yield)); addItem(i18n("Add shares"), QVariant(MyMoneySplit::AddShares)); addItem(i18n("Remove shares"), QVariant(MyMoneySplit::RemoveShares)); addItem(i18n("Split shares"), QVariant(MyMoneySplit::SplitShares)); addItem(i18n("Interest Income"), QVariant(MyMoneySplit::InterestIncome)); connect(this, SIGNAL(itemSelected(QString)), this, SLOT(slotSetActivity(QString))); } void KMyMoneyActivityCombo::setActivity(MyMoneySplit::investTransactionTypeE activity) { m_activity = activity; QString num; setSelectedItem(num.setNum(activity)); } void KMyMoneyActivityCombo::slotSetActivity(const QString& id) { QString num; for (int i = MyMoneySplit::BuyShares; i <= MyMoneySplit::InterestIncome; ++i) { num.setNum(i); if (num == id) { m_activity = static_cast(i); break; } } emit activitySelected(m_activity); update(); } KMyMoneyGeneralCombo::KMyMoneyGeneralCombo(QWidget* w) : KComboBox(w) { connect(this, SIGNAL(highlighted(int)), this, SLOT(slotChangeItem(int))); } KMyMoneyGeneralCombo::~KMyMoneyGeneralCombo() { } void KMyMoneyGeneralCombo::setCurrentItem(int id) { setCurrentIndex(findData(QVariant(id), Qt::UserRole, Qt::MatchExactly)); } int KMyMoneyGeneralCombo::currentItem() const { return itemData(currentIndex()).toInt(); } void KMyMoneyGeneralCombo::clear() { KComboBox::clear(); } void KMyMoneyGeneralCombo::insertItem(const QString& txt, int id, int idx) { KComboBox::insertItem(idx, txt, QVariant(id)); } void KMyMoneyGeneralCombo::removeItem(int id) { KComboBox::removeItem(findData(QVariant(id), Qt::UserRole, Qt::MatchExactly)); } void KMyMoneyGeneralCombo::slotChangeItem(int idx) { emit itemSelected(itemData(idx).toInt()); } KMyMoneyPeriodCombo::KMyMoneyPeriodCombo(QWidget* parent) : KMyMoneyGeneralCombo(parent) { insertItem(i18n("All dates"), MyMoneyTransactionFilter::allDates); insertItem(i18n("As of today"), MyMoneyTransactionFilter::asOfToday); insertItem(i18n("Today"), MyMoneyTransactionFilter::today); insertItem(i18n("Current month"), MyMoneyTransactionFilter::currentMonth); insertItem(i18n("Current quarter"), MyMoneyTransactionFilter::currentQuarter); insertItem(i18n("Current year"), MyMoneyTransactionFilter::currentYear); insertItem(i18n("Current fiscal year"), MyMoneyTransactionFilter::currentFiscalYear); insertItem(i18n("Month to date"), MyMoneyTransactionFilter::monthToDate); insertItem(i18n("Year to date"), MyMoneyTransactionFilter::yearToDate); insertItem(i18n("Year to month"), MyMoneyTransactionFilter::yearToMonth); insertItem(i18n("Last month"), MyMoneyTransactionFilter::lastMonth); insertItem(i18n("Last year"), MyMoneyTransactionFilter::lastYear); insertItem(i18n("Last fiscal year"), MyMoneyTransactionFilter::lastFiscalYear); insertItem(i18n("Last 7 days"), MyMoneyTransactionFilter::last7Days); insertItem(i18n("Last 30 days"), MyMoneyTransactionFilter::last30Days); insertItem(i18n("Last 3 months"), MyMoneyTransactionFilter::last3Months); insertItem(i18n("Last quarter"), MyMoneyTransactionFilter::lastQuarter); insertItem(i18n("Last 6 months"), MyMoneyTransactionFilter::last6Months); insertItem(i18n("Last 11 months"), MyMoneyTransactionFilter::last11Months); insertItem(i18n("Last 12 months"), MyMoneyTransactionFilter::last12Months); insertItem(i18n("Next 7 days"), MyMoneyTransactionFilter::next7Days); insertItem(i18n("Next 30 days"), MyMoneyTransactionFilter::next30Days); insertItem(i18n("Next 3 months"), MyMoneyTransactionFilter::next3Months); insertItem(i18n("Next quarter"), MyMoneyTransactionFilter::nextQuarter); insertItem(i18n("Next 6 months"), MyMoneyTransactionFilter::next6Months); insertItem(i18n("Next 12 months"), MyMoneyTransactionFilter::next12Months); insertItem(i18n("Next 18 months"), MyMoneyTransactionFilter::next18Months); insertItem(i18n("Last 3 months to next 3 months"), MyMoneyTransactionFilter::last3ToNext3Months); insertItem(i18n("User defined"), MyMoneyTransactionFilter::userDefined); } void KMyMoneyPeriodCombo::setCurrentItem(MyMoneyTransactionFilter::dateOptionE id) { if (id >= MyMoneyTransactionFilter::dateOptionCount) id = MyMoneyTransactionFilter::userDefined; KMyMoneyGeneralCombo::setCurrentItem(id); } MyMoneyTransactionFilter::dateOptionE KMyMoneyPeriodCombo::currentItem() const { return static_cast(KMyMoneyGeneralCombo::currentItem()); } QDate KMyMoneyPeriodCombo::start(MyMoneyTransactionFilter::dateOptionE id) { QDate start, end; MyMoneyTransactionFilter::translateDateRange(id, start, end); return start; } QDate KMyMoneyPeriodCombo::end(MyMoneyTransactionFilter::dateOptionE id) { QDate start, end; MyMoneyTransactionFilter::translateDateRange(id, start, end); return end; } #if 0 void KMyMoneyPeriodCombo::dates(QDate& start, QDate& end, MyMoneyTransactionFilter::dateOptionE id) { } #endif KMyMoneyOccurrenceCombo::KMyMoneyOccurrenceCombo(QWidget* parent) : KMyMoneyGeneralCombo(parent) { } MyMoneySchedule::occurrenceE KMyMoneyOccurrenceCombo::currentItem() const { return static_cast(KMyMoneyGeneralCombo::currentItem()); } KMyMoneyOccurrencePeriodCombo::KMyMoneyOccurrencePeriodCombo(QWidget* parent) : KMyMoneyOccurrenceCombo(parent) { addItem(i18nc("Schedule occurrence period", MyMoneySchedule::occurrencePeriodToString(MyMoneySchedule::OCCUR_ONCE).toLatin1()), MyMoneySchedule::OCCUR_ONCE); addItem(i18nc("Schedule occurrence period", MyMoneySchedule::occurrencePeriodToString(MyMoneySchedule::OCCUR_DAILY).toLatin1()), MyMoneySchedule::OCCUR_DAILY); addItem(i18nc("Schedule occurrence period", MyMoneySchedule::occurrencePeriodToString(MyMoneySchedule::OCCUR_WEEKLY).toLatin1()), MyMoneySchedule::OCCUR_WEEKLY); addItem(i18nc("Schedule occurrence period", MyMoneySchedule::occurrencePeriodToString(MyMoneySchedule::OCCUR_EVERYHALFMONTH).toLatin1()), MyMoneySchedule::OCCUR_EVERYHALFMONTH); addItem(i18nc("Schedule occurrence period", MyMoneySchedule::occurrencePeriodToString(MyMoneySchedule::OCCUR_MONTHLY).toLatin1()), MyMoneySchedule::OCCUR_MONTHLY); addItem(i18nc("Schedule occurrence period", MyMoneySchedule::occurrencePeriodToString(MyMoneySchedule::OCCUR_YEARLY).toLatin1()), MyMoneySchedule::OCCUR_YEARLY); } KMyMoneyFrequencyCombo::KMyMoneyFrequencyCombo(QWidget* parent) : KMyMoneyOccurrenceCombo(parent) { addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_ONCE).toLatin1()), MyMoneySchedule::OCCUR_ONCE); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_DAILY).toLatin1()), MyMoneySchedule::OCCUR_DAILY); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_WEEKLY).toLatin1()), MyMoneySchedule::OCCUR_WEEKLY); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYOTHERWEEK).toLatin1()), MyMoneySchedule::OCCUR_EVERYOTHERWEEK); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYHALFMONTH).toLatin1()), MyMoneySchedule::OCCUR_EVERYHALFMONTH); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYTHREEWEEKS).toLatin1()), MyMoneySchedule::OCCUR_EVERYTHREEWEEKS); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYTHIRTYDAYS).toLatin1()), MyMoneySchedule::OCCUR_EVERYTHIRTYDAYS); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYFOURWEEKS).toLatin1()), MyMoneySchedule::OCCUR_EVERYFOURWEEKS); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_MONTHLY).toLatin1()), MyMoneySchedule::OCCUR_MONTHLY); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYEIGHTWEEKS).toLatin1()), MyMoneySchedule::OCCUR_EVERYEIGHTWEEKS); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYOTHERMONTH).toLatin1()), MyMoneySchedule::OCCUR_EVERYOTHERMONTH); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYTHREEMONTHS).toLatin1()), MyMoneySchedule::OCCUR_EVERYTHREEMONTHS); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYFOURMONTHS).toLatin1()), MyMoneySchedule::OCCUR_EVERYFOURMONTHS); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_TWICEYEARLY).toLatin1()), MyMoneySchedule::OCCUR_TWICEYEARLY); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_YEARLY).toLatin1()), MyMoneySchedule::OCCUR_YEARLY); addItem(i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(MyMoneySchedule::OCCUR_EVERYOTHERYEAR).toLatin1()), MyMoneySchedule::OCCUR_EVERYOTHERYEAR); connect(this, SIGNAL(currentIndexChanged(int)), this, SLOT(slotCurrentDataChanged())); } int KMyMoneyFrequencyCombo::daysBetweenEvents() const { return MyMoneySchedule::daysBetweenEvents(currentItem()); } int KMyMoneyFrequencyCombo::eventsPerYear() const { return MyMoneySchedule::eventsPerYear(currentItem()); } QVariant KMyMoneyFrequencyCombo::currentData() const { return itemData(currentIndex(), Qt::UserRole); } void KMyMoneyFrequencyCombo::setCurrentData(QVariant data) { setItemData(currentIndex(), data, Qt::UserRole); } void KMyMoneyFrequencyCombo::slotCurrentDataChanged() { emit currentDataChanged(currentData()); } #include "moc_kmymoneymvccombo.cpp" diff --git a/kmymoney/widgets/registeritem.cpp b/kmymoney/widgets/registeritem.cpp index 157a7799d..553cb616b 100644 --- a/kmymoney/widgets/registeritem.cpp +++ b/kmymoney/widgets/registeritem.cpp @@ -1,122 +1,128 @@ /*************************************************************************** registeritem.cpp - description ------------------- begin : Tue Jun 13 2006 copyright : (C) 2000-2006 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 "registeritem.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "register.h" #include "kmymoneyglobalsettings.h" using namespace KMyMoneyRegister; QDate RegisterItem::nullDate; QString RegisterItem::nullString; MyMoneyMoney RegisterItem::nullValue; RegisterItem::RegisterItem() : m_parent(0), m_prev(0), - m_next(0) + m_next(0), + m_alternate(false), + m_needResize(false), + m_visible(false) { init(); } RegisterItem::RegisterItem(Register* parent) : m_parent(parent), m_prev(0), - m_next(0) + m_next(0), + m_alternate(false), + m_needResize(false), + m_visible(false) { init(); parent->addItem(this); } void RegisterItem::init() { m_startRow = 0; m_rowsRegister = 1; m_rowsForm = 1; m_visible = true; } RegisterItem::~RegisterItem() { m_parent->removeItem(this); } void RegisterItem::setParent(Register* parent) { m_parent = parent; } void RegisterItem::setNumRowsRegister(int rows) { if (rows != m_rowsRegister) { m_rowsRegister = rows; if (m_parent) m_parent->forceUpdateLists(); } } bool RegisterItem::markVisible(bool visible) { if (m_visible == visible) return false; m_visible = visible; return true; } void RegisterItem::setVisible(bool visible) { if (markVisible(visible) && m_parent) { int numRows = m_parent->rowCount(); if (visible) { for (int i = startRow(); i < startRow() + numRowsRegister(); ++i) { if (numRows > i) { m_parent->showRow(i); m_parent->setRowHeight(i, rowHeightHint()); } } } else { for (int i = startRow(); i < startRow() + numRowsRegister(); ++i) { if (numRows > i) { m_parent->hideRow(i); } } } } } int RegisterItem::rowHeightHint() const { if (!m_visible) return 0; if (m_parent) { return m_parent->rowHeightHint(); } QFontMetrics fm(KMyMoneyGlobalSettings::listCellFont()); return fm.lineSpacing() + 6; } diff --git a/libkgpgfile/kgpgfile.cpp b/libkgpgfile/kgpgfile.cpp index 169f37701..e435c9079 100644 --- a/libkgpgfile/kgpgfile.cpp +++ b/libkgpgfile/kgpgfile.cpp @@ -1,460 +1,459 @@ /*************************************************************************** kgpgfile.cpp ------------------- copyright : (C) 2004,2005,2009 by Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * 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 // ---------------------------------------------------------------------------- // Project Includes #include "kgpgfile.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #ifdef Gpgmepp_FOUND #include #include #include #include #include #include -#include class KGPGFile::Private { public: Private() { m_fileRead = 0; m_fileWrite = 0; GpgME::initializeLibrary(); ctx = GpgME::Context::createForProtocol(GpgME::OpenPGP); if (!ctx) qDebug("Failed to create the GpgME context for the OpenPGP protocol"); } ~Private() { delete ctx; } QString m_fn; QFile* m_fileRead; QSaveFile* m_fileWrite; GpgME::Error m_lastError; GpgME::Context* ctx; GpgME::Data m_data; std::vector< GpgME::Key > m_recipients; // the result set of the last key list job std::vector< GpgME::Key > m_keys; }; KGPGFile::KGPGFile(const QString& fn, const QString& homedir, const QString& options) : d(new Private) { // only kept for interface compatibility Q_UNUSED(homedir); Q_UNUSED(options); KGPGFile::setFileName(fn); } KGPGFile::~KGPGFile() { close(); delete d; } void KGPGFile::setFileName(const QString& fn) { d->m_fn = fn; if (!fn.isEmpty() && fn[0] == '~') { d->m_fn = QDir::homePath() + fn.mid(1); } else if (QDir::isRelativePath(d->m_fn)) { QDir dir(fn); d->m_fn = dir.absolutePath(); } // qDebug("setName: '%s'", d->m_fn.toLatin1().data()); } void KGPGFile::flush() { // no functionality } void KGPGFile::addRecipient(const QString& recipient) { // skip a possible leading 0x in the id QString cmp = recipient; if (cmp.startsWith(QLatin1String("0x"))) cmp = cmp.mid(2); QStringList keylist; keyList(keylist, false, cmp); if (d->m_keys.size() > 0) d->m_recipients.push_back(d->m_keys.front()); } bool KGPGFile::open(OpenMode mode) { if (isOpen()) { return false; } if (d->m_fn.isEmpty()) { setOpenMode(NotOpen); return false; } if (!d->ctx) { setOpenMode(NotOpen); return false; } setOpenMode(mode); if (!(isReadable() || isWritable())) { setOpenMode(NotOpen); return false; } if (isWritable()) { if (d->m_recipients.empty()) { setOpenMode(NotOpen); return false; } // write out in ASCII armor mode d->ctx->setArmor(true); d->m_fileWrite = new QSaveFile; } else if (isReadable()) { d->m_fileRead = new QFile; } // open the 'physical' file // Since some of the methods in QFile are not virtual, we need to // differentiate here between the QFile* and the QSaveFile* case if (isReadable()) { d->m_fileRead->setFileName(d->m_fn); if (!d->m_fileRead->open(mode)) { setOpenMode(NotOpen); return false; } GpgME::Data dcipher(d->m_fileRead->handle()); d->m_lastError = d->ctx->decrypt(dcipher, d->m_data).error(); if (d->m_lastError.encodedError()) { return false; } d->m_data.seek(0, SEEK_SET); } else if (isWritable()) { d->m_fileWrite->setFileName(d->m_fn); if (!d->m_fileWrite->open(mode)) { setOpenMode(NotOpen); return false; } } return true; } void KGPGFile::close() { if (!isOpen()) { return; } if (!d->ctx) return; if (isWritable()) { d->m_data.seek(0, SEEK_SET); GpgME::Data dcipher(d->m_fileWrite->handle()); d->m_lastError = d->ctx->encrypt(d->m_recipients, d->m_data, dcipher, GpgME::Context::AlwaysTrust).error(); if (d->m_lastError.encodedError()) { setErrorString(QLatin1String("Failure while writing temporary file for file: '") + QLatin1String(d->m_lastError.asString()) + QLatin1String("'")); } else if (!d->m_fileWrite->commit()) { setErrorString("Failure while commiting file changes."); } } delete d->m_fileWrite; delete d->m_fileRead; d->m_fileWrite = 0; d->m_fileRead = 0; d->m_recipients.clear(); setOpenMode(NotOpen); } qint64 KGPGFile::writeData(const char *data, qint64 maxlen) { if (!isOpen()) return EOF; if (!isWritable()) return EOF; // qDebug("write %d bytes", qint32(maxlen & 0xFFFFFFFF)); // write out the data and make sure that we do not cross // size_t boundaries. qint64 bytesWritten = 0; while (maxlen) { qint64 len = 2 ^ 31; if (len > maxlen) len = maxlen; bytesWritten += d->m_data.write(data, len); data = &data[len]; maxlen -= len; } // qDebug("%d bytes written", qint32(bytesWritten & 0xFFFFFFFF)); return bytesWritten; } qint64 KGPGFile::readData(char *data, qint64 maxlen) { if (maxlen == 0) return 0; if (!isOpen()) return EOF; if (!isReadable()) return EOF; // read requested block of data and make sure that we do not cross // size_t boundaries. qint64 bytesRead = 0; while (maxlen) { qint64 len = 2 ^ 31; if (len > maxlen) len = maxlen; bytesRead += d->m_data.read(data, len); data = &data[len]; maxlen -= len; } return bytesRead; } QString KGPGFile::errorToString() const { return QString::fromUtf8(d->m_lastError.asString()); } bool KGPGFile::GPGAvailable() { GpgME::initializeLibrary(); bool rc = (GpgME::checkEngine(GpgME::OpenPGP) == 0); // qDebug("KGPGFile::GPGAvailable returns %d", rc); return rc; } bool KGPGFile::keyAvailable(const QString& name) { KGPGFile file; QStringList keys; file.keyList(keys, false, name); // qDebug("keyAvailable returns %d for '%s'", keys.count(), qPrintable(name)); return keys.count() != 0; } void KGPGFile::publicKeyList(QStringList& list) { // qDebug("Reading public keys"); KGPGFile file; file.keyList(list); } void KGPGFile::secretKeyList(QStringList& list) { // qDebug("Reading secrect keys"); KGPGFile file; file.keyList(list, true); } QDateTime KGPGFile::keyExpires(const QString& name) { QDateTime expirationDate; // skip a possible leading 0x in the id QString cmp = name; if (cmp.startsWith(QLatin1String("0x"))) cmp = cmp.mid(2); QStringList keylist; keyList(keylist, false, cmp); // in case we have no or more than one matching key // or the key does not have subkeys, we return an invalid date if (d->m_keys.size() == 1 && d->m_keys[0].subkeys().size() > 0 && !d->m_keys[0].subkeys()[0].neverExpires()) { expirationDate.setTime_t(d->m_keys[0].subkeys()[0].expirationTime()); } return expirationDate; } void KGPGFile::keyList(QStringList& list, bool secretKeys, const QString& pattern) { d->m_keys.clear(); list.clear(); if (d->ctx && !d->ctx->startKeyListing(pattern.toUtf8().constData(), secretKeys)) { GpgME::Error error; for (;;) { GpgME::Key key; key = d->ctx->nextKey(error); if (error.encodedError() != GPG_ERR_NO_ERROR) break; bool needPushBack = true; std::vector userIDs = key.userIDs(); std::vector subkeys = key.subkeys(); for (unsigned int i = 0; i < userIDs.size(); ++i) { if (subkeys.size() > 0) { for (unsigned int j = 0; j < subkeys.size(); ++j) { const GpgME::Subkey& skey = subkeys[j]; if (((skey.canEncrypt() && !secretKeys) || (skey.isSecret() && secretKeys)) && !(skey.isRevoked() || skey.isExpired() || skey.isInvalid() || skey.isDisabled())) { QString entry = QString("%1:%2").arg(key.shortKeyID()).arg(userIDs[i].id()); list += entry; if (needPushBack) { d->m_keys.push_back(key); needPushBack = false; } } else { // qDebug("Skip key '%s'", key.shortKeyID()); } } } else { // we have no subkey, so we operate on the main key if (((key.canEncrypt() && !secretKeys) || (key.hasSecret() && secretKeys)) && !(key.isRevoked() || key.isExpired() || key.isInvalid() || key.isDisabled())) { QString entry = QString("%1:%2").arg(key.shortKeyID()).arg(userIDs[i].id()); list += entry; if (needPushBack) { d->m_keys.push_back(key); needPushBack = false; } } else { // qDebug("Skip key '%s'", key.shortKeyID()); } } } } d->ctx->endKeyListing(); } } #else // not Gpgmepp_FOUND // NOOP implementation KGPGFile::KGPGFile(const QString& fn, const QString& homedir, const QString& options) : d(0) { Q_UNUSED(fn); Q_UNUSED(homedir); Q_UNUSED(options); } KGPGFile::~KGPGFile() { } bool KGPGFile::open(OpenMode mode) { Q_UNUSED(mode); return false; } void KGPGFile::close() { } void KGPGFile::flush() { } qint64 KGPGFile::readData(char *data, qint64 maxlen) { Q_UNUSED(data); Q_UNUSED(maxlen); return 0; } qint64 KGPGFile::writeData(const char *data, qint64 maxlen) { Q_UNUSED(data); Q_UNUSED(maxlen); return 0; } void KGPGFile::addRecipient(const QString& recipient) { Q_UNUSED(recipient); } QString KGPGFile::errorToString() const { return QString(); } bool KGPGFile::GPGAvailable(void) { return false; } bool KGPGFile::keyAvailable(const QString& name) { Q_UNUSED(name); return false; } void KGPGFile::secretKeyList(QStringList& list) { Q_UNUSED(list); } void KGPGFile::publicKeyList(QStringList& list) { Q_UNUSED(list); } QDateTime KGPGFile::keyExpires(const QString& name) { Q_UNUSED(name); return QDateTime(); } #endif diff --git a/maintainer/release-windows-packages b/maintainer/release-windows-packages new file mode 100755 index 000000000..f3037c4d6 --- /dev/null +++ b/maintainer/release-windows-packages @@ -0,0 +1,198 @@ +#!/bin/sh +# +# unpack windows rpm's from opensuse download server, upload files to kde.org and file a related release ticket +# +# Author: Ralf Habacker +# +# requirements: +# +# osc - opensuse build service command line client +# +# syntax: release-windows-packages +# +# run ./release-windows-packages to see all modes +# +NAME=kmymoney +ROOT=home\:rhabacker\:branches\: + +PACKAGENAME32=mingw32-$NAME +ROOT32=${ROOT}windows\:mingw\:win32\:kmymoney +SRCROOT32=${ROOT32} +ARCHOUT32=i686-w64-mingw32 + +PACKAGENAME64=mingw64-$NAME +#ROOT64=${ROOT}windows\:mingw\:win64\:KDE4 +#SRCROOT64=windows\:mingw\:win64 +#ARCHOUT64=x86_64-w64-mingw32 + +REPO=openSUSE_Leap_42.2 +SRCREPO=$REPO +VERSION=4.8.1 + +PHABURL=https://phabricator.kde.org +oscoptions="-A https://api.opensuse.org" +apitoken=cli-uxo23l4q5qrzoyscbz5kp4zcngqp +options='projectPHIDs[]=PHID-PROJ-3qa4tomwgrmcmp4ym2ow' + +self=$(realpath $0) + +if ! test -d "work"; then + mkdir work +fi + +case $1 in +clean) ## clean working area + rm -rf work/* + ;; + +download) ## download rpm packages + cd work + rm -rf binaries + osc $oscoptions getbinaries $ROOT32 $PACKAGENAME32-installer $REPO x86_64 + if test -n "$ROOT64"; then + osc $oscoptions getbinaries $ROOT64 $PACKAGENAME64-installer $REPO x86_64 + fi + cd .. + $self downloadsrc + touch work/$1.finished + ;; + +downloadsrc) ## download source + cd work + # fetch source package + src32pkg=$(osc $oscoptions ls -b -r $SRCREPO -a x86_64 $SRCROOT32 $PACKAGENAME32 | grep src) + osc $oscoptions getbinaries --sources $SRCROOT32 $PACKAGENAME32 $SRCREPO x86_64 $src32pkg + # we only need once source package + #src64pkg=$(osc $oscoptions ls -b -r $SRCREPO -a x86_64 $SRCROOT64 mingw64-umbrello | grep src) + #osc $oscoptions getbinaries --sources $SRCROOT64 mingw64-umbrello $SRCREPO x86_64 $src64pkg + # fetch debug packages + debug32pkg=$(osc $oscoptions ls -b -r $SRCREPO -a x86_64 $SRCROOT32 $PACKAGENAME32 | grep debug) + osc $oscoptions getbinaries $SRCROOT32 $PACKAGENAME32 $SRCREPO x86_64 $debug32pkg + if test -n "$ROOT64"; then + debug64pkg=$(osc $oscoptions ls -b -r $SRCREPO -a x86_64 $SRCROOT64 $PACKAGENAME64 | grep debug) + osc $oscoptions getbinaries $SRCROOT64 $PACKAGENAME64 $SRCREPO x86_64 $debug64pkg + fi + touch $1.finished + ;; + +unpack) ## unpack rpm files + cd work + files=$(cd binaries; find -name *installer* -o -name *portable* -o -name *src* -o -name *debug* | grep "$VERSION" | sed 's,^.,binaries,g') + if test -d tmp; then + rm -rf tmp + fi + mkdir -p tmp + for i in $(echo $files); do + (cd tmp; rpm2cpio ../$i | cpio -idmv) + done + touch $1.finished + ;; + +movepackage) ## move windows binary packages into upload folder + cd work + rm -rf out + mkdir -p out + find tmp/ -type f -name '*.exe' -exec cp {} out \; + find tmp/ -type f -name '*.7z' -exec cp {} out \; + touch $1.finished + ;; + +packdebug) ## package debug package + cd work + rm -rf out/*debug*.7z + arch=mingw32 + version=$(find binaries/ -name *$arch*debug* | sed 's,.*debug-,,g;s,\.noarch.*,,g') + if test -z "$version"; then + echo no version found + exit 1; + fi + dir=tmp/usr/i686-w64-mingw32/sys-root/mingw + outfile=$PWD/out/$NAME-$ARCHOUT32-$version-debug.7z + (cd $dir; 7z a -r -mx=9 $outfile *.debug *.sym) + + if test -n "$ROOT64"; then + arch=mingw64 + version=$(find binaries/ -name *$arch*debug* | sed 's,.*debug-,,g;s,\.noarch.*,,g') + dir=tmp/usr/x86_64-w64-mingw32/sys-root/mingw + outfile=$PWD/out/$NAME-$ARCHOUT64-$version-debug.7z + (cd $dir; 7z a -r -mx=9 $outfile *.debug *.sym) + fi + touch $1 + ;; + +repacksource) ## repackage source tar ball to 7z + # repackage source package + srcfile=$(find work/tmp -name "$NAME*.xz") + outfile=$(basename $srcfile | sed 's,\.tar\.xz,\.7z,g') + (mkdir -p work/srctmp; cd work/srctmp; tar -xJf ../../$srcfile; 7za a ../out/$outfile *; cd ..; rm -rf srctmp) + touch work/$1.finished + ;; + +createsha) ## create sha256sums + (cd work/out; find -type f -name '*.7z' -o -name '*.exe' | sed 's,\./,,g' | sort | xargs sha256sum > $NAME.sha256sum) + touch work/$1.finished + ;; + +upload) ## upload files to staging area + for i in $(find work/out -name '*.7z' -o -name '*.exe'); do + curl -T $i ftp://upload.kde.org/incoming/ + done + touch work/$1.finished + ;; + +createdescription) ## create ticket description + description="Please move the $NAME related files which has been uploaded to upload.kde.org/incoming to download mirror 'stable/$NAME/$VERSION' location and please update the symbolic link 'stable/$NAME/latest' to 'stable/$NAME/$VERSION'" + sums=$(cat work/out/$NAME.sha256sum | gawk 'BEGIN { print "dir shasum file"} $2 ~ /i686/ { print "win32 " $0 } $2 ~ /x86_64/ { print "win64 " $0 } $2 ~ /[a-z]+-[0-9]/ { print "src " $0 }') + echo -e "$description\n\n$sums" + touch work/$1.finished + ;; + +ticket) ## submit phabricator ticket + description=$($0 createdescription) + curl $PHABURL/api/maniphest.createtask \ + -d api.token=$apitoken \ + -d "title=tarball move request for stable/$NAME/$VERSION" \ + -d "description=$description" \ + -d "$options" + touch work/$1.finished + ;; + +sf) ## run all required targets for releasing on sourceforge + $self clean + $self download + $self unpack + $self movepackage + $self repacksource + $self packdebug + $self createsha + echo "All release related files are located in work/out" + ls work/out + touch work/$1.finished + ;; + +kde) ## run all required targets for releasing on download.kde.org + $self clean + $self download + $self unpack + $self movepackage + $self repacksource + $self packdebug + $self createsha + $self upload + echo "Content for ticket creating:" + $self createdescription + echo run "$0 ticket" to submit ticket + touch work/$1.finished + ;; + +*) + echo "Make sure to setup VERSION inside $0 and run" + echo "$0 all" + echo + echo "or run single targets" + echo + gawk '$0 ~ /^[a-z].*) ##/ { sub(/) ##/,"",$0); a = $1; $1 = ""; printf(" %-20s - %s\n",a, $0); }' $0 + ;; +esac + +exit 0 diff --git a/tools/xea2kmt.cpp b/tools/xea2kmt.cpp index 55973a3d3..84ab93c87 100644 --- a/tools/xea2kmt.cpp +++ b/tools/xea2kmt.cpp @@ -1,535 +1,651 @@ /*************************************************************************** xea2kmt.cpp ------------------- copyright : (C) 2014 by Ralf Habacker ****************************************************************************/ /*************************************************************************** * * * 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 "../kmymoney/mymoney/mymoneyaccount.h" +#include #include #include #include #include #include #include QDebug operator <<(QDebug out, const QXmlStreamNamespaceDeclaration &a) { out << "QXmlStreamNamespaceDeclaration(" << "prefix:" << a.prefix().toString() << "namespaceuri:" << a.namespaceUri().toString() << ")"; return out; } QDebug operator <<(QDebug out, const QXmlStreamAttribute &a) { out << "QXmlStreamAttribute(" << "prefix:" << a.prefix().toString() << "namespaceuri:" << a.namespaceUri().toString() << "name:" << a.name().toString() << " value:" << a.value().toString() << ")"; return out; } bool debug = false; bool withID = false; bool noLevel1Names = false; bool withTax = false; bool prefixNameWithCode = false; +typedef QMap DirNameMapType; + +/** + * map to hold differences from gnucash to kmymoney template directory + * @return directory name map + */ +DirNameMapType &getDirNameMap() +{ + static DirNameMapType dirNameMap; + dirNameMap["cs"] = "cs_CZ"; + dirNameMap["da"] = "dk"; + dirNameMap["ja"] = "ja_JP"; + dirNameMap["ko"] = "ko_KR"; + dirNameMap["nb"] = "nb_NO"; + dirNameMap["nl"] = "nl_NL"; + dirNameMap["ru"] = "ru_RU"; + return dirNameMap; +} + int toKMyMoneyAccountType(const QString &type) { if(type == "ROOT") return MyMoneyAccount::UnknownAccountType; else if (type == "BANK") return MyMoneyAccount::Checkings; else if (type == "CASH") return MyMoneyAccount::Cash; else if (type == "CREDIT") return MyMoneyAccount::CreditCard; else if (type == "INVEST") return MyMoneyAccount::Investment; else if (type == "RECEIVABLE") return MyMoneyAccount::Asset; else if (type == "ASSET") return MyMoneyAccount::Asset; else if (type == "PAYABLE") return MyMoneyAccount::Liability; else if (type == "LIABILITY") return MyMoneyAccount::Liability; else if (type == "CURRENCY") return MyMoneyAccount::Currency; else if (type == "INCOME") return MyMoneyAccount::Income; else if (type == "EXPENSE") return MyMoneyAccount::Expense; else if (type == "STOCK") return MyMoneyAccount::Stock; else if (type == "MUTUAL") return MyMoneyAccount::Stock; else if (type == "EQUITY") return MyMoneyAccount::Equity; else return 99; // unknown } class TemplateAccount { public: typedef QList List; typedef QList PointerList; typedef QMap SlotList; QString id; QString type; QString name; QString code; QString parent; SlotList slotList; TemplateAccount() { } TemplateAccount(const TemplateAccount &b) : id(b.id), type(b.type), name(b.name), code(b.code), parent(b.parent), slotList(b.slotList) { } void clear() { id = ""; type = ""; name = ""; code = ""; parent = ""; slotList.clear(); } bool readSlots(QXmlStreamReader &xml) { while (!xml.atEnd()) { QXmlStreamReader::TokenType type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QStringRef _name = xml.name(); if (_name == "slot") { type = xml.readNext(); if (type == QXmlStreamReader::Characters) type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { QStringRef name = xml.name(); QString key, value; if (name == "key") key = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); type = xml.readNext(); if (type == QXmlStreamReader::Characters) type = xml.readNext(); if (type == QXmlStreamReader::StartElement) { name = xml.name(); if (name == "value") value = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); } if (!key.isEmpty() && !value.isEmpty()) slotList[key] = value; } } } else if (type == QXmlStreamReader::EndElement) { QStringRef _name = xml.name(); if (_name == "slots") return true; } } return true; } bool read(QXmlStreamReader &xml) { while (!xml.atEnd()) { xml.readNext(); QStringRef _name = xml.name(); if (xml.isEndElement() && _name == "account") { if (prefixNameWithCode && !code.isEmpty() && !name.startsWith(code)) name = code + " " + name; return true; } if (xml.isStartElement()) { if (_name == "name") name = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "id") id = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "type") type = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "code") code = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "parent") parent = xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); else if (_name == "slots") readSlots(xml); else { xml.readElementText(QXmlStreamReader::SkipChildElements).trimmed(); if (debug) qDebug() << "skipping" << _name.toString(); } } } return false; } }; QDebug operator <<(QDebug out, const TemplateAccount &a) { out << "TemplateAccount(" << "name:" << a.name << "id:" << a.id << "type:" << a.type << "code:" << a.code << "parent:" << a.parent << "slotList:" << a.slotList << ")\n"; return out; } QDebug operator <<(QDebug out, const TemplateAccount::PointerList &a) { out << "TemplateAccount::List("; foreach(const TemplateAccount *account, a) out << *account; out << ")"; return out; } class TemplateFile { public: QString title; QString longDescription; QString shortDescription; TemplateAccount::List accounts; bool read(QXmlStreamReader &xml) { Q_ASSERT(xml.isStartElement() && xml.name() == "gnc-account-example"); while (xml.readNextStartElement()) { QStringRef name = xml.name(); if (xml.name() == "title") title = xml.readElementText().trimmed(); else if (xml.name() == "short-description") shortDescription = xml.readElementText().trimmed().replace(" ", " "); else if (xml.name() == "long-description") longDescription = xml.readElementText().trimmed().replace(" ", " "); else if (xml.name() == "account") { TemplateAccount account; if (account.read(xml)) accounts.append(account); } else { if (debug) qDebug() << "skipping" << name.toString(); xml.skipCurrentElement(); } } return true; } bool writeAsXml(QXmlStreamWriter &xml) { xml.writeStartElement("","title"); xml.writeCharacters(title); xml.writeEndElement(); xml.writeStartElement("","shortdesc"); xml.writeCharacters(shortDescription); xml.writeEndElement(); xml.writeStartElement("","longdesc"); xml.writeCharacters(longDescription); xml.writeEndElement(); xml.writeStartElement("","accounts"); bool result = writeAccountsAsXml(xml); xml.writeEndElement(); return result; } bool writeAccountsAsXml(QXmlStreamWriter &xml, const QString &id="", int index=0) { TemplateAccount::PointerList list; if (index == 0) list = accountsByType("ROOT"); else list = accountsByParentID(id); foreach(TemplateAccount *account, list) { if (account->type != "ROOT") { xml.writeStartElement("","account"); xml.writeAttribute("type", QString::number(toKMyMoneyAccountType(account->type))); xml.writeAttribute("name", noLevel1Names && index < 2 ? "" : account->name); if (withID) xml.writeAttribute("id", account->id); if (withTax) { if (account->slotList.contains("tax-related")) { xml.writeStartElement("flag"); xml.writeAttribute("name","Tax"); xml.writeAttribute("value",account->slotList["tax-related"]); xml.writeEndElement(); } } } index++; writeAccountsAsXml(xml, account->id, index); index--; xml.writeEndElement(); } return true; } TemplateAccount *account(const QString &id) { for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.id == id) return &account; } return 0; } TemplateAccount::PointerList accountsByType(const QString &type) { TemplateAccount::PointerList list; for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.type == type) list.append(&account); } return list; } static bool nameLessThan(TemplateAccount *a1, TemplateAccount *a2) { return a1->name < a2->name; } TemplateAccount::PointerList accountsByParentID(const QString &parentID) { TemplateAccount::PointerList list; for(int i=0; i < accounts.size(); i++) { TemplateAccount &account = accounts[i]; if (account.parent == parentID) list.append(&account); } qSort(list.begin(), list.end(), nameLessThan); return list; } bool dumpTemplates(const QString &id="", int index=0) { TemplateAccount::PointerList list; if (index == 0) list = accountsByType("ROOT"); else list = accountsByParentID(id); foreach(TemplateAccount *account, list) { QString a; a.fill(' ', index); qDebug() << a << account->name << toKMyMoneyAccountType(account->type); index++; dumpTemplates(account->id, index); index--; } return true; } }; QDebug operator <<(QDebug out, const TemplateFile &a) { out << "TemplateFile(" << "title:" << a.title << "short description:" << a.shortDescription << "long description:" << a.longDescription << "accounts:"; foreach(const TemplateAccount &account, a.accounts) out << account; out << ")"; return out; } class GnuCashAccountTemplateReader { public: GnuCashAccountTemplateReader() { } bool read(const QString &filename) { QFile file(filename); QTextStream in(&file); in.setCodec("utf-8"); if(!file.open(QIODevice::ReadOnly)) return false; inFileName = filename; return read(in.device()); } TemplateFile &result() { return _template; } bool dumpTemplates() { return _template.dumpTemplates(); } bool writeAsXml(const QString &filename=QString()) { if (filename.isEmpty()) { QTextStream stream(stdout); return writeAsXml(stream.device()); } else { QFile file(filename); if(!file.open(QIODevice::WriteOnly)) return false; return writeAsXml(&file); } } protected: bool checkAndUpdateAvailableNamespaces(QXmlStreamReader &xml) { if (xml.namespaceDeclarations().size() < 5) { qWarning() << "gnucash template file is missing required name space declarations; adding by self"; } xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("act", "http://www.gnucash.org/XML/act")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc", "http://www.gnucash.org/XML/gnc")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("gnc-act", "http://www.gnucash.org/XML/gnc-act")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("cmdty","http://www.gnucash.org/XML/cmdty")); xml.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("slot","http://www.gnucash.org/XML/slot")); return true; } bool read(QIODevice *device) { xml.setDevice(device); while(!xml.atEnd()) { xml.readNext(); if (xml.isStartElement()) { if (xml.name() == "gnc-account-example") { checkAndUpdateAvailableNamespaces(xml); _template.read(xml); } else xml.raiseError(QObject::tr("The file is not an gnucash account template file.")); } } if (xml.error() != QXmlStreamReader::NoError) qWarning() << xml.errorString(); return !xml.error(); } bool writeAsXml(QIODevice *device) { - QXmlStreamWriter xml(device); + QXmlStreamWriter xml(device); xml.setAutoFormatting(true); xml.setAutoFormattingIndent(1); xml.setCodec("utf-8"); xml.writeStartDocument(); - QString fileName = inFileName.replace(QRegExp(".*/accounts"),"accounts"); + QString fileName = inFileName; + fileName.replace(QRegExp(".*/accounts"),"accounts"); xml.writeComment(QString("\n" " Converted using xea2kmt from GnuCash sources\n" "\n" " %1\n" "\n" " Please check the source file for possible copyright\n" " and license information.\n" ).arg(fileName)); xml.writeDTD(""); xml.writeStartElement("","kmymoney-account-template"); bool result = _template.writeAsXml(xml); xml.writeEndElement(); xml.writeEndDocument(); return result; } QXmlStreamReader xml; TemplateFile _template; QString inFileName; }; +void scanDir(QDir dir, QStringList &files) +{ + dir.setNameFilters(QStringList("*.gnucash-xea")); + dir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks); + + if (debug) + qDebug() << "Scanning: " << dir.path(); + + QStringList fileList = dir.entryList(); + for (int i=0; i []"; + qWarning() << argv[0] << " --in-dir --out-dir "; qWarning() << "options:"; qWarning() << " --debug - output debug information"; qWarning() << " --help - this page"; qWarning() << " --no-level1-names - do not export account names for top level accounts"; qWarning() << " --prefix-name-with-code - prefix account name with account code if present"; qWarning() << " --with-id - write account id attribute"; qWarning() << " --with-tax-related - parse and export gnucash 'tax-related' flag"; + qWarning() << " --in-dir - search for gnucash templates files in "; + qWarning() << " --out-dir - generate kmymoney templates below