void MyMoneyQifWriter::writeTransactionEntry(QTextStream &s, const MyMoneyTransaction& t, const QString& accountId) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySplit split = t.splitByAccount(accountId); s << "D" << m_qifProfile.date(t.postDate()) << endl; switch(split.reconcileFlag()) { case MyMoneySplit::Cleared: s << "C*" << endl; break; case MyMoneySplit::Reconciled: case MyMoneySplit::Frozen: s << "CX" << endl; break; default: break; } if(split.memo().length() > 0) { QString m = split.memo(); m.replace('\n', "\\n"); s << "M" << m << endl; } s << "T" << m_qifProfile.value('T', split.value()) << endl; if(split.number().length() > 0) s << "N" << split.number() << endl; if(!split.payeeId().isEmpty()) { MyMoneyPayee payee = file->payee(split.payeeId()); s << "P" << payee.name() << endl; } QValueList<MyMoneySplit> list = t.splits(); if(list.count() > 1) { MyMoneySplit sp = t.splitByAccount(accountId, false); MyMoneyAccount acc = file->account(sp.accountId()); if(acc.accountGroup() != MyMoneyAccount::Income && acc.accountGroup() != MyMoneyAccount::Expense) { s << "L" << m_qifProfile.accountDelimiter()[0] << MyMoneyFile::instance()->accountToCategory(sp.accountId()) << m_qifProfile.accountDelimiter()[1] << endl; } else { s << "L" << file->accountToCategory(sp.accountId()) << endl; } if(list.count() > 2) { QValueList<MyMoneySplit>::ConstIterator it; for(it = list.begin(); it != list.end(); ++it) { if(!((*it) == split)) { writeSplitEntry(s, *it); } } } } s << "^" << endl; }
void KSelectTransactionsDlg::addTransaction(const MyMoneyTransaction& t) { QList<MyMoneySplit>::const_iterator it_s; for (it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s) { if ((*it_s).accountId() == m_account.id()) { KMyMoneyRegister::Transaction* tr = KMyMoneyRegister::Register::transactionFactory(m_register, t, (*it_s), 0); // force full detail display tr->setNumRowsRegister(tr->numRowsRegister(true)); break; } } }
void MyMoneyStorageDump::dumpTransaction(QTextStream& s, IMyMoneyStorage* storage, const MyMoneyTransaction& it_t) { s << " ID = " << it_t.id() << "\n"; s << " Postdate = " << it_t.postDate().toString(Qt::ISODate) << "\n"; s << " EntryDate = " << it_t.entryDate().toString(Qt::ISODate) << "\n"; s << " Commodity = [" << it_t.commodity() << "]\n"; s << " Memo = " << it_t.memo() << "\n"; s << " BankID = " << it_t.bankID() << "\n"; dumpKVP("KVP:", s, it_t, 2); s << " Splits\n"; s << " ------\n"; QList<MyMoneySplit>::ConstIterator it_s; for (it_s = it_t.splits().constBegin(); it_s != it_t.splits().constEnd(); ++it_s) { s << " ID = " << (*it_s).id() << "\n"; s << " Transaction = " << (*it_s).transactionId() << "\n"; s << " Payee = " << (*it_s).payeeId(); if (!(*it_s).payeeId().isEmpty()) { MyMoneyPayee p = storage->payee((*it_s).payeeId()); s << " (" << p.name() << ")" << "\n"; } else s << " ()\n"; for (int i = 0; i < (*it_s).tagIdList().size(); i++) { s << " Tag = " << (*it_s).tagIdList()[i]; if (!(*it_s).tagIdList()[i].isEmpty()) { MyMoneyTag ta = storage->tag((*it_s).tagIdList()[i]); s << " (" << ta.name() << ")" << "\n"; } else s << " ()\n"; } s << " Account = " << (*it_s).accountId(); MyMoneyAccount acc; try { acc = storage->account((*it_s).accountId()); s << " (" << acc.name() << ") [" << acc.currencyId() << "]\n"; } catch (const MyMoneyException &) { s << " (---) [---]\n"; } s << " Memo = " << (*it_s).memo() << "\n"; if ((*it_s).value() == MyMoneyMoney::autoCalc) s << " Value = will be calculated" << "\n"; else s << " Value = " << (*it_s).value().formatMoney("", 2) << " (" << (*it_s).value().toString() << ")\n"; s << " Shares = " << (*it_s).shares().formatMoney("", 2) << " (" << (*it_s).shares().toString() << ")\n"; s << " Action = '" << (*it_s).action() << "'\n"; s << " Nr = '" << (*it_s).number() << "'\n"; s << " ReconcileFlag = '" << reconcileToString((*it_s).reconcileFlag()) << "'\n"; if ((*it_s).reconcileFlag() != MyMoneySplit::NotReconciled) { s << " ReconcileDate = " << (*it_s).reconcileDate().toString(Qt::ISODate) << "\n"; } s << " BankID = " << (*it_s).bankID() << "\n"; dumpKVP("KVP:", s, (*it_s), 4); s << "\n"; } s << "\n"; }
MyMoneyTransaction KMyMoneyUtils::scheduledTransaction(const MyMoneySchedule& schedule) { MyMoneyTransaction t = schedule.transaction(); try { if (schedule.type() == MyMoneySchedule::TYPE_LOANPAYMENT) { calculateAutoLoan(schedule, t, QMap<QString, MyMoneyMoney>()); } } catch (const MyMoneyException &e) { qDebug("Unable to load schedule details for '%s' during transaction match: %s", qPrintable(schedule.name()), qPrintable(e.what())); } t.clearId(); t.setEntryDate(QDate()); return t; }
MyMoneySchedule KNewLoanWizard::schedule() const { MyMoneySchedule sched(field("nameEdit").toString(), MyMoneySchedule::TYPE_LOANPAYMENT, MyMoneySchedule::occurrenceE(field("paymentFrequencyUnitEdit").toInt()), 1, MyMoneySchedule::STYPE_OTHER, QDate(), QDate(), false, false); MyMoneyTransaction t = transaction(); t.setPostDate(field("nextDueDateEdit").toDate()); sched.setTransaction(t); return sched; }
void MyMoneyQifWriter::writeAccountEntry(QTextStream &s, const QString& accountId, const QDate& startDate, const QDate& endDate) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; account = file->account(accountId); MyMoneyTransactionFilter filter(accountId); filter.setDateFilter(startDate, endDate); QValueList<MyMoneyTransaction> list = file->transactionList(filter); QString openingBalanceTransactionId; s << "!Type:" << m_qifProfile.profileType() << endl; if(!startDate.isValid() || startDate <= account.openingDate()) { s << "D" << m_qifProfile.date(account.openingDate()) << endl; openingBalanceTransactionId = file->openingBalanceTransaction(account); MyMoneySplit split; if(!openingBalanceTransactionId.isEmpty()) { MyMoneyTransaction openingBalanceTransaction = file->transaction(openingBalanceTransactionId); split = openingBalanceTransaction.splitByAccount(account.id(), true /* match */); } s << "T" << m_qifProfile.value('T', split.value()) << endl; } else { s << "D" << m_qifProfile.date(startDate) << endl; s << "T" << m_qifProfile.value('T', file->balance(accountId, startDate.addDays(-1))) << endl; } s << "CX" << endl; s << "P" << m_qifProfile.openingBalanceText() << endl; s << "L"; if(m_qifProfile.accountDelimiter().length()) s << m_qifProfile.accountDelimiter()[0]; s << account.name(); if(m_qifProfile.accountDelimiter().length() > 1) s << m_qifProfile.accountDelimiter()[1]; s << endl; s << "^" << endl; QValueList<MyMoneyTransaction>::ConstIterator it; signalProgress(0, list.count()); int count = 0; for(it = list.begin(); it != list.end(); ++it) { // don't include the openingBalanceTransaction again if((*it).id() != openingBalanceTransactionId) writeTransactionEntry(s, *it, accountId); signalProgress(++count, 0); } }
void 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 &) { } }
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; } } } } }
bool KEndingBalanceDlg::createTransaction(MyMoneyTransaction &t, const int sign, const MyMoneyMoney& amount, const QString& category, const QDate& date) { t = MyMoneyTransaction(); if (category.isEmpty() || !date.isValid()) return true; MyMoneySplit s1, s2; MyMoneyMoney val = amount * MyMoneyMoney(sign, 1); try { t.setPostDate(date); t.setCommodity(d->m_account.currencyId()); s1.setPayeeId(field("payeeEdit").toString()); s1.setReconcileFlag(MyMoneySplit::Cleared); s1.setAccountId(d->m_account.id()); s1.setValue(-val); s1.setShares(-val); s2 = s1; s2.setAccountId(category); s2.setValue(val); t.addSplit(s1); t.addSplit(s2); QMap<QString, MyMoneyMoney> priceInfo; // just empty MyMoneyMoney shares; if (!KCurrencyCalculator::setupSplitPrice(shares, t, s2, priceInfo, this)) { t = MyMoneyTransaction(); return false; } s2.setShares(shares); t.modifySplit(s2); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); t = MyMoneyTransaction(); return false; } return true; }
KMyMoneyUtils::transactionTypeE KMyMoneyUtils::transactionType(const MyMoneyTransaction& t) { if (!stockSplit(t).id().isEmpty()) return InvestmentTransaction; if (t.splitCount() < 2) { return Unknown; } else if (t.splitCount() > 2) { // FIXME check for loan transaction here return SplitTransaction; } QString ida, idb; if (t.splits().size() > 0) ida = t.splits()[0].accountId(); if (t.splits().size() > 1) idb = t.splits()[1].accountId(); if (ida.isEmpty() || idb.isEmpty()) return Unknown; MyMoneyAccount a, b; a = MyMoneyFile::instance()->account(ida); b = MyMoneyFile::instance()->account(idb); if ((a.accountGroup() == MyMoneyAccount::Asset || a.accountGroup() == MyMoneyAccount::Liability) && (b.accountGroup() == MyMoneyAccount::Asset || b.accountGroup() == MyMoneyAccount::Liability)) return Transfer; return Normal; }
const MyMoneySplit KMyMoneyUtils::stockSplit(const MyMoneyTransaction& t) { QList<MyMoneySplit>::ConstIterator it_s; MyMoneySplit investmentAccountSplit; for (it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s) { if (!(*it_s).accountId().isEmpty()) { MyMoneyAccount acc = MyMoneyFile::instance()->account((*it_s).accountId()); if (acc.isInvest()) { return *it_s; } // if we have a reference to an investment account, we remember it here if (acc.accountType() == MyMoneyAccount::Investment) investmentAccountSplit = *it_s; } } // if we haven't found a stock split, we see if we've seen // an investment account on the way. If so, we return it. if (!investmentAccountSplit.id().isEmpty()) return investmentAccountSplit; // if none was found, we return an empty split. return MyMoneySplit(); }
bool MyMoneySplit::replaceId(const QString& newId, const QString& oldId) { bool changed = false; if (m_payee == oldId) { m_payee = newId; changed = true; } else if (m_account == oldId) { m_account = newId; changed = true; } if (isMatched()) { MyMoneyTransaction t = matchedTransaction(); if (t.replaceId(newId, oldId)) { removeMatch(); addMatch(t); changed = true; } } return changed; }
KSplitTransactionDlg::KSplitTransactionDlg(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyAccount& acc, const bool amountValid, const bool deposit, const MyMoneyMoney& calculatedValue, const QMap<QString, MyMoneyMoney>& priceInfo, QWidget* parent) : KSplitTransactionDlgDecl(parent), m_account(acc), m_split(s), m_precision(2), m_amountValid(amountValid), m_isDeposit(deposit), m_calculatedValue(calculatedValue) { setModal(true); QHBoxLayout *mainLayout = new QHBoxLayout; setLayout(mainLayout); mainLayout->addWidget(horizontalLayoutWidget); m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QPushButton *okButton = m_buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); QPushButton *user1Button = new QPushButton; m_buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole); QPushButton *user2Button = new QPushButton; m_buttonBox->addButton(user2Button, QDialogButtonBox::ActionRole); QPushButton *user3Button = new QPushButton; m_buttonBox->addButton(user3Button, QDialogButtonBox::ActionRole); connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject())); m_buttonBox->setOrientation(Qt::Vertical); mainLayout->addWidget(m_buttonBox); //set custom buttons //clearAll button user1Button->setText(i18n("Clear &All")); user1Button->setToolTip(i18n("Clear all splits")); user1Button->setWhatsThis(i18n("Use this to clear all splits of this transaction")); user1Button->setIcon(QIcon::fromTheme("edit-clear")); //clearZero button user2Button->setText(i18n("Clear &Zero")); user2Button->setToolTip(i18n("Removes all splits that have a value of zero")); user2Button->setIcon(QIcon::fromTheme("edit-clear")); //merge button user3Button->setText(i18n("&Merge")); user3Button->setToolTip(i18n("Merges splits with the same category to one split")); user3Button->setWhatsThis(i18n("In case you have multiple split entries to the same category and you like to keep them as a single split")); // make finish the default m_buttonBox->button(QDialogButtonBox::Cancel)->setDefault(true); // setup the focus m_buttonBox->button(QDialogButtonBox::Cancel)->setFocusPolicy(Qt::NoFocus); okButton->setFocusPolicy(Qt::NoFocus); user1Button->setFocusPolicy(Qt::NoFocus); // connect signals with slots connect(transactionsTable, SIGNAL(transactionChanged(MyMoneyTransaction)), this, SLOT(slotSetTransaction(MyMoneyTransaction))); connect(transactionsTable, SIGNAL(createCategory(QString,QString&)), this, SLOT(slotCreateCategory(QString,QString&))); connect(transactionsTable, SIGNAL(objectCreation(bool)), this, SIGNAL(objectCreation(bool))); connect(transactionsTable, SIGNAL(returnPressed()), this, SLOT(accept())); connect(transactionsTable, SIGNAL(escapePressed()), this, SLOT(reject())); connect(transactionsTable, SIGNAL(editStarted()), this, SLOT(slotEditStarted())); connect(transactionsTable, SIGNAL(editFinished()), this, SLOT(slotUpdateButtons())); connect(m_buttonBox->button(QDialogButtonBox::Cancel), SIGNAL(clicked()), this, SLOT(reject())); connect(m_buttonBox->button(QDialogButtonBox::Ok), SIGNAL(clicked()), this, SLOT(accept())); connect(user1Button, SIGNAL(clicked()), this, SLOT(slotClearAllSplits())); connect(user3Button, SIGNAL(clicked()), this, SLOT(slotMergeSplits())); connect(user2Button, SIGNAL(clicked()), this, SLOT(slotClearUnusedSplits())); // setup the precision try { MyMoneySecurity currency = MyMoneyFile::instance()->currency(t.commodity()); m_precision = MyMoneyMoney::denomToPrec(m_account.fraction(currency)); } catch (const MyMoneyException &) { } slotSetTransaction(t); // pass on those vars transactionsTable->setup(priceInfo, m_precision); QSize size(width(), height()); KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTransactionEditor"); size = grp.readEntry("Geometry", size); size.setHeight(size.height() - 1); QDialog::resize(size.expandedTo(minimumSizeHint())); // Trick: it seems, that the initial sizing of the dialog does // not work correctly. At least, the columns do not get displayed // correct. Reason: the return value of transactionsTable->visibleWidth() // is incorrect. If the widget is visible, resizing works correctly. // So, we let the dialog show up and resize it then. It's not really // clean, but the only way I got the damned thing working. QTimer::singleShot(10, this, SLOT(initSize())); }
void MyMoneyForecast::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap<QString, MyMoneyMoney>& balances) { if (schedule.type() == MyMoneySchedule::TYPE_LOANPAYMENT) { //get amortization and interest autoCalc splits MyMoneySplit amortizationSplit = transaction.amortizationSplit(); MyMoneySplit interestSplit = transaction.interestSplit(); if(!amortizationSplit.id().isEmpty() && !interestSplit.id().isEmpty()) { MyMoneyAccountLoan acc(MyMoneyFile::instance()->account(amortizationSplit.accountId())); MyMoneyFinancialCalculator calc; QDate dueDate; // FIXME: setup dueDate according to when the interest should be calculated // current implementation: take the date of the next payment according to // the schedule. If the calculation is based on the payment reception, and // the payment is overdue then take the current date dueDate = schedule.nextDueDate(); if(acc.interestCalculation() == MyMoneyAccountLoan::paymentReceived) { if(dueDate < QDate::currentDate()) dueDate = QDate::currentDate(); } // we need to calculate the balance at the time the payment is due MyMoneyMoney balance; if(balances.count() == 0) balance = MyMoneyFile::instance()->balance(acc.id(), dueDate.addDays(-1)); else balance = balances[acc.id()]; /* QValueList<MyMoneyTransaction> list; QValueList<MyMoneyTransaction>::ConstIterator it; MyMoneySplit split; MyMoneyTransactionFilter filter(acc.id()); filter.setDateFilter(QDate(), dueDate.addDays(-1)); list = MyMoneyFile::instance()->transactionList(filter); for(it = list.begin(); it != list.end(); ++it) { try { split = (*it).splitByAccount(acc.id()); balance += split.value(); } catch(MyMoneyException *e) { // account is not referenced within this transaction delete e; } } */ // FIXME: for now, we only support interest calculation at the end of the period calc.setBep(); // FIXME: for now, we only support periodic compounding calc.setDisc(); calc.setPF(MyMoneySchedule::eventsPerYear(schedule.occurence())); MyMoneySchedule::occurenceE compoundingOccurence = static_cast<MyMoneySchedule::occurenceE>(acc.interestCompounding()); if(compoundingOccurence == MyMoneySchedule::OCCUR_ANY) compoundingOccurence = schedule.occurence(); calc.setCF(MyMoneySchedule::eventsPerYear(compoundingOccurence)); calc.setPv(balance.toDouble()); calc.setIr(static_cast<FCALC_DOUBLE> (acc.interestRate(dueDate).abs().toDouble())); calc.setPmt(acc.periodicPayment().toDouble()); MyMoneyMoney interest(calc.interestDue()), amortization; interest = interest.abs(); // make sure it's positive for now amortization = acc.periodicPayment() - interest; if(acc.accountType() == MyMoneyAccount::AssetLoan) { interest = -interest; amortization = -amortization; } amortizationSplit.setShares(amortization); interestSplit.setShares(interest); // FIXME: for now we only assume loans to be in the currency of the transaction amortizationSplit.setValue(amortization); interestSplit.setValue(interest); transaction.modifySplit(amortizationSplit); transaction.modifySplit(interestSplit); } } }
void MyMoneyStorageXML::writeTransaction(QDomElement& transaction, const MyMoneyTransaction& tx) { tx.writeXML(*m_doc, transaction); }
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); }
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); }
void MyMoneyForecast::addScheduledTransactions (void) { MyMoneyFile* file = MyMoneyFile::instance(); // now process all the schedules that may have an impact QValueList<MyMoneySchedule> schedule; schedule = file->scheduleList("", MyMoneySchedule::TYPE_ANY, MyMoneySchedule::OCCUR_ANY, MyMoneySchedule::STYPE_ANY, QDate(), forecastEndDate()); if(schedule.count() > 0) { QValueList<MyMoneySchedule>::Iterator it; do { qBubbleSort(schedule); it = schedule.begin(); if(it == schedule.end()) break; if((*it).isFinished()) { schedule.erase(it); continue; } QDate date = (*it).nextPayment((*it).lastPayment()); if(!date.isValid()) { schedule.remove(it); continue; } QDate nextDate = (*it).adjustedNextPayment((*it).adjustedDate((*it).lastPayment(), (*it).weekendOption())); if (nextDate > forecastEndDate()) { // We're done with this schedule, let's move on to the next schedule.remove(it); continue; } // found the next schedule. process it MyMoneyAccount acc = (*it).account(); if(!acc.id().isEmpty()) { try { if(acc.accountType() != MyMoneyAccount::Investment) { MyMoneyTransaction t = (*it).transaction(); // only process the entry, if it is still active if(!(*it).isFinished() && nextDate != QDate()) { // make sure we have all 'starting balances' so that the autocalc works QValueList<MyMoneySplit>::const_iterator it_s; QMap<QString, MyMoneyMoney> balanceMap; for(it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s ) { MyMoneyAccount acc = file->account((*it_s).accountId()); if(isForecastAccount(acc)) { // collect all overdues on the first day QDate forecastDate = nextDate; if(QDate::currentDate() >= nextDate) forecastDate = QDate::currentDate().addDays(1); dailyBalances balance; balance = m_accountList[acc.id()]; for(QDate f_day = QDate::currentDate(); f_day < forecastDate; ) { balanceMap[acc.id()] += m_accountList[acc.id()][f_day]; f_day = f_day.addDays(1); } } } // take care of the autoCalc stuff calculateAutoLoan(*it, t, balanceMap); // now add the splits to the balances for(it_s = t.splits().begin(); it_s != t.splits().end(); ++it_s ) { MyMoneyAccount acc = file->account((*it_s).accountId()); if(isForecastAccount(acc)) { dailyBalances balance; balance = m_accountList[acc.id()]; //int offset = QDate::currentDate().daysTo(nextDate); //if(offset <= 0) { // collect all overdues on the first day // offset = 1; //} // collect all overdues on the first day QDate forecastDate = nextDate; if(QDate::currentDate() >= nextDate) forecastDate = QDate::currentDate().addDays(1); if(acc.accountType() == MyMoneyAccount::Income) { balance[forecastDate] += ((*it_s).shares() * MyMoneyMoney(-1, 1)); } else { balance[forecastDate] += (*it_s).shares(); } m_accountList[acc.id()] = balance; } } } } (*it).setLastPayment(date); } catch(MyMoneyException* e) { kdDebug(2) << __func__ << " Schedule " << (*it).id() << " (" << (*it).name() << "): " << e->what() << endl; schedule.remove(it); delete e; } } else { // remove schedule from list schedule.remove(it); } } while(1); } #if 0 { s << "\n\nAdded scheduled transactions\n"; QMap<QString, dailyBalances>::Iterator it_a; QMap<QString, QString>::ConstIterator it_n; for(it_n = m_nameIdx.begin(); it_n != m_nameIdx.end(); ++it_n) { MyMoneyAccount acc = file->account(*it_n); it_a = m_accountList.find(*it_n); s << "\"" << acc.name() << "\","; for(int i = 0; i < 90; ++i) { s << "\"" << (*it_a)[i].formatMoney("") << "\","; } s << "\n"; } } #endif }
MyMoneyTransaction KNewLoanWizard::transaction() const { MyMoneyTransaction t; bool hasInterest = !field("interestRateEdit").value<MyMoneyMoney>().isZero(); MyMoneySplit sPayment, sInterest, sAmortization; // setup accounts. at this point, we cannot fill in the id of the // account that the amortization will be performed on, because we // create the account. So the id is yet unknown. sPayment.setAccountId(field("paymentAccountEdit").toStringList().first()); //Only create the interest split if not zero if (hasInterest) { sInterest.setAccountId(field("interestAccountEdit").toStringList().first()); sInterest.setValue(MyMoneyMoney::autoCalc); sInterest.setShares(sInterest.value()); sInterest.setAction(MyMoneySplit::ActionInterest); } // values if (field("borrowButton").toBool()) { sPayment.setValue(-field("paymentEdit").value<MyMoneyMoney>()); } else { sPayment.setValue(field("paymentEdit").value<MyMoneyMoney>()); } sAmortization.setValue(MyMoneyMoney::autoCalc); // don't forget the shares sPayment.setShares(sPayment.value()); sAmortization.setShares(sAmortization.value()); // setup the commodity MyMoneyAccount acc = MyMoneyFile::instance()->account(sPayment.accountId()); t.setCommodity(acc.currencyId()); // actions sPayment.setAction(MyMoneySplit::ActionAmortization); sAmortization.setAction(MyMoneySplit::ActionAmortization); // payee QString payeeId = field("payeeEdit").toString(); sPayment.setPayeeId(payeeId); sAmortization.setPayeeId(payeeId); MyMoneyAccount account("Phony-ID", MyMoneyAccount()); sAmortization.setAccountId(account.id()); // IMPORTANT: Payment split must be the first one, because // the schedule view expects it this way during display t.addSplit(sPayment); t.addSplit(sAmortization); if (hasInterest) { t.addSplit(sInterest); } // copy the splits from the other costs and update the payment split foreach (const MyMoneySplit& it, m_transaction.splits()) { if (it.accountId() != account.id()) { MyMoneySplit sp = it; sp.clearId(); t.addSplit(sp); sPayment.setValue(sPayment.value() - sp.value()); sPayment.setShares(sPayment.value()); t.modifySplit(sPayment); } } return t; }