void TransactionMatcher::checkTransaction(const MyMoneyTransaction& tm, const MyMoneyTransaction& ti, const MyMoneySplit& si, QPair<MyMoneyTransaction, MyMoneySplit>& lastMatch, TransactionMatcher::autoMatchResultE& result, int variation) const
{
  Q_UNUSED(ti);


  const QValueList<MyMoneySplit>& splits = tm.splits();
  QValueList<MyMoneySplit>::const_iterator it_s;
  for(it_s = splits.begin(); it_s != splits.end(); ++it_s) {
    MyMoneyMoney upper((*it_s).shares());
    MyMoneyMoney lower(upper);
    if((variation > 0) && (variation < 100)) {
      lower = lower - (lower.abs() * MyMoneyMoney(variation, 100));
      upper = upper + (upper.abs() * MyMoneyMoney(variation, 100));
    }
    // we only check for duplicates / matches if the sign
    // of the amount for this split is identical
    if((si.shares() >= lower) && (si.shares() <= upper)) {
      // check for duplicate (we can only do that, if we have a bankID)
      if(!si.bankID().isEmpty()) {
        if((*it_s).bankID() == si.bankID()) {
          lastMatch = QPair<MyMoneyTransaction, MyMoneySplit>(tm, *it_s);
          result = matchedDuplicate;
          break;
        }
        // in case the stored split already has a bankid
        // assigned, it must be a different one and therefore
        // will certainly not match
        if(!(*it_s).bankID().isEmpty())
          continue;
      }
      // check if this is the one that matches
      if((*it_s).accountId() == si.accountId()
      && (si.shares() >= lower) && (si.shares() <= upper)
      && !(*it_s).isMatched()) {
        if(tm.postDate() == ti.postDate()) {
          lastMatch = QPair<MyMoneyTransaction, MyMoneySplit>(tm, *it_s);
          result = matchedExact;
        } else if(result != matchedExact) {
          lastMatch = QPair<MyMoneyTransaction, MyMoneySplit>(tm, *it_s);
          result = matched;
        }
      }
    }
  }
}
Exemple #2
0
void MyMoneySplitTest::testReadXML()
{
  MyMoneySplit s;
  QString ref_ok = QString(
                     "<!DOCTYPE TEST>\n"
                     "<SPLIT-CONTAINER>\n"
                     " <SPLIT payee=\"P000001\" reconciledate=\"\" shares=\"96379/100\" action=\"Deposit\" bankid=\"SPID\" number=\"124\" reconcileflag=\"2\" memo=\"MyMemo\" value=\"96379/1000\" account=\"A000076\">\n"
                     "  <TAG id=\"G000001\"/>\n"
                     " </SPLIT>\n"
                     "</SPLIT-CONTAINER>\n");

  QString ref_false = QString(
                        "<!DOCTYPE TEST>\n"
                        "<SPLIT-CONTAINER>\n"
                        " <SPLITS payee=\"P000001\" reconciledate=\"\" shares=\"96379/100\" action=\"Deposit\" bankid=\"SPID\" number=\"124\" reconcileflag=\"2\" memo=\"\" value=\"96379/1000\" account=\"A000076\" />\n"
                        " <TAG id=\"G000001\"/>\n"
                        "</SPLIT-CONTAINER>\n");

  QDomDocument doc;
  QDomElement node;
  doc.setContent(ref_false);
  node = doc.documentElement().firstChild().toElement();

  try {
    s = MyMoneySplit(node);
    QFAIL("Missing expected exception");
  } catch (const MyMoneyException &) {
  }

  doc.setContent(ref_ok);
  node = doc.documentElement().firstChild().toElement();

  try {
    s = MyMoneySplit(node);
    QVERIFY(s.id().isEmpty());
    QVERIFY(s.payeeId() == "P000001");
    QList<QString> tagIdList;
    tagIdList << "G000001";
    QVERIFY(s.tagIdList() == tagIdList);
    QVERIFY(s.reconcileDate() == QDate());
    QVERIFY(s.shares() == MyMoneyMoney(96379, 100));
    QVERIFY(s.value() == MyMoneyMoney(96379, 1000));
    QVERIFY(s.number() == "124");
    QVERIFY(s.bankID() == "SPID");
    QVERIFY(s.reconcileFlag() == MyMoneySplit::Reconciled);
    QVERIFY(s.action() == MyMoneySplit::ActionDeposit);
    QVERIFY(s.accountId() == "A000076");
    QVERIFY(s.memo() == "MyMemo");
  } catch (const MyMoneyException &) {
  }

}
Exemple #3
0
void CsvUtil::dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList<MyMoneySplit>& feeSplits, QList<MyMoneySplit>& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, MyMoneySplit::investTransactionTypeE& transactionType)
{
  // collect the splits. split references the stock account and should already
  // be set up. assetAccountSplit references the corresponding asset account (maybe
  // empty), feeSplits is the list of all expenses and interestSplits
  // the list of all incomes
  MyMoneyFile* file = MyMoneyFile::instance();
  QList<MyMoneySplit>::ConstIterator it_s;
  for (it_s = transaction.splits().constBegin(); it_s != transaction.splits().constEnd(); ++it_s) {
    MyMoneyAccount acc = file->account((*it_s).accountId());
    if ((*it_s).id() == split.id()) {
      security = file->security(acc.currencyId());
    } else if (acc.accountGroup() == MyMoneyAccount::Expense) {
      feeSplits.append(*it_s);
    } else if (acc.accountGroup() == MyMoneyAccount::Income) {
      interestSplits.append(*it_s);
    } else {
      assetAccountSplit = *it_s;
    }
  }

  // determine transaction type
  if (split.action() == MyMoneySplit::ActionAddShares) {
    transactionType = (!split.shares().isNegative()) ? MyMoneySplit::AddShares : MyMoneySplit::RemoveShares;
  } else if (split.action() == MyMoneySplit::ActionBuyShares) {
    transactionType = (!split.value().isNegative()) ? MyMoneySplit::BuyShares : MyMoneySplit::SellShares;
  } else if (split.action() == MyMoneySplit::ActionDividend) {
    transactionType = MyMoneySplit::Dividend;
  } else if (split.action() == MyMoneySplit::ActionReinvestDividend) {
    transactionType = MyMoneySplit::ReinvestDividend;
  } else if (split.action() == MyMoneySplit::ActionYield) {
    transactionType = MyMoneySplit::Yield;
  } else if (split.action() == MyMoneySplit::ActionSplitShares) {
    transactionType = MyMoneySplit::SplitShares;
  } else if (split.action() == MyMoneySplit::ActionInterestIncome) {
    transactionType = MyMoneySplit::InterestIncome;
  } else
    transactionType = MyMoneySplit::BuyShares;

  currency.setTradingSymbol("???");
  try {
    currency = file->security(transaction.commodity());
  } catch (const MyMoneyException &) {
  }
}
Exemple #4
0
void MyMoneySplitTest::testAssignmentConstructor()
{
  testSetFunctions();

  MyMoneySplit n;

  n = *m;

  QVERIFY(n.accountId() == "Account");
  QVERIFY(n.memo() == "Memo");
  QVERIFY(n.reconcileDate() == QDate(1, 2, 3));
  QVERIFY(n.reconcileFlag() == MyMoneySplit::Cleared);
  QVERIFY(n.shares() == MyMoneyMoney(1234, 100));
  QVERIFY(n.value() == MyMoneyMoney(3456, 100));
  QVERIFY(n.id() == "MyID");
  QVERIFY(n.payeeId() == "Payee");
  QList<QString> tagIdList;
  tagIdList << "Tag";
  QVERIFY(m->tagIdList() == tagIdList);
  QVERIFY(n.action() == "Action");
  QVERIFY(n.transactionId() == "TestTransaction");
  QVERIFY(n.value("Key") == "Value");
}
void TransactionMatcher::match(MyMoneyTransaction tm, MyMoneySplit sm, MyMoneyTransaction ti, MyMoneySplit si, bool allowImportedTransactions)
{
  const MyMoneySecurity& sec = MyMoneyFile::instance()->security(m_account.currencyId());

  // Now match the transactions.
  //
  // 'Matching' the transactions entails DELETING the end transaction,
  // and MODIFYING the start transaction as needed.
  //
  // There are a variety of ways that a transaction can conflict.
  // Post date, splits, amount are the ones that seem to matter.
  // TODO: Handle these conflicts intelligently, at least warning
  // the user, or better yet letting the user choose which to use.
  //
  // For now, we will just use the transaction details from the start
  // transaction.  The only thing we'll take from the end transaction
  // are the bank ID's.
  //
  // What we have to do here is iterate over the splits in the end
  // transaction, and find the corresponding split in the start
  // transaction.  If there is a bankID in the end split but not the
  // start split, add it to the start split.  If there is a bankID
  // in BOTH, then this transaction cannot be merged (both transactions
  // were imported!!)  If the corresponding start split cannot  be
  // found and the end split has a bankID, we should probably just fail.
  // Although we could ADD it to the transaction.

  // ipwizard: Don't know if iterating over the transactions is a good idea.
  // In case of a split transaction recorded with KMyMoney and the transaction
  // data being imported consisting only of a single category assignment, this
  // does not make much sense. The same applies for investment transactions
  // stored in KMyMoney against imported transactions. I think a better solution
  // is to just base the match on the splits referencing the same (currently
  // selected) account.

  // verify, that tm is a manual (non-matched) transaction
  // allow matching two manual transactions

  if ((!allowImportedTransactions && tm.isImported()) || sm.isMatched())
    throw MYMONEYEXCEPTION(i18n("First transaction does not match requirement for matching"));

  // verify that the amounts are the same, otherwise we should not be matching!
  if (sm.shares() != si.shares()) {
    throw MYMONEYEXCEPTION(i18n("Splits for %1 have conflicting values (%2,%3)", m_account.name(), MyMoneyUtils::formatMoney(sm.shares(), m_account, sec), MyMoneyUtils::formatMoney(si.shares(), m_account, sec)));
  }

  // check that dates are within user's setting
  const int gap = abs(tm.postDate().toJulianDay() - ti.postDate().toJulianDay());
  if (gap > KMyMoneyGlobalSettings::matchInterval()) {
    int rc = KMessageBox::questionYesNo(0, i18np("The transaction dates are one day apart. Do you want to match them anyway?",
                                        "The transaction dates are %1 days apart. Do you want to match them anyway?", gap));
    if (rc == KMessageBox::No) {
      return;
    }
  }

  // ipwizard: I took over the code to keep the bank id found in the endMatchTransaction
  // This might not work for QIF imports as they don't setup this information. It sure
  // makes sense for OFX and HBCI.
  const QString& bankID = si.bankID();
  if (!bankID.isEmpty()) {
    try {
      if (sm.bankID().isEmpty()) {
        sm.setBankID(bankID);
        tm.modifySplit(sm);
      }
    } catch (const MyMoneyException &e) {
      QString estr = e.what();
      throw MYMONEYEXCEPTION(i18n("Unable to match all splits (%1)", estr));
    }
  }
  //
  //  we now allow matching of two non-imported transactions
  //

  // mark the split as cleared if it does not have a reconciliation information yet
  if (sm.reconcileFlag() == MyMoneySplit::NotReconciled) {
    sm.setReconcileFlag(MyMoneySplit::Cleared);
  }

  // if we don't have a payee assigned to the manually entered transaction
  // we use the one we found in the imported transaction
  if (sm.payeeId().isEmpty() && !si.payeeId().isEmpty()) {
    sm.setValue("kmm-orig-payee", sm.payeeId());
    sm.setPayeeId(si.payeeId());
  }

  // We use the imported postdate and keep the previous one for unmatch
  if (tm.postDate() != ti.postDate()) {
    sm.setValue("kmm-orig-postdate", tm.postDate().toString(Qt::ISODate));
    tm.setPostDate(ti.postDate());
  }

  // combine the two memos into one
  QString memo = sm.memo();
  if (!si.memo().isEmpty() && si.memo() != memo) {
    sm.setValue("kmm-orig-memo", memo);
    if (!memo.isEmpty())
      memo += '\n';
    memo += si.memo();
  }
  sm.setMemo(memo);

  // remember the split we matched
  sm.setValue("kmm-match-split", si.id());

  sm.addMatch(ti);
  tm.modifySplit(sm);

  ti.modifySplit(si);///
  MyMoneyFile::instance()->modifyTransaction(tm);
  // Delete the end transaction if it was stored in the engine
  if (!ti.id().isEmpty())
    MyMoneyFile::instance()->removeTransaction(ti);
}
int KSplitTransactionDlg::exec()
{
    // for deposits, we invert the sign of all splits.
    // don't forget to revert when we're done ;-)
    if (m_isDeposit) {
        for (int i = 0; i < m_transaction.splits().count(); ++i) {
            MyMoneySplit split = m_transaction.splits()[i];
            split.setValue(-split.value());
            split.setShares(-split.shares());
            m_transaction.modifySplit(split);
        }
    }

    int rc;
    do {
        transactionsTable->setFocus();

        // initialize the display
        transactionsTable->setTransaction(m_transaction, m_split, m_account);
        updateSums();

        rc = KSplitTransactionDlgDecl::exec();

        if (rc == Accepted) {
            if (!diffAmount().isZero()) {
                KSplitCorrectionDlgDecl* corrDlg = new KSplitCorrectionDlgDecl(this);
                QVBoxLayout *mainLayout = new QVBoxLayout;
                corrDlg->setLayout(mainLayout);
                mainLayout->addWidget(corrDlg->findChild<QWidget*>("verticalLayout"));
                QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
                connect(buttonBox, SIGNAL(accepted()), corrDlg, SLOT(accept()));
                connect(buttonBox, SIGNAL(rejected()), corrDlg, SLOT(reject()));
                mainLayout->addWidget(buttonBox);
                corrDlg->buttonGroup->setId(corrDlg->continueBtn, 0);
                corrDlg->buttonGroup->setId(corrDlg->changeBtn, 1);
                corrDlg->buttonGroup->setId(corrDlg->distributeBtn, 2);
                corrDlg->buttonGroup->setId(corrDlg->leaveBtn, 3);

                corrDlg->setModal(true);

                MyMoneySplit split = m_transaction.splits()[0];
                QString total = (-split.value()).formatMoney("", m_precision);
                QString sums = splitsValue().formatMoney("", m_precision);
                QString diff = diffAmount().formatMoney("", m_precision);

                // now modify the text items of the dialog to contain the correct values
                QString q = i18n("The total amount of this transaction is %1 while "
                                 "the sum of the splits is %2. The remaining %3 are "
                                 "unassigned.", total, sums, diff);
                corrDlg->explanation->setText(q);

                q = i18n("Change &total amount of transaction to %1.", sums);
                corrDlg->changeBtn->setText(q);

                q = i18n("&Distribute difference of %1 among all splits.", diff);
                corrDlg->distributeBtn->setText(q);
                // FIXME remove the following line once distribution among
                //       all splits is implemented
                corrDlg->distributeBtn->hide();


                // if we have only two splits left, we don't allow leaving sth. unassigned.
                if (m_transaction.splitCount() < 3) {
                    q = i18n("&Leave total amount of transaction at %1.", total);
                } else {
                    q = i18n("&Leave %1 unassigned.", diff);
                }
                corrDlg->leaveBtn->setText(q);

                if ((rc = corrDlg->exec()) == Accepted) {
                    switch (corrDlg->buttonGroup->checkedId()) {
                    case 0:       // continue to edit
                        rc = Rejected;
                        break;

                    case 1:       // modify total
                        split.setValue(-splitsValue());
                        split.setShares(-splitsValue());
                        m_transaction.modifySplit(split);
                        break;

                    case 2:       // distribute difference
                        qDebug("distribution of difference not yet supported in KSplitTransactionDlg::slotFinishClicked()");
                        break;

                    case 3:       // leave unassigned
                        break;
                    }
                }
                delete corrDlg;
            }
        } else
            break;

    } while (rc != Accepted);

    // for deposits, we inverted the sign of all splits.
    // now we revert it back, so that things are left correct
    if (m_isDeposit) {
        for (int i = 0; i < m_transaction.splits().count(); ++i) {
            MyMoneySplit split = m_transaction.splits()[i];
            split.setValue(-split.value());
            split.setShares(-split.shares());
            m_transaction.modifySplit(split);
        }
    }

    return rc;
}
void TransactionMatcher::match(MyMoneyTransaction tm, MyMoneySplit sm, MyMoneyTransaction ti, MyMoneySplit si, bool allowImportedTransactions)
{
  const MyMoneySecurity& sec = MyMoneyFile::instance()->security(m_account.currencyId());

  // Now match the transactions.
  //
  // 'Matching' the transactions entails DELETING the end transaction,
  // and MODIFYING the start transaction as needed.
  //
  // There are a variety of ways that a transaction can conflict.
  // Post date, splits, amount are the ones that seem to matter.
  // TODO: Handle these conflicts intelligently, at least warning
  // the user, or better yet letting the user choose which to use.
  //
  // For now, we will just use the transaction details from the start
  // transaction.  The only thing we'll take from the end transaction
  // are the bank ID's.
  //
  // What we have to do here is iterate over the splits in the end
  // transaction, and find the corresponding split in the start
  // transaction.  If there is a bankID in the end split but not the
  // start split, add it to the start split.  If there is a bankID
  // in BOTH, then this transaction cannot be merged (both transactions
  // were imported!!)  If the corresponding start split cannot  be
  // found and the end split has a bankID, we should probably just fail.
  // Although we could ADD it to the transaction.

  // ipwizard: Don't know if iterating over the transactions is a good idea.
  // In case of a split transaction recorded with KMyMoney and the transaction
  // data being imported consisting only of a single category assignment, this
  // does not make much sense. The same applies for investment transactions
  // stored in KMyMoney against imported transactions. I think a better solution
  // is to just base the match on the splits referencing the same (currently
  // selected) account.

  // verify, that tm is a manually (non-matched) transaction and ti an imported one
  if(sm.isMatched() || (!allowImportedTransactions && tm.isImported()))
    throw new MYMONEYEXCEPTION(i18n("First transaction does not match requirement for matching"));
  if(!ti.isImported())
    throw new MYMONEYEXCEPTION(i18n("Second transaction does not match requirement for matching"));

  // verify that the amounts are the same, otherwise we should not be matching!
  if(sm.shares() != si.shares()) {
    throw new MYMONEYEXCEPTION(i18n("Splits for %1 have conflicting values (%2,%3)").arg(m_account.name()).arg(sm.shares().formatMoney(m_account, sec), si.shares().formatMoney(m_account, sec)));
  }

  // ipwizard: I took over the code to keep the bank id found in the endMatchTransaction
  // This might not work for QIF imports as they don't setup this information. It sure
  // makes sense for OFX and HBCI.
  const QString& bankID = si.bankID();
  if (!bankID.isEmpty()) {
    try {
      if (sm.bankID().isEmpty() ) {
        sm.setBankID( bankID );
        tm.modifySplit(sm);
      } else if(sm.bankID() != bankID) {
        throw new MYMONEYEXCEPTION(i18n("Both of these transactions have been imported into %1.  Therefore they cannot be matched.  Matching works with one imported transaction and one non-imported transaction.").arg(m_account.name()));
      }
    } catch(MyMoneyException *e) {
      QString estr = e->what();
      delete e;
      throw new MYMONEYEXCEPTION(i18n("Unable to match all splits (%1)").arg(estr));
    }
  }

#if 0 // Ace's original code
  // TODO (Ace) Add in another error to catch the case where a user
  // tries to match two hand-entered transactions.
  QValueList<MyMoneySplit> endSplits = endMatchTransaction.splits();
  QValueList<MyMoneySplit>::const_iterator it_split = endSplits.begin();
  while (it_split != endSplits.end())
  {
    // find the corresponding split in the start transaction
    MyMoneySplit startSplit;
    QString accountid = (*it_split).accountId();
    try
    {
      startSplit = startMatchTransaction.splitByAccount( accountid );
    }
      // only exception is thrown if we cannot find a split like this
    catch(MyMoneyException *e)
    {
      delete e;
      startSplit = (*it_split);
      startSplit.clearId();
      startMatchTransaction.addSplit(startSplit);
    }

    // verify that the amounts are the same, otherwise we should not be
    // matching!
    if ( (*it_split).value() != startSplit.value() )
    {
      QString accountname = MyMoneyFile::instance()->account(accountid).name();
      throw new MYMONEYEXCEPTION(i18n("Splits for %1 have conflicting values (%2,%3)").arg(accountname).arg((*it_split).value().formatMoney(),startSplit.value().formatMoney()));
    }

    QString bankID = (*it_split).bankID();
    if ( ! bankID.isEmpty() )
    {
      try
      {
        if ( startSplit.bankID().isEmpty() )
        {
          startSplit.setBankID( bankID );
          startMatchTransaction.modifySplit(startSplit);
        }
        else
        {
          QString accountname = MyMoneyFile::instance()->account(accountid).name();
          throw new MYMONEYEXCEPTION(i18n("Both of these transactions have been imported into %1.  Therefore they cannot be matched.  Matching works with one imported transaction and one non-imported transaction.").arg(accountname));
        }
      }
      catch(MyMoneyException *e)
      {
        QString estr = e->what();
        delete e;
        throw new MYMONEYEXCEPTION(i18n("Unable to match all splits (%1)").arg(estr));
      }
    }
    ++it_split;
  }
#endif

  // mark the split as cleared if it does not have a reconciliation information yet
  if(sm.reconcileFlag() == MyMoneySplit::NotReconciled) {
    sm.setReconcileFlag(MyMoneySplit::Cleared);
  }

  // if we don't have a payee assigned to the manually entered transaction
  // we use the one we found in the imported transaction
  if(sm.payeeId().isEmpty() && !si.payeeId().isEmpty()) {
    sm.setValue("kmm-orig-payee", sm.payeeId());
    sm.setPayeeId(si.payeeId());
  }

  // We use the imported postdate and keep the previous one for unmatch
  if(tm.postDate() != ti.postDate()) {
    sm.setValue("kmm-orig-postdate", tm.postDate().toString(Qt::ISODate));
    tm.setPostDate(ti.postDate());
  }

  // combine the two memos into one
  QString memo = sm.memo();
  if(!si.memo().isEmpty() && si.memo() != memo) {
    sm.setValue("kmm-orig-memo", memo);
    if(!memo.isEmpty())
      memo += "\n";
    memo += si.memo();
  }
  sm.setMemo(memo);

  // remember the split we matched
  sm.setValue("kmm-match-split", si.id());

  sm.addMatch(ti);
  tm.modifySplit(sm);

  MyMoneyFile::instance()->modifyTransaction(tm);
  // Delete the end transaction if it was stored in the engine
  if(!ti.id().isEmpty())
    MyMoneyFile::instance()->removeTransaction(ti);
}